| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | using Elecciones.Database; | 
					
						
							|  |  |  | using Elecciones.Database.Entities; | 
					
						
							|  |  |  | using Elecciones.Infrastructure.Services; | 
					
						
							|  |  |  | using Microsoft.EntityFrameworkCore; | 
					
						
							|  |  |  | using System.Collections.Concurrent; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace Elecciones.Worker; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | public class CriticalDataWorker : BackgroundService | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   private readonly ILogger<CriticalDataWorker> _logger; | 
					
						
							|  |  |  |   private readonly SharedTokenService _tokenService; | 
					
						
							|  |  |  |   private readonly IServiceProvider _serviceProvider; | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |   private readonly IElectoralApiService _apiService; | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |   public CriticalDataWorker( | 
					
						
							|  |  |  |       ILogger<CriticalDataWorker> logger, | 
					
						
							|  |  |  |       SharedTokenService tokenService, | 
					
						
							|  |  |  |       IServiceProvider serviceProvider, | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |       IElectoralApiService apiService) | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |   { | 
					
						
							|  |  |  |     _logger = logger; | 
					
						
							|  |  |  |     _tokenService = tokenService; | 
					
						
							|  |  |  |     _serviceProvider = serviceProvider; | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |     _apiService = apiService; | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   protected override async Task ExecuteAsync(CancellationToken stoppingToken) | 
					
						
							|  |  |  |   { | 
					
						
							|  |  |  |     _logger.LogInformation("Worker de Datos Críticos iniciado."); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |     catch (TaskCanceledException) { return; } | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |     int cicloContador = 0; | 
					
						
							|  |  |  |     while (!stoppingToken.IsCancellationRequested) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       var cicloInicio = DateTime.UtcNow; | 
					
						
							|  |  |  |       cicloContador++; | 
					
						
							|  |  |  |       _logger.LogInformation("--- Iniciando Ciclo de Datos Críticos #{ciclo} ---", cicloContador); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken); | 
					
						
							|  |  |  |       if (string.IsNullOrEmpty(authToken)) | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         _logger.LogError("Ciclo Crítico: No se pudo obtener token. Reintentando en 30 segundos."); | 
					
						
							|  |  |  |         await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); | 
					
						
							|  |  |  |         continue; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       await SondearResultadosMunicipalesAsync(authToken, stoppingToken); | 
					
						
							|  |  |  |       await SondearResumenProvincialAsync(authToken, stoppingToken); | 
					
						
							|  |  |  |       await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       var cicloFin = DateTime.UtcNow; | 
					
						
							|  |  |  |       var duracionCiclo = cicloFin - cicloInicio; | 
					
						
							|  |  |  |       _logger.LogInformation("--- Ciclo de Datos Críticos #{ciclo} completado en {duration:N2} segundos. ---", cicloContador, duracionCiclo.TotalSeconds); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       var tiempoDeEspera = TimeSpan.FromSeconds(30) - duracionCiclo; | 
					
						
							|  |  |  |       if (tiempoDeEspera < TimeSpan.Zero) tiempoDeEspera = TimeSpan.Zero; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       try | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         await Task.Delay(tiempoDeEspera, stoppingToken); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |       catch (TaskCanceledException) { break; } | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken) | 
					
						
							|  |  |  |   { | 
					
						
							|  |  |  |     try | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       using var scope = _serviceProvider.CreateScope(); | 
					
						
							|  |  |  |       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       var municipiosASondear = await dbContext.AmbitosGeograficos | 
					
						
							|  |  |  |           .AsNoTracking() | 
					
						
							|  |  |  |           .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) | 
					
						
							|  |  |  |           .ToListAsync(stoppingToken); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |       var todasLasCategorias = await dbContext.CategoriasElectorales | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |           .AsNoTracking() | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |           .ToListAsync(stoppingToken); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |       if (!municipiosASondear.Any() || !todasLasCategorias.Any()) | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |       { | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |         _logger.LogWarning("No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados."); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |       _logger.LogInformation("Iniciando sondeo de resultados para {m} municipios y {c} categorías...", municipiosASondear.Count, todasLasCategorias.Count); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |       foreach (var municipio in municipiosASondear) | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |       { | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |         if (stoppingToken.IsCancellationRequested) break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var tareasCategoria = todasLasCategorias.Select(async categoria => | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |           var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoria.Id); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |           if (resultados != null) | 
					
						
							|  |  |  |           { | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |             using var innerScope = _serviceProvider.CreateScope(); | 
					
						
							|  |  |  |             var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |             // --- LLAMADA CORRECTA --- | 
					
						
							|  |  |  |             await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |         await Task.WhenAll(tareasCategoria); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     catch (Exception ex) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       _logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |   private async Task GuardarResultadosDeAmbitoAsync( | 
					
						
							|  |  |  |       EleccionesDbContext dbContext, int ambitoId, int categoriaId, | 
					
						
							|  |  |  |       Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |   { | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |     var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |     if (estadoRecuento == null) | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |       estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId, CategoriaId = categoriaId }; | 
					
						
							|  |  |  |       dbContext.EstadosRecuentos.Add(estadoRecuento); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |     estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime(); | 
					
						
							|  |  |  |     estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; | 
					
						
							|  |  |  |     estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas; | 
					
						
							|  |  |  |     estadoRecuento.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje; | 
					
						
							|  |  |  |     estadoRecuento.CantidadElectores = resultadosDto.EstadoRecuento.CantidadElectores; | 
					
						
							|  |  |  |     estadoRecuento.CantidadVotantes = resultadosDto.EstadoRecuento.CantidadVotantes; | 
					
						
							|  |  |  |     estadoRecuento.ParticipacionPorcentaje = resultadosDto.EstadoRecuento.ParticipacionPorcentaje; | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |     if (resultadosDto.ValoresTotalizadosOtros != null) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       estadoRecuento.VotosEnBlanco = resultadosDto.ValoresTotalizadosOtros.VotosEnBlanco; | 
					
						
							|  |  |  |       estadoRecuento.VotosEnBlancoPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosEnBlancoPorcentaje; | 
					
						
							|  |  |  |       estadoRecuento.VotosNulos = resultadosDto.ValoresTotalizadosOtros.VotosNulos; | 
					
						
							|  |  |  |       estadoRecuento.VotosNulosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosNulosPorcentaje; | 
					
						
							|  |  |  |       estadoRecuento.VotosRecurridos = resultadosDto.ValoresTotalizadosOtros.VotosRecurridos; | 
					
						
							|  |  |  |       estadoRecuento.VotosRecurridosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosRecurridosPorcentaje; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |     foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync( | 
					
						
							|  |  |  |           rv => rv.AmbitoGeograficoId == ambitoId && | 
					
						
							|  |  |  |                 rv.CategoriaId == categoriaId && | 
					
						
							|  |  |  |                 rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion, | 
					
						
							|  |  |  |           stoppingToken | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (resultadoVoto == null) | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |       { | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |         resultadoVoto = new ResultadoVoto | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |           AmbitoGeograficoId = ambitoId, | 
					
						
							|  |  |  |           CategoriaId = categoriaId, | 
					
						
							|  |  |  |           AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |         dbContext.ResultadosVotos.Add(resultadoVoto); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |       resultadoVoto.CantidadVotos = votoPositivoDto.Votos; | 
					
						
							|  |  |  |       resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje; | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 11:01:54 -03:00
										 |  |  |     try | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       await dbContext.SaveChangesAsync(stoppingToken); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     catch (DbUpdateException ex) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       _logger.LogError(ex, "DbUpdateException al guardar resultados para AmbitoId {ambitoId} y CategoriaId {categoriaId}", ambitoId, categoriaId); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /// <summary> | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |   /// Obtiene y actualiza el resumen de votos y el estado del recuento a nivel provincial. | 
					
						
							|  |  |  |   /// Esta versión actualizada guarda tanto los votos por agrupación (en ResumenesVotos) | 
					
						
							|  |  |  |   /// como el estado general del recuento, incluyendo la fecha de totalización (en EstadosRecuentosGenerales), | 
					
						
							|  |  |  |   /// asegurando que toda la operación sea atómica mediante una transacción de base de datos. | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |   /// </summary> | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |   /// <param name="authToken">El token de autenticación válido para la sesión.</param> | 
					
						
							|  |  |  |   /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |   private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken) | 
					
						
							|  |  |  |   { | 
					
						
							|  |  |  |     try | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |       // Creamos un scope de DbContext para esta operación. | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |       using var scope = _serviceProvider.CreateScope(); | 
					
						
							|  |  |  |       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |       // Obtenemos el registro de la Provincia (NivelId 10). | 
					
						
							|  |  |  |       var provincia = await dbContext.AmbitosGeograficos | 
					
						
							|  |  |  |           .AsNoTracking() | 
					
						
							|  |  |  |           .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Si no encontramos el ámbito de la provincia, no podemos continuar. | 
					
						
							|  |  |  |       if (provincia == null) | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) para el sondeo de resumen."); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |       // Llamamos a la API para obtener el resumen de datos provincial. | 
					
						
							|  |  |  |       var resumenDto = await _apiService.GetResumenAsync(authToken, provincia.DistritoId!); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |       // Solo procedemos si la API devolvió una respuesta válida y no nula. | 
					
						
							|  |  |  |       if (resumenDto != null) | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |       { | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |         // Iniciamos una transacción explícita. Esto garantiza que todas las operaciones de base de datos | 
					
						
							|  |  |  |         // dentro de este bloque (el DELETE, los INSERTs y los UPDATEs) se completen con éxito, | 
					
						
							|  |  |  |         // o si algo falla, se reviertan todas, manteniendo la consistencia de los datos. | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |         await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |         // --- 1. ACTUALIZAR LA TABLA 'ResumenesVotos' --- | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Verificamos si la respuesta contiene una lista de votos positivos. | 
					
						
							|  |  |  |         if (resumenDto.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           // Estrategia "Borrar y Reemplazar": vaciamos la tabla antes de insertar los nuevos datos. | 
					
						
							|  |  |  |           await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ResumenesVotos", stoppingToken); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // Añadimos cada nuevo registro de voto al DbContext. | 
					
						
							|  |  |  |           foreach (var voto in nuevosVotos) | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             dbContext.ResumenesVotos.Add(new ResumenVoto | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               AmbitoGeograficoId = provincia.Id, | 
					
						
							|  |  |  |               AgrupacionPoliticaId = voto.IdAgrupacion, | 
					
						
							|  |  |  |               Votos = voto.Votos, | 
					
						
							|  |  |  |               VotosPorcentaje = voto.VotosPorcentaje | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // --- 2. ACTUALIZAR LA TABLA 'EstadosRecuentosGenerales' --- | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |         // El endpoint de Resumen no especifica una categoría, por lo que aplicamos sus datos de estado de recuento | 
					
						
							|  |  |  |         // a todas las categorías que tenemos en nuestra base de datos. | 
					
						
							|  |  |  |         var todasLasCategorias = await dbContext.CategoriasElectorales.AsNoTracking().ToListAsync(stoppingToken); | 
					
						
							|  |  |  |         foreach (var categoria in todasLasCategorias) | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |           // Buscamos el registro existente usando la clave primaria compuesta. | 
					
						
							|  |  |  |           var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { provincia.Id, categoria.Id }, stoppingToken); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // Si no existe, lo creamos. | 
					
						
							|  |  |  |           if (registroDb == null) | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             registroDb = new EstadoRecuentoGeneral { AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id }; | 
					
						
							|  |  |  |             dbContext.EstadosRecuentosGenerales.Add(registroDb); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // Parseamos la fecha de forma segura para evitar errores con cadenas vacías o nulas. | 
					
						
							|  |  |  |           if (DateTime.TryParse(resumenDto.FechaTotalizacion, out var parsedDate)) | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |           { | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |             registroDb.FechaTotalizacion = parsedDate.ToUniversalTime(); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // Mapeamos el resto de los datos del estado del recuento. | 
					
						
							|  |  |  |           registroDb.MesasEsperadas = resumenDto.EstadoRecuento.MesasEsperadas; | 
					
						
							|  |  |  |           registroDb.MesasTotalizadas = resumenDto.EstadoRecuento.MesasTotalizadas; | 
					
						
							|  |  |  |           registroDb.MesasTotalizadasPorcentaje = resumenDto.EstadoRecuento.MesasTotalizadasPorcentaje; | 
					
						
							|  |  |  |           registroDb.CantidadElectores = resumenDto.EstadoRecuento.CantidadElectores; | 
					
						
							|  |  |  |           registroDb.CantidadVotantes = resumenDto.EstadoRecuento.CantidadVotantes; | 
					
						
							|  |  |  |           registroDb.ParticipacionPorcentaje = resumenDto.EstadoRecuento.ParticipacionPorcentaje; | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |         // 3. CONFIRMAR Y GUARDAR | 
					
						
							|  |  |  |         // Guardamos todos los cambios preparados (DELETEs, INSERTs, UPDATEs) en la base de datos. | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |         await dbContext.SaveChangesAsync(stoppingToken); | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |         // Confirmamos la transacción para hacer los cambios permanentes. | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |         await transaction.CommitAsync(stoppingToken); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |         _logger.LogInformation("Sondeo de Resumen Provincial completado. Las tablas han sido actualizadas."); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |       } | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |       { | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |         // Si la API no devolvió datos (ej. devuelve null), no hacemos nada en la BD. | 
					
						
							|  |  |  |         _logger.LogInformation("Sondeo de Resumen Provincial completado. No se recibieron datos nuevos."); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |     catch (OperationCanceledException) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       _logger.LogInformation("Sondeo de resumen provincial cancelado."); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |     catch (Exception ex) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2025-08-23 13:19:35 -03:00
										 |  |  |       // Capturamos cualquier otro error inesperado para que el worker no se detenga. | 
					
						
							|  |  |  |       _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Resumen Provincial."); | 
					
						
							| 
									
										
										
										
											2025-08-20 16:58:18 -03:00
										 |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /// <summary> | 
					
						
							|  |  |  |   /// Obtiene y actualiza el estado general del recuento a nivel provincial para CADA categoría electoral. | 
					
						
							|  |  |  |   /// Esta versión es robusta: consulta dinámicamente las categorías, usa la clave primaria compuesta | 
					
						
							|  |  |  |   /// de la base de datos y guarda todos los cambios en una única transacción al final. | 
					
						
							|  |  |  |   /// </summary> | 
					
						
							|  |  |  |   /// <param name="authToken">El token de autenticación válido para la sesión.</param> | 
					
						
							|  |  |  |   /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | 
					
						
							|  |  |  |   private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken) | 
					
						
							|  |  |  |   { | 
					
						
							|  |  |  |     try | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       // PASO 1: Crear un "scope" para obtener una instancia fresca de DbContext. | 
					
						
							|  |  |  |       // Esto es una práctica recomendada para servicios de larga duración para evitar problemas de concurrencia. | 
					
						
							|  |  |  |       using var scope = _serviceProvider.CreateScope(); | 
					
						
							|  |  |  |       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // PASO 2: Obtener el ámbito geográfico de la Provincia. | 
					
						
							|  |  |  |       // Necesitamos este objeto para obtener su 'DistritoId' ("02"), que es requerido por la API. | 
					
						
							|  |  |  |       var provincia = await dbContext.AmbitosGeograficos | 
					
						
							|  |  |  |           .AsNoTracking() // Optimización: Solo necesitamos leer datos, no modificarlos. | 
					
						
							|  |  |  |           .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Comprobación de seguridad: Si la sincronización inicial falló y no tenemos el registro de la provincia, | 
					
						
							|  |  |  |       // no podemos continuar. Registramos una advertencia y salimos del método. | 
					
						
							|  |  |  |       if (provincia == null) | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general."); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // PASO 3: Obtener todas las categorías electorales disponibles desde nuestra base de datos. | 
					
						
							|  |  |  |       // Esto hace que el método sea dinámico y no dependa de IDs fijos en el código. | 
					
						
							|  |  |  |       var categoriasParaSondear = await dbContext.CategoriasElectorales | 
					
						
							|  |  |  |           .AsNoTracking() | 
					
						
							|  |  |  |           .ToListAsync(stoppingToken); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (!categoriasParaSondear.Any()) | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         _logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento."); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {count} categorías...", categoriasParaSondear.Count); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // PASO 4: Iterar sobre cada categoría para obtener su estado de recuento individual. | 
					
						
							|  |  |  |       foreach (var categoria in categoriasParaSondear) | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         // Salimos limpiamente del bucle si la aplicación se está deteniendo. | 
					
						
							|  |  |  |         if (stoppingToken.IsCancellationRequested) break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Llamamos a la API con el distrito y la CATEGORÍA ACTUAL del bucle. | 
					
						
							|  |  |  |         var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Solo procedemos si la API devolvió datos válidos. | 
					
						
							|  |  |  |         if (estadoDto != null) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           // Lógica "Upsert" (Update or Insert): | 
					
						
							|  |  |  |           // Buscamos un registro existente usando la CLAVE PRIMARIA COMPUESTA. | 
					
						
							|  |  |  |           var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync( | 
					
						
							|  |  |  |               new object[] { provincia.Id, categoria.Id }, | 
					
						
							|  |  |  |               cancellationToken: stoppingToken | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // Si no se encuentra (FindAsync devuelve null), es un registro nuevo. | 
					
						
							|  |  |  |           if (registroDb == null) | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             // Creamos una nueva instancia de la entidad. | 
					
						
							|  |  |  |             registroDb = new EstadoRecuentoGeneral | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               AmbitoGeograficoId = provincia.Id, | 
					
						
							|  |  |  |               CategoriaId = categoria.Id // Asignamos ambas partes de la clave primaria. | 
					
						
							|  |  |  |             }; | 
					
						
							|  |  |  |             // Y la añadimos al ChangeTracker de EF para que la inserte en la BD. | 
					
						
							|  |  |  |             dbContext.EstadosRecuentosGenerales.Add(registroDb); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // Mapeamos los datos del DTO de la API a nuestra entidad de base de datos. | 
					
						
							|  |  |  |           // Esto se hace tanto para registros nuevos como para los existentes que se van a actualizar. | 
					
						
							|  |  |  |           registroDb.MesasEsperadas = estadoDto.MesasEsperadas; | 
					
						
							|  |  |  |           registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas; | 
					
						
							|  |  |  |           registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; | 
					
						
							|  |  |  |           registroDb.CantidadElectores = estadoDto.CantidadElectores; | 
					
						
							|  |  |  |           registroDb.CantidadVotantes = estadoDto.CantidadVotantes; | 
					
						
							|  |  |  |           registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // PASO 5: Guardar todos los cambios en la base de datos. | 
					
						
							|  |  |  |       // Al llamar a SaveChangesAsync UNA SOLA VEZ fuera del bucle, EF Core agrupa | 
					
						
							|  |  |  |       // todas las inserciones y actualizaciones en una única transacción eficiente. | 
					
						
							|  |  |  |       await dbContext.SaveChangesAsync(stoppingToken); | 
					
						
							|  |  |  |       _logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     catch (Exception ex) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       // Capturamos cualquier excepción inesperada para que no detenga el worker y la registramos. | 
					
						
							|  |  |  |       _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |