699 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			699 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| //Elecciones.Worker/CriticalDataWorker.cs
 | |
| 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 const int EleccionId = 2;
 | |
|   private readonly ILogger<CriticalDataWorker> _logger;
 | |
|   private readonly SharedTokenService _tokenService;
 | |
|   private readonly IServiceProvider _serviceProvider;
 | |
|   private readonly IElectoralApiService _apiService;
 | |
|   private readonly WorkerConfigService _configService;
 | |
| 
 | |
|   public CriticalDataWorker(
 | |
|       ILogger<CriticalDataWorker> 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 Datos Críticos iniciado.");
 | |
| 
 | |
|     try
 | |
|     {
 | |
|       await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken);
 | |
|     }
 | |
|     catch (TaskCanceledException) { return; }
 | |
| 
 | |
|     int cicloContador = 0;
 | |
|     while (!stoppingToken.IsCancellationRequested)
 | |
|     {
 | |
|       var cicloInicio = DateTime.UtcNow;
 | |
|       cicloContador++;
 | |
|       _logger.LogInformation("--- Iniciando Ciclo de Datos Críticos #{ciclo} ---", cicloContador);
 | |
| 
 | |
|       var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);
 | |
|       if (string.IsNullOrEmpty(authToken))
 | |
|       {
 | |
|         _logger.LogError("Ciclo Crítico: No se pudo obtener token. Reintentando en 30 segundos.");
 | |
|         await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       var settings = await _configService.GetSettingsAsync();
 | |
| 
 | |
|       if (settings.Prioridad == "Resultados" && settings.ResultadosActivado)
 | |
|       {
 | |
|         _logger.LogInformation("Ejecutando tareas de Resultados en alta prioridad.");
 | |
|         await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
 | |
|         await SondearResultadosMunicipalesAsync(authToken, stoppingToken);
 | |
|         await SondearResumenProvincialAsync(authToken, stoppingToken);
 | |
|       }
 | |
|       else if (settings.Prioridad == "Telegramas" && settings.BajasActivado)
 | |
|       {
 | |
|         _logger.LogInformation("Ejecutando tareas de Baja Prioridad en alta prioridad.");
 | |
|         await SondearProyeccionBancasAsync(authToken, stoppingToken);
 | |
|         //await SondearNuevosTelegramasAsync(authToken, stoppingToken);
 | |
|       }
 | |
|       else
 | |
|       {
 | |
|         _logger.LogInformation("Worker de alta prioridad inactivo según la configuración.");
 | |
|       }
 | |
| 
 | |
|       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);
 | |
|       }
 | |
|       catch (TaskCanceledException) { break; }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// <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.");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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 { 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.");
 | |
|     }
 | |
|   }
 | |
| }
 |