Feat Workers Prioridades y Nivel Serilog
This commit is contained in:
		| @@ -1,3 +1,4 @@ | ||||
| //Elecciones.Worker/LowPriorityDataWorker.cs | ||||
| using Elecciones.Database; | ||||
| using Elecciones.Database.Entities; | ||||
| using Elecciones.Infrastructure.Services; | ||||
| @@ -11,6 +12,7 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|   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; | ||||
| @@ -19,12 +21,14 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|   ILogger<LowPriorityDataWorker> 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) | ||||
| @@ -46,30 +50,25 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       // --- LÓGICA DE EJECUCIÓN INDEPENDIENTE --- | ||||
|       var settings = await _configService.GetSettingsAsync(); | ||||
|  | ||||
|       // 1. TAREA DE BANCAS: Siempre se ejecuta y se espera. Es rápida. | ||||
|       _logger.LogInformation("Iniciando sondeo de Bancas..."); | ||||
|       await SondearProyeccionBancasAsync(authToken, stoppingToken); | ||||
|       _logger.LogInformation("Sondeo de Bancas completado."); | ||||
|  | ||||
|       // 2. TAREA DE TELEGRAMAS: "Dispara y Olvida" de forma segura. | ||||
|       // Comprobamos si la tarea anterior de telegramas ya ha terminado. | ||||
|       if (_telegramasTask == null || _telegramasTask.IsCompleted) | ||||
|       if (settings.Prioridad == "Telegramas" && settings.ResultadosActivado) | ||||
|       { | ||||
|         _logger.LogInformation("Iniciando sondeo de Telegramas en segundo plano..."); | ||||
|         // Lanzamos la tarea de telegramas pero NO la esperamos con 'await'. | ||||
|         // Guardamos una referencia a la tarea en nuestra variable de estado. | ||||
|         _telegramasTask = SondearNuevosTelegramasAsync(authToken, stoppingToken); | ||||
|         _logger.LogInformation("Ejecutando tareas de Resultados en baja prioridad."); | ||||
|         await SondearResultadosMunicipalesAsync(authToken, stoppingToken); | ||||
|         await SondearResumenProvincialAsync(authToken, stoppingToken); | ||||
|         await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); | ||||
|       } | ||||
|       else if (settings.Prioridad == "Resultados" && settings.BajasActivado) | ||||
|       { | ||||
|         _logger.LogInformation("Ejecutando tareas de Baja Prioridad en baja prioridad."); | ||||
|         await SondearProyeccionBancasAsync(authToken, stoppingToken); | ||||
|         await SondearNuevosTelegramasAsync(authToken, stoppingToken); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         // Si la descarga anterior todavía está en curso, nos saltamos este sondeo | ||||
|         // para no acumular tareas y sobrecargar el sistema. | ||||
|         _logger.LogInformation("El sondeo de telegramas anterior sigue en ejecución. Se omitirá en este ciclo."); | ||||
|         _logger.LogInformation("Worker de baja prioridad inactivo según la configuración."); | ||||
|       } | ||||
|  | ||||
|       _logger.LogInformation("--- Ciclo de Datos de Baja Prioridad completado. Esperando 5 minutos. ---"); | ||||
|       try | ||||
|       { | ||||
|         await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); | ||||
| @@ -81,6 +80,337 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|     { | ||||
|       using var scope = _serviceProvider.CreateScope(); | ||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|       var municipiosASondear = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() | ||||
|           .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       var todasLasCategorias = await dbContext.CategoriasElectorales | ||||
|           .AsNoTracking() | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       if (!municipiosASondear.Any() || !todasLasCategorias.Any()) | ||||
|       { | ||||
|         _logger.LogWarning("No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados."); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       _logger.LogInformation("Iniciando sondeo de resultados para {m} municipios y {c} categorías...", municipiosASondear.Count, todasLasCategorias.Count); | ||||
|  | ||||
|       foreach (var municipio in municipiosASondear) | ||||
|       { | ||||
|         if (stoppingToken.IsCancellationRequested) break; | ||||
|  | ||||
|         var tareasCategoria = todasLasCategorias.Select(async categoria => | ||||
|         { | ||||
|           var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoria.Id); | ||||
|  | ||||
|           if (resultados != null) | ||||
|           { | ||||
|             using var innerScope = _serviceProvider.CreateScope(); | ||||
|             var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|             // --- LLAMADA CORRECTA --- | ||||
|             await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken); | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|         await Task.WhenAll(tareasCategoria); | ||||
|       } | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|       _logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async Task GuardarResultadosDeAmbitoAsync( | ||||
|       EleccionesDbContext dbContext, int ambitoId, int categoriaId, | ||||
|       Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) | ||||
|   { | ||||
|     var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken); | ||||
|  | ||||
|     if (estadoRecuento == null) | ||||
|     { | ||||
|       estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId, CategoriaId = categoriaId }; | ||||
|       dbContext.EstadosRecuentos.Add(estadoRecuento); | ||||
|     } | ||||
|  | ||||
|     estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime(); | ||||
|     estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; | ||||
|     estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas; | ||||
|     estadoRecuento.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje; | ||||
|     estadoRecuento.CantidadElectores = resultadosDto.EstadoRecuento.CantidadElectores; | ||||
|     estadoRecuento.CantidadVotantes = resultadosDto.EstadoRecuento.CantidadVotantes; | ||||
|     estadoRecuento.ParticipacionPorcentaje = resultadosDto.EstadoRecuento.ParticipacionPorcentaje; | ||||
|  | ||||
|     if (resultadosDto.ValoresTotalizadosOtros != null) | ||||
|     { | ||||
|       estadoRecuento.VotosEnBlanco = resultadosDto.ValoresTotalizadosOtros.VotosEnBlanco; | ||||
|       estadoRecuento.VotosEnBlancoPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosEnBlancoPorcentaje; | ||||
|       estadoRecuento.VotosNulos = resultadosDto.ValoresTotalizadosOtros.VotosNulos; | ||||
|       estadoRecuento.VotosNulosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosNulosPorcentaje; | ||||
|       estadoRecuento.VotosRecurridos = resultadosDto.ValoresTotalizadosOtros.VotosRecurridos; | ||||
|       estadoRecuento.VotosRecurridosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosRecurridosPorcentaje; | ||||
|     } | ||||
|  | ||||
|     foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos) | ||||
|     { | ||||
|       var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync( | ||||
|           rv => rv.AmbitoGeograficoId == ambitoId && | ||||
|                 rv.CategoriaId == categoriaId && | ||||
|                 rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion, | ||||
|           stoppingToken | ||||
|       ); | ||||
|  | ||||
|       if (resultadoVoto == null) | ||||
|       { | ||||
|         resultadoVoto = new ResultadoVoto | ||||
|         { | ||||
|           AmbitoGeograficoId = ambitoId, | ||||
|           CategoriaId = categoriaId, | ||||
|           AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion | ||||
|         }; | ||||
|         dbContext.ResultadosVotos.Add(resultadoVoto); | ||||
|       } | ||||
|       resultadoVoto.CantidadVotos = votoPositivoDto.Votos; | ||||
|       resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje; | ||||
|     } | ||||
|  | ||||
|     try | ||||
|     { | ||||
|       await dbContext.SaveChangesAsync(stoppingToken); | ||||
|     } | ||||
|     catch (DbUpdateException ex) | ||||
|     { | ||||
|       _logger.LogError(ex, "DbUpdateException al guardar resultados para AmbitoId {ambitoId} y CategoriaId {categoriaId}", ambitoId, categoriaId); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// 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. | ||||
|   /// </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 SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|     { | ||||
|       // Creamos un scope de DbContext para esta operación. | ||||
|       using var scope = _serviceProvider.CreateScope(); | ||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|       // 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; | ||||
|       } | ||||
|  | ||||
|       // Llamamos a la API para obtener el resumen de datos provincial. | ||||
|       var resumenDto = await _apiService.GetResumenAsync(authToken, provincia.DistritoId!); | ||||
|  | ||||
|       // Solo procedemos si la API devolvió una respuesta válida y no nula. | ||||
|       if (resumenDto != null) | ||||
|       { | ||||
|         // 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. | ||||
|         await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); | ||||
|  | ||||
|         // --- 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' --- | ||||
|  | ||||
|         // 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) | ||||
|         { | ||||
|           // 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)) | ||||
|           { | ||||
|             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; | ||||
|         } | ||||
|  | ||||
|         // 3. CONFIRMAR Y GUARDAR | ||||
|         // Guardamos todos los cambios preparados (DELETEs, INSERTs, UPDATEs) en la base de datos. | ||||
|         await dbContext.SaveChangesAsync(stoppingToken); | ||||
|         // Confirmamos la transacción para hacer los cambios permanentes. | ||||
|         await transaction.CommitAsync(stoppingToken); | ||||
|  | ||||
|         _logger.LogInformation("Sondeo de Resumen Provincial completado. Las tablas han sido actualizadas."); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         // 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."); | ||||
|       } | ||||
|     } | ||||
|     catch (OperationCanceledException) | ||||
|     { | ||||
|       _logger.LogInformation("Sondeo de resumen provincial cancelado."); | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|       // 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."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <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."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <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. | ||||
| @@ -286,7 +616,7 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|         { | ||||
|           hasReceivedAnyNewData = true; | ||||
|  | ||||
|           // --- CORRECCIÓN DE SEGURIDAD: Usar TryParse para la fecha --- | ||||
|           // --- SEGURIDAD: Usar TryParse para la fecha --- | ||||
|           DateTime fechaTotalizacion; | ||||
|           if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) | ||||
|           { | ||||
| @@ -326,7 +656,7 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|           { | ||||
|             hasReceivedAnyNewData = true; | ||||
|  | ||||
|             // --- APLICAMOS LA MISMA CORRECCIÓN DE SEGURIDAD AQUÍ --- | ||||
|             // --- APLICAMOS SEGURIDAD AQUÍ --- | ||||
|             DateTime fechaTotalizacion; | ||||
|             if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) | ||||
|             { | ||||
| @@ -439,7 +769,6 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|                 var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); | ||||
|                 if (telegramaFile != null) | ||||
|                 { | ||||
|                   // --- INICIO DE LA CORRECCIÓN --- | ||||
|                   // 1. Buscamos el AmbitoGeografico específico de la MESA que estamos procesando. | ||||
|                   var ambitoMesa = await innerDbContext.AmbitosGeograficos | ||||
|                       .AsNoTracking() | ||||
| @@ -463,7 +792,6 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|                   { | ||||
|                     _logger.LogWarning("No se encontró un ámbito geográfico para la mesa con MesaId {MesaId}. El telegrama no será guardado.", mesaId); | ||||
|                   } | ||||
|                   // --- FIN DE LA CORRECCIÓN --- | ||||
|                 } | ||||
|                 await Task.Delay(250, stoppingToken); | ||||
|               } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user