- Se remueve la iteración sobre distritos. Se consulta solo por categorías electorales.
814 lines
36 KiB
C#
814 lines
36 KiB
C#
//Elecciones.Worker/LowPriorityDataWorker.cs
|
|
using Elecciones.Database;
|
|
using Elecciones.Database.Entities;
|
|
using Elecciones.Infrastructure.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Elecciones.Worker;
|
|
|
|
public class LowPriorityDataWorker : BackgroundService
|
|
{
|
|
private const int EleccionId = 2;
|
|
private readonly ILogger<LowPriorityDataWorker> _logger;
|
|
private readonly SharedTokenService _tokenService;
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private readonly IElectoralApiService _apiService;
|
|
private readonly WorkerConfigService _configService;
|
|
|
|
// Una variable para rastrear la tarea de telegramas, si está en ejecución.
|
|
private Task? _telegramasTask;
|
|
|
|
public LowPriorityDataWorker(
|
|
ILogger<LowPriorityDataWorker> logger,
|
|
SharedTokenService tokenService,
|
|
IServiceProvider serviceProvider,
|
|
IElectoralApiService apiService,
|
|
WorkerConfigService configService)
|
|
{
|
|
_logger = logger;
|
|
_tokenService = tokenService;
|
|
_serviceProvider = serviceProvider;
|
|
_apiService = apiService;
|
|
_configService = configService;
|
|
}
|
|
|
|
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.
|
|
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 SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
|
|
await SondearResultadosMunicipalesAsync(authToken, stoppingToken);
|
|
await SondearResumenProvincialAsync(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.");
|
|
}
|
|
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>();
|
|
|
|
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 { EleccionId = EleccionId, 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;
|
|
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)
|
|
{
|
|
// PASO 1: VERIFICAR SI LA AGRUPACIÓN YA EXISTE EN NUESTRA BD
|
|
var agrupacion = await dbContext.AgrupacionesPoliticas.FindAsync(votoPositivoDto.IdAgrupacion);
|
|
|
|
// PASO 2: SI NO EXISTE, LA CREAMOS "SOBRE LA MARCHA"
|
|
if (agrupacion == null)
|
|
{
|
|
_logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada en el catálogo local. Creándola desde los datos de resultados.",
|
|
votoPositivoDto.IdAgrupacion, votoPositivoDto.NombreAgrupacion);
|
|
|
|
agrupacion = new AgrupacionPolitica
|
|
{
|
|
Id = votoPositivoDto.IdAgrupacion,
|
|
Nombre = votoPositivoDto.NombreAgrupacion,
|
|
// El IdTelegrama puede ser nulo, usamos el operador '??' para asignar un string vacío si es así.
|
|
IdTelegrama = votoPositivoDto.IdAgrupacionTelegrama ?? string.Empty
|
|
};
|
|
await dbContext.AgrupacionesPoliticas.AddAsync(agrupacion, stoppingToken);
|
|
// No es necesario llamar a SaveChangesAsync aquí, se hará al final.
|
|
}
|
|
|
|
// PASO 3: CONTINUAR CON LA LÓGICA DE GUARDADO DEL VOTO
|
|
var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync(
|
|
rv => rv.AmbitoGeograficoId == ambitoId &&
|
|
rv.CategoriaId == categoriaId &&
|
|
rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion,
|
|
stoppingToken
|
|
);
|
|
|
|
if (resultadoVoto == null)
|
|
{
|
|
resultadoVoto = new ResultadoVoto
|
|
{
|
|
EleccionId = EleccionId,
|
|
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>
|
|
/// 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
|
|
{
|
|
_logger.LogInformation("Iniciando sondeo de Resúmenes Provinciales...");
|
|
|
|
using var scope = _serviceProvider.CreateScope();
|
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
|
|
|
var provinciasASondear = await dbContext.AmbitosGeograficos
|
|
.AsNoTracking()
|
|
.Where(a => a.NivelId == 10 && a.DistritoId != null)
|
|
.ToListAsync(stoppingToken);
|
|
|
|
var todasLasCategorias = await dbContext.CategoriasElectorales
|
|
.AsNoTracking()
|
|
.ToListAsync(stoppingToken);
|
|
|
|
if (!provinciasASondear.Any() || !todasLasCategorias.Any())
|
|
{
|
|
_logger.LogWarning("No se encontraron Provincias o Categorías para sondear resúmenes.");
|
|
return;
|
|
}
|
|
|
|
foreach (var provincia in provinciasASondear)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
|
|
foreach (var categoria in todasLasCategorias)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
|
|
// Usamos GetResultados sin seccionId/municipioId para obtener el resumen del distrito.
|
|
var resultadosDto = await _apiService.GetResultadosAsync(authToken, provincia.DistritoId!, null, null, categoria.Id);
|
|
|
|
if (resultadosDto?.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos)
|
|
{
|
|
// 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
|
|
{
|
|
EleccionId = EleccionId,
|
|
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);
|
|
}
|
|
} // Fin bucle categorías
|
|
} // Fin bucle provincias
|
|
|
|
_logger.LogInformation("Sondeo de Resúmenes Provinciales completado.");
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("Sondeo de resúmenes provinciales cancelado.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_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
|
|
{
|
|
using var scope = _serviceProvider.CreateScope();
|
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
|
|
|
var provinciasASondear = await dbContext.AmbitosGeograficos
|
|
.AsNoTracking()
|
|
.Where(a => a.NivelId == 10 && a.DistritoId != null)
|
|
.ToListAsync(stoppingToken);
|
|
|
|
// Busca NivelId 1 (País) o 0 como fallback.
|
|
var ambitoNacional = await dbContext.AmbitosGeograficos
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(a => a.NivelId == 1 || a.NivelId == 0, stoppingToken);
|
|
|
|
var categoriasParaSondear = await dbContext.CategoriasElectorales
|
|
.AsNoTracking()
|
|
.ToListAsync(stoppingToken);
|
|
|
|
if (!provinciasASondear.Any() || !categoriasParaSondear.Any())
|
|
{
|
|
_logger.LogWarning("No se encontraron Provincias o Categorías para sondear estado general.");
|
|
return;
|
|
}
|
|
|
|
_logger.LogInformation("Iniciando sondeo de Estado Recuento General para {provCount} provincias, el total nacional y {catCount} categorías...", provinciasASondear.Count, categoriasParaSondear.Count);
|
|
|
|
// Sondeo a nivel provincial
|
|
foreach (var provincia in provinciasASondear)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
foreach (var categoria in categoriasParaSondear)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
|
|
var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id);
|
|
if (estadoDto != null)
|
|
{
|
|
var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { provincia.Id, categoria.Id }, stoppingToken);
|
|
if (registroDb == null)
|
|
{
|
|
registroDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id };
|
|
dbContext.EstadosRecuentosGenerales.Add(registroDb);
|
|
}
|
|
registroDb.FechaTotalizacion = DateTime.UtcNow;
|
|
registroDb.MesasEsperadas = estadoDto.MesasEsperadas;
|
|
registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas;
|
|
registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje;
|
|
registroDb.CantidadElectores = estadoDto.CantidadElectores;
|
|
registroDb.CantidadVotantes = estadoDto.CantidadVotantes;
|
|
registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bloque para el sondeo a nivel nacional
|
|
if (ambitoNacional != null && !stoppingToken.IsCancellationRequested)
|
|
{
|
|
_logger.LogInformation("Sondeando totales a nivel Nacional (Ambito ID: {ambitoId})...", ambitoNacional.Id);
|
|
foreach (var categoria in categoriasParaSondear)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
|
|
var estadoNacionalDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, "", categoria.Id);
|
|
|
|
if (estadoNacionalDto != null)
|
|
{
|
|
var registroNacionalDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { ambitoNacional.Id, categoria.Id }, stoppingToken);
|
|
if (registroNacionalDb == null)
|
|
{
|
|
registroNacionalDb = new EstadoRecuentoGeneral { EleccionId = EleccionId, AmbitoGeograficoId = ambitoNacional.Id, CategoriaId = categoria.Id };
|
|
dbContext.EstadosRecuentosGenerales.Add(registroNacionalDb);
|
|
}
|
|
registroNacionalDb.FechaTotalizacion = DateTime.UtcNow;
|
|
registroNacionalDb.MesasEsperadas = estadoNacionalDto.MesasEsperadas;
|
|
registroNacionalDb.MesasTotalizadas = estadoNacionalDto.MesasTotalizadas;
|
|
registroNacionalDb.MesasTotalizadasPorcentaje = estadoNacionalDto.MesasTotalizadasPorcentaje;
|
|
registroNacionalDb.CantidadElectores = estadoNacionalDto.CantidadElectores;
|
|
registroNacionalDb.CantidadVotantes = estadoNacionalDto.CantidadVotantes;
|
|
registroNacionalDb.ParticipacionPorcentaje = estadoNacionalDto.ParticipacionPorcentaje;
|
|
_logger.LogInformation("Datos nacionales para categoría '{catNombre}' actualizados.", categoria.Nombre);
|
|
}
|
|
}
|
|
}
|
|
else if (ambitoNacional == null)
|
|
{
|
|
_logger.LogWarning("No se encontró el ámbito geográfico para el Nivel Nacional (NivelId 1 o 0). No se pueden capturar los totales del país.");
|
|
}
|
|
|
|
// Guardar todos los cambios
|
|
if (dbContext.ChangeTracker.HasChanges())
|
|
{
|
|
await dbContext.SaveChangesAsync(stoppingToken);
|
|
_logger.LogInformation("Sondeo de Estado Recuento General completado. Se han guardado los cambios en la base de datos.");
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation("Sondeo de Estado Recuento General completado. No se detectaron cambios.");
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("Sondeo de Estado Recuento General cancelado.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General.");
|
|
}
|
|
}
|
|
|
|
/// <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...");
|
|
var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);
|
|
if (string.IsNullOrEmpty(authToken))
|
|
{
|
|
_logger.LogError("No se pudo obtener token para la sincronización de catálogos.");
|
|
return;
|
|
}
|
|
|
|
using var scope = _serviceProvider.CreateScope();
|
|
var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();
|
|
|
|
// 1. SINCRONIZAR CATEGORÍAS
|
|
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.");
|
|
return;
|
|
}
|
|
var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList();
|
|
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 });
|
|
}
|
|
}
|
|
await dbContext.SaveChangesAsync(stoppingToken);
|
|
_logger.LogInformation("Catálogo de Categorías Electorales sincronizado.");
|
|
|
|
// 2. SINCRONIZAR AGRUPACIONES POLÍTICAS
|
|
_logger.LogInformation("Iniciando sincronización de Agrupaciones Políticas...");
|
|
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
|
foreach (var categoria in distinctCategorias)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
|
|
// Se pasa 'null' como distritoId para obtener todas las agrupaciones de la categoría.
|
|
var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, null, categoria.CategoriaId);
|
|
|
|
if (agrupacionesApi != null)
|
|
{
|
|
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); // Añadir al diccionario para evitar duplicados en el mismo ciclo
|
|
}
|
|
}
|
|
}
|
|
}
|
|
int agrupacionesGuardadas = await dbContext.SaveChangesAsync(stoppingToken);
|
|
_logger.LogInformation("Catálogo de Agrupaciones Políticas sincronizado. Se guardaron {count} nuevos registros.", agrupacionesGuardadas);
|
|
|
|
// 3. SINCRONIZAR ÁMBITOS GEOGRÁFICOS
|
|
_logger.LogInformation("Iniciando sincronización de Ámbitos Geográficos...");
|
|
var ambitosEnDbKeys = new HashSet<string>(
|
|
await dbContext.AmbitosGeograficos.Select(a => $"{a.NivelId}|{a.DistritoId}|{a.SeccionProvincialId}|{a.SeccionId}|{a.MunicipioId}|{a.CircuitoId}|{a.EstablecimientoId}|{a.MesaId}").ToListAsync(stoppingToken)
|
|
);
|
|
int totalNuevosAmbitos = 0;
|
|
|
|
foreach (var categoria in distinctCategorias)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId);
|
|
if (catalogoDto != null)
|
|
{
|
|
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 (ambitosEnDbKeys.Add(claveUnica)) // HashSet.Add devuelve true si el elemento no existía
|
|
{
|
|
string nombreCorregido = ambitoDto.Nombre;
|
|
if (ambitoDto.CodigoAmbitos.DistritoId == "01" && ambitoDto.NivelId == 30 && int.TryParse(ambitoDto.Nombre, out int numeroComuna))
|
|
{
|
|
nombreCorregido = $"COMUNA {numeroComuna}";
|
|
}
|
|
dbContext.AmbitosGeograficos.Add(new AmbitoGeografico
|
|
{
|
|
Nombre = nombreCorregido,
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (dbContext.ChangeTracker.HasChanges())
|
|
{
|
|
int ambitosGuardados = await dbContext.SaveChangesAsync(stoppingToken);
|
|
totalNuevosAmbitos += ambitosGuardados;
|
|
_logger.LogInformation("Guardados {count} nuevos ámbitos para la categoría '{catNombre}'.", ambitosGuardados, categoria.Nombre);
|
|
}
|
|
}
|
|
_logger.LogInformation("Catálogo de Ámbitos Geográficos sincronizado. Se guardaron {count} nuevos registros en total.", totalNuevosAmbitos);
|
|
_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.
|
|
/// </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);
|
|
|
|
// --- MODIFICACIÓN 1: Obtener todos los ámbitos en una sola consulta ---
|
|
var ambitosASondear = await dbContext.AmbitosGeograficos
|
|
.AsNoTracking()
|
|
.Where(a => (a.NivelId == 10 || a.NivelId == 20) && a.DistritoId != null)
|
|
.ToListAsync(stoppingToken);
|
|
|
|
var provincia = ambitosASondear.FirstOrDefault(a => a.NivelId == 10);
|
|
var seccionesElectorales = ambitosASondear.Where(a => a.NivelId == 20).ToList();
|
|
|
|
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;
|
|
|
|
// --- MODIFICACIÓN 2: Usar un diccionario para no buscar repetidamente en la BD ---
|
|
var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);
|
|
|
|
// Bucle combinado para todos los ámbitos
|
|
foreach (var ambito in ambitosASondear)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
|
|
foreach (var categoria in categoriasDeBancas)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
|
|
// Llamada a la API (lógica adaptada para ambos niveles)
|
|
var repartoBancasDto = await _apiService.GetBancasAsync(authToken, ambito.DistritoId!, ambito.SeccionProvincialId, categoria.Id);
|
|
|
|
if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
|
|
{
|
|
hasReceivedAnyNewData = true;
|
|
|
|
DateTime fechaTotalizacion;
|
|
if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate))
|
|
{
|
|
_logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas. Usando la hora actual.", repartoBancasDto.FechaTotalizacion);
|
|
fechaTotalizacion = DateTime.UtcNow;
|
|
}
|
|
else
|
|
{
|
|
fechaTotalizacion = parsedDate.ToUniversalTime();
|
|
}
|
|
|
|
foreach (var banca in bancas)
|
|
{
|
|
// --- MODIFICACIÓN 3: Lógica de "Upsert" para Agrupaciones ---
|
|
if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion))
|
|
{
|
|
_logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada. Creándola desde los datos de bancas.", banca.IdAgrupacion, banca.NombreAgrupacion);
|
|
|
|
var nuevaAgrupacion = new AgrupacionPolitica
|
|
{
|
|
Id = banca.IdAgrupacion,
|
|
Nombre = banca.NombreAgrupacion,
|
|
IdTelegrama = banca.IdAgrupacionTelegrama ?? string.Empty
|
|
};
|
|
|
|
await dbContext.AgrupacionesPoliticas.AddAsync(nuevaAgrupacion, stoppingToken);
|
|
agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); // Añadir al diccionario para no volver a crearla
|
|
}
|
|
|
|
todasLasProyecciones.Add(new ProyeccionBanca
|
|
{
|
|
EleccionId = EleccionId,
|
|
AmbitoGeograficoId = ambito.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);
|
|
|
|
// Si se crearon nuevas agrupaciones, se guardarán aquí primero.
|
|
await dbContext.SaveChangesAsync(stoppingToken);
|
|
|
|
// Luego, procedemos con las proyecciones.
|
|
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, 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>();
|
|
|
|
// La obtención de partidos y categorías no cambia
|
|
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 })
|
|
{
|
|
// Creamos el DbContext para la operación de guardado
|
|
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);
|
|
|
|
var originalTimeout = innerDbContext.Database.GetCommandTimeout();
|
|
try
|
|
{
|
|
innerDbContext.Database.SetCommandTimeout(180);
|
|
_logger.LogDebug("Timeout de BD aumentado a 180s para descarga de telegramas.");
|
|
|
|
int contadorLote = 0;
|
|
const int tamanoLote = 100;
|
|
|
|
foreach (var mesaId in nuevosTelegramasIds)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) return;
|
|
|
|
var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId);
|
|
if (telegramaFile != null)
|
|
{
|
|
var ambitoMesa = await innerDbContext.AmbitosGeograficos.AsNoTracking()
|
|
.FirstOrDefaultAsync(a => a.MesaId == mesaId, stoppingToken);
|
|
|
|
if (ambitoMesa != null)
|
|
{
|
|
var nuevoTelegrama = new Telegrama
|
|
{
|
|
EleccionId = EleccionId,
|
|
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);
|
|
}
|
|
}
|
|
await Task.Delay(250, stoppingToken);
|
|
|
|
if (contadorLote >= tamanoLote)
|
|
{
|
|
await innerDbContext.SaveChangesAsync(stoppingToken);
|
|
_logger.LogInformation("Guardado un lote de {count} telegramas.", contadorLote);
|
|
contadorLote = 0;
|
|
}
|
|
}
|
|
|
|
if (contadorLote > 0)
|
|
{
|
|
await innerDbContext.SaveChangesAsync(stoppingToken);
|
|
_logger.LogInformation("Guardado el último lote de {count} telegramas.", contadorLote);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
innerDbContext.Database.SetCommandTimeout(originalTimeout);
|
|
_logger.LogDebug("Timeout de BD restaurado a su valor original ({timeout}s).", originalTimeout);
|
|
}
|
|
} // 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)
|
|
}
|
|
}
|
|
_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.");
|
|
}
|
|
}
|
|
} |