diff --git a/Elecciones-Web/src/Elecciones.Worker/Elecciones.Worker.csproj b/Elecciones-Web/src/Elecciones.Worker/Elecciones.Worker.csproj
index b79f54c..151dee8 100644
--- a/Elecciones-Web/src/Elecciones.Worker/Elecciones.Worker.csproj
+++ b/Elecciones-Web/src/Elecciones.Worker/Elecciones.Worker.csproj
@@ -11,6 +11,7 @@
+
diff --git a/Elecciones-Web/src/Elecciones.Worker/Program.cs b/Elecciones-Web/src/Elecciones.Worker/Program.cs
index 74111b5..9af9cfa 100644
--- a/Elecciones-Web/src/Elecciones.Worker/Program.cs
+++ b/Elecciones-Web/src/Elecciones.Worker/Program.cs
@@ -9,6 +9,8 @@ using Serilog;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
+using Polly;
+using Polly.Extensions.Http;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
@@ -37,7 +39,7 @@ builder.Services.AddHttpClient("ElectoralApiClient", client =>
{
client.BaseAddress = new Uri(baseUrl);
}
-
+
// --- TIMEOUT MÁS LARGO ---
// Aumentamos el tiempo de espera a 90 segundos.
// Esto le dará a las peticiones lentas de la API tiempo suficiente para responder.
@@ -75,7 +77,9 @@ builder.Services.AddHttpClient("ElectoralApiClient", client =>
}
return handler;
-});
+})
+
+.AddPolicyHandler(GetRetryPolicy());
builder.Services.AddSingleton();
@@ -83,7 +87,8 @@ builder.Services.AddHostedService();
var host = builder.Build();
-try {
+try
+{
host.Run();
}
catch (Exception ex)
@@ -93,4 +98,20 @@ catch (Exception ex)
finally
{
Log.CloseAndFlush();
+}
+
+static IAsyncPolicy 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);
+ });
}
\ No newline at end of file
diff --git a/Elecciones-Web/src/Elecciones.Worker/Worker.cs b/Elecciones-Web/src/Elecciones.Worker/Worker.cs
index 00267fc..2ab3e8d 100644
--- a/Elecciones-Web/src/Elecciones.Worker/Worker.cs
+++ b/Elecciones-Web/src/Elecciones.Worker/Worker.cs
@@ -318,68 +318,110 @@ public class Worker : BackgroundService
await dbContext.SaveChangesAsync(stoppingToken);
}
+ ///
+ /// 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.
+ ///
+ /// El token de autenticación válido para la sesión.
+ /// El token de cancelación para detener la operación.
private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken)
{
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();
var dbContext = scope.ServiceProvider.GetRequiredService();
+ // 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
- .AsNoTracking()
+ .AsNoTracking() // Optimización de rendimiento: solo vamos a leer estos datos.
.Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS"))
.ToListAsync(stoppingToken);
+ // Si por alguna razón estas categorías no están en la BD, no podemos continuar.
if (!categoriasDeBancas.Any())
{
_logger.LogWarning("No se encontraron categorías para 'Senadores' o 'Diputados' en la BD. Omitiendo sondeo de bancas.");
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()
- .Where(a => a.NivelId == 4 && a.DistritoId != null && a.SeccionId != null)
+ .Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null)
.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;
}
- _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;
- 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)
{
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 })
{
+ // Si esta es la PRIMERA VEZ en todo el sondeo que recibimos datos válidos...
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);
+ // Activamos la bandera para no volver a ejecutar este borrado.
hasReceivedAnyNewData = true;
}
+ // Procesamos cada banca obtenida en la respuesta de la API.
foreach (var banca in repartoBancas.RepartoBancas)
{
+ // Creamos una nueva entidad 'ProyeccionBanca'.
var nuevaProyeccion = new ProyeccionBanca
{
AmbitoGeograficoId = seccion.Id,
AgrupacionPoliticaId = banca.IdAgrupacion,
NroBancas = banca.NroBancas
};
+ // Y la añadimos al ChangeTracker de EF para que la inserte.
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)
{
await dbContext.SaveChangesAsync(stoppingToken);
@@ -387,83 +429,127 @@ public class Worker : BackgroundService
}
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)
{
+ // 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.");
}
}
+ ///
+ /// 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.
+ ///
+ /// El token de autenticación válido para la sesión.
+ /// El token de cancelación para detener la operación.
private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken)
{
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();
var dbContext = scope.ServiceProvider.GetRequiredService();
- var secciones = await dbContext.AmbitosGeograficos
- .AsNoTracking()
- .Where(a => a.NivelId == 4 && a.DistritoId != null && a.SeccionId != null)
+ // PASO 2: Obtener los Partidos/Municipios (NivelId = 30)
+ // --- CORRECCIÓN CLAVE ---
+ // 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);
- 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;
}
- _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 })
{
+ // 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();
+
+ // 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
.Where(t => idsDeApi.Contains(t.Id))
.Select(t => t.Id)
.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();
+
+ // Si no hay telegramas nuevos para este partido, simplemente continuamos con el siguiente.
if (!nuevosTelegramasIds.Any())
{
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)
{
if (stoppingToken.IsCancellationRequested) break;
+
+ // Descargamos el contenido completo del telegrama (imagen base64 y metadatos).
var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId);
if (telegramaFile != null)
{
var nuevoTelegrama = new Telegrama
{
Id = telegramaFile.NombreArchivo,
- AmbitoGeograficoId = seccion.Id,
+ AmbitoGeograficoId = partido.Id, // Lo asociamos al ID del partido.
ContenidoBase64 = telegramaFile.Imagen,
FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).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);
}
}
+
+ // 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);
}
}
+
_logger.LogInformation("Sondeo de Telegramas completado.");
}
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.");
}
}
+ ///
+ /// 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.
+ ///
private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken)
{
try
@@ -475,21 +561,40 @@ public class Worker : BackgroundService
if (provincia == null) return;
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);
- 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,
AgrupacionPoliticaId = voto.IdAgrupacion,
Votos = voto.Votos,
VotosPorcentaje = voto.VotosPorcentaje
- }, stoppingToken);
+ });
}
+
+ // 3. Guardamos los cambios y confirmamos la transacción.
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)
diff --git a/Elecciones-Web/src/Elecciones.Worker/obj/Debug/net9.0/Elecciones.Worker.AssemblyInfo.cs b/Elecciones-Web/src/Elecciones.Worker/obj/Debug/net9.0/Elecciones.Worker.AssemblyInfo.cs
index d100b33..469da4a 100644
--- a/Elecciones-Web/src/Elecciones.Worker/obj/Debug/net9.0/Elecciones.Worker.AssemblyInfo.cs
+++ b/Elecciones-Web/src/Elecciones.Worker/obj/Debug/net9.0/Elecciones.Worker.AssemblyInfo.cs
@@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[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.AssemblyTitleAttribute("Elecciones.Worker")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
diff --git a/Elecciones-Web/src/Elecciones.Worker/obj/Elecciones.Worker.csproj.nuget.dgspec.json b/Elecciones-Web/src/Elecciones.Worker/obj/Elecciones.Worker.csproj.nuget.dgspec.json
index ea147cf..ee7ae44 100644
--- a/Elecciones-Web/src/Elecciones.Worker/obj/Elecciones.Worker.csproj.nuget.dgspec.json
+++ b/Elecciones-Web/src/Elecciones.Worker/obj/Elecciones.Worker.csproj.nuget.dgspec.json
@@ -290,6 +290,10 @@
"target": "Package",
"version": "[9.0.8, )"
},
+ "Microsoft.Extensions.Http.Polly": {
+ "target": "Package",
+ "version": "[9.0.8, )"
+ },
"Serilog.Extensions.Hosting": {
"target": "Package",
"version": "[9.0.0, )"