Feat Workers Prioridades y Nivel Serilog
This commit is contained in:
		| @@ -1,3 +1,4 @@ | ||||
| //Elecciones.Worker/CriticalDataWorker.cs | ||||
| using Elecciones.Database; | ||||
| using Elecciones.Database.Entities; | ||||
| using Elecciones.Infrastructure.Services; | ||||
| @@ -12,17 +13,20 @@ public class CriticalDataWorker : BackgroundService | ||||
|   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) | ||||
|       IElectoralApiService apiService, | ||||
|       WorkerConfigService configService) | ||||
|   { | ||||
|     _logger = logger; | ||||
|     _tokenService = tokenService; | ||||
|     _serviceProvider = serviceProvider; | ||||
|     _apiService = apiService; | ||||
|     _configService = configService; | ||||
|   } | ||||
|  | ||||
|   protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
| @@ -50,9 +54,25 @@ public class CriticalDataWorker : BackgroundService | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       await SondearResultadosMunicipalesAsync(authToken, stoppingToken); | ||||
|       await SondearResumenProvincialAsync(authToken, stoppingToken); | ||||
|       await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); | ||||
|       var settings = await _configService.GetSettingsAsync(); | ||||
|  | ||||
|       if (settings.Prioridad == "Resultados" && settings.ResultadosActivado) | ||||
|       { | ||||
|         _logger.LogInformation("Ejecutando tareas de Resultados en alta prioridad."); | ||||
|         await SondearResultadosMunicipalesAsync(authToken, stoppingToken); | ||||
|         await SondearResumenProvincialAsync(authToken, stoppingToken); | ||||
|         await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); | ||||
|       } | ||||
|       else if (settings.Prioridad == "Telegramas" && settings.BajasActivado) | ||||
|       { | ||||
|         _logger.LogInformation("Ejecutando tareas de Baja Prioridad en alta prioridad."); | ||||
|         await SondearProyeccionBancasAsync(authToken, stoppingToken); | ||||
|         await SondearNuevosTelegramasAsync(authToken, stoppingToken); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         _logger.LogInformation("Worker de alta prioridad inactivo según la configuración."); | ||||
|       } | ||||
|  | ||||
|       var cicloFin = DateTime.UtcNow; | ||||
|       var duracionCiclo = cicloFin - cicloInicio; | ||||
| @@ -69,6 +89,252 @@ public class CriticalDataWorker : BackgroundService | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral. | ||||
|   /// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas, | ||||
|   /// guarda la CategoriaId y usa una transacción atómica para la escritura en base de datos. | ||||
|   /// </summary> | ||||
|   /// <param name="authToken">El token de autenticación válido para la sesión.</param> | ||||
|   /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||
|   private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|     { | ||||
|       using var scope = _serviceProvider.CreateScope(); | ||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|       var categoriasDeBancas = await dbContext.CategoriasElectorales | ||||
|           .AsNoTracking() | ||||
|           .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       var provincia = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() | ||||
|           .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); | ||||
|  | ||||
|       var seccionesElectorales = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() | ||||
|           .Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null) | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       if (!categoriasDeBancas.Any() || provincia == null) | ||||
|       { | ||||
|         _logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas."); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       _logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial y para {count} Secciones Electorales...", seccionesElectorales.Count); | ||||
|  | ||||
|       var todasLasProyecciones = new List<ProyeccionBanca>(); | ||||
|       bool hasReceivedAnyNewData = false; | ||||
|  | ||||
|       // Bucle para el nivel Provincial | ||||
|       foreach (var categoria in categoriasDeBancas) | ||||
|       { | ||||
|         if (stoppingToken.IsCancellationRequested) break; | ||||
|         var repartoBancasDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id); | ||||
|  | ||||
|         if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) | ||||
|         { | ||||
|           hasReceivedAnyNewData = true; | ||||
|  | ||||
|           // --- SEGURIDAD: Usar TryParse para la fecha --- | ||||
|           DateTime fechaTotalizacion; | ||||
|           if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) | ||||
|           { | ||||
|             // Si la fecha es inválida (nula, vacía, mal formada), lo registramos y usamos la hora actual como respaldo. | ||||
|             _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas provinciales. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); | ||||
|             fechaTotalizacion = DateTime.UtcNow; | ||||
|           } | ||||
|           else | ||||
|           { | ||||
|             fechaTotalizacion = parsedDate.ToUniversalTime(); | ||||
|           } | ||||
|  | ||||
|           foreach (var banca in bancas) | ||||
|           { | ||||
|             todasLasProyecciones.Add(new ProyeccionBanca | ||||
|             { | ||||
|               AmbitoGeograficoId = provincia.Id, | ||||
|               AgrupacionPoliticaId = banca.IdAgrupacion, | ||||
|               NroBancas = banca.NroBancas, | ||||
|               CategoriaId = categoria.Id, | ||||
|               FechaTotalizacion = fechaTotalizacion | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Bucle para el nivel de Sección Electoral | ||||
|       foreach (var seccion in seccionesElectorales) | ||||
|       { | ||||
|         if (stoppingToken.IsCancellationRequested) break; | ||||
|         foreach (var categoria in categoriasDeBancas) | ||||
|         { | ||||
|           if (stoppingToken.IsCancellationRequested) break; | ||||
|           var repartoBancasDto = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id); | ||||
|  | ||||
|           if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) | ||||
|           { | ||||
|             hasReceivedAnyNewData = true; | ||||
|  | ||||
|             // --- APLICAMOS LA MISMA SEGURIDAD AQUÍ --- | ||||
|             DateTime fechaTotalizacion; | ||||
|             if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) | ||||
|             { | ||||
|               _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas de sección. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); | ||||
|               fechaTotalizacion = DateTime.UtcNow; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|               fechaTotalizacion = parsedDate.ToUniversalTime(); | ||||
|             } | ||||
|  | ||||
|             foreach (var banca in bancas) | ||||
|             { | ||||
|               todasLasProyecciones.Add(new ProyeccionBanca | ||||
|               { | ||||
|                 AmbitoGeograficoId = seccion.Id, | ||||
|                 AgrupacionPoliticaId = banca.IdAgrupacion, | ||||
|                 NroBancas = banca.NroBancas, | ||||
|                 CategoriaId = categoria.Id, | ||||
|                 FechaTotalizacion = fechaTotalizacion | ||||
|               }); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (hasReceivedAnyNewData) | ||||
|       { | ||||
|         _logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos..."); | ||||
|         await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); | ||||
|  | ||||
|         await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken); | ||||
|         await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken); | ||||
|         await dbContext.SaveChangesAsync(stoppingToken); | ||||
|         await transaction.CommitAsync(stoppingToken); | ||||
|  | ||||
|         _logger.LogInformation("La tabla de proyecciones ha sido actualizada con {count} registros.", todasLasProyecciones.Count); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada."); | ||||
|       } | ||||
|     } | ||||
|     catch (OperationCanceledException) | ||||
|     { | ||||
|       _logger.LogInformation("Sondeo de bancas cancelado."); | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|       _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Busca y descarga nuevos telegramas de forma masiva y concurrente. | ||||
|   /// Este método crea una lista de todas las combinaciones de Partido/Categoría, | ||||
|   /// las consulta a la API con un grado de paralelismo controlado, y cada tarea concurrente | ||||
|   /// maneja su propia lógica de descarga y guardado en la base de datos. | ||||
|   /// </summary> | ||||
|   /// <param name="authToken">El token de autenticación válido para la sesión.</param> | ||||
|   /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||
|   private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|     { | ||||
|       _logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas (modo de bajo perfil) ---"); | ||||
|  | ||||
|       using var scope = _serviceProvider.CreateScope(); | ||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|       var partidos = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() | ||||
|           .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       var categorias = await dbContext.CategoriasElectorales | ||||
|           .AsNoTracking() | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       if (!partidos.Any() || !categorias.Any()) return; | ||||
|  | ||||
|       foreach (var partido in partidos) | ||||
|       { | ||||
|         foreach (var categoria in categorias) | ||||
|         { | ||||
|           if (stoppingToken.IsCancellationRequested) return; | ||||
|  | ||||
|           var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id); | ||||
|  | ||||
|           if (listaTelegramasApi is { Count: > 0 }) | ||||
|           { | ||||
|             using var innerScope = _serviceProvider.CreateScope(); | ||||
|             var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|             var idsYaEnDb = await innerDbContext.Telegramas | ||||
|                 .Where(t => listaTelegramasApi.Contains(t.Id)) | ||||
|                 .Select(t => t.Id) | ||||
|                 .ToListAsync(stoppingToken); | ||||
|  | ||||
|             var nuevosTelegramasIds = listaTelegramasApi.Except(idsYaEnDb).ToList(); | ||||
|  | ||||
|             if (nuevosTelegramasIds.Any()) | ||||
|             { | ||||
|               _logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre); | ||||
|  | ||||
|               foreach (var mesaId in nuevosTelegramasIds) | ||||
|               { | ||||
|                 if (stoppingToken.IsCancellationRequested) return; | ||||
|  | ||||
|                 var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); | ||||
|                 if (telegramaFile != null) | ||||
|                 { | ||||
|                   // 1. Buscamos el AmbitoGeografico específico de la MESA que estamos procesando. | ||||
|                   var ambitoMesa = await innerDbContext.AmbitosGeograficos | ||||
|                       .AsNoTracking() | ||||
|                       .FirstOrDefaultAsync(a => a.MesaId == mesaId, stoppingToken); | ||||
|  | ||||
|                   // 2. Solo guardamos el telegrama si encontramos su ámbito de mesa correspondiente. | ||||
|                   if (ambitoMesa != null) | ||||
|                   { | ||||
|                     var nuevoTelegrama = new Telegrama | ||||
|                     { | ||||
|                       Id = telegramaFile.NombreArchivo, | ||||
|                       // 3. Usamos el ID del ÁMBITO DE LA MESA, no el del municipio. | ||||
|                       AmbitoGeograficoId = ambitoMesa.Id, | ||||
|                       ContenidoBase64 = telegramaFile.Imagen, | ||||
|                       FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), | ||||
|                       FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() | ||||
|                     }; | ||||
|                     await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken); | ||||
|                   } | ||||
|                   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); | ||||
|               } | ||||
|               await innerDbContext.SaveChangesAsync(stoppingToken); | ||||
|             } | ||||
|           } | ||||
|           await Task.Delay(100, stoppingToken); | ||||
|         } | ||||
|       } | ||||
|       _logger.LogInformation("Sondeo de Telegramas completado."); | ||||
|     } | ||||
|     catch (OperationCanceledException) | ||||
|     { | ||||
|       _logger.LogInformation("Sondeo de telegramas cancelado."); | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|       _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|   | ||||
		Reference in New Issue
	
	Block a user