Fix Worker 1348
This commit is contained in:
		| @@ -11,6 +11,7 @@ | |||||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" /> |     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" /> |     <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" /> |     <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" /> | ||||||
|  |     <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.8" /> | ||||||
|     <PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" /> |     <PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" /> | ||||||
|     <PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" /> |     <PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" /> | ||||||
|     <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> |     <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> | ||||||
|   | |||||||
| @@ -9,6 +9,8 @@ using Serilog; | |||||||
| using System.Net.Http; | using System.Net.Http; | ||||||
| using System.Net.Security; | using System.Net.Security; | ||||||
| using System.Security.Authentication; | using System.Security.Authentication; | ||||||
|  | using Polly; | ||||||
|  | using Polly.Extensions.Http; | ||||||
|  |  | ||||||
| Log.Logger = new LoggerConfiguration() | Log.Logger = new LoggerConfiguration() | ||||||
|     .WriteTo.Console() |     .WriteTo.Console() | ||||||
| @@ -75,7 +77,9 @@ builder.Services.AddHttpClient("ElectoralApiClient", client => | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return handler; |     return handler; | ||||||
| }); | }) | ||||||
|  |  | ||||||
|  | .AddPolicyHandler(GetRetryPolicy()); | ||||||
|  |  | ||||||
| builder.Services.AddSingleton<IElectoralApiService, ElectoralApiService>(); | builder.Services.AddSingleton<IElectoralApiService, ElectoralApiService>(); | ||||||
|  |  | ||||||
| @@ -83,7 +87,8 @@ builder.Services.AddHostedService<Worker>(); | |||||||
|  |  | ||||||
| var host = builder.Build(); | var host = builder.Build(); | ||||||
|  |  | ||||||
| try { | try | ||||||
|  | { | ||||||
|     host.Run(); |     host.Run(); | ||||||
| } | } | ||||||
| catch (Exception ex) | catch (Exception ex) | ||||||
| @@ -94,3 +99,19 @@ finally | |||||||
| { | { | ||||||
|     Log.CloseAndFlush(); |     Log.CloseAndFlush(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() | ||||||
|  | { | ||||||
|  |     return HttpPolicyExtensions | ||||||
|  |         // Manejar peticiones que fallaron por errores de red O que devolvieron un error de servidor (como 504) | ||||||
|  |         .HandleTransientHttpError() | ||||||
|  |         // O que devolvieron un código 504 Gateway Timeout específicamente | ||||||
|  |         .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.GatewayTimeout) | ||||||
|  |         // Esperar y reintentar. La espera se duplica en cada reintento. | ||||||
|  |         .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),  | ||||||
|  |             onRetry: (outcome, timespan, retryAttempt, context) => | ||||||
|  |             { | ||||||
|  |                 // Opcional: Loguear cada reintento. Necesitarías pasar ILogger aquí. | ||||||
|  |                 // Log.Warning("Retrying due to {StatusCode}. Waited {seconds}s. Attempt {retryAttempt}/3", outcome.Result.StatusCode, timespan.TotalSeconds, retryAttempt); | ||||||
|  |             }); | ||||||
|  | } | ||||||
| @@ -318,68 +318,110 @@ public class Worker : BackgroundService | |||||||
|         await dbContext.SaveChangesAsync(stoppingToken); |         await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Sondea la proyección de bancas para diputados y senadores. | ||||||
|  |     /// Este método busca dinámicamente en la base de datos las categorías relevantes (Senadores/Diputados) | ||||||
|  |     /// y los ámbitos de "Sección Electoral" (NivelId = 20), que es el nivel al que se reparten las bancas. | ||||||
|  |     /// Luego, consulta la API para cada combinación y actualiza la tabla de proyecciones. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="authToken">El token de autenticación válido para la sesión.</param> | ||||||
|  |     /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||||
|     private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) |     private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) | ||||||
|     { |     { | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|  |             // PASO 1: Preparar el entorno | ||||||
|  |             // Creamos un scope de DbContext para esta operación específica, una buena práctica en servicios de larga duración. | ||||||
|             using var scope = _serviceProvider.CreateScope(); |             using var scope = _serviceProvider.CreateScope(); | ||||||
|             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|  |             // PASO 2: Obtener las categorías que reparten bancas (Senadores y Diputados) | ||||||
|  |             // Hacemos una consulta a nuestra tabla local de categorías para que el código sea dinámico | ||||||
|  |             // y no dependa de IDs fijos (hardcodeados). | ||||||
|             var categoriasDeBancas = await dbContext.CategoriasElectorales |             var categoriasDeBancas = await dbContext.CategoriasElectorales | ||||||
|                 .AsNoTracking() |                 .AsNoTracking() // Optimización de rendimiento: solo vamos a leer estos datos. | ||||||
|                 .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) |                 .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) | ||||||
|                 .ToListAsync(stoppingToken); |                 .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|  |             // Si por alguna razón estas categorías no están en la BD, no podemos continuar. | ||||||
|             if (!categoriasDeBancas.Any()) |             if (!categoriasDeBancas.Any()) | ||||||
|             { |             { | ||||||
|                 _logger.LogWarning("No se encontraron categorías para 'Senadores' o 'Diputados' en la BD. Omitiendo sondeo de bancas."); |                 _logger.LogWarning("No se encontraron categorías para 'Senadores' o 'Diputados' en la BD. Omitiendo sondeo de bancas."); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             var secciones = await dbContext.AmbitosGeograficos |             // PASO 3: Obtener las "Secciones Electorales" (NivelId 20) | ||||||
|  |             // Esta es la corrección clave. Basado en la respuesta real de la API, las Secciones Electorales | ||||||
|  |             // (Primera, Segunda, Tercera, etc.) usan NivelId = 20. | ||||||
|  |             var seccionesElectorales = await dbContext.AmbitosGeograficos | ||||||
|                 .AsNoTracking() |                 .AsNoTracking() | ||||||
|                 .Where(a => a.NivelId == 4 && a.DistritoId != null && a.SeccionId != null) |                 .Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null) | ||||||
|                 .ToListAsync(stoppingToken); |                 .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|             if (!secciones.Any()) |             // Si no se encuentra ninguna Sección Electoral en la BD (lo cual sería raro después de una sincronización exitosa), | ||||||
|  |             // registramos una advertencia y salimos. | ||||||
|  |             if (!seccionesElectorales.Any()) | ||||||
|             { |             { | ||||||
|                 _logger.LogWarning("No se encontraron ámbitos de tipo 'Sección Electoral' en la BD para sondear bancas."); |                 _logger.LogWarning("No se encontraron ámbitos de tipo 'Sección Electoral' (NivelId 20) en la BD para sondear bancas."); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             _logger.LogInformation("Iniciando sondeo de Bancas para {count} secciones y {catCount} categorías...", secciones.Count, categoriasDeBancas.Count); |             _logger.LogInformation("Iniciando sondeo de Bancas para {count} secciones electorales y {catCount} categorías...", seccionesElectorales.Count, categoriasDeBancas.Count); | ||||||
|  |  | ||||||
|  |             // PASO 4: Iterar, consultar la API y preparar los datos para guardar | ||||||
|  |  | ||||||
|  |             // Esta bandera es crucial para la estrategia de "borrar y reemplazar". | ||||||
|  |             // Nos asegura que la tabla de proyecciones se vacía UNA SOLA VEZ, justo antes de insertar | ||||||
|  |             // el primer lote de datos nuevos, evitando datos inconsistentes. | ||||||
|             bool hasReceivedAnyNewData = false; |             bool hasReceivedAnyNewData = false; | ||||||
|             foreach (var seccion in secciones) |  | ||||||
|  |             // Bucle externo: recorremos cada una de las 8 Secciones Electorales. | ||||||
|  |             foreach (var seccion in seccionesElectorales) | ||||||
|             { |             { | ||||||
|                 if (stoppingToken.IsCancellationRequested) break; |                 if (stoppingToken.IsCancellationRequested) break; // Salida limpia si la aplicación se detiene. | ||||||
|  |  | ||||||
|  |                 // Bucle interno: para cada sección, consultamos las bancas de Senadores y Diputados. | ||||||
|                 foreach (var categoria in categoriasDeBancas) |                 foreach (var categoria in categoriasDeBancas) | ||||||
|                 { |                 { | ||||||
|                     if (stoppingToken.IsCancellationRequested) break; |                     if (stoppingToken.IsCancellationRequested) break; | ||||||
|                     var repartoBancas = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionId!, categoria.Id); |  | ||||||
|  |                     // Llamamos a la API. El endpoint 'getBancas' requiere 'distritoId' y 'seccionProvincialId', | ||||||
|  |                     // que son precisamente los datos que tenemos en nuestros ámbitos de NivelId = 20. | ||||||
|  |                     var repartoBancas = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id); | ||||||
|  |  | ||||||
|  |                     // Verificamos que la respuesta de la API no sea nula y que contenga al menos una banca repartida. | ||||||
|                     if (repartoBancas?.RepartoBancas is { Count: > 0 }) |                     if (repartoBancas?.RepartoBancas is { Count: > 0 }) | ||||||
|                     { |                     { | ||||||
|  |                         // Si esta es la PRIMERA VEZ en todo el sondeo que recibimos datos válidos... | ||||||
|                         if (!hasReceivedAnyNewData) |                         if (!hasReceivedAnyNewData) | ||||||
|                         { |                         { | ||||||
|                             _logger.LogInformation("Se recibieron nuevos datos de bancas. Limpiando la tabla de proyecciones para evitar duplicados..."); |                             _logger.LogInformation("Se recibieron nuevos datos de bancas. Limpiando la tabla de proyecciones para la actualización..."); | ||||||
|  |                             // ...ejecutamos un comando SQL para borrar todos los datos viejos de la tabla. | ||||||
|                             await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken); |                             await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken); | ||||||
|  |                             // Activamos la bandera para no volver a ejecutar este borrado. | ||||||
|                             hasReceivedAnyNewData = true; |                             hasReceivedAnyNewData = true; | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|  |                         // Procesamos cada banca obtenida en la respuesta de la API. | ||||||
|                         foreach (var banca in repartoBancas.RepartoBancas) |                         foreach (var banca in repartoBancas.RepartoBancas) | ||||||
|                         { |                         { | ||||||
|  |                             // Creamos una nueva entidad 'ProyeccionBanca'. | ||||||
|                             var nuevaProyeccion = new ProyeccionBanca |                             var nuevaProyeccion = new ProyeccionBanca | ||||||
|                             { |                             { | ||||||
|                                 AmbitoGeograficoId = seccion.Id, |                                 AmbitoGeograficoId = seccion.Id, | ||||||
|                                 AgrupacionPoliticaId = banca.IdAgrupacion, |                                 AgrupacionPoliticaId = banca.IdAgrupacion, | ||||||
|                                 NroBancas = banca.NroBancas |                                 NroBancas = banca.NroBancas | ||||||
|                             }; |                             }; | ||||||
|  |                             // Y la añadimos al ChangeTracker de EF para que la inserte. | ||||||
|                             await dbContext.ProyeccionesBancas.AddAsync(nuevaProyeccion, stoppingToken); |                             await dbContext.ProyeccionesBancas.AddAsync(nuevaProyeccion, stoppingToken); | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             // PASO 5: Guardar los cambios en la Base de Datos | ||||||
|  |             // Si la bandera 'hasReceivedAnyNewData' se activó, significa que hemos añadido nuevas proyecciones | ||||||
|  |             // al DbContext y necesitamos persistirlas. | ||||||
|             if (hasReceivedAnyNewData) |             if (hasReceivedAnyNewData) | ||||||
|             { |             { | ||||||
|                 await dbContext.SaveChangesAsync(stoppingToken); |                 await dbContext.SaveChangesAsync(stoppingToken); | ||||||
| @@ -387,83 +429,127 @@ public class Worker : BackgroundService | |||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
|                 _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos, la tabla no fue modificada."); |                 // Si no se recibieron datos nuevos, no hacemos nada en la BD. | ||||||
|  |                 _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada."); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|  |             // Capturamos cualquier error inesperado para que no detenga el worker por completo. | ||||||
|             _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas."); |             _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas."); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Busca en la API si hay nuevos telegramas totalizados que no se encuentren en la base de datos local. | ||||||
|  |     /// Este método itera sobre cada Partido/Municipio (que la API identifica como "Sección" con NivelId = 30), | ||||||
|  |     /// obtiene la lista de IDs de telegramas para cada uno, los compara con los IDs locales, | ||||||
|  |     /// y finalmente descarga y guarda solo los que son nuevos. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="authToken">El token de autenticación válido para la sesión.</param> | ||||||
|  |     /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||||
|     private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken) |     private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken) | ||||||
|     { |     { | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|  |             // PASO 1: Preparar el DbContext | ||||||
|  |             // Creamos un scope para obtener una instancia fresca de DbContext para esta operación. | ||||||
|             using var scope = _serviceProvider.CreateScope(); |             using var scope = _serviceProvider.CreateScope(); | ||||||
|             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|             var secciones = await dbContext.AmbitosGeograficos |             // PASO 2: Obtener los Partidos/Municipios (NivelId = 30) | ||||||
|                 .AsNoTracking() |             // --- CORRECCIÓN CLAVE --- | ||||||
|                 .Where(a => a.NivelId == 4 && a.DistritoId != null && a.SeccionId != null) |             // La API requiere un 'seccionId' para listar telegramas, y el manual lo define como "Id del partido". | ||||||
|  |             // La respuesta de getCatalogo nos confirmó que los registros de Partidos/Municipios usan NivelId = 30. | ||||||
|  |             var partidos = await dbContext.AmbitosGeograficos | ||||||
|  |                 .AsNoTracking() // Optimización: Solo necesitamos leer estos datos. | ||||||
|  |                 .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) | ||||||
|                 .ToListAsync(stoppingToken); |                 .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|             if (!secciones.Any()) |             // Si la sincronización inicial no cargó ningún partido, no podemos continuar. | ||||||
|  |             if (!partidos.Any()) | ||||||
|             { |             { | ||||||
|                 _logger.LogWarning("No hay Secciones Electorales en la BD para sondear telegramas."); |                 _logger.LogWarning("No se encontraron Partidos (NivelId 30) en la BD para sondear telegramas."); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             _logger.LogInformation("Iniciando sondeo de Telegramas nuevos..."); |             _logger.LogInformation("Iniciando sondeo de Telegramas nuevos para {count} partidos...", partidos.Count); | ||||||
|  |  | ||||||
|             foreach (var seccion in secciones) |             // PASO 3: Iterar sobre cada Partido para buscar telegramas. | ||||||
|  |             foreach (var partido in partidos) | ||||||
|             { |             { | ||||||
|                 if (stoppingToken.IsCancellationRequested) break; |                 if (stoppingToken.IsCancellationRequested) break; // Salida limpia si la aplicación se detiene. | ||||||
|  |  | ||||||
|                 var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, seccion.DistritoId!, seccion.SeccionId!); |                 // 3.1 - OBTENER LA LISTA COMPLETA DE IDs DE LA API PARA EL PARTIDO ACTUAL | ||||||
|  |                 var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!); | ||||||
|  |  | ||||||
|  |                 // Solo procesamos si la API devolvió una lista con al menos un telegrama. | ||||||
|                 if (listaTelegramasApi is { Count: > 0 }) |                 if (listaTelegramasApi is { Count: > 0 }) | ||||||
|                 { |                 { | ||||||
|  |                     // La API devuelve una lista de arrays de string (ej. [ ["id1"], ["id2"] ]). | ||||||
|  |                     // Usamos .Select(t => t[0]) para extraer solo el ID de cada sub-array. | ||||||
|                     var idsDeApi = listaTelegramasApi.Select(t => t[0]).Distinct().ToList(); |                     var idsDeApi = listaTelegramasApi.Select(t => t[0]).Distinct().ToList(); | ||||||
|  |  | ||||||
|  |                     // 3.2 - COMPARAR CON LA BASE DE DATOS LOCAL PARA ENCONTRAR LOS NUEVOS | ||||||
|  |                     // Esta es una consulta muy eficiente: le pedimos a la BD que nos devuelva solo los IDs que | ||||||
|  |                     // ya existen de la lista que nos acaba de dar la API. | ||||||
|                     var idsYaEnDb = await dbContext.Telegramas |                     var idsYaEnDb = await dbContext.Telegramas | ||||||
|                         .Where(t => idsDeApi.Contains(t.Id)) |                         .Where(t => idsDeApi.Contains(t.Id)) | ||||||
|                         .Select(t => t.Id) |                         .Select(t => t.Id) | ||||||
|                         .ToListAsync(stoppingToken); |                         .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|  |                     // Comparamos las dos listas para obtener una tercera: la de los IDs que están en la API pero no en nuestra BD. | ||||||
|                     var nuevosTelegramasIds = idsDeApi.Except(idsYaEnDb).ToList(); |                     var nuevosTelegramasIds = idsDeApi.Except(idsYaEnDb).ToList(); | ||||||
|  |  | ||||||
|  |                     // Si no hay telegramas nuevos para este partido, simplemente continuamos con el siguiente. | ||||||
|                     if (!nuevosTelegramasIds.Any()) |                     if (!nuevosTelegramasIds.Any()) | ||||||
|                     { |                     { | ||||||
|                         continue; |                         continue; | ||||||
|                     } |                     } | ||||||
|                     _logger.LogInformation("Se encontraron {count} telegramas nuevos en la sección {seccionId}. Descargando...", nuevosTelegramasIds.Count, seccion.SeccionId); |  | ||||||
|  |  | ||||||
|  |                     _logger.LogInformation("Se encontraron {count} telegramas nuevos en el partido '{nombrePartido}' ({seccionId}). Descargando...", nuevosTelegramasIds.Count, partido.Nombre, partido.SeccionId); | ||||||
|  |  | ||||||
|  |                     // 3.3 - DESCARGAR Y GUARDAR CADA NUEVO TELEGRAMA | ||||||
|                     foreach (var mesaId in nuevosTelegramasIds) |                     foreach (var mesaId in nuevosTelegramasIds) | ||||||
|                     { |                     { | ||||||
|                         if (stoppingToken.IsCancellationRequested) break; |                         if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |  | ||||||
|  |                         // Descargamos el contenido completo del telegrama (imagen base64 y metadatos). | ||||||
|                         var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); |                         var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); | ||||||
|                         if (telegramaFile != null) |                         if (telegramaFile != null) | ||||||
|                         { |                         { | ||||||
|                             var nuevoTelegrama = new Telegrama |                             var nuevoTelegrama = new Telegrama | ||||||
|                             { |                             { | ||||||
|                                 Id = telegramaFile.NombreArchivo, |                                 Id = telegramaFile.NombreArchivo, | ||||||
|                                 AmbitoGeograficoId = seccion.Id, |                                 AmbitoGeograficoId = partido.Id, // Lo asociamos al ID del partido. | ||||||
|                                 ContenidoBase64 = telegramaFile.Imagen, |                                 ContenidoBase64 = telegramaFile.Imagen, | ||||||
|                                 FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), |                                 FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), | ||||||
|                                 FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() |                                 FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() | ||||||
|                             }; |                             }; | ||||||
|  |                             // Añadimos la nueva entidad a la sesión de EF Core para su inserción. | ||||||
|                             await dbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken); |                             await dbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken); | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|  |                     // Guardamos los cambios en la base de datos después de procesar todos los telegramas nuevos de UN partido. | ||||||
|  |                     // Esto es más eficiente que guardar en cada iteración del bucle interno. | ||||||
|                     await dbContext.SaveChangesAsync(stoppingToken); |                     await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             _logger.LogInformation("Sondeo de Telegramas completado."); |             _logger.LogInformation("Sondeo de Telegramas completado."); | ||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|  |             // Capturamos cualquier error para que no detenga el worker y lo registramos para depuración. | ||||||
|             _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas."); |             _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas."); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Obtiene y actualiza el resumen de votos a nivel provincial. | ||||||
|  |     /// Esta versión mejorada utiliza una transacción para garantizar la consistencia de los datos. | ||||||
|  |     /// </summary> | ||||||
|     private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken) |     private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken) | ||||||
|     { |     { | ||||||
|         try |         try | ||||||
| @@ -475,21 +561,40 @@ public class Worker : BackgroundService | |||||||
|             if (provincia == null) return; |             if (provincia == null) return; | ||||||
|  |  | ||||||
|             var resumen = await _apiService.GetResumenAsync(authToken, provincia.DistritoId!); |             var resumen = await _apiService.GetResumenAsync(authToken, provincia.DistritoId!); | ||||||
|             if (resumen?.ValoresTotalizadosPositivos is { Count: > 0 }) |  | ||||||
|  |             // --- CAMBIO CLAVE: Lógica de actualización robusta --- | ||||||
|  |             // Solo procedemos si la respuesta de la API es válida Y contiene datos de votos positivos. | ||||||
|  |             if (resumen?.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos) | ||||||
|             { |             { | ||||||
|  |                 // Usamos una transacción explícita para asegurar que la operación sea atómica: | ||||||
|  |                 // O se completa todo (borrado e inserción), o no se hace nada. | ||||||
|  |                 await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); | ||||||
|  |  | ||||||
|  |                 // 1. Borramos los datos viejos. | ||||||
|                 await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ResumenesVotos", stoppingToken); |                 await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ResumenesVotos", stoppingToken); | ||||||
|                 foreach (var voto in resumen.ValoresTotalizadosPositivos) |  | ||||||
|  |                 // 2. Insertamos los nuevos datos. | ||||||
|  |                 foreach (var voto in nuevosVotos) | ||||||
|                 { |                 { | ||||||
|                     await dbContext.ResumenesVotos.AddAsync(new ResumenVoto |                     dbContext.ResumenesVotos.Add(new ResumenVoto | ||||||
|                     { |                     { | ||||||
|                         AmbitoGeograficoId = provincia.Id, |                         AmbitoGeograficoId = provincia.Id, | ||||||
|                         AgrupacionPoliticaId = voto.IdAgrupacion, |                         AgrupacionPoliticaId = voto.IdAgrupacion, | ||||||
|                         Votos = voto.Votos, |                         Votos = voto.Votos, | ||||||
|                         VotosPorcentaje = voto.VotosPorcentaje |                         VotosPorcentaje = voto.VotosPorcentaje | ||||||
|                     }, stoppingToken); |                     }); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 // 3. Guardamos los cambios y confirmamos la transacción. | ||||||
|                 await dbContext.SaveChangesAsync(stoppingToken); |                 await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|                 _logger.LogInformation("Sondeo de Resumen Provincial completado."); |                 await transaction.CommitAsync(stoppingToken); | ||||||
|  |  | ||||||
|  |                 _logger.LogInformation("Sondeo de Resumen Provincial completado. La tabla ha sido actualizada."); | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 // Si la API no devuelve datos de votos, no hacemos NADA en la base de datos. | ||||||
|  |                 _logger.LogInformation("Sondeo de Resumen Provincial completado. No se recibieron datos nuevos, la tabla no fue modificada."); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ using System.Reflection; | |||||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")] | [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")] | ||||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+30f1e751b770bf730fc48b1baefb00f560694f35")] | [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+a4e47b6e3d1f8b0746f4f910f56a94e17b2e030c")] | ||||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Worker")] | [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Worker")] | ||||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")] | [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")] | ||||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||||
|   | |||||||
| @@ -290,6 +290,10 @@ | |||||||
|               "target": "Package", |               "target": "Package", | ||||||
|               "version": "[9.0.8, )" |               "version": "[9.0.8, )" | ||||||
|             }, |             }, | ||||||
|  |             "Microsoft.Extensions.Http.Polly": { | ||||||
|  |               "target": "Package", | ||||||
|  |               "version": "[9.0.8, )" | ||||||
|  |             }, | ||||||
|             "Serilog.Extensions.Hosting": { |             "Serilog.Extensions.Hosting": { | ||||||
|               "target": "Package", |               "target": "Package", | ||||||
|               "version": "[9.0.0, )" |               "version": "[9.0.0, )" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user