From 705683861c0427dd5294f6e9193bfadeef7d4a48 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Oct 2025 18:40:23 -0300 Subject: [PATCH] =?UTF-8?q?Fix=20Cat=C3=A1logo=20Maestro=20de=20Agrupacion?= =?UTF-8?q?es=20Pol=C3=ADticas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Se remueve la iteración sobre distritos. Se consulta solo por categorías electorales. --- .../Services/ElectoralApiService.cs | 22 ++- .../Services/IElectoralApiService.cs | 2 +- .../LowPriorityDataWorker.cs | 144 ++++++------------ 3 files changed, 59 insertions(+), 109 deletions(-) diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs b/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs index d2b20ce..7839198 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs +++ b/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs @@ -71,25 +71,21 @@ public class ElectoralApiService : IElectoralApiService return null;*/ } - public async Task?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId) + public async Task?> 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 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); request.Headers.Add("Authorization", $"Bearer {authToken}"); var response = await client.SendAsync(request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync>() : null; - /* } - // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. - return null;*/ } public async Task GetResultadosAsync(string authToken, string distritoId, string? seccionId, string? municipioId, int categoriaId) diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/Services/IElectoralApiService.cs b/Elecciones-Web/src/Elecciones.Infrastructure/Services/IElectoralApiService.cs index 130b240..dba5404 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/Services/IElectoralApiService.cs +++ b/Elecciones-Web/src/Elecciones.Infrastructure/Services/IElectoralApiService.cs @@ -10,7 +10,7 @@ public interface IElectoralApiService Task GetAuthTokenAsync(); // Métodos para catálogos Task GetCatalogoAmbitosAsync(string authToken, int categoriaId); - Task?> GetAgrupacionesAsync(string authToken, string distritoId, int categoriaId); + Task?> GetAgrupacionesAsync(string authToken, string? distritoId, int categoriaId); Task GetResultadosAsync(string authToken, string distritoId, string? seccionId, string? municipioId, int categoriaId); Task GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId); Task?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = null); diff --git a/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs b/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs index 04f77ab..a375e5e 100644 --- a/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs @@ -118,7 +118,6 @@ public class LowPriorityDataWorker : BackgroundService using var innerScope = _serviceProvider.CreateScope(); var innerDbContext = innerScope.ServiceProvider.GetRequiredService(); - // --- LLAMADA CORRECTA --- await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken); } }); @@ -440,32 +439,24 @@ public class LowPriorityDataWorker : BackgroundService try { _logger.LogInformation("Iniciando sincronización de catálogos maestros..."); - - // --- CORRECCIÓN: Usar el _tokenService inyectado --- var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken); - - if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested) + if (string.IsNullOrEmpty(authToken)) { - _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; } - // Creamos un scope de servicios para obtener una instancia fresca de DbContext. using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); - // PASO 2: Sincronizar las categorías electorales. - // Es un catálogo pequeño y es la base para las siguientes consultas. + // 1. SINCRONIZAR CATEGORÍAS 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."); + _logger.LogWarning("La API no devolvió datos para el catálogo de Categorías."); 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) { @@ -474,62 +465,65 @@ public class LowPriorityDataWorker : BackgroundService 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); + _logger.LogInformation("Catálogo de Categorías Electorales sincronizado."); - // 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(); - 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); - } - } - + // 2. SINCRONIZAR AGRUPACIONES POLÍTICAS + _logger.LogInformation("Iniciando sincronización de Agrupaciones Políticas..."); 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) { if (stoppingToken.IsCancellationRequested) break; - _logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId); + // Se pasa 'null' como distritoId para obtener todas las agrupaciones de la categoría. + var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, null, categoria.CategoriaId); + + if (agrupacionesApi != null) + { + foreach (var agrupacionDto in agrupacionesApi) + { + if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion)) + { + var nuevaAgrupacion = new AgrupacionPolitica + { + Id = agrupacionDto.IdAgrupacion, + IdTelegrama = agrupacionDto.IdAgrupacionTelegrama, + Nombre = agrupacionDto.NombreAgrupacion + }; + dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion); + agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); // Añadir al diccionario para evitar duplicados en el mismo ciclo + } + } + } + } + int agrupacionesGuardadas = await dbContext.SaveChangesAsync(stoppingToken); + _logger.LogInformation("Catálogo de Agrupaciones Políticas sincronizado. Se guardaron {count} nuevos registros.", agrupacionesGuardadas); + + // 3. SINCRONIZAR ÁMBITOS GEOGRÁFICOS + _logger.LogInformation("Iniciando sincronización de Ámbitos Geográficos..."); + var ambitosEnDbKeys = new HashSet( + 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) { - // 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 (ambitosEnDbKeys.Add(claveUnica)) // HashSet.Add devuelve true si el elemento no existía { 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)) + 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 + dbContext.AmbitosGeograficos.Add(new AmbitoGeografico { - // Usamos el nombre corregido en lugar del original. Nombre = nombreCorregido, NivelId = ambitoDto.NivelId, DistritoId = ambitoDto.CodigoAmbitos.DistritoId, @@ -539,58 +533,18 @@ public class LowPriorityDataWorker : BackgroundService 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) - { - if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion)) - { - var nuevaAgrupacion = new AgrupacionPolitica - { - Id = agrupacionDto.IdAgrupacion, - IdTelegrama = agrupacionDto.IdAgrupacionTelegrama, - Nombre = agrupacionDto.NombreAgrupacion - }; - dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion); - agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); - } - } - } - } - catch (Exception ex) - { - _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. - // Esto divide la inserción masiva de ~50,000 registros en 3 transacciones más pequeñas, - // 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); + int ambitosGuardados = await dbContext.SaveChangesAsync(stoppingToken); + totalNuevosAmbitos += ambitosGuardados; + _logger.LogInformation("Guardados {count} nuevos ámbitos para la categoría '{catNombre}'.", ambitosGuardados, 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("Catálogo de Ámbitos Geográficos sincronizado. Se guardaron {count} nuevos registros en total.", totalNuevosAmbitos); _logger.LogInformation("Sincronización de catálogos maestros finalizada."); } catch (Exception ex)