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,29 +83,36 @@ 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..."); |         _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(); |         var authToken = await _apiService.GetAuthTokenAsync(); | ||||||
|         if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested) |         if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested) | ||||||
|         { |         { | ||||||
|                 _logger.LogError("No se pudo obtener token para la sincronización de catálogos."); |             _logger.LogError("No se pudo obtener token para la sincronización de catálogos. La operación se cancela."); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // Creamos un scope de servicios para obtener una instancia fresca de DbContext. | ||||||
|         using var scope = _serviceProvider.CreateScope(); |         using var scope = _serviceProvider.CreateScope(); | ||||||
|         var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |         var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|             // --- 1. SINCRONIZAR CATEGORÍAS ELECTORALES --- |         // 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); |         var categoriasApi = await _apiService.GetCategoriasAsync(authToken); | ||||||
|         if (categoriasApi is null || !categoriasApi.Any()) |         if (categoriasApi is null || !categoriasApi.Any()) | ||||||
|         { |         { | ||||||
|                 _logger.LogWarning("No se recibieron datos del catálogo de Categorías."); |             _logger.LogWarning("La API no devolvió datos para el catálogo de Categorías. La sincronización no puede continuar."); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -120,16 +127,17 @@ public class Worker : BackgroundService | |||||||
|                 dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden }); |                 dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden }); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         // Guardamos las categorías primero para asegurar su existencia. | ||||||
|         await dbContext.SaveChangesAsync(stoppingToken); |         await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|  |  | ||||||
|             // --- 2. SINCRONIZAR ÁMBITOS Y AGRUPACIONES POR CADA CATEGORÍA --- |         // PASO 3: Cargar los catálogos existentes en memoria para una comparación eficiente. | ||||||
|  |         // Esto evita hacer miles de consultas a la BD dentro de un bucle. | ||||||
|          |          | ||||||
|  |         // Para los ámbitos, creamos una clave única robusta que funciona incluso con campos nulos. | ||||||
|         var ambitosEnDb = new Dictionary<string, AmbitoGeografico>(); |         var ambitosEnDb = new Dictionary<string, AmbitoGeografico>(); | ||||||
|         var todosLosAmbitos = await dbContext.AmbitosGeograficos.ToListAsync(stoppingToken); |         var todosLosAmbitos = await dbContext.AmbitosGeograficos.ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|         foreach (var ambito in todosLosAmbitos) |         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}"; |             string clave = $"{ambito.NivelId}|{ambito.DistritoId}|{ambito.SeccionProvincialId}|{ambito.SeccionId}|{ambito.MunicipioId}|{ambito.CircuitoId}|{ambito.EstablecimientoId}|{ambito.MesaId}"; | ||||||
|             if (!ambitosEnDb.ContainsKey(clave)) |             if (!ambitosEnDb.ContainsKey(clave)) | ||||||
|             { |             { | ||||||
| @@ -139,6 +147,10 @@ public class Worker : BackgroundService | |||||||
|  |  | ||||||
|         var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); |         var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); | ||||||
|          |          | ||||||
|  |         // Variable para llevar la cuenta del total de registros insertados. | ||||||
|  |         int totalCambiosGuardados = 0; | ||||||
|  |  | ||||||
|  |         // PASO 4: Iterar sobre cada categoría para sincronizar sus ámbitos y agrupaciones. | ||||||
|         foreach (var categoria in distinctCategorias) |         foreach (var categoria in distinctCategorias) | ||||||
|         { |         { | ||||||
|             if (stoppingToken.IsCancellationRequested) break; |             if (stoppingToken.IsCancellationRequested) break; | ||||||
| @@ -147,9 +159,9 @@ public class Worker : BackgroundService | |||||||
|             var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId); |             var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId); | ||||||
|             if (catalogoDto != null) |             if (catalogoDto != null) | ||||||
|             { |             { | ||||||
|  |                 // 4.1 - Procesar y añadir ÁMBITOS nuevos al DbContext | ||||||
|                 foreach (var ambitoDto in catalogoDto.Ambitos) |                 foreach (var ambitoDto in catalogoDto.Ambitos) | ||||||
|                 { |                 { | ||||||
|                         // Volvemos a generar la misma clave única para la comparación |  | ||||||
|                     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}"; |                     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)) |                     if (!ambitosEnDb.ContainsKey(claveUnica)) | ||||||
| @@ -167,14 +179,15 @@ public class Worker : BackgroundService | |||||||
|                             MesaId = ambitoDto.CodigoAmbitos.MesaId, |                             MesaId = ambitoDto.CodigoAmbitos.MesaId, | ||||||
|                         }; |                         }; | ||||||
|                         dbContext.AmbitosGeograficos.Add(nuevoAmbito); |                         dbContext.AmbitosGeograficos.Add(nuevoAmbito); | ||||||
|                             ambitosEnDb.Add(claveUnica, nuevoAmbito); |                         ambitosEnDb.Add(claveUnica, nuevoAmbito); // Añadir también al diccionario en memoria | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                     // 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); |                         var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId); | ||||||
| @@ -198,22 +211,31 @@ public class Worker : BackgroundService | |||||||
|                     } |                     } | ||||||
|                     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. | ||||||
|  |             if (dbContext.ChangeTracker.HasChanges()) | ||||||
|  |             { | ||||||
|  |                 int cambiosEnLote = await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|  |                 totalCambiosGuardados += cambiosEnLote; | ||||||
|  |                 _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."); |         _logger.LogInformation("Sincronización de catálogos maestros finalizada."); | ||||||
|     } |     } | ||||||
|     catch (Exception ex) |     catch (Exception ex) | ||||||
|     { |     { | ||||||
|         _logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos."); |         _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,56 +247,49 @@ 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) | ||||||
|  |                 // El MunicipioId es opcional en la BD, lo quitamos del Where para asegurar que traiga todos los partidos | ||||||
|                 .Select(a => new { a.Id, a.Nombre, a.MunicipioId, a.SeccionId, a.DistritoId }) |                 .Select(a => new { a.Id, a.Nombre, a.MunicipioId, a.SeccionId, a.DistritoId }) | ||||||
|                 .ToListAsync(stoppingToken); |                 .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); | ||||||
|  |  | ||||||
|             foreach (var municipio in municipiosASondear) |  | ||||||
|             { |  | ||||||
|                 if (stoppingToken.IsCancellationRequested) break; |  | ||||||
|  |  | ||||||
|             var categoriaConcejales = await dbContext.CategoriasElectorales |             var categoriaConcejales = await dbContext.CategoriasElectorales | ||||||
|                 .AsNoTracking() |                 .AsNoTracking() | ||||||
|                 .FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken); |                 .FirstOrDefaultAsync(c => c.Nombre.Contains("CONCEJALES"), stoppingToken); | ||||||
|  |  | ||||||
|                 if (categoriaConcejales != null) |             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) | ||||||
|  |             { | ||||||
|  |                 if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |  | ||||||
|                 var resultados = await _apiService.GetResultadosAsync( |                 var resultados = await _apiService.GetResultadosAsync( | ||||||
|                         authToken, |                     authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoriaConcejales.Id | ||||||
|                         municipio.DistritoId!, |  | ||||||
|                         municipio.SeccionId!, |  | ||||||
|                         null, |  | ||||||
|                         categoriaConcejales.Id |  | ||||||
|                 ); |                 ); | ||||||
|  |  | ||||||
|                 if (resultados != null) |                 if (resultados != null) | ||||||
|                 { |                 { | ||||||
|                         try |                     todosLosResultados[municipio.Id] = resultados; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (todosLosResultados.Any()) | ||||||
|             { |             { | ||||||
|                             await GuardarResultadosDeAmbitoAsync(dbContext, municipio.Id, resultados, stoppingToken); |                 // La llamada ahora es correcta porque el método receptor espera 3 argumentos | ||||||
|                             // Ahora 'municipio.Nombre' existe y el log funcionará |                 await GuardarResultadosDeMunicipiosAsync(dbContext, todosLosResultados, stoppingToken); | ||||||
|                             _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); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
| @@ -283,15 +298,43 @@ 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) | ||||||
|  |         { | ||||||
|  |             var ambitoId = kvp.Key; | ||||||
|  |             var resultadosDto = kvp.Value; | ||||||
|  |  | ||||||
|  |             // Lógica Upsert para EstadoRecuento | ||||||
|  |             if (!estadosRecuentoExistentes.TryGetValue(ambitoId, out var estadoRecuento)) | ||||||
|             { |             { | ||||||
|                 estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId }; |                 estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId }; | ||||||
|                 dbContext.EstadosRecuentos.Add(estadoRecuento); |                 dbContext.EstadosRecuentos.Add(estadoRecuento); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             // Mapeo completo de propiedades para EstadoRecuento | ||||||
|             estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime(); |             estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime(); | ||||||
|             estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; |             estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; | ||||||
|             estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas; |             estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas; | ||||||
| @@ -310,12 +353,15 @@ public class Worker : BackgroundService | |||||||
|                 estadoRecuento.VotosRecurridosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosRecurridosPorcentaje; |                 estadoRecuento.VotosRecurridosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosRecurridosPorcentaje; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             // Lógica Upsert para ResultadosVotos | ||||||
|  |             var votosDeAmbitoExistentes = resultadosVotosExistentes.GetValueOrDefault(ambitoId); | ||||||
|             foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos) |             foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos) | ||||||
|             { |             { | ||||||
|             var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync( |                 ResultadoVoto? resultadoVoto = null; | ||||||
|                 rv => rv.AmbitoGeograficoId == ambitoId && rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion, |                 if (votosDeAmbitoExistentes != null) | ||||||
|                 stoppingToken |                 { | ||||||
|             ); |                     votosDeAmbitoExistentes.TryGetValue(votoPositivoDto.IdAgrupacion, out resultadoVoto); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 if (resultadoVoto == null) |                 if (resultadoVoto == null) | ||||||
|                 { |                 { | ||||||
| @@ -329,127 +375,124 @@ public class Worker : BackgroundService | |||||||
|                 resultadoVoto.CantidadVotos = votoPositivoDto.Votos; |                 resultadoVoto.CantidadVotos = votoPositivoDto.Votos; | ||||||
|                 resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje; |                 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 seccion in seccionesElectorales) |  | ||||||
|             { |  | ||||||
|                 if (stoppingToken.IsCancellationRequested) break; // Salida limpia si la aplicación se detiene. |  | ||||||
|  |  | ||||||
|                 // 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', |                 // Llamamos a la API sin 'seccionProvincialId' para obtener el total provincial. | ||||||
|                     // que son precisamente los datos que tenemos en nuestros ámbitos de NivelId = 20. |                 var repartoBancas = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, 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... |  | ||||||
|                         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; |                     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'. |                         // Guardamos la proyección asociándola al ID del ámbito de la provincia. | ||||||
|                             var nuevaProyeccion = new ProyeccionBanca |                         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) | ||||||
|  |             { | ||||||
|  |                 if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |                 foreach (var categoria in categoriasDeBancas) | ||||||
|  |                 { | ||||||
|  |                     if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |  | ||||||
|  |                     var repartoBancas = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id); | ||||||
|  |  | ||||||
|  |                     if (repartoBancas?.RepartoBancas is { Count: > 0 }) | ||||||
|  |                     { | ||||||
|  |                         hasReceivedAnyNewData = true; | ||||||
|  |                         foreach (var banca in repartoBancas.RepartoBancas) | ||||||
|  |                         { | ||||||
|  |                             nuevasProyecciones.Add(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