Fix Cambios de optimizaciones
This commit is contained in:
		| @@ -72,11 +72,17 @@ public class ElectoralApiService : IElectoralApiService | |||||||
|         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResultadosDto>() : null; |         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResultadosDto>() : null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string seccionId, int categoriaId) |     public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId) | ||||||
|     { |     { | ||||||
|         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|         // Usamos la categoriaId recibida en lugar de una fija |         var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}"; | ||||||
|         var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&seccionId={seccionId}&categoriaId={categoriaId}"; |  | ||||||
|  |         // Añadimos el seccionProvincialId a la URL SÓLO si tiene un valor. | ||||||
|  |         if (!string.IsNullOrEmpty(seccionProvincialId)) | ||||||
|  |         { | ||||||
|  |             requestUri += $"&seccionProvincialId={seccionProvincialId}"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         var request = new HttpRequestMessage(HttpMethod.Get, requestUri); |         var request = new HttpRequestMessage(HttpMethod.Get, requestUri); | ||||||
|         request.Headers.Add("Authorization", $"Bearer {authToken}"); |         request.Headers.Add("Authorization", $"Bearer {authToken}"); | ||||||
|         var response = await client.SendAsync(request); |         var response = await client.SendAsync(request); | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ public interface IElectoralApiService | |||||||
|     Task<CatalogoDto?> GetCatalogoAmbitosAsync(string authToken, int categoriaId); |     Task<CatalogoDto?> GetCatalogoAmbitosAsync(string authToken, int categoriaId); | ||||||
|     Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId); |     Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId); | ||||||
|     Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string seccionId, string? municipioId, int categoriaId); |     Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string seccionId, string? municipioId, int categoriaId); | ||||||
|     Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string seccionId, int categoriaId); |     Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId); | ||||||
|     Task<List<string[]>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = null); |     Task<List<string[]>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = null); | ||||||
|     Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId); |     Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId); | ||||||
|     Task<ResumenDto?> GetResumenAsync(string authToken, string distritoId); |     Task<ResumenDto?> GetResumenAsync(string authToken, string distritoId); | ||||||
|   | |||||||
| @@ -83,137 +83,159 @@ public class Worker : BackgroundService | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Descarga y sincroniza los catálogos base (Categorías, Ámbitos, Agrupaciones) | /// Descarga y sincroniza los catálogos base (Categorías, Ámbitos, Agrupaciones) | ||||||
|     /// desde la API a la base de datos local. | /// desde la API a la base de datos local. Se ejecuta una sola vez al iniciar el worker. | ||||||
|     /// </summary> | /// Utiliza una estrategia de guardado en lotes para manejar grandes volúmenes de datos | ||||||
|     private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingToken) | /// sin sobrecargar la base de datos. | ||||||
|  | /// </summary> | ||||||
|  | /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||||
|  | private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingToken) | ||||||
|  | { | ||||||
|  |     try | ||||||
|     { |     { | ||||||
|         try |         _logger.LogInformation("Iniciando sincronización de catálogos maestros..."); | ||||||
|  |          | ||||||
|  |         // PASO 1: Obtener el token de autenticación. Sin él, no podemos hacer nada. | ||||||
|  |         var authToken = await _apiService.GetAuthTokenAsync(); | ||||||
|  |         if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested) | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("Iniciando sincronización de catálogos maestros..."); |             _logger.LogError("No se pudo obtener token para la sincronización de catálogos. La operación se cancela."); | ||||||
|             var authToken = await _apiService.GetAuthTokenAsync(); |             return; | ||||||
|             if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested) |         } | ||||||
|  |  | ||||||
|  |         // Creamos un scope de servicios para obtener una instancia fresca de DbContext. | ||||||
|  |         using var scope = _serviceProvider.CreateScope(); | ||||||
|  |         var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|  |         // PASO 2: Sincronizar las categorías electorales. | ||||||
|  |         // Es un catálogo pequeño y es la base para las siguientes consultas. | ||||||
|  |         var categoriasApi = await _apiService.GetCategoriasAsync(authToken); | ||||||
|  |         if (categoriasApi is null || !categoriasApi.Any()) | ||||||
|  |         { | ||||||
|  |             _logger.LogWarning("La API no devolvió datos para el catálogo de Categorías. La sincronización no puede continuar."); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList(); | ||||||
|  |         _logger.LogInformation("Se procesarán {count} categorías electorales.", distinctCategorias.Count); | ||||||
|  |  | ||||||
|  |         var categoriasEnDb = await dbContext.CategoriasElectorales.ToDictionaryAsync(c => c.Id, c => c, stoppingToken); | ||||||
|  |         foreach (var categoriaDto in distinctCategorias) | ||||||
|  |         { | ||||||
|  |             if (!categoriasEnDb.ContainsKey(categoriaDto.CategoriaId)) | ||||||
|             { |             { | ||||||
|                 _logger.LogError("No se pudo obtener token para la sincronización de catálogos."); |                 dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden }); | ||||||
|                 return; |  | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|  |         // Guardamos las categorías primero para asegurar su existencia. | ||||||
|  |         await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|  |  | ||||||
|             using var scope = _serviceProvider.CreateScope(); |         // PASO 3: Cargar los catálogos existentes en memoria para una comparación eficiente. | ||||||
|             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |         // Esto evita hacer miles de consultas a la BD dentro de un bucle. | ||||||
|          |          | ||||||
|             // --- 1. SINCRONIZAR CATEGORÍAS ELECTORALES --- |         // Para los ámbitos, creamos una clave única robusta que funciona incluso con campos nulos. | ||||||
|             var categoriasApi = await _apiService.GetCategoriasAsync(authToken); |         var ambitosEnDb = new Dictionary<string, AmbitoGeografico>(); | ||||||
|             if (categoriasApi is null || !categoriasApi.Any()) |         var todosLosAmbitos = await dbContext.AmbitosGeograficos.ToListAsync(stoppingToken); | ||||||
|  |         foreach (var ambito in todosLosAmbitos) | ||||||
|  |         { | ||||||
|  |             string clave = $"{ambito.NivelId}|{ambito.DistritoId}|{ambito.SeccionProvincialId}|{ambito.SeccionId}|{ambito.MunicipioId}|{ambito.CircuitoId}|{ambito.EstablecimientoId}|{ambito.MesaId}"; | ||||||
|  |             if (!ambitosEnDb.ContainsKey(clave)) | ||||||
|             { |             { | ||||||
|                 _logger.LogWarning("No se recibieron datos del catálogo de Categorías."); |                 ambitosEnDb.Add(clave, ambito); | ||||||
|                 return; |  | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|             var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList(); |         var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); | ||||||
|             _logger.LogInformation("Se procesarán {count} categorías electorales.", distinctCategorias.Count); |  | ||||||
|          |          | ||||||
|             var categoriasEnDb = await dbContext.CategoriasElectorales.ToDictionaryAsync(c => c.Id, c => c, stoppingToken); |         // Variable para llevar la cuenta del total de registros insertados. | ||||||
|             foreach (var categoriaDto in distinctCategorias) |         int totalCambiosGuardados = 0; | ||||||
|  |  | ||||||
|  |         // PASO 4: Iterar sobre cada categoría para sincronizar sus ámbitos y agrupaciones. | ||||||
|  |         foreach (var categoria in distinctCategorias) | ||||||
|  |         { | ||||||
|  |             if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |             _logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId); | ||||||
|  |  | ||||||
|  |             var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId); | ||||||
|  |             if (catalogoDto != null) | ||||||
|             { |             { | ||||||
|                 if (!categoriasEnDb.ContainsKey(categoriaDto.CategoriaId)) |                 // 4.1 - Procesar y añadir ÁMBITOS nuevos al DbContext | ||||||
|  |                 foreach (var ambitoDto in catalogoDto.Ambitos) | ||||||
|                 { |                 { | ||||||
|                     dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden }); |                     string claveUnica = $"{ambitoDto.NivelId}|{ambitoDto.CodigoAmbitos.DistritoId}|{ambitoDto.CodigoAmbitos.SeccionProvincialId}|{ambitoDto.CodigoAmbitos.SeccionId}|{ambitoDto.CodigoAmbitos.MunicipioId}|{ambitoDto.CodigoAmbitos.CircuitoId}|{ambitoDto.CodigoAmbitos.EstablecimientoId}|{ambitoDto.CodigoAmbitos.MesaId}"; | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             await dbContext.SaveChangesAsync(stoppingToken); |  | ||||||
|  |  | ||||||
|             // --- 2. SINCRONIZAR ÁMBITOS Y AGRUPACIONES POR CADA CATEGORÍA --- |                     if (!ambitosEnDb.ContainsKey(claveUnica)) | ||||||
|  |  | ||||||
|             var ambitosEnDb = new Dictionary<string, AmbitoGeografico>(); |  | ||||||
|             var todosLosAmbitos = await dbContext.AmbitosGeograficos.ToListAsync(stoppingToken); |  | ||||||
|  |  | ||||||
|             foreach (var ambito in todosLosAmbitos) |  | ||||||
|             { |  | ||||||
|                 // Creamos una clave única que SIEMPRE funciona, incluso con nulos. |  | ||||||
|                 string clave = $"{ambito.NivelId}|{ambito.DistritoId}|{ambito.SeccionProvincialId}|{ambito.SeccionId}|{ambito.MunicipioId}|{ambito.CircuitoId}|{ambito.EstablecimientoId}|{ambito.MesaId}"; |  | ||||||
|                 if (!ambitosEnDb.ContainsKey(clave)) |  | ||||||
|                 { |  | ||||||
|                     ambitosEnDb.Add(clave, ambito); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); |  | ||||||
|  |  | ||||||
|             foreach (var categoria in distinctCategorias) |  | ||||||
|             { |  | ||||||
|                 if (stoppingToken.IsCancellationRequested) break; |  | ||||||
|                 _logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId); |  | ||||||
|  |  | ||||||
|                 var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId); |  | ||||||
|                 if (catalogoDto != null) |  | ||||||
|                 { |  | ||||||
|                     foreach (var ambitoDto in catalogoDto.Ambitos) |  | ||||||
|                     { |                     { | ||||||
|                         // Volvemos a generar la misma clave única para la comparación |                         var nuevoAmbito = new AmbitoGeografico | ||||||
|                         string claveUnica = $"{ambitoDto.NivelId}|{ambitoDto.CodigoAmbitos.DistritoId}|{ambitoDto.CodigoAmbitos.SeccionProvincialId}|{ambitoDto.CodigoAmbitos.SeccionId}|{ambitoDto.CodigoAmbitos.MunicipioId}|{ambitoDto.CodigoAmbitos.CircuitoId}|{ambitoDto.CodigoAmbitos.EstablecimientoId}|{ambitoDto.CodigoAmbitos.MesaId}"; |  | ||||||
|  |  | ||||||
|                         if (!ambitosEnDb.ContainsKey(claveUnica)) |  | ||||||
|                         { |                         { | ||||||
|                             var nuevoAmbito = new AmbitoGeografico |                             Nombre = ambitoDto.Nombre, | ||||||
|                             { |                             NivelId = ambitoDto.NivelId, | ||||||
|                                 Nombre = ambitoDto.Nombre, |                             DistritoId = ambitoDto.CodigoAmbitos.DistritoId, | ||||||
|                                 NivelId = ambitoDto.NivelId, |                             SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId, | ||||||
|                                 DistritoId = ambitoDto.CodigoAmbitos.DistritoId, |                             SeccionId = ambitoDto.CodigoAmbitos.SeccionId, | ||||||
|                                 SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId, |                             MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId, | ||||||
|                                 SeccionId = ambitoDto.CodigoAmbitos.SeccionId, |                             CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId, | ||||||
|                                 MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId, |                             EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId, | ||||||
|                                 CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId, |                             MesaId = ambitoDto.CodigoAmbitos.MesaId, | ||||||
|                                 EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId, |                         }; | ||||||
|                                 MesaId = ambitoDto.CodigoAmbitos.MesaId, |                         dbContext.AmbitosGeograficos.Add(nuevoAmbito); | ||||||
|                             }; |                         ambitosEnDb.Add(claveUnica, nuevoAmbito); // Añadir también al diccionario en memoria | ||||||
|                             dbContext.AmbitosGeograficos.Add(nuevoAmbito); |  | ||||||
|                             ambitosEnDb.Add(claveUnica, nuevoAmbito); |  | ||||||
|                         } |  | ||||||
|                     } |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                     // Lógica para sincronizar AGRUPACIONES POLÍTICAS |                 // 4.2 - Procesar y añadir AGRUPACIONES nuevas al DbContext | ||||||
|                     var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10); |                 var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10); | ||||||
|                     if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId)) |                 if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId)) | ||||||
|  |                 { | ||||||
|  |                     // Usamos un try-catch porque no todas las categorías tienen agrupaciones a nivel provincial. | ||||||
|  |                     try | ||||||
|                     { |                     { | ||||||
|                         try |                         var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId); | ||||||
|  |                         if (agrupacionesApi != null && agrupacionesApi.Any()) | ||||||
|                         { |                         { | ||||||
|                             var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId); |                             foreach (var agrupacionDto in agrupacionesApi) | ||||||
|                             if (agrupacionesApi != null && agrupacionesApi.Any()) |  | ||||||
|                             { |                             { | ||||||
|                                 foreach (var agrupacionDto in agrupacionesApi) |                                 if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion)) | ||||||
|                                 { |                                 { | ||||||
|                                     if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion)) |                                     var nuevaAgrupacion = new AgrupacionPolitica | ||||||
|                                     { |                                     { | ||||||
|                                         var nuevaAgrupacion = new AgrupacionPolitica |                                         Id = agrupacionDto.IdAgrupacion, | ||||||
|                                         { |                                         IdTelegrama = agrupacionDto.IdAgrupacionTelegrama, | ||||||
|                                             Id = agrupacionDto.IdAgrupacion, |                                         Nombre = agrupacionDto.NombreAgrupacion | ||||||
|                                             IdTelegrama = agrupacionDto.IdAgrupacionTelegrama, |                                     }; | ||||||
|                                             Nombre = agrupacionDto.NombreAgrupacion |                                     dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion); | ||||||
|                                         }; |                                     agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); | ||||||
|                                         dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion); |  | ||||||
|                                         agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); |  | ||||||
|                                     } |  | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                         catch (Exception ex) |                     } | ||||||
|                         { |                     catch (Exception ex) | ||||||
|                             _logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}). Esto puede ser normal si la categoría no aplica a nivel provincial.", categoria.Nombre, categoria.CategoriaId); |                     { | ||||||
|                         } |                         _logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}).", categoria.Nombre, categoria.CategoriaId); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // --- 3. GUARDADO FINAL --- |             // Después de procesar todos los ámbitos y agrupaciones de UNA categoría, guardamos los cambios. | ||||||
|             int cambiosGuardados = await dbContext.SaveChangesAsync(stoppingToken); |             // Esto divide la inserción masiva de ~50,000 registros en 3 transacciones más pequeñas, | ||||||
|             _logger.LogInformation("{count} nuevos registros de catálogo han sido guardados en la base de datos.", cambiosGuardados); |             // evitando timeouts y fallos en la base de datos. | ||||||
|             _logger.LogInformation("Sincronización de catálogos maestros finalizada."); |             if (dbContext.ChangeTracker.HasChanges()) | ||||||
|         } |             { | ||||||
|         catch (Exception ex) |                 int cambiosEnLote = await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|         { |                 totalCambiosGuardados += cambiosEnLote; | ||||||
|             _logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos."); |                 _logger.LogInformation("Guardados {count} registros de catálogo para la categoría '{catNombre}'.", cambiosEnLote, categoria.Nombre); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // Ya no hay un SaveChangesAsync() gigante aquí. | ||||||
|  |         _logger.LogInformation("{count} nuevos registros de catálogo han sido guardados en total.", totalCambiosGuardados); | ||||||
|  |         _logger.LogInformation("Sincronización de catálogos maestros finalizada."); | ||||||
|     } |     } | ||||||
|  |     catch (Exception ex) | ||||||
|  |     { | ||||||
|  |         _logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos."); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|     // El resto de los métodos (SondearResultadosMunicipalesAsync, GuardarResultadosDeAmbitoAsync, etc.) |     // El resto de los métodos (SondearResultadosMunicipalesAsync, GuardarResultadosDeAmbitoAsync, etc.) | ||||||
|     // se mantienen como en la versión anterior que te proporcioné. Los incluyo aquí para |     // se mantienen como en la versión anterior que te proporcioné. Los incluyo aquí para | ||||||
| @@ -225,57 +247,50 @@ public class Worker : BackgroundService | |||||||
|             using var scope = _serviceProvider.CreateScope(); |             using var scope = _serviceProvider.CreateScope(); | ||||||
|             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|             // Cambiamos la búsqueda a NivelId = 30, que según la API |  | ||||||
|             // son los registros de "Sección" (Partidos/Municipios). |  | ||||||
|             var municipiosASondear = await dbContext.AmbitosGeograficos |             var municipiosASondear = await dbContext.AmbitosGeograficos | ||||||
|             .AsNoTracking() |                 .AsNoTracking() | ||||||
|             .Where(a => a.NivelId == 30 && a.MunicipioId != null && a.DistritoId != null && a.SeccionId != null) |                 .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) | ||||||
|             .Select(a => new { a.Id, a.Nombre, a.MunicipioId, a.SeccionId, a.DistritoId }) |                 // El MunicipioId es opcional en la BD, lo quitamos del Where para asegurar que traiga todos los partidos | ||||||
|             .ToListAsync(stoppingToken); |                 .Select(a => new { a.Id, a.Nombre, a.MunicipioId, a.SeccionId, a.DistritoId }) | ||||||
|  |                 .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|             if (!municipiosASondear.Any()) |             if (!municipiosASondear.Any()) | ||||||
|             { |             { | ||||||
|                 // Este log ahora mostrará 'NivelId 30' si falla. |  | ||||||
|                 _logger.LogWarning("No se encontraron Partidos (NivelId 30) en la BD para sondear resultados."); |                 _logger.LogWarning("No se encontraron Partidos (NivelId 30) en la BD para sondear resultados."); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             _logger.LogInformation("Iniciando sondeo de resultados para {count} municipios (Partidos)...", municipiosASondear.Count); |             _logger.LogInformation("Iniciando sondeo de resultados para {count} municipios (Partidos)...", municipiosASondear.Count); | ||||||
|  |  | ||||||
|  |             var categoriaConcejales = await dbContext.CategoriasElectorales | ||||||
|  |                 .AsNoTracking() | ||||||
|  |                 .FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken); | ||||||
|  |  | ||||||
|  |             if (categoriaConcejales == null) | ||||||
|  |             { | ||||||
|  |                 _logger.LogWarning("No se encontró la categoría 'CONCEJALES'. Omitiendo sondeo de resultados municipales."); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var todosLosResultados = new Dictionary<int, Elecciones.Core.DTOs.ResultadosDto>(); | ||||||
|             foreach (var municipio in municipiosASondear) |             foreach (var municipio in municipiosASondear) | ||||||
|             { |             { | ||||||
|                 if (stoppingToken.IsCancellationRequested) break; |                 if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |  | ||||||
|                 var categoriaConcejales = await dbContext.CategoriasElectorales |                 var resultados = await _apiService.GetResultadosAsync( | ||||||
|                     .AsNoTracking() |                     authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaConcejales.Id | ||||||
|                     .FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken); |                 ); | ||||||
|  |  | ||||||
|                 if (categoriaConcejales != null) |                 if (resultados != null) | ||||||
|                 { |                 { | ||||||
|                     var resultados = await _apiService.GetResultadosAsync( |                     todosLosResultados[municipio.Id] = resultados; | ||||||
|                         authToken, |  | ||||||
|                         municipio.DistritoId!, |  | ||||||
|                         municipio.SeccionId!, |  | ||||||
|                         null, |  | ||||||
|                         categoriaConcejales.Id |  | ||||||
|                     ); |  | ||||||
|  |  | ||||||
|                     if (resultados != null) |  | ||||||
|                     { |  | ||||||
|                         try |  | ||||||
|                         { |  | ||||||
|                             await GuardarResultadosDeAmbitoAsync(dbContext, municipio.Id, resultados, stoppingToken); |  | ||||||
|                             // Ahora 'municipio.Nombre' existe y el log funcionará |  | ||||||
|                             _logger.LogInformation("Resultados para el municipio '{nombre}' (ID: {id}) guardados/actualizados.", municipio.Nombre, municipio.MunicipioId); |  | ||||||
|                         } |  | ||||||
|                         catch (Exception ex) |  | ||||||
|                         { |  | ||||||
|                             // Y aquí también funcionará, dándonos un error mucho más útil |  | ||||||
|                             _logger.LogError(ex, "FALLO CRÍTICO al guardar resultados para el municipio '{nombre}' (ID: {id}).", municipio.Nombre, municipio.MunicipioId); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             if (todosLosResultados.Any()) | ||||||
|  |             { | ||||||
|  |                 // La llamada ahora es correcta porque el método receptor espera 3 argumentos | ||||||
|  |                 await GuardarResultadosDeMunicipiosAsync(dbContext, todosLosResultados, stoppingToken); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
| @@ -283,173 +298,201 @@ public class Worker : BackgroundService | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async Task GuardarResultadosDeAmbitoAsync(EleccionesDbContext dbContext, int ambitoId, Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) |     /// Realiza una operación "Upsert" (Update o Insert) de forma masiva y optimizada. | ||||||
|  |     /// Este método es llamado por SondearResultadosMunicipalesAsync. | ||||||
|  |     /// </summary> | ||||||
|  |     private async Task GuardarResultadosDeMunicipiosAsync( | ||||||
|  |         EleccionesDbContext dbContext, | ||||||
|  |         Dictionary<int, Elecciones.Core.DTOs.ResultadosDto> todosLosResultados, | ||||||
|  |         CancellationToken stoppingToken) // <-- PARÁMETRO AÑADIDO | ||||||
|     { |     { | ||||||
|         var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId }, cancellationToken: stoppingToken); |         // Obtenemos los IDs de todos los ámbitos que vamos a actualizar. | ||||||
|         if (estadoRecuento == null) |         var ambitoIds = todosLosResultados.Keys; | ||||||
|  |  | ||||||
|  |         // --- OPTIMIZACIÓN 1: Cargar todos los datos existentes en memoria UNA SOLA VEZ --- | ||||||
|  |         var estadosRecuentoExistentes = await dbContext.EstadosRecuentos | ||||||
|  |             .Where(e => ambitoIds.Contains(e.AmbitoGeograficoId)) | ||||||
|  |             .ToDictionaryAsync(e => e.AmbitoGeograficoId, stoppingToken); | ||||||
|  |  | ||||||
|  |         var resultadosVotosExistentes = await dbContext.ResultadosVotos | ||||||
|  |             .Where(rv => ambitoIds.Contains(rv.AmbitoGeograficoId)) | ||||||
|  |             .GroupBy(rv => rv.AmbitoGeograficoId) | ||||||
|  |             .ToDictionaryAsync(g => g.Key, g => g.ToDictionary(item => item.AgrupacionPoliticaId), stoppingToken); | ||||||
|  |  | ||||||
|  |         _logger.LogInformation("Procesando en memoria los resultados de {count} municipios.", todosLosResultados.Count); | ||||||
|  |  | ||||||
|  |         // --- OPTIMIZACIÓN 2: Procesar todo en memoria --- | ||||||
|  |         foreach (var kvp in todosLosResultados) | ||||||
|         { |         { | ||||||
|             estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId }; |             var ambitoId = kvp.Key; | ||||||
|             dbContext.EstadosRecuentos.Add(estadoRecuento); |             var resultadosDto = kvp.Value; | ||||||
|         } |  | ||||||
|  |  | ||||||
|         estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime(); |             // Lógica Upsert para EstadoRecuento | ||||||
|         estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; |             if (!estadosRecuentoExistentes.TryGetValue(ambitoId, out var estadoRecuento)) | ||||||
|         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.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion, |  | ||||||
|                 stoppingToken |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|             if (resultadoVoto == null) |  | ||||||
|             { |             { | ||||||
|                 resultadoVoto = new ResultadoVoto |                 estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId }; | ||||||
|                 { |                 dbContext.EstadosRecuentos.Add(estadoRecuento); | ||||||
|                     AmbitoGeograficoId = ambitoId, |             } | ||||||
|                     AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion |  | ||||||
|                 }; |             // Mapeo completo de propiedades para EstadoRecuento | ||||||
|                 dbContext.ResultadosVotos.Add(resultadoVoto); |             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; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Lógica Upsert para ResultadosVotos | ||||||
|  |             var votosDeAmbitoExistentes = resultadosVotosExistentes.GetValueOrDefault(ambitoId); | ||||||
|  |             foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos) | ||||||
|  |             { | ||||||
|  |                 ResultadoVoto? resultadoVoto = null; | ||||||
|  |                 if (votosDeAmbitoExistentes != null) | ||||||
|  |                 { | ||||||
|  |                     votosDeAmbitoExistentes.TryGetValue(votoPositivoDto.IdAgrupacion, out resultadoVoto); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if (resultadoVoto == null) | ||||||
|  |                 { | ||||||
|  |                     resultadoVoto = new ResultadoVoto | ||||||
|  |                     { | ||||||
|  |                         AmbitoGeograficoId = ambitoId, | ||||||
|  |                         AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion | ||||||
|  |                     }; | ||||||
|  |                     dbContext.ResultadosVotos.Add(resultadoVoto); | ||||||
|  |                 } | ||||||
|  |                 resultadoVoto.CantidadVotos = votoPositivoDto.Votos; | ||||||
|  |                 resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje; | ||||||
|             } |             } | ||||||
|             resultadoVoto.CantidadVotos = votoPositivoDto.Votos; |  | ||||||
|             resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // --- OPTIMIZACIÓN 3: Guardar todos los cambios en UNA SOLA TRANSACCIÓN --- | ||||||
|  |         _logger.LogInformation("Guardando todos los cambios de resultados municipales en la base de datos..."); | ||||||
|  |         // Ahora 'stoppingToken' es reconocido aquí | ||||||
|         await dbContext.SaveChangesAsync(stoppingToken); |         await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|  |         _logger.LogInformation("Guardado completado."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Sondea la proyección de bancas para diputados y senadores. |     /// Sondea la proyección de bancas. Este método ahora es más completo: | ||||||
|     /// Este método busca dinámicamente en la base de datos las categorías relevantes (Senadores/Diputados) |     /// 1. Consulta el reparto de bancas a nivel PROVINCIAL para cada categoría. | ||||||
|     /// y los ámbitos de "Sección Electoral" (NivelId = 20), que es el nivel al que se reparten las bancas. |     /// 2. Consulta el reparto de bancas desglosado por SECCIÓN ELECTORAL para cada categoría. | ||||||
|     /// Luego, consulta la API para cada combinación y actualiza la tabla de proyecciones. |  | ||||||
|     /// </summary> |     /// </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) |     private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) | ||||||
|     { |     { | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             // PASO 1: Preparar el entorno |  | ||||||
|             // Creamos un scope de DbContext para esta operación específica, una buena práctica en servicios de larga duración. |  | ||||||
|             using var scope = _serviceProvider.CreateScope(); |             using var scope = _serviceProvider.CreateScope(); | ||||||
|             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|             // PASO 2: Obtener las categorías que reparten bancas (Senadores y Diputados) |  | ||||||
|             // Hacemos una consulta a nuestra tabla local de categorías para que el código sea dinámico |  | ||||||
|             // y no dependa de IDs fijos (hardcodeados). |  | ||||||
|             var categoriasDeBancas = await dbContext.CategoriasElectorales |             var categoriasDeBancas = await dbContext.CategoriasElectorales | ||||||
|                 .AsNoTracking() // Optimización de rendimiento: solo vamos a leer estos datos. |                 .AsNoTracking() | ||||||
|                 .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) |                 .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) | ||||||
|                 .ToListAsync(stoppingToken); |                 .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|             // Si por alguna razón estas categorías no están en la BD, no podemos continuar. |             var provincia = await dbContext.AmbitosGeograficos | ||||||
|             if (!categoriasDeBancas.Any()) |                 .AsNoTracking() | ||||||
|             { |                 .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); | ||||||
|                 _logger.LogWarning("No se encontraron categorías para 'Senadores' o 'Diputados' en la BD. Omitiendo sondeo de bancas."); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // PASO 3: Obtener las "Secciones Electorales" (NivelId 20) |  | ||||||
|             // Esta es la corrección clave. Basado en la respuesta real de la API, las Secciones Electorales |  | ||||||
|             // (Primera, Segunda, Tercera, etc.) usan NivelId = 20. |  | ||||||
|             var seccionesElectorales = await dbContext.AmbitosGeograficos |             var seccionesElectorales = await dbContext.AmbitosGeograficos | ||||||
|                 .AsNoTracking() |                 .AsNoTracking() | ||||||
|                 .Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null) |                 .Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null) | ||||||
|                 .ToListAsync(stoppingToken); |                 .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|             // Si no se encuentra ninguna Sección Electoral en la BD (lo cual sería raro después de una sincronización exitosa), |             if (!categoriasDeBancas.Any() || provincia == null) | ||||||
|             // registramos una advertencia y salimos. |  | ||||||
|             if (!seccionesElectorales.Any()) |  | ||||||
|             { |             { | ||||||
|                 _logger.LogWarning("No se encontraron ámbitos de tipo 'Sección Electoral' (NivelId 20) en la BD para sondear bancas."); |                 _logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo."); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             _logger.LogInformation("Iniciando sondeo de Bancas para {count} secciones electorales y {catCount} categorías...", seccionesElectorales.Count, categoriasDeBancas.Count); |             _logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial y para {count} Secciones Electorales...", seccionesElectorales.Count); | ||||||
|  |  | ||||||
|             // PASO 4: Iterar, consultar la API y preparar los datos para guardar |  | ||||||
|  |  | ||||||
|             // Esta bandera es crucial para la estrategia de "borrar y reemplazar". |  | ||||||
|             // Nos asegura que la tabla de proyecciones se vacía UNA SOLA VEZ, justo antes de insertar |  | ||||||
|             // el primer lote de datos nuevos, evitando datos inconsistentes. |  | ||||||
|             bool hasReceivedAnyNewData = false; |             bool hasReceivedAnyNewData = false; | ||||||
|  |             var nuevasProyecciones = new List<ProyeccionBanca>(); | ||||||
|  |  | ||||||
|             // Bucle externo: recorremos cada una de las 8 Secciones Electorales. |             // --- NUEVA LÓGICA: Bucle para el nivel Provincial --- | ||||||
|  |             foreach (var categoria in categoriasDeBancas) | ||||||
|  |             { | ||||||
|  |                 if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |  | ||||||
|  |                 // Llamamos a la API sin 'seccionProvincialId' para obtener el total provincial. | ||||||
|  |                 var repartoBancas = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id); | ||||||
|  |  | ||||||
|  |                 if (repartoBancas?.RepartoBancas is { Count: > 0 }) | ||||||
|  |                 { | ||||||
|  |                     hasReceivedAnyNewData = true; | ||||||
|  |                     foreach (var banca in repartoBancas.RepartoBancas) | ||||||
|  |                     { | ||||||
|  |                         // Guardamos la proyección asociándola al ID del ámbito de la provincia. | ||||||
|  |                         nuevasProyecciones.Add(new ProyeccionBanca | ||||||
|  |                         { | ||||||
|  |                             AmbitoGeograficoId = provincia.Id, | ||||||
|  |                             AgrupacionPoliticaId = banca.IdAgrupacion, | ||||||
|  |                             NroBancas = banca.NroBancas | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // --- LÓGICA EXISTENTE: Bucle para el nivel de Sección Electoral --- | ||||||
|             foreach (var seccion in seccionesElectorales) |             foreach (var seccion in seccionesElectorales) | ||||||
|             { |             { | ||||||
|                 if (stoppingToken.IsCancellationRequested) break; // Salida limpia si la aplicación se detiene. |                 if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |  | ||||||
|                 // Bucle interno: para cada sección, consultamos las bancas de Senadores y Diputados. |  | ||||||
|                 foreach (var categoria in categoriasDeBancas) |                 foreach (var categoria in categoriasDeBancas) | ||||||
|                 { |                 { | ||||||
|                     if (stoppingToken.IsCancellationRequested) break; |                     if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |  | ||||||
|                     // Llamamos a la API. El endpoint 'getBancas' requiere 'distritoId' y 'seccionProvincialId', |  | ||||||
|                     // que son precisamente los datos que tenemos en nuestros ámbitos de NivelId = 20. |  | ||||||
|                     var repartoBancas = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id); |                     var repartoBancas = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id); | ||||||
|  |  | ||||||
|                     // Verificamos que la respuesta de la API no sea nula y que contenga al menos una banca repartida. |  | ||||||
|                     if (repartoBancas?.RepartoBancas is { Count: > 0 }) |                     if (repartoBancas?.RepartoBancas is { Count: > 0 }) | ||||||
|                     { |                     { | ||||||
|                         // Si esta es la PRIMERA VEZ en todo el sondeo que recibimos datos válidos... |                         hasReceivedAnyNewData = true; | ||||||
|                         if (!hasReceivedAnyNewData) |  | ||||||
|                         { |  | ||||||
|                             _logger.LogInformation("Se recibieron nuevos datos de bancas. Limpiando la tabla de proyecciones para la actualización..."); |  | ||||||
|                             // ...ejecutamos un comando SQL para borrar todos los datos viejos de la tabla. |  | ||||||
|                             await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken); |  | ||||||
|                             // Activamos la bandera para no volver a ejecutar este borrado. |  | ||||||
|                             hasReceivedAnyNewData = true; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         // Procesamos cada banca obtenida en la respuesta de la API. |  | ||||||
|                         foreach (var banca in repartoBancas.RepartoBancas) |                         foreach (var banca in repartoBancas.RepartoBancas) | ||||||
|                         { |                         { | ||||||
|                             // Creamos una nueva entidad 'ProyeccionBanca'. |                             nuevasProyecciones.Add(new ProyeccionBanca | ||||||
|                             var nuevaProyeccion = new ProyeccionBanca |  | ||||||
|                             { |                             { | ||||||
|                                 AmbitoGeograficoId = seccion.Id, |                                 AmbitoGeograficoId = seccion.Id, | ||||||
|                                 AgrupacionPoliticaId = banca.IdAgrupacion, |                                 AgrupacionPoliticaId = banca.IdAgrupacion, | ||||||
|                                 NroBancas = banca.NroBancas |                                 NroBancas = banca.NroBancas | ||||||
|                             }; |                             }); | ||||||
|                             // Y la añadimos al ChangeTracker de EF para que la inserte. |  | ||||||
|                             await dbContext.ProyeccionesBancas.AddAsync(nuevaProyeccion, stoppingToken); |  | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // PASO 5: Guardar los cambios en la Base de Datos |             // --- LÓGICA DE GUARDADO CENTRALIZADA --- | ||||||
|             // Si la bandera 'hasReceivedAnyNewData' se activó, significa que hemos añadido nuevas proyecciones |  | ||||||
|             // al DbContext y necesitamos persistirlas. |  | ||||||
|             if (hasReceivedAnyNewData) |             if (hasReceivedAnyNewData) | ||||||
|             { |             { | ||||||
|  |                 _logger.LogInformation("Se recibieron {count} nuevos datos de bancas. Actualizando la tabla de proyecciones...", nuevasProyecciones.Count); | ||||||
|  |  | ||||||
|  |                 // Usamos una transacción para asegurar la consistencia. | ||||||
|  |                 await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); | ||||||
|  |  | ||||||
|  |                 await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken); | ||||||
|  |                 await dbContext.ProyeccionesBancas.AddRangeAsync(nuevasProyecciones, stoppingToken); | ||||||
|                 await dbContext.SaveChangesAsync(stoppingToken); |                 await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|                 _logger.LogInformation("Sondeo de Bancas completado. La tabla de proyecciones ha sido actualizada con nuevos datos."); |                 await transaction.CommitAsync(stoppingToken); | ||||||
|  |  | ||||||
|  |                 _logger.LogInformation("Sondeo de Bancas completado. La tabla de proyecciones ha sido actualizada."); | ||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
|                 // Si no se recibieron datos nuevos, no hacemos nada en la BD. |  | ||||||
|                 _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada."); |                 _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada."); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|             // Capturamos cualquier error inesperado para que no detenga el worker por completo. |  | ||||||
|             _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas."); |             _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas."); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user