Files
Elecciones-2025/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs

684 lines
30 KiB
C#
Raw Normal View History

//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;
private readonly WorkerConfigService _configService;
2025-08-20 16:58:18 -03:00
public CriticalDataWorker(
ILogger<CriticalDataWorker> logger,
SharedTokenService tokenService,
IServiceProvider serviceProvider,
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;
_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;
}
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
}
}
/// <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;
2025-09-08 10:53:53 -03:00
const int tamanoLote = 5; // Guardaremos de 100 en 100
2025-09-08 10:22:51 -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()
.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
}
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);
}
}
}
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.");
}
}
}