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

812 lines
36 KiB
C#
Raw Normal View History

//Elecciones.Worker/LowPriorityDataWorker.cs
2025-08-20 16:58:18 -03:00
using Elecciones.Database;
using Elecciones.Database.Entities;
using Elecciones.Infrastructure.Services;
using Microsoft.EntityFrameworkCore;
namespace Elecciones.Worker;
public class LowPriorityDataWorker : BackgroundService
{
private readonly ILogger<LowPriorityDataWorker> _logger;
private readonly SharedTokenService _tokenService;
private readonly IServiceProvider _serviceProvider;
private readonly IElectoralApiService _apiService;
private readonly WorkerConfigService _configService;
2025-08-20 16:58:18 -03:00
// Una variable para rastrear la tarea de telegramas, si está en ejecución.
private Task? _telegramasTask;
2025-08-20 16:58:18 -03:00
public LowPriorityDataWorker(
ILogger<LowPriorityDataWorker> logger,
SharedTokenService tokenService,
IServiceProvider serviceProvider,
IElectoralApiService apiService,
WorkerConfigService configService)
2025-08-20 16:58:18 -03:00
{
_logger = logger;
_tokenService = tokenService;
_serviceProvider = serviceProvider;
_apiService = apiService;
_configService = configService;
2025-08-20 16:58:18 -03:00
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker de Baja Prioridad iniciado.");
// La sincronización inicial sigue siendo un paso de bloqueo, es necesario.
2025-08-20 16:58:18 -03:00
await SincronizarCatalogosMaestrosAsync(stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("--- Iniciando Ciclo de Datos de Baja Prioridad ---");
var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);
if (string.IsNullOrEmpty(authToken))
{
_logger.LogError("Ciclo de Baja Prioridad: No se pudo obtener token. Reintentando en 1 minuto.");
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
continue;
}
var settings = await _configService.GetSettingsAsync();
if (settings.Prioridad == "Telegramas" && settings.ResultadosActivado)
{
_logger.LogInformation("Ejecutando tareas de Resultados en baja prioridad.");
await SondearResultadosMunicipalesAsync(authToken, stoppingToken);
await SondearResumenProvincialAsync(authToken, stoppingToken);
await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
}
else if (settings.Prioridad == "Resultados" && settings.BajasActivado)
{
_logger.LogInformation("Ejecutando tareas de Baja Prioridad en baja prioridad.");
await SondearProyeccionBancasAsync(authToken, stoppingToken);
await SondearNuevosTelegramasAsync(authToken, stoppingToken);
}
else
{
_logger.LogInformation("Worker de baja prioridad inactivo según la configuración.");
}
2025-08-20 16:58:18 -03:00
try
{
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
catch (TaskCanceledException)
{
break;
}
}
}
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);
var todasLasCategorias = await dbContext.CategoriasElectorales
.AsNoTracking()
.ToListAsync(stoppingToken);
if (!municipiosASondear.Any() || !todasLasCategorias.Any())
{
_logger.LogWarning("No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados.");
return;
}
_logger.LogInformation("Iniciando sondeo de resultados para {m} municipios y {c} categorías...", municipiosASondear.Count, todasLasCategorias.Count);
foreach (var municipio in municipiosASondear)
{
if (stoppingToken.IsCancellationRequested) break;
var tareasCategoria = todasLasCategorias.Select(async categoria =>
{
var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoria.Id);
if (resultados != null)
{
using var innerScope = _serviceProvider.CreateScope();
var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
// --- LLAMADA CORRECTA ---
await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken);
}
});
await Task.WhenAll(tareasCategoria);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales.");
}
}
private async Task GuardarResultadosDeAmbitoAsync(
EleccionesDbContext dbContext, int ambitoId, int categoriaId,
Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken)
{
var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken);
if (estadoRecuento == null)
{
estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId, CategoriaId = categoriaId };
dbContext.EstadosRecuentos.Add(estadoRecuento);
}
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;
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-10-15 11:44:22 -03:00
estadoRecuento.VotosComando = resultadosDto.ValoresTotalizadosOtros.VotosComando;
estadoRecuento.VotosComandoPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosComandoPorcentaje;
estadoRecuento.VotosImpugnados = resultadosDto.ValoresTotalizadosOtros.VotosImpugnados;
estadoRecuento.VotosImpugnadosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosImpugnadosPorcentaje;
}
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)
{
resultadoVoto = new ResultadoVoto
{
AmbitoGeograficoId = ambitoId,
CategoriaId = categoriaId,
AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion
};
dbContext.ResultadosVotos.Add(resultadoVoto);
}
resultadoVoto.CantidadVotos = votoPositivoDto.Votos;
resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje;
}
try
{
await dbContext.SaveChangesAsync(stoppingToken);
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "DbUpdateException al guardar resultados para AmbitoId {ambitoId} y CategoriaId {categoriaId}", ambitoId, categoriaId);
}
}
/// <summary>
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
/// Obtiene y actualiza el resumen de votos y el estado del recuento a nivel provincial para CADA categoría.
/// Este método itera sobre todas las provincias y categorías, obteniendo sus resultados consolidados
/// y guardándolos en las tablas 'ResumenesVotos' y 'EstadosRecuentosGenerales'.
/// </summary>
private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken)
{
try
{
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
_logger.LogInformation("Iniciando sondeo de Resúmenes Provinciales...");
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
var provinciasASondear = await dbContext.AmbitosGeograficos
.AsNoTracking()
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
.Where(a => a.NivelId == 10 && a.DistritoId != null)
.ToListAsync(stoppingToken);
var todasLasCategorias = await dbContext.CategoriasElectorales
.AsNoTracking()
.ToListAsync(stoppingToken);
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
if (!provinciasASondear.Any() || !todasLasCategorias.Any())
{
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
_logger.LogWarning("No se encontraron Provincias o Categorías para sondear resúmenes.");
return;
}
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
foreach (var provincia in provinciasASondear)
{
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
if (stoppingToken.IsCancellationRequested) break;
foreach (var categoria in todasLasCategorias)
{
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
if (stoppingToken.IsCancellationRequested) break;
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
// Usamos GetResultados sin seccionId/municipioId para obtener el resumen del distrito.
var resultadosDto = await _apiService.GetResultadosAsync(authToken, provincia.DistritoId!, null, null, categoria.Id);
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
if (resultadosDto?.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos)
{
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
// Usamos una transacción para asegurar que el borrado y la inserción sean atómicos.
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
// A. Borrar los resúmenes viejos SOLO para esta provincia y categoría.
await dbContext.ResumenesVotos
.Where(rv => rv.AmbitoGeograficoId == provincia.Id && rv.CategoriaId == categoria.Id)
.ExecuteDeleteAsync(stoppingToken);
// B. Añadir los nuevos resúmenes.
foreach (var voto in nuevosVotos)
{
dbContext.ResumenesVotos.Add(new ResumenVoto
{
AmbitoGeograficoId = provincia.Id,
CategoriaId = categoria.Id,
AgrupacionPoliticaId = voto.IdAgrupacion,
Votos = voto.Votos,
VotosPorcentaje = voto.VotosPorcentaje
});
}
// C. Guardar los cambios en la tabla ResumenesVotos.
await dbContext.SaveChangesAsync(stoppingToken);
// No es necesario actualizar EstadosRecuentosGenerales aquí,
// ya que el método SondearEstadoRecuentoGeneralAsync se encarga de eso
// de forma más específica y eficiente.
await transaction.CommitAsync(stoppingToken);
}
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
} // Fin bucle categorías
} // Fin bucle provincias
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
_logger.LogInformation("Sondeo de Resúmenes Provinciales completado.");
}
catch (OperationCanceledException)
{
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
_logger.LogInformation("Sondeo de resúmenes provinciales cancelado.");
}
catch (Exception ex)
{
feat(Worker): Adaptación integral para la API de Elecciones Nacionales Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito. Cambios principales: - **Refactorización de `SondearResumenProvincialAsync`:** - Se elimina la dependencia del endpoint obsoleto `/getResumen`. - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados. - **Expansión de `SondearResultadosMunicipalesAsync`:** - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad. - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios. - **Modificación del Modelo de Datos:** - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`. - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio. - **Ajustes de Nulabilidad en API Service:** - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625. - **Deshabilitación de Seeders de Ejemplo:** - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Resúmenes Provinciales.");
}
}
/// <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.");
}
}
2025-08-20 16:58:18 -03:00
/// <summary>
/// Descarga y sincroniza los catálogos base (Categorías, Ámbitos, Agrupaciones)
/// desde la API a la base de datos local. Se ejecuta una sola vez al iniciar el worker.
/// Utiliza una estrategia de guardado en lotes para manejar grandes volúmenes de datos
/// sin sobrecargar la base de datos.
/// </summary>
/// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingToken)
{
try
{
_logger.LogInformation("Iniciando sincronización de catálogos maestros...");
// --- CORRECCIÓN: Usar el _tokenService inyectado ---
var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);
if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested)
{
_logger.LogError("No se pudo obtener token para la sincronización de catálogos. La operación se cancela.");
return;
}
// Creamos un scope de servicios para obtener una instancia fresca de DbContext.
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
// PASO 2: Sincronizar las categorías electorales.
// Es un catálogo pequeño y es la base para las siguientes consultas.
var categoriasApi = await _apiService.GetCategoriasAsync(authToken);
if (categoriasApi is null || !categoriasApi.Any())
{
_logger.LogWarning("La API no devolvió datos para el catálogo de Categorías. La sincronización no puede continuar.");
return;
}
var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList();
_logger.LogInformation("Se procesarán {count} categorías electorales.", distinctCategorias.Count);
var categoriasEnDb = await dbContext.CategoriasElectorales.ToDictionaryAsync(c => c.Id, c => c, stoppingToken);
foreach (var categoriaDto in distinctCategorias)
{
if (!categoriasEnDb.ContainsKey(categoriaDto.CategoriaId))
{
dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden });
}
}
// Guardamos las categorías primero para asegurar su existencia.
await dbContext.SaveChangesAsync(stoppingToken);
// PASO 3: Cargar los catálogos existentes en memoria para una comparación eficiente.
// Esto evita hacer miles de consultas a la BD dentro de un bucle.
// Para los ámbitos, creamos una clave única robusta que funciona incluso con campos nulos.
var ambitosEnDb = new Dictionary<string, AmbitoGeografico>();
var todosLosAmbitos = await dbContext.AmbitosGeograficos.ToListAsync(stoppingToken);
foreach (var ambito in todosLosAmbitos)
{
string clave = $"{ambito.NivelId}|{ambito.DistritoId}|{ambito.SeccionProvincialId}|{ambito.SeccionId}|{ambito.MunicipioId}|{ambito.CircuitoId}|{ambito.EstablecimientoId}|{ambito.MesaId}";
if (!ambitosEnDb.ContainsKey(clave))
{
ambitosEnDb.Add(clave, ambito);
}
}
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
// Variable para llevar la cuenta del total de registros insertados.
int totalCambiosGuardados = 0;
// PASO 4: Iterar sobre cada categoría para sincronizar sus ámbitos y agrupaciones.
foreach (var categoria in distinctCategorias)
{
if (stoppingToken.IsCancellationRequested) break;
_logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId);
var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId);
if (catalogoDto != null)
{
// 4.1 - Procesar y añadir ÁMBITOS nuevos al DbContext
foreach (var ambitoDto in catalogoDto.Ambitos)
{
string claveUnica = $"{ambitoDto.NivelId}|{ambitoDto.CodigoAmbitos.DistritoId}|{ambitoDto.CodigoAmbitos.SeccionProvincialId}|{ambitoDto.CodigoAmbitos.SeccionId}|{ambitoDto.CodigoAmbitos.MunicipioId}|{ambitoDto.CodigoAmbitos.CircuitoId}|{ambitoDto.CodigoAmbitos.EstablecimientoId}|{ambitoDto.CodigoAmbitos.MesaId}";
if (!ambitosEnDb.ContainsKey(claveUnica))
{
var nuevoAmbito = new AmbitoGeografico
{
Nombre = ambitoDto.Nombre,
NivelId = ambitoDto.NivelId,
DistritoId = ambitoDto.CodigoAmbitos.DistritoId,
SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId,
SeccionId = ambitoDto.CodigoAmbitos.SeccionId,
MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId,
CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId,
EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId,
MesaId = ambitoDto.CodigoAmbitos.MesaId,
};
dbContext.AmbitosGeograficos.Add(nuevoAmbito);
ambitosEnDb.Add(claveUnica, nuevoAmbito); // Añadir también al diccionario en memoria
}
}
// 4.2 - Procesar y añadir AGRUPACIONES nuevas al DbContext
var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10);
if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId))
{
// Usamos un try-catch porque no todas las categorías tienen agrupaciones a nivel provincial.
try
{
var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId);
if (agrupacionesApi != null && agrupacionesApi.Any())
{
foreach (var agrupacionDto in agrupacionesApi)
{
if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
{
var nuevaAgrupacion = new AgrupacionPolitica
{
Id = agrupacionDto.IdAgrupacion,
IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
Nombre = agrupacionDto.NombreAgrupacion
};
dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}).", categoria.Nombre, categoria.CategoriaId);
}
}
}
// Después de procesar todos los ámbitos y agrupaciones de UNA categoría, guardamos los cambios.
// Esto divide la inserción masiva de ~50,000 registros en 3 transacciones más pequeñas,
// evitando timeouts y fallos en la base de datos.
if (dbContext.ChangeTracker.HasChanges())
{
int cambiosEnLote = await dbContext.SaveChangesAsync(stoppingToken);
totalCambiosGuardados += cambiosEnLote;
_logger.LogInformation("Guardados {count} registros de catálogo para la categoría '{catNombre}'.", cambiosEnLote, categoria.Nombre);
}
}
// Ya no hay un SaveChangesAsync() gigante aquí.
_logger.LogInformation("{count} nuevos registros de catálogo han sido guardados en total.", totalCambiosGuardados);
_logger.LogInformation("Sincronización de catálogos maestros finalizada.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos.");
}
}
/// <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.
2025-08-23 12:27:27 -03:00
/// </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>
2025-08-20 16:58:18 -03:00
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)
{
2025-08-20 17:51:26 -03:00
_logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas.");
2025-08-20 16:58:18 -03:00
return;
}
_logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial y para {count} Secciones Electorales...", seccionesElectorales.Count);
2025-08-20 17:38:51 -03:00
var todasLasProyecciones = new List<ProyeccionBanca>();
2025-08-20 17:51:26 -03:00
bool hasReceivedAnyNewData = false;
2025-08-20 16:58:18 -03:00
2025-08-20 17:51:26 -03:00
// Bucle para el nivel Provincial
2025-08-20 16:58:18 -03:00
foreach (var categoria in categoriasDeBancas)
{
if (stoppingToken.IsCancellationRequested) break;
var repartoBancasDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id);
2025-08-20 17:51:26 -03:00
if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
2025-08-20 17:51:26 -03:00
{
2025-08-23 12:27:27 -03:00
hasReceivedAnyNewData = true;
2025-08-20 17:51:26 -03:00
// --- 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();
}
2025-08-23 12:27:27 -03:00
foreach (var banca in bancas)
2025-08-20 16:58:18 -03:00
{
2025-08-20 17:38:51 -03:00
todasLasProyecciones.Add(new ProyeccionBanca
2025-08-20 16:58:18 -03:00
{
AmbitoGeograficoId = provincia.Id,
AgrupacionPoliticaId = banca.IdAgrupacion,
2025-08-23 12:27:27 -03:00
NroBancas = banca.NroBancas,
CategoriaId = categoria.Id,
FechaTotalizacion = fechaTotalizacion
2025-08-20 16:58:18 -03:00
});
}
}
}
2025-08-20 17:51:26 -03:00
// Bucle para el nivel de Sección Electoral
2025-08-20 16:58:18 -03:00
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);
2025-08-20 16:58:18 -03:00
if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
2025-08-20 16:58:18 -03:00
{
2025-08-20 17:38:51 -03:00
hasReceivedAnyNewData = true;
// --- APLICAMOS 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();
}
2025-08-23 12:27:27 -03:00
foreach (var banca in bancas)
2025-08-20 16:58:18 -03:00
{
2025-08-20 17:38:51 -03:00
todasLasProyecciones.Add(new ProyeccionBanca
2025-08-20 16:58:18 -03:00
{
AmbitoGeograficoId = seccion.Id,
AgrupacionPoliticaId = banca.IdAgrupacion,
2025-08-23 12:27:27 -03:00
NroBancas = banca.NroBancas,
CategoriaId = categoria.Id,
FechaTotalizacion = fechaTotalizacion
2025-08-20 16:58:18 -03:00
});
}
}
}
}
2025-08-20 17:38:51 -03:00
if (hasReceivedAnyNewData)
2025-08-20 16:58:18 -03:00
{
2025-08-20 17:51:26 -03:00
_logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos...");
2025-08-20 16:58:18 -03:00
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
2025-08-20 17:38:51 -03:00
await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken);
2025-08-20 16:58:18 -03:00
await dbContext.SaveChangesAsync(stoppingToken);
await transaction.CommitAsync(stoppingToken);
2025-08-20 17:51:26 -03:00
_logger.LogInformation("La tabla de proyecciones ha sido actualizada con {count} registros.", todasLasProyecciones.Count);
2025-08-20 16:58:18 -03:00
}
else
{
2025-08-20 17:38:51 -03:00
_logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada.");
2025-08-20 16:58:18 -03:00
}
}
2025-08-20 17:51:26 -03:00
catch (OperationCanceledException)
{
_logger.LogInformation("Sondeo de bancas cancelado.");
}
2025-08-20 16:58:18 -03:00
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>();
2025-09-08 14:19:16 -03:00
// La obtención de partidos y categorías no cambia
var partidos = await dbContext.AmbitosGeograficos.AsNoTracking()
2025-08-20 16:58:18 -03:00
.Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
.ToListAsync(stoppingToken);
2025-09-08 14:19:16 -03:00
var categorias = await dbContext.CategoriasElectorales.AsNoTracking().ToListAsync(stoppingToken);
2025-08-20 16:58:18 -03:00
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 })
{
2025-09-08 14:19:16 -03:00
// Creamos el DbContext para la operación de guardado
2025-08-20 16:58:18 -03:00
using var innerScope = _serviceProvider.CreateScope();
var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
var idsYaEnDb = await innerDbContext.Telegramas
.Where(t => listaTelegramasApi.Contains(t.Id))
2025-09-08 14:19:16 -03:00
.Select(t => t.Id).ToListAsync(stoppingToken);
2025-08-20 16:58:18 -03:00
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 14:19:16 -03:00
var originalTimeout = innerDbContext.Database.GetCommandTimeout();
try
2025-08-20 16:58:18 -03:00
{
2025-09-08 14:19:16 -03:00
innerDbContext.Database.SetCommandTimeout(180);
_logger.LogDebug("Timeout de BD aumentado a 180s para descarga de telegramas.");
int contadorLote = 0;
const int tamanoLote = 100;
2025-08-20 16:58:18 -03:00
2025-09-08 14:19:16 -03:00
foreach (var mesaId in nuevosTelegramasIds)
2025-08-20 16:58:18 -03:00
{
2025-09-08 14:19:16 -03:00
if (stoppingToken.IsCancellationRequested) return;
2025-09-08 14:19:16 -03:00
var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId);
if (telegramaFile != null)
{
2025-09-08 14:19:16 -03:00
var ambitoMesa = await innerDbContext.AmbitosGeograficos.AsNoTracking()
.FirstOrDefaultAsync(a => a.MesaId == mesaId, stoppingToken);
if (ambitoMesa != null)
{
2025-09-08 14:19:16 -03:00
var nuevoTelegrama = new Telegrama
{
Id = telegramaFile.NombreArchivo,
AmbitoGeograficoId = ambitoMesa.Id,
ContenidoBase64 = telegramaFile.Imagen,
FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(),
FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime()
};
await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken);
contadorLote++;
}
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 14:19:16 -03:00
await Task.Delay(250, stoppingToken);
if (contadorLote >= tamanoLote)
2025-08-20 16:58:18 -03:00
{
2025-09-08 14:19:16 -03:00
await innerDbContext.SaveChangesAsync(stoppingToken);
_logger.LogInformation("Guardado un lote de {count} telegramas.", contadorLote);
contadorLote = 0;
}
2025-08-20 16:58:18 -03:00
}
2025-09-07 23:04:36 -03:00
2025-09-08 14:19:16 -03:00
if (contadorLote > 0)
2025-09-07 23:04:36 -03:00
{
await innerDbContext.SaveChangesAsync(stoppingToken);
2025-09-08 14:19:16 -03:00
_logger.LogInformation("Guardado el último lote de {count} telegramas.", contadorLote);
2025-09-07 23:04:36 -03:00
}
}
2025-09-08 14:19:16 -03:00
finally
2025-09-07 23:04:36 -03:00
{
2025-09-08 14:19:16 -03:00
innerDbContext.Database.SetCommandTimeout(originalTimeout);
_logger.LogDebug("Timeout de BD restaurado a su valor original ({timeout}s).", originalTimeout);
2025-08-20 16:58:18 -03:00
}
2025-09-08 14:19:16 -03:00
} // Fin del if (nuevosTelegramasIds.Any())
// Movemos el delay aquí para que solo se ejecute si hubo telegramas en la respuesta de la API
await Task.Delay(100, stoppingToken);
} // Fin del if (listaTelegramasApi is not null)
2025-08-20 16:58:18 -03:00
}
}
_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.");
}
}
}