Feat CarouselNacional y Fix Workers

This commit is contained in:
2025-10-17 15:49:15 -03:00
parent 4bc257df43
commit ae846f2d48
12 changed files with 430 additions and 148 deletions

View File

@@ -56,9 +56,9 @@ public class LowPriorityDataWorker : BackgroundService
if (settings.Prioridad == "Telegramas" && settings.ResultadosActivado)
{
_logger.LogInformation("Ejecutando tareas de Resultados en baja prioridad.");
await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
await SondearResultadosMunicipalesAsync(authToken, stoppingToken);
await SondearResumenProvincialAsync(authToken, stoppingToken);
await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
}
else if (settings.Prioridad == "Resultados" && settings.BajasActivado)
{
@@ -320,92 +320,110 @@ public class LowPriorityDataWorker : BackgroundService
{
try
{
// PASO 1: Crear un "scope" para obtener una instancia fresca de DbContext.
// Esto es una práctica recomendada para servicios de larga duración para evitar problemas de concurrencia.
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
// PASO 2: Obtener el ámbito geográfico de la Provincia.
// Necesitamos este objeto para obtener su 'DistritoId' ("02"), que es requerido por la API.
var provincia = await dbContext.AmbitosGeograficos
.AsNoTracking() // Optimización: Solo necesitamos leer datos, no modificarlos.
.FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);
var provinciasASondear = await dbContext.AmbitosGeograficos
.AsNoTracking()
.Where(a => a.NivelId == 10 && a.DistritoId != null)
.ToListAsync(stoppingToken);
// Comprobación de seguridad: Si la sincronización inicial falló y no tenemos el registro de la provincia,
// no podemos continuar. Registramos una advertencia y salimos del método.
if (provincia == null)
{
_logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general.");
return;
}
// Busca NivelId 1 (País) o 0 como fallback.
var ambitoNacional = await dbContext.AmbitosGeograficos
.AsNoTracking()
.FirstOrDefaultAsync(a => a.NivelId == 1 || a.NivelId == 0, stoppingToken);
// PASO 3: Obtener todas las categorías electorales disponibles desde nuestra base de datos.
// Esto hace que el método sea dinámico y no dependa de IDs fijos en el código.
var categoriasParaSondear = await dbContext.CategoriasElectorales
.AsNoTracking()
.ToListAsync(stoppingToken);
if (!categoriasParaSondear.Any())
if (!provinciasASondear.Any() || !categoriasParaSondear.Any())
{
_logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento.");
_logger.LogWarning("No se encontraron Provincias o Categorías para sondear estado general.");
return;
}
_logger.LogInformation("Iniciando sondeo de Estado Recuento General para {count} categorías...", categoriasParaSondear.Count);
_logger.LogInformation("Iniciando sondeo de Estado Recuento General para {provCount} provincias, el total nacional y {catCount} categorías...", provinciasASondear.Count, categoriasParaSondear.Count);
// PASO 4: Iterar sobre cada categoría para obtener su estado de recuento individual.
foreach (var categoria in categoriasParaSondear)
// Sondeo a nivel provincial
foreach (var provincia in provinciasASondear)
{
// Salimos limpiamente del bucle si la aplicación se está deteniendo.
if (stoppingToken.IsCancellationRequested) break;
// Llamamos a la API con el distrito y la CATEGORÍA ACTUAL del bucle.
var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id);
// Solo procedemos si la API devolvió datos válidos.
if (estadoDto != null)
foreach (var categoria in categoriasParaSondear)
{
// Lógica "Upsert" (Update or Insert):
// Buscamos un registro existente usando la CLAVE PRIMARIA COMPUESTA.
var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(
new object[] { provincia.Id, categoria.Id },
cancellationToken: stoppingToken
);
if (stoppingToken.IsCancellationRequested) break;
// Si no se encuentra (FindAsync devuelve null), es un registro nuevo.
if (registroDb == null)
var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id);
if (estadoDto != null)
{
// Creamos una nueva instancia de la entidad.
registroDb = new EstadoRecuentoGeneral
var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { provincia.Id, categoria.Id }, stoppingToken);
if (registroDb == null)
{
EleccionId = EleccionId,
AmbitoGeograficoId = provincia.Id,
CategoriaId = categoria.Id // Asignamos ambas partes de la clave primaria.
};
// Y la añadimos al ChangeTracker de EF para que la inserte en la BD.
dbContext.EstadosRecuentosGenerales.Add(registroDb);
registroDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id };
dbContext.EstadosRecuentosGenerales.Add(registroDb);
}
registroDb.FechaTotalizacion = DateTime.UtcNow;
registroDb.MesasEsperadas = estadoDto.MesasEsperadas;
registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas;
registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje;
registroDb.CantidadElectores = estadoDto.CantidadElectores;
registroDb.CantidadVotantes = estadoDto.CantidadVotantes;
registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje;
}
// Mapeamos los datos del DTO de la API a nuestra entidad de base de datos.
// Esto se hace tanto para registros nuevos como para los existentes que se van a actualizar.
registroDb.MesasEsperadas = estadoDto.MesasEsperadas;
registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas;
registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje;
registroDb.CantidadElectores = estadoDto.CantidadElectores;
registroDb.CantidadVotantes = estadoDto.CantidadVotantes;
registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje;
}
}
// PASO 5: Guardar todos los cambios en la base de datos.
// Al llamar a SaveChangesAsync UNA SOLA VEZ fuera del bucle, EF Core agrupa
// todas las inserciones y actualizaciones en una única transacción eficiente.
await dbContext.SaveChangesAsync(stoppingToken);
_logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías.");
// Bloque para el sondeo a nivel nacional
if (ambitoNacional != null && !stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Sondeando totales a nivel Nacional (Ambito ID: {ambitoId})...", ambitoNacional.Id);
foreach (var categoria in categoriasParaSondear)
{
if (stoppingToken.IsCancellationRequested) break;
var estadoNacionalDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, "", categoria.Id);
if (estadoNacionalDto != null)
{
var registroNacionalDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { ambitoNacional.Id, categoria.Id }, stoppingToken);
if (registroNacionalDb == null)
{
registroNacionalDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = ambitoNacional.Id, CategoriaId = categoria.Id };
dbContext.EstadosRecuentosGenerales.Add(registroNacionalDb);
}
registroNacionalDb.FechaTotalizacion = DateTime.UtcNow;
registroNacionalDb.MesasEsperadas = estadoNacionalDto.MesasEsperadas;
registroNacionalDb.MesasTotalizadas = estadoNacionalDto.MesasTotalizadas;
registroNacionalDb.MesasTotalizadasPorcentaje = estadoNacionalDto.MesasTotalizadasPorcentaje;
registroNacionalDb.CantidadElectores = estadoNacionalDto.CantidadElectores;
registroNacionalDb.CantidadVotantes = estadoNacionalDto.CantidadVotantes;
registroNacionalDb.ParticipacionPorcentaje = estadoNacionalDto.ParticipacionPorcentaje;
_logger.LogInformation("Datos nacionales para categoría '{catNombre}' actualizados.", categoria.Nombre);
}
}
}
else if (ambitoNacional == null)
{
_logger.LogWarning("No se encontró el ámbito geográfico para el Nivel Nacional (NivelId 1 o 0). No se pueden capturar los totales del país.");
}
// Guardar todos los cambios
if (dbContext.ChangeTracker.HasChanges())
{
await dbContext.SaveChangesAsync(stoppingToken);
_logger.LogInformation("Sondeo de Estado Recuento General completado. Se han guardado los cambios en la base de datos.");
}
else
{
_logger.LogInformation("Sondeo de Estado Recuento General completado. No se detectaron cambios.");
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Sondeo de Estado Recuento General cancelado.");
}
catch (Exception ex)
{
// Capturamos cualquier excepción inesperada para que no detenga el worker y la registramos.
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General.");
}
}