diff --git a/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs b/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs index 8163414..d2b20ce 100644 --- a/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs +++ b/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs @@ -163,17 +163,24 @@ public class ElectoralApiService : IElectoralApiService { var response = await client.SendAsync(request); - // --- CORRECCIÓN FINAL --- - // Eliminamos la comprobación de ContentLength. Confiamos en el try-catch. if (response.IsSuccessStatusCode) { + // Verificamos si la respuesta realmente tiene contenido. + // ContentLength puede ser null, así que lo verificamos primero. + if (response.Content.Headers.ContentLength == 0) + { + _logger.LogInformation("La API devolvió 200 OK pero con cuerpo vacío para getBancas. URI: {uri}", requestUri); + return null; // Tratamos un cuerpo vacío como "sin datos". + } + try { + // Solo intentamos deserializar si sabemos que hay contenido. return await response.Content.ReadFromJsonAsync(); } catch (JsonException ex) { - // Esto se activará si el cuerpo está vacío o no es un JSON válido. + // Si aún así falla (ej. el contenido es "[]" en lugar de "{}"), lo capturamos como una advertencia. _logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getBancas. URI: {uri}", requestUri); return null; } diff --git a/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs b/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs index 5b35ea6..8bfafc2 100644 --- a/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/CriticalDataWorker.cs @@ -109,15 +109,15 @@ public class CriticalDataWorker : BackgroundService .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) .ToListAsync(stoppingToken); - var provincia = await dbContext.AmbitosGeograficos + // --- MODIFICACIÓN 1: Obtener todos los ámbitos en una sola consulta --- + var ambitosASondear = await dbContext.AmbitosGeograficos .AsNoTracking() - .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); - - var seccionesElectorales = await dbContext.AmbitosGeograficos - .AsNoTracking() - .Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null) + .Where(a => (a.NivelId == 10 || a.NivelId == 20) && a.DistritoId != null) .ToListAsync(stoppingToken); + var provincia = ambitosASondear.FirstOrDefault(a => a.NivelId == 10); + var seccionesElectorales = ambitosASondear.Where(a => a.NivelId == 20).ToList(); + if (!categoriasDeBancas.Any() || provincia == null) { _logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas."); @@ -129,62 +129,29 @@ public class CriticalDataWorker : BackgroundService var todasLasProyecciones = new List(); bool hasReceivedAnyNewData = false; - // Bucle para el nivel Provincial - foreach (var categoria in categoriasDeBancas) + // --- MODIFICACIÓN 2: Usar un diccionario para no buscar repetidamente en la BD --- + var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); + + // Bucle combinado para todos los ámbitos + foreach (var ambito in ambitosASondear) { if (stoppingToken.IsCancellationRequested) break; - var repartoBancasDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id); - if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) - { - hasReceivedAnyNewData = true; - - // --- SEGURIDAD: Usar TryParse para la fecha --- - DateTime fechaTotalizacion; - if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) - { - // Si la fecha es inválida (nula, vacía, mal formada), lo registramos y usamos la hora actual como respaldo. - _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas provinciales. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); - fechaTotalizacion = DateTime.UtcNow; - } - else - { - fechaTotalizacion = parsedDate.ToUniversalTime(); - } - - foreach (var banca in bancas) - { - todasLasProyecciones.Add(new ProyeccionBanca - { - EleccionId = EleccionId, - AmbitoGeograficoId = provincia.Id, - AgrupacionPoliticaId = banca.IdAgrupacion, - NroBancas = banca.NroBancas, - CategoriaId = categoria.Id, - FechaTotalizacion = fechaTotalizacion - }); - } - } - } - - // 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 repartoBancasDto = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id); + + // Llamada a la API (lógica adaptada para ambos niveles) + var repartoBancasDto = await _apiService.GetBancasAsync(authToken, ambito.DistritoId!, ambito.SeccionProvincialId, categoria.Id); if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) { hasReceivedAnyNewData = true; - // --- APLICAMOS LA MISMA SEGURIDAD AQUÍ --- DateTime fechaTotalizacion; if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) { - _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas de sección. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); + _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); fechaTotalizacion = DateTime.UtcNow; } else @@ -194,10 +161,26 @@ public class CriticalDataWorker : BackgroundService foreach (var banca in bancas) { + // --- MODIFICACIÓN 3: Lógica de "Upsert" para Agrupaciones --- + if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion)) + { + _logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada. Creándola desde los datos de bancas.", banca.IdAgrupacion, banca.NombreAgrupacion); + + var nuevaAgrupacion = new AgrupacionPolitica + { + Id = banca.IdAgrupacion, + Nombre = banca.NombreAgrupacion, + IdTelegrama = banca.IdAgrupacionTelegrama ?? string.Empty + }; + + await dbContext.AgrupacionesPoliticas.AddAsync(nuevaAgrupacion, stoppingToken); + agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); // Añadir al diccionario para no volver a crearla + } + todasLasProyecciones.Add(new ProyeccionBanca { EleccionId = EleccionId, - AmbitoGeograficoId = seccion.Id, + AmbitoGeograficoId = ambito.Id, AgrupacionPoliticaId = banca.IdAgrupacion, NroBancas = banca.NroBancas, CategoriaId = categoria.Id, @@ -213,6 +196,10 @@ public class CriticalDataWorker : BackgroundService _logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos..."); await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); + // Si se crearon nuevas agrupaciones, se guardarán aquí primero. + await dbContext.SaveChangesAsync(stoppingToken); + + // Luego, procedemos con las proyecciones. await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken); await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken); await dbContext.SaveChangesAsync(stoppingToken); @@ -222,7 +209,7 @@ public class CriticalDataWorker : BackgroundService } else { - _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, la tabla no fue modificada."); } } catch (OperationCanceledException) diff --git a/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs b/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs index adba84d..f03c931 100644 --- a/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs +++ b/Elecciones-Web/src/Elecciones.Worker/LowPriorityDataWorker.cs @@ -600,15 +600,15 @@ public class LowPriorityDataWorker : BackgroundService .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) .ToListAsync(stoppingToken); - var provincia = await dbContext.AmbitosGeograficos + // --- MODIFICACIÓN 1: Obtener todos los ámbitos en una sola consulta --- + var ambitosASondear = await dbContext.AmbitosGeograficos .AsNoTracking() - .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); - - var seccionesElectorales = await dbContext.AmbitosGeograficos - .AsNoTracking() - .Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null) + .Where(a => (a.NivelId == 10 || a.NivelId == 20) && a.DistritoId != null) .ToListAsync(stoppingToken); + var provincia = ambitosASondear.FirstOrDefault(a => a.NivelId == 10); + var seccionesElectorales = ambitosASondear.Where(a => a.NivelId == 20).ToList(); + if (!categoriasDeBancas.Any() || provincia == null) { _logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas."); @@ -620,62 +620,29 @@ public class LowPriorityDataWorker : BackgroundService var todasLasProyecciones = new List(); bool hasReceivedAnyNewData = false; - // Bucle para el nivel Provincial - foreach (var categoria in categoriasDeBancas) + // --- MODIFICACIÓN 2: Usar un diccionario para no buscar repetidamente en la BD --- + var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken); + + // Bucle combinado para todos los ámbitos + foreach (var ambito in ambitosASondear) { if (stoppingToken.IsCancellationRequested) break; - var repartoBancasDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id); - if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) - { - hasReceivedAnyNewData = true; - - // --- SEGURIDAD: Usar TryParse para la fecha --- - DateTime fechaTotalizacion; - if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) - { - // Si la fecha es inválida (nula, vacía, mal formada), lo registramos y usamos la hora actual como respaldo. - _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas provinciales. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); - fechaTotalizacion = DateTime.UtcNow; - } - else - { - fechaTotalizacion = parsedDate.ToUniversalTime(); - } - - foreach (var banca in bancas) - { - todasLasProyecciones.Add(new ProyeccionBanca - { - EleccionId = EleccionId, - AmbitoGeograficoId = provincia.Id, - AgrupacionPoliticaId = banca.IdAgrupacion, - NroBancas = banca.NroBancas, - CategoriaId = categoria.Id, - FechaTotalizacion = fechaTotalizacion - }); - } - } - } - - // 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 repartoBancasDto = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id); + + // Llamada a la API (lógica adaptada para ambos niveles) + var repartoBancasDto = await _apiService.GetBancasAsync(authToken, ambito.DistritoId!, ambito.SeccionProvincialId, categoria.Id); if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) { hasReceivedAnyNewData = true; - // --- APLICAMOS SEGURIDAD AQUÍ --- DateTime fechaTotalizacion; if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) { - _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas de sección. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); + _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); fechaTotalizacion = DateTime.UtcNow; } else @@ -685,10 +652,26 @@ public class LowPriorityDataWorker : BackgroundService foreach (var banca in bancas) { + // --- MODIFICACIÓN 3: Lógica de "Upsert" para Agrupaciones --- + if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion)) + { + _logger.LogWarning("Agrupación con ID {AgrupacionId} ('{Nombre}') no encontrada. Creándola desde los datos de bancas.", banca.IdAgrupacion, banca.NombreAgrupacion); + + var nuevaAgrupacion = new AgrupacionPolitica + { + Id = banca.IdAgrupacion, + Nombre = banca.NombreAgrupacion, + IdTelegrama = banca.IdAgrupacionTelegrama ?? string.Empty + }; + + await dbContext.AgrupacionesPoliticas.AddAsync(nuevaAgrupacion, stoppingToken); + agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion); // Añadir al diccionario para no volver a crearla + } + todasLasProyecciones.Add(new ProyeccionBanca { EleccionId = EleccionId, - AmbitoGeograficoId = seccion.Id, + AmbitoGeograficoId = ambito.Id, AgrupacionPoliticaId = banca.IdAgrupacion, NroBancas = banca.NroBancas, CategoriaId = categoria.Id, @@ -704,6 +687,10 @@ public class LowPriorityDataWorker : BackgroundService _logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos..."); await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); + // Si se crearon nuevas agrupaciones, se guardarán aquí primero. + await dbContext.SaveChangesAsync(stoppingToken); + + // Luego, procedemos con las proyecciones. await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken); await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken); await dbContext.SaveChangesAsync(stoppingToken); @@ -713,7 +700,7 @@ public class LowPriorityDataWorker : BackgroundService } else { - _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, la tabla no fue modificada."); } } catch (OperationCanceledException)