Fix Catálogo Maestro de Agrupaciones Políticas
- Se remueve la iteración sobre distritos. Se consulta solo por categorías electorales.
This commit is contained in:
		| @@ -71,25 +71,21 @@ public class ElectoralApiService : IElectoralApiService | |||||||
|          return null;*/ |          return null;*/ | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId) |     public async Task<List<AgrupacionDto>?> GetAgrupacionesAsync(string authToken, string? distritoId, int categoriaId) | ||||||
|     { |     { | ||||||
|         // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). |  | ||||||
|         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. |  | ||||||
|         /* |  | ||||||
|                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |  | ||||||
|  |  | ||||||
|                 // Si se nos concede el permiso para proceder... |  | ||||||
|                 if (lease.IsAcquired) |  | ||||||
|                 {*/ |  | ||||||
|         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|         var requestUri = $"/api/catalogo/getAgrupaciones?distritoId={distritoId}&categoriaId={categoriaId}"; |  | ||||||
|  |         var requestUri = $"/api/catalogo/getAgrupaciones?categoriaId={categoriaId}"; | ||||||
|  |         if (!string.IsNullOrEmpty(distritoId)) | ||||||
|  |         { | ||||||
|  |             requestUri += $"&distritoId={distritoId}"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         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); | ||||||
|  |  | ||||||
|         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<AgrupacionDto>>() : null; |         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<AgrupacionDto>>() : null; | ||||||
|         /* } |  | ||||||
|          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. |  | ||||||
|          return null;*/ |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string? seccionId, string? municipioId, int categoriaId) |     public async Task<ResultadosDto?> GetResultadosAsync(string authToken, string distritoId, string? seccionId, string? municipioId, int categoriaId) | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ public interface IElectoralApiService | |||||||
|     Task<TokenResponse?> GetAuthTokenAsync(); |     Task<TokenResponse?> GetAuthTokenAsync(); | ||||||
|     // Métodos para catálogos |     // Métodos para catálogos | ||||||
|     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? seccionProvincialId, 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); | ||||||
|   | |||||||
| @@ -118,7 +118,6 @@ public class LowPriorityDataWorker : BackgroundService | |||||||
|             using var innerScope = _serviceProvider.CreateScope(); |             using var innerScope = _serviceProvider.CreateScope(); | ||||||
|             var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |             var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|             // --- LLAMADA CORRECTA --- |  | ||||||
|             await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken); |             await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken); | ||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
| @@ -440,32 +439,24 @@ public class LowPriorityDataWorker : BackgroundService | |||||||
|     try |     try | ||||||
|     { |     { | ||||||
|       _logger.LogInformation("Iniciando sincronización de catálogos maestros..."); |       _logger.LogInformation("Iniciando sincronización de catálogos maestros..."); | ||||||
|  |  | ||||||
|       // --- CORRECCIÓN: Usar el _tokenService inyectado --- |  | ||||||
|       var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken); |       var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken); | ||||||
|  |       if (string.IsNullOrEmpty(authToken)) | ||||||
|       if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested) |  | ||||||
|       { |       { | ||||||
|         _logger.LogError("No se pudo obtener token para la sincronización de catálogos. La operación se cancela."); |         _logger.LogError("No se pudo obtener token para la sincronización de catálogos."); | ||||||
|         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>(); | ||||||
|  |  | ||||||
|       // PASO 2: Sincronizar las categorías electorales. |       // 1. SINCRONIZAR CATEGORÍAS | ||||||
|       // 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("La API no devolvió datos para el catálogo de Categorías. La sincronización no puede continuar."); |         _logger.LogWarning("La API no devolvió datos para el catálogo de Categorías."); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList(); |       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); |       var categoriasEnDb = await dbContext.CategoriasElectorales.ToDictionaryAsync(c => c.Id, c => c, stoppingToken); | ||||||
|       foreach (var categoriaDto in distinctCategorias) |       foreach (var categoriaDto in distinctCategorias) | ||||||
|       { |       { | ||||||
| @@ -474,86 +465,20 @@ public class LowPriorityDataWorker : 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); | ||||||
|  |       _logger.LogInformation("Catálogo de Categorías Electorales sincronizado."); | ||||||
|  |  | ||||||
|       // PASO 3: Cargar los catálogos existentes en memoria para una comparación eficiente. |       // 2. SINCRONIZAR AGRUPACIONES POLÍTICAS | ||||||
|       // Esto evita hacer miles de consultas a la BD dentro de un bucle. |       _logger.LogInformation("Iniciando sincronización de Agrupaciones Políticas..."); | ||||||
|  |  | ||||||
|       // Para los ámbitos, creamos una clave única robusta que funciona incluso con campos nulos. |  | ||||||
|       var ambitosEnDb = new Dictionary<string, AmbitoGeografico>(); |  | ||||||
|       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)) |  | ||||||
|         { |  | ||||||
|           ambitosEnDb.Add(clave, ambito); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       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; | ||||||
|         _logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId); |  | ||||||
|  |  | ||||||
|         var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId); |         // Se pasa 'null' como distritoId para obtener todas las agrupaciones de la categoría. | ||||||
|         if (catalogoDto != null) |         var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, null, categoria.CategoriaId); | ||||||
|         { |  | ||||||
|           // 4.1 - Procesar y añadir ÁMBITOS nuevos al DbContext |  | ||||||
|           foreach (var ambitoDto in catalogoDto.Ambitos) |  | ||||||
|           { |  | ||||||
|             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 (agrupacionesApi != null) | ||||||
|             { |  | ||||||
|               string nombreCorregido = ambitoDto.Nombre; |  | ||||||
|  |  | ||||||
|               // VERIFICAMOS SI ES UNA COMUNA DE CABA |  | ||||||
|               // Condición: El DistritoId es "01" (CABA) Y el NivelId corresponde a Departamento/Comuna (30) |  | ||||||
|               // Y el nombre es simplemente un número. |  | ||||||
|               if (ambitoDto.CodigoAmbitos.DistritoId == "01" && |  | ||||||
|                   ambitoDto.NivelId == 30 && |  | ||||||
|                   int.TryParse(ambitoDto.Nombre, out int numeroComuna)) |  | ||||||
|               { |  | ||||||
|                 // Si cumple las condiciones, le damos el formato correcto. |  | ||||||
|                 nombreCorregido = $"COMUNA {numeroComuna}"; |  | ||||||
|                 _logger.LogInformation("Nombre de comuna de CABA corregido: de '{Original}' a '{Corregido}'", ambitoDto.Nombre, nombreCorregido); |  | ||||||
|               } |  | ||||||
|  |  | ||||||
|               var nuevoAmbito = new AmbitoGeografico |  | ||||||
|               { |  | ||||||
|                 // Usamos el nombre corregido en lugar del original. |  | ||||||
|                 Nombre = nombreCorregido, |  | ||||||
|                 NivelId = ambitoDto.NivelId, |  | ||||||
|                 DistritoId = ambitoDto.CodigoAmbitos.DistritoId, |  | ||||||
|                 SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId, |  | ||||||
|                 SeccionId = ambitoDto.CodigoAmbitos.SeccionId, |  | ||||||
|                 MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId, |  | ||||||
|                 CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId, |  | ||||||
|                 EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId, |  | ||||||
|                 MesaId = ambitoDto.CodigoAmbitos.MesaId, |  | ||||||
|               }; |  | ||||||
|               dbContext.AmbitosGeograficos.Add(nuevoAmbito); |  | ||||||
|               ambitosEnDb.Add(claveUnica, nuevoAmbito); // Añadir también al diccionario en memoria |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|           // 4.2 - Procesar y añadir AGRUPACIONES nuevas al DbContext |  | ||||||
|           var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10); |  | ||||||
|           if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId)) |  | ||||||
|           { |  | ||||||
|             // Usamos un try-catch porque no todas las categorías tienen agrupaciones a nivel provincial. |  | ||||||
|             try |  | ||||||
|             { |  | ||||||
|               var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId); |  | ||||||
|               if (agrupacionesApi != null && agrupacionesApi.Any()) |  | ||||||
|         { |         { | ||||||
|           foreach (var agrupacionDto in agrupacionesApi) |           foreach (var agrupacionDto in agrupacionesApi) | ||||||
|           { |           { | ||||||
| @@ -566,31 +491,60 @@ public class LowPriorityDataWorker : BackgroundService | |||||||
|                 Nombre = agrupacionDto.NombreAgrupacion |                 Nombre = agrupacionDto.NombreAgrupacion | ||||||
|               }; |               }; | ||||||
|               dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion); |               dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion); | ||||||
|                     agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); |               agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); // Añadir al diccionario para evitar duplicados en el mismo ciclo | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|             catch (Exception ex) |       int agrupacionesGuardadas = await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|             { |       _logger.LogInformation("Catálogo de Agrupaciones Políticas sincronizado. Se guardaron {count} nuevos registros.", agrupacionesGuardadas); | ||||||
|               _logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}).", categoria.Nombre, categoria.CategoriaId); |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Después de procesar todos los ámbitos y agrupaciones de UNA categoría, guardamos los cambios. |       // 3. SINCRONIZAR ÁMBITOS GEOGRÁFICOS | ||||||
|         // Esto divide la inserción masiva de ~50,000 registros en 3 transacciones más pequeñas, |       _logger.LogInformation("Iniciando sincronización de Ámbitos Geográficos..."); | ||||||
|         // evitando timeouts y fallos en la base de datos. |       var ambitosEnDbKeys = new HashSet<string>( | ||||||
|  |           await dbContext.AmbitosGeograficos.Select(a => $"{a.NivelId}|{a.DistritoId}|{a.SeccionProvincialId}|{a.SeccionId}|{a.MunicipioId}|{a.CircuitoId}|{a.EstablecimientoId}|{a.MesaId}").ToListAsync(stoppingToken) | ||||||
|  |       ); | ||||||
|  |       int totalNuevosAmbitos = 0; | ||||||
|  |  | ||||||
|  |       foreach (var categoria in distinctCategorias) | ||||||
|  |       { | ||||||
|  |         if (stoppingToken.IsCancellationRequested) break; | ||||||
|  |         var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId); | ||||||
|  |         if (catalogoDto != null) | ||||||
|  |         { | ||||||
|  |           foreach (var ambitoDto in catalogoDto.Ambitos) | ||||||
|  |           { | ||||||
|  |             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 (ambitosEnDbKeys.Add(claveUnica)) // HashSet.Add devuelve true si el elemento no existía | ||||||
|  |             { | ||||||
|  |               string nombreCorregido = ambitoDto.Nombre; | ||||||
|  |               if (ambitoDto.CodigoAmbitos.DistritoId == "01" && ambitoDto.NivelId == 30 && int.TryParse(ambitoDto.Nombre, out int numeroComuna)) | ||||||
|  |               { | ||||||
|  |                 nombreCorregido = $"COMUNA {numeroComuna}"; | ||||||
|  |               } | ||||||
|  |               dbContext.AmbitosGeograficos.Add(new AmbitoGeografico | ||||||
|  |               { | ||||||
|  |                 Nombre = nombreCorregido, | ||||||
|  |                 NivelId = ambitoDto.NivelId, | ||||||
|  |                 DistritoId = ambitoDto.CodigoAmbitos.DistritoId, | ||||||
|  |                 SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId, | ||||||
|  |                 SeccionId = ambitoDto.CodigoAmbitos.SeccionId, | ||||||
|  |                 MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId, | ||||||
|  |                 CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId, | ||||||
|  |                 EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId, | ||||||
|  |                 MesaId = ambitoDto.CodigoAmbitos.MesaId, | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|         if (dbContext.ChangeTracker.HasChanges()) |         if (dbContext.ChangeTracker.HasChanges()) | ||||||
|         { |         { | ||||||
|           int cambiosEnLote = await dbContext.SaveChangesAsync(stoppingToken); |           int ambitosGuardados = await dbContext.SaveChangesAsync(stoppingToken); | ||||||
|           totalCambiosGuardados += cambiosEnLote; |           totalNuevosAmbitos += ambitosGuardados; | ||||||
|           _logger.LogInformation("Guardados {count} registros de catálogo para la categoría '{catNombre}'.", cambiosEnLote, categoria.Nombre); |           _logger.LogInformation("Guardados {count} nuevos ámbitos para la categoría '{catNombre}'.", ambitosGuardados, categoria.Nombre); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |       _logger.LogInformation("Catálogo de Ámbitos Geográficos sincronizado. Se guardaron {count} nuevos registros en total.", totalNuevosAmbitos); | ||||||
|       // 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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user