2025-09-06 21:44:52 -03:00
//Elecciones.Worker/CriticalDataWorker.cs
2025-08-20 16:58:18 -03:00
using Elecciones.Database ;
using Elecciones.Database.Entities ;
using Elecciones.Infrastructure.Services ;
using Microsoft.EntityFrameworkCore ;
using System.Collections.Concurrent ;
namespace Elecciones.Worker ;
public class CriticalDataWorker : BackgroundService
{
private readonly ILogger < CriticalDataWorker > _logger ;
private readonly SharedTokenService _tokenService ;
private readonly IServiceProvider _serviceProvider ;
2025-08-23 11:01:54 -03:00
private readonly IElectoralApiService _apiService ;
2025-09-06 21:44:52 -03:00
private readonly WorkerConfigService _configService ;
2025-08-20 16:58:18 -03:00
public CriticalDataWorker (
ILogger < CriticalDataWorker > logger ,
SharedTokenService tokenService ,
IServiceProvider serviceProvider ,
2025-09-06 21:44:52 -03:00
IElectoralApiService apiService ,
WorkerConfigService configService )
2025-08-20 16:58:18 -03:00
{
_logger = logger ;
_tokenService = tokenService ;
_serviceProvider = serviceProvider ;
2025-08-23 11:01:54 -03:00
_apiService = apiService ;
2025-09-06 21:44:52 -03:00
_configService = configService ;
2025-08-20 16:58:18 -03:00
}
protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
{
_logger . LogInformation ( "Worker de Datos Críticos iniciado." ) ;
try
{
await Task . Delay ( TimeSpan . FromMinutes ( 2 ) , stoppingToken ) ;
}
2025-08-23 11:01:54 -03:00
catch ( TaskCanceledException ) { return ; }
2025-08-20 16:58:18 -03:00
int cicloContador = 0 ;
while ( ! stoppingToken . IsCancellationRequested )
{
var cicloInicio = DateTime . UtcNow ;
cicloContador + + ;
_logger . LogInformation ( "--- Iniciando Ciclo de Datos Críticos #{ciclo} ---" , cicloContador ) ;
var authToken = await _tokenService . GetValidAuthTokenAsync ( stoppingToken ) ;
if ( string . IsNullOrEmpty ( authToken ) )
{
_logger . LogError ( "Ciclo Crítico: No se pudo obtener token. Reintentando en 30 segundos." ) ;
await Task . Delay ( TimeSpan . FromSeconds ( 30 ) , stoppingToken ) ;
continue ;
}
2025-09-06 21:44:52 -03:00
var settings = await _configService . GetSettingsAsync ( ) ;
if ( settings . Prioridad = = "Resultados" & & settings . ResultadosActivado )
{
_logger . LogInformation ( "Ejecutando tareas de Resultados en alta prioridad." ) ;
await SondearResultadosMunicipalesAsync ( authToken , stoppingToken ) ;
await SondearResumenProvincialAsync ( authToken , stoppingToken ) ;
await SondearEstadoRecuentoGeneralAsync ( authToken , stoppingToken ) ;
}
else if ( settings . Prioridad = = "Telegramas" & & settings . BajasActivado )
{
_logger . LogInformation ( "Ejecutando tareas de Baja Prioridad en alta prioridad." ) ;
await SondearProyeccionBancasAsync ( authToken , stoppingToken ) ;
await SondearNuevosTelegramasAsync ( authToken , stoppingToken ) ;
}
else
{
_logger . LogInformation ( "Worker de alta prioridad inactivo según la configuración." ) ;
}
2025-08-20 16:58:18 -03:00
var cicloFin = DateTime . UtcNow ;
var duracionCiclo = cicloFin - cicloInicio ;
_logger . LogInformation ( "--- Ciclo de Datos Críticos #{ciclo} completado en {duration:N2} segundos. ---" , cicloContador , duracionCiclo . TotalSeconds ) ;
var tiempoDeEspera = TimeSpan . FromSeconds ( 30 ) - duracionCiclo ;
if ( tiempoDeEspera < TimeSpan . Zero ) tiempoDeEspera = TimeSpan . Zero ;
try
{
await Task . Delay ( tiempoDeEspera , stoppingToken ) ;
}
2025-08-23 11:01:54 -03:00
catch ( TaskCanceledException ) { break ; }
2025-08-20 16:58:18 -03:00
}
}
2025-09-06 21:44:52 -03:00
/// <summary>
/// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral.
/// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas,
/// guarda la CategoriaId y usa una transacción atómica para la escritura en base de datos.
/// </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 )
{
try
{
using var scope = _serviceProvider . CreateScope ( ) ;
var dbContext = scope . ServiceProvider . GetRequiredService < EleccionesDbContext > ( ) ;
var categoriasDeBancas = await dbContext . CategoriasElectorales
. AsNoTracking ( )
. Where ( c = > c . Nombre . Contains ( "SENADORES" ) | | c . Nombre . Contains ( "DIPUTADOS" ) )
. ToListAsync ( stoppingToken ) ;
var provincia = await dbContext . AmbitosGeograficos
. AsNoTracking ( )
. FirstOrDefaultAsync ( a = > a . NivelId = = 10 , stoppingToken ) ;
var seccionesElectorales = await dbContext . AmbitosGeograficos
. AsNoTracking ( )
. Where ( a = > a . NivelId = = 20 & & a . DistritoId ! = null & & a . SeccionProvincialId ! = null )
. ToListAsync ( stoppingToken ) ;
if ( ! categoriasDeBancas . Any ( ) | | provincia = = null )
{
_logger . LogWarning ( "No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas." ) ;
return ;
}
_logger . LogInformation ( "Iniciando sondeo de Bancas a nivel Provincial y para {count} Secciones Electorales..." , seccionesElectorales . Count ) ;
var todasLasProyecciones = new List < ProyeccionBanca > ( ) ;
bool hasReceivedAnyNewData = false ;
// Bucle para el nivel Provincial
foreach ( var categoria in categoriasDeBancas )
{
if ( stoppingToken . IsCancellationRequested ) break ;
var repartoBancasDto = await _apiService . GetBancasAsync ( authToken , provincia . DistritoId ! , null , categoria . Id ) ;
if ( repartoBancasDto ? . RepartoBancas is { Count : > 0 } bancas )
{
hasReceivedAnyNewData = true ;
// --- SEGURIDAD: Usar TryParse para la fecha ---
DateTime fechaTotalizacion ;
if ( ! DateTime . TryParse ( repartoBancasDto . FechaTotalizacion , out var parsedDate ) )
{
// Si la fecha es inválida (nula, vacía, mal formada), lo registramos y usamos la hora actual como respaldo.
_logger . LogWarning ( "No se pudo parsear FechaTotalizacion ('{dateString}') para bancas provinciales. Usando la hora actual." , repartoBancasDto . FechaTotalizacion ) ;
fechaTotalizacion = DateTime . UtcNow ;
}
else
{
fechaTotalizacion = parsedDate . ToUniversalTime ( ) ;
}
foreach ( var banca in bancas )
{
todasLasProyecciones . Add ( new ProyeccionBanca
{
AmbitoGeograficoId = provincia . Id ,
AgrupacionPoliticaId = banca . IdAgrupacion ,
NroBancas = banca . NroBancas ,
CategoriaId = categoria . Id ,
FechaTotalizacion = fechaTotalizacion
} ) ;
}
}
}
// Bucle para el nivel de Sección Electoral
foreach ( var seccion in seccionesElectorales )
{
if ( stoppingToken . IsCancellationRequested ) break ;
foreach ( var categoria in categoriasDeBancas )
{
if ( stoppingToken . IsCancellationRequested ) break ;
var repartoBancasDto = await _apiService . GetBancasAsync ( authToken , seccion . DistritoId ! , seccion . SeccionProvincialId ! , categoria . Id ) ;
if ( repartoBancasDto ? . RepartoBancas is { Count : > 0 } bancas )
{
hasReceivedAnyNewData = true ;
// --- APLICAMOS LA MISMA SEGURIDAD AQUÍ ---
DateTime fechaTotalizacion ;
if ( ! DateTime . TryParse ( repartoBancasDto . FechaTotalizacion , out var parsedDate ) )
{
_logger . LogWarning ( "No se pudo parsear FechaTotalizacion ('{dateString}') para bancas de sección. Usando la hora actual." , repartoBancasDto . FechaTotalizacion ) ;
fechaTotalizacion = DateTime . UtcNow ;
}
else
{
fechaTotalizacion = parsedDate . ToUniversalTime ( ) ;
}
foreach ( var banca in bancas )
{
todasLasProyecciones . Add ( new ProyeccionBanca
{
AmbitoGeograficoId = seccion . Id ,
AgrupacionPoliticaId = banca . IdAgrupacion ,
NroBancas = banca . NroBancas ,
CategoriaId = categoria . Id ,
FechaTotalizacion = fechaTotalizacion
} ) ;
}
}
}
}
if ( hasReceivedAnyNewData )
{
_logger . LogInformation ( "Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos..." ) ;
await using var transaction = await dbContext . Database . BeginTransactionAsync ( stoppingToken ) ;
await dbContext . Database . ExecuteSqlRawAsync ( "DELETE FROM ProyeccionesBancas" , stoppingToken ) ;
await dbContext . ProyeccionesBancas . AddRangeAsync ( todasLasProyecciones , stoppingToken ) ;
await dbContext . SaveChangesAsync ( stoppingToken ) ;
await transaction . CommitAsync ( stoppingToken ) ;
_logger . LogInformation ( "La tabla de proyecciones ha sido actualizada con {count} registros." , todasLasProyecciones . Count ) ;
}
else
{
_logger . LogInformation ( "Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada." ) ;
}
}
catch ( OperationCanceledException )
{
_logger . LogInformation ( "Sondeo de bancas cancelado." ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Ocurrió un error CRÍTICO en el sondeo de Bancas." ) ;
}
}
/// <summary>
/// Busca y descarga nuevos telegramas de forma masiva y concurrente.
/// Este método crea una lista de todas las combinaciones de Partido/Categoría,
/// las consulta a la API con un grado de paralelismo controlado, y cada tarea concurrente
/// maneja su propia lógica de descarga y guardado en la base de datos.
/// </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 )
{
try
{
_logger . LogInformation ( "--- Iniciando sondeo de Nuevos Telegramas (modo de bajo perfil) ---" ) ;
using var scope = _serviceProvider . CreateScope ( ) ;
var dbContext = scope . ServiceProvider . GetRequiredService < EleccionesDbContext > ( ) ;
var partidos = await dbContext . AmbitosGeograficos
. AsNoTracking ( )
. Where ( a = > a . NivelId = = 30 & & a . DistritoId ! = null & & a . SeccionId ! = null )
. ToListAsync ( stoppingToken ) ;
var categorias = await dbContext . CategoriasElectorales
. AsNoTracking ( )
. ToListAsync ( stoppingToken ) ;
if ( ! partidos . Any ( ) | | ! categorias . Any ( ) ) return ;
foreach ( var partido in partidos )
{
foreach ( var categoria in categorias )
{
if ( stoppingToken . IsCancellationRequested ) return ;
var listaTelegramasApi = await _apiService . GetTelegramasTotalizadosAsync ( authToken , partido . DistritoId ! , partido . SeccionId ! , categoria . Id ) ;
if ( listaTelegramasApi is { Count : > 0 } )
{
using var innerScope = _serviceProvider . CreateScope ( ) ;
var innerDbContext = innerScope . ServiceProvider . GetRequiredService < EleccionesDbContext > ( ) ;
var idsYaEnDb = await innerDbContext . Telegramas
. Where ( t = > listaTelegramasApi . Contains ( t . Id ) )
. Select ( t = > t . Id )
. ToListAsync ( stoppingToken ) ;
var nuevosTelegramasIds = listaTelegramasApi . Except ( idsYaEnDb ) . ToList ( ) ;
if ( nuevosTelegramasIds . Any ( ) )
{
_logger . LogInformation ( "Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando..." , nuevosTelegramasIds . Count , partido . Nombre , categoria . Nombre ) ;
2025-09-08 10:22:51 -03:00
int contadorLote = 0 ;
const int tamanoLote = 100 ; // Guardaremos de 100 en 100
2025-09-06 21:44:52 -03:00
foreach ( var mesaId in nuevosTelegramasIds )
{
if ( stoppingToken . IsCancellationRequested ) return ;
var telegramaFile = await _apiService . GetTelegramaFileAsync ( authToken , mesaId ) ;
if ( telegramaFile ! = null )
{
2025-09-08 10:22:51 -03:00
var ambitoMesa = await innerDbContext . AmbitosGeograficos . AsNoTracking ( )
2025-09-06 21:44:52 -03:00
. FirstOrDefaultAsync ( a = > a . MesaId = = mesaId , stoppingToken ) ;
if ( ambitoMesa ! = null )
{
var nuevoTelegrama = new Telegrama
{
Id = telegramaFile . NombreArchivo ,
// 3. Usamos el ID del ÁMBITO DE LA MESA, no el del municipio.
AmbitoGeograficoId = ambitoMesa . Id ,
ContenidoBase64 = telegramaFile . Imagen ,
FechaEscaneo = DateTime . Parse ( telegramaFile . FechaEscaneo ) . ToUniversalTime ( ) ,
FechaTotalizacion = DateTime . Parse ( telegramaFile . FechaTotalizacion ) . ToUniversalTime ( )
} ;
await innerDbContext . Telegramas . AddAsync ( nuevoTelegrama , stoppingToken ) ;
2025-09-08 10:22:51 -03:00
contadorLote + + ; // Incrementamos el contador
2025-09-06 21:44:52 -03:00
}
else
{
_logger . LogWarning ( "No se encontró un ámbito geográfico para la mesa con MesaId {MesaId}. El telegrama no será guardado." , mesaId ) ;
}
}
2025-09-08 10:22:51 -03:00
await Task . Delay ( 250 , stoppingToken ) ; // Mantenemos el delay para no saturar la API
// Si hemos alcanzado el tamaño del lote, guardamos y reseteamos.
if ( contadorLote > = tamanoLote )
{
await innerDbContext . SaveChangesAsync ( stoppingToken ) ;
_logger . LogInformation ( "Guardado un lote de {count} telegramas." , contadorLote ) ;
contadorLote = 0 ; // Reseteamos el contador para el siguiente lote
}
}
// Guardamos cualquier registro restante que no haya completado un lote completo.
if ( contadorLote > 0 )
{
await innerDbContext . SaveChangesAsync ( stoppingToken ) ;
_logger . LogInformation ( "Guardado el último lote de {count} telegramas." , contadorLote ) ;
2025-09-06 21:44:52 -03:00
}
}
}
await Task . Delay ( 100 , stoppingToken ) ;
}
}
_logger . LogInformation ( "Sondeo de Telegramas completado." ) ;
}
catch ( OperationCanceledException )
{
_logger . LogInformation ( "Sondeo de telegramas cancelado." ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Ocurrió un error CRÍTICO en el sondeo de Telegramas." ) ;
}
}
2025-08-20 16:58:18 -03:00
private async Task SondearResultadosMunicipalesAsync ( string authToken , CancellationToken stoppingToken )
{
try
{
using var scope = _serviceProvider . CreateScope ( ) ;
var dbContext = scope . ServiceProvider . GetRequiredService < EleccionesDbContext > ( ) ;
var municipiosASondear = await dbContext . AmbitosGeograficos
. AsNoTracking ( )
. Where ( a = > a . NivelId = = 30 & & a . DistritoId ! = null & & a . SeccionId ! = null )
. ToListAsync ( stoppingToken ) ;
2025-08-23 11:01:54 -03:00
var todasLasCategorias = await dbContext . CategoriasElectorales
2025-08-20 16:58:18 -03:00
. AsNoTracking ( )
2025-08-23 11:01:54 -03:00
. ToListAsync ( stoppingToken ) ;
2025-08-20 16:58:18 -03:00
2025-08-23 11:01:54 -03:00
if ( ! municipiosASondear . Any ( ) | | ! todasLasCategorias . Any ( ) )
2025-08-20 16:58:18 -03:00
{
2025-08-23 11:01:54 -03:00
_logger . LogWarning ( "No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados." ) ;
2025-08-20 16:58:18 -03:00
return ;
}
2025-08-23 11:01:54 -03:00
_logger . LogInformation ( "Iniciando sondeo de resultados para {m} municipios y {c} categorías..." , municipiosASondear . Count , todasLasCategorias . Count ) ;
2025-08-20 16:58:18 -03:00
2025-08-23 11:01:54 -03:00
foreach ( var municipio in municipiosASondear )
2025-08-20 16:58:18 -03:00
{
2025-08-23 11:01:54 -03:00
if ( stoppingToken . IsCancellationRequested ) break ;
var tareasCategoria = todasLasCategorias . Select ( async categoria = >
2025-08-20 16:58:18 -03:00
{
2025-08-23 11:01:54 -03:00
var resultados = await _apiService . GetResultadosAsync ( authToken , municipio . DistritoId ! , municipio . SeccionId ! , null , categoria . Id ) ;
2025-08-20 16:58:18 -03:00
if ( resultados ! = null )
{
2025-08-23 11:01:54 -03:00
using var innerScope = _serviceProvider . CreateScope ( ) ;
var innerDbContext = innerScope . ServiceProvider . GetRequiredService < EleccionesDbContext > ( ) ;
2025-08-20 16:58:18 -03:00
2025-08-23 11:01:54 -03:00
// --- LLAMADA CORRECTA ---
await GuardarResultadosDeAmbitoAsync ( innerDbContext , municipio . Id , categoria . Id , resultados , stoppingToken ) ;
}
} ) ;
2025-08-20 16:58:18 -03:00
2025-08-23 11:01:54 -03:00
await Task . WhenAll ( tareasCategoria ) ;
2025-08-20 16:58:18 -03:00
}
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Ocurrió un error inesperado durante el sondeo de resultados municipales." ) ;
}
}
2025-08-23 11:01:54 -03:00
private async Task GuardarResultadosDeAmbitoAsync (
EleccionesDbContext dbContext , int ambitoId , int categoriaId ,
Elecciones . Core . DTOs . ResultadosDto resultadosDto , CancellationToken stoppingToken )
2025-08-20 16:58:18 -03:00
{
2025-08-23 11:01:54 -03:00
var estadoRecuento = await dbContext . EstadosRecuentos . FindAsync ( new object [ ] { ambitoId , categoriaId } , stoppingToken ) ;
2025-08-20 16:58:18 -03:00
2025-08-23 11:01:54 -03:00
if ( estadoRecuento = = null )
2025-08-20 16:58:18 -03:00
{
2025-08-23 11:01:54 -03:00
estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId , CategoriaId = categoriaId } ;
dbContext . EstadosRecuentos . Add ( estadoRecuento ) ;
}
2025-08-20 16:58:18 -03:00
2025-08-23 11:01:54 -03:00
estadoRecuento . FechaTotalizacion = DateTime . Parse ( resultadosDto . FechaTotalizacion ) . ToUniversalTime ( ) ;
estadoRecuento . MesasEsperadas = resultadosDto . EstadoRecuento . MesasEsperadas ;
estadoRecuento . MesasTotalizadas = resultadosDto . EstadoRecuento . MesasTotalizadas ;
estadoRecuento . MesasTotalizadasPorcentaje = resultadosDto . EstadoRecuento . MesasTotalizadasPorcentaje ;
estadoRecuento . CantidadElectores = resultadosDto . EstadoRecuento . CantidadElectores ;
estadoRecuento . CantidadVotantes = resultadosDto . EstadoRecuento . CantidadVotantes ;
estadoRecuento . ParticipacionPorcentaje = resultadosDto . EstadoRecuento . ParticipacionPorcentaje ;
2025-08-20 16:58:18 -03:00
2025-08-23 11:01:54 -03:00
if ( resultadosDto . ValoresTotalizadosOtros ! = null )
{
estadoRecuento . VotosEnBlanco = resultadosDto . ValoresTotalizadosOtros . VotosEnBlanco ;
estadoRecuento . VotosEnBlancoPorcentaje = resultadosDto . ValoresTotalizadosOtros . VotosEnBlancoPorcentaje ;
estadoRecuento . VotosNulos = resultadosDto . ValoresTotalizadosOtros . VotosNulos ;
estadoRecuento . VotosNulosPorcentaje = resultadosDto . ValoresTotalizadosOtros . VotosNulosPorcentaje ;
estadoRecuento . VotosRecurridos = resultadosDto . ValoresTotalizadosOtros . VotosRecurridos ;
estadoRecuento . VotosRecurridosPorcentaje = resultadosDto . ValoresTotalizadosOtros . VotosRecurridosPorcentaje ;
}
2025-08-20 16:58:18 -03:00
2025-08-23 11:01:54 -03:00
foreach ( var votoPositivoDto in resultadosDto . ValoresTotalizadosPositivos )
{
var resultadoVoto = await dbContext . ResultadosVotos . FirstOrDefaultAsync (
rv = > rv . AmbitoGeograficoId = = ambitoId & &
rv . CategoriaId = = categoriaId & &
rv . AgrupacionPoliticaId = = votoPositivoDto . IdAgrupacion ,
stoppingToken
) ;
if ( resultadoVoto = = null )
2025-08-20 16:58:18 -03:00
{
2025-08-23 11:01:54 -03:00
resultadoVoto = new ResultadoVoto
2025-08-20 16:58:18 -03:00
{
2025-08-23 11:01:54 -03:00
AmbitoGeograficoId = ambitoId ,
CategoriaId = categoriaId ,
AgrupacionPoliticaId = votoPositivoDto . IdAgrupacion
} ;
dbContext . ResultadosVotos . Add ( resultadoVoto ) ;
2025-08-20 16:58:18 -03:00
}
2025-08-23 11:01:54 -03:00
resultadoVoto . CantidadVotos = votoPositivoDto . Votos ;
resultadoVoto . PorcentajeVotos = votoPositivoDto . VotosPorcentaje ;
2025-08-20 16:58:18 -03:00
}
2025-08-23 11:01:54 -03:00
try
{
await dbContext . SaveChangesAsync ( stoppingToken ) ;
}
catch ( DbUpdateException ex )
{
_logger . LogError ( ex , "DbUpdateException al guardar resultados para AmbitoId {ambitoId} y CategoriaId {categoriaId}" , ambitoId , categoriaId ) ;
}
2025-08-20 16:58:18 -03:00
}
/// <summary>
2025-08-23 13:19:35 -03:00
/// Obtiene y actualiza el resumen de votos y el estado del recuento a nivel provincial.
/// Esta versión actualizada guarda tanto los votos por agrupación (en ResumenesVotos)
/// como el estado general del recuento, incluyendo la fecha de totalización (en EstadosRecuentosGenerales),
/// asegurando que toda la operación sea atómica mediante una transacción de base de datos.
2025-08-20 16:58:18 -03:00
/// </summary>
2025-08-23 13:19:35 -03:00
/// <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>
2025-08-20 16:58:18 -03:00
private async Task SondearResumenProvincialAsync ( string authToken , CancellationToken stoppingToken )
{
try
{
2025-08-23 13:19:35 -03:00
// Creamos un scope de DbContext para esta operación.
2025-08-20 16:58:18 -03:00
using var scope = _serviceProvider . CreateScope ( ) ;
var dbContext = scope . ServiceProvider . GetRequiredService < EleccionesDbContext > ( ) ;
2025-08-23 13:19:35 -03:00
// Obtenemos el registro de la Provincia (NivelId 10).
var provincia = await dbContext . AmbitosGeograficos
. AsNoTracking ( )
. FirstOrDefaultAsync ( a = > a . NivelId = = 10 , stoppingToken ) ;
// Si no encontramos el ámbito de la provincia, no podemos continuar.
if ( provincia = = null )
{
_logger . LogWarning ( "No se encontró el ámbito 'Provincia' (NivelId 10) para el sondeo de resumen." ) ;
return ;
}
2025-08-20 16:58:18 -03:00
2025-08-23 13:19:35 -03:00
// Llamamos a la API para obtener el resumen de datos provincial.
var resumenDto = await _apiService . GetResumenAsync ( authToken , provincia . DistritoId ! ) ;
2025-08-20 16:58:18 -03:00
2025-08-23 13:19:35 -03:00
// Solo procedemos si la API devolvió una respuesta válida y no nula.
if ( resumenDto ! = null )
2025-08-20 16:58:18 -03:00
{
2025-08-23 13:19:35 -03:00
// Iniciamos una transacción explícita. Esto garantiza que todas las operaciones de base de datos
// dentro de este bloque (el DELETE, los INSERTs y los UPDATEs) se completen con éxito,
// o si algo falla, se reviertan todas, manteniendo la consistencia de los datos.
2025-08-20 16:58:18 -03:00
await using var transaction = await dbContext . Database . BeginTransactionAsync ( stoppingToken ) ;
2025-08-23 13:19:35 -03:00
// --- 1. ACTUALIZAR LA TABLA 'ResumenesVotos' ---
// Verificamos si la respuesta contiene una lista de votos positivos.
if ( resumenDto . ValoresTotalizadosPositivos is { Count : > 0 } nuevosVotos )
{
// Estrategia "Borrar y Reemplazar": vaciamos la tabla antes de insertar los nuevos datos.
await dbContext . Database . ExecuteSqlRawAsync ( "DELETE FROM ResumenesVotos" , stoppingToken ) ;
// Añadimos cada nuevo registro de voto al DbContext.
foreach ( var voto in nuevosVotos )
{
dbContext . ResumenesVotos . Add ( new ResumenVoto
{
AmbitoGeograficoId = provincia . Id ,
AgrupacionPoliticaId = voto . IdAgrupacion ,
Votos = voto . Votos ,
VotosPorcentaje = voto . VotosPorcentaje
} ) ;
}
}
// --- 2. ACTUALIZAR LA TABLA 'EstadosRecuentosGenerales' ---
2025-08-20 16:58:18 -03:00
2025-08-23 13:19:35 -03:00
// El endpoint de Resumen no especifica una categoría, por lo que aplicamos sus datos de estado de recuento
// a todas las categorías que tenemos en nuestra base de datos.
var todasLasCategorias = await dbContext . CategoriasElectorales . AsNoTracking ( ) . ToListAsync ( stoppingToken ) ;
foreach ( var categoria in todasLasCategorias )
2025-08-20 16:58:18 -03:00
{
2025-08-23 13:19:35 -03:00
// Buscamos el registro existente usando la clave primaria compuesta.
var registroDb = await dbContext . EstadosRecuentosGenerales . FindAsync ( new object [ ] { provincia . Id , categoria . Id } , stoppingToken ) ;
// Si no existe, lo creamos.
if ( registroDb = = null )
{
registroDb = new EstadoRecuentoGeneral { AmbitoGeograficoId = provincia . Id , CategoriaId = categoria . Id } ;
dbContext . EstadosRecuentosGenerales . Add ( registroDb ) ;
}
// Parseamos la fecha de forma segura para evitar errores con cadenas vacías o nulas.
if ( DateTime . TryParse ( resumenDto . FechaTotalizacion , out var parsedDate ) )
2025-08-20 16:58:18 -03:00
{
2025-08-23 13:19:35 -03:00
registroDb . FechaTotalizacion = parsedDate . ToUniversalTime ( ) ;
}
// Mapeamos el resto de los datos del estado del recuento.
registroDb . MesasEsperadas = resumenDto . EstadoRecuento . MesasEsperadas ;
registroDb . MesasTotalizadas = resumenDto . EstadoRecuento . MesasTotalizadas ;
registroDb . MesasTotalizadasPorcentaje = resumenDto . EstadoRecuento . MesasTotalizadasPorcentaje ;
registroDb . CantidadElectores = resumenDto . EstadoRecuento . CantidadElectores ;
registroDb . CantidadVotantes = resumenDto . EstadoRecuento . CantidadVotantes ;
registroDb . ParticipacionPorcentaje = resumenDto . EstadoRecuento . ParticipacionPorcentaje ;
2025-08-20 16:58:18 -03:00
}
2025-08-23 13:19:35 -03:00
// 3. CONFIRMAR Y GUARDAR
// Guardamos todos los cambios preparados (DELETEs, INSERTs, UPDATEs) en la base de datos.
2025-08-20 16:58:18 -03:00
await dbContext . SaveChangesAsync ( stoppingToken ) ;
2025-08-23 13:19:35 -03:00
// Confirmamos la transacción para hacer los cambios permanentes.
2025-08-20 16:58:18 -03:00
await transaction . CommitAsync ( stoppingToken ) ;
2025-08-23 13:19:35 -03:00
_logger . LogInformation ( "Sondeo de Resumen Provincial completado. Las tablas han sido actualizadas." ) ;
2025-08-20 16:58:18 -03:00
}
else
{
2025-08-23 13:19:35 -03:00
// Si la API no devolvió datos (ej. devuelve null), no hacemos nada en la BD.
_logger . LogInformation ( "Sondeo de Resumen Provincial completado. No se recibieron datos nuevos." ) ;
2025-08-20 16:58:18 -03:00
}
}
2025-08-23 13:19:35 -03:00
catch ( OperationCanceledException )
{
_logger . LogInformation ( "Sondeo de resumen provincial cancelado." ) ;
}
2025-08-20 16:58:18 -03:00
catch ( Exception ex )
{
2025-08-23 13:19:35 -03:00
// Capturamos cualquier otro error inesperado para que el worker no se detenga.
_logger . LogError ( ex , "Ocurrió un error CRÍTICO en el sondeo de Resumen Provincial." ) ;
2025-08-20 16:58:18 -03:00
}
}
/// <summary>
/// Obtiene y actualiza el estado general del recuento a nivel provincial para CADA categoría electoral.
/// Esta versión es robusta: consulta dinámicamente las categorías, usa la clave primaria compuesta
/// de la base de datos y guarda todos los cambios en una única transacción al final.
/// </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 SondearEstadoRecuentoGeneralAsync ( string authToken , CancellationToken stoppingToken )
{
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 ) ;
// 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 ;
}
// 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 ( ) )
{
_logger . LogWarning ( "No hay categorías en la BD para sondear el estado general del recuento." ) ;
return ;
}
_logger . LogInformation ( "Iniciando sondeo de Estado Recuento General para {count} categorías..." , categoriasParaSondear . Count ) ;
// PASO 4: Iterar sobre cada categoría para obtener su estado de recuento individual.
foreach ( var categoria in categoriasParaSondear )
{
// 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 )
{
// 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
) ;
// Si no se encuentra (FindAsync devuelve null), es un registro nuevo.
if ( registroDb = = null )
{
// Creamos una nueva instancia de la entidad.
registroDb = new EstadoRecuentoGeneral
{
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 ) ;
}
// 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." ) ;
}
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." ) ;
}
}
}