Files
Elecciones-2025/Elecciones-Web/src/Elecciones.Infrastructure/Services/ElectoralApiService.cs
dmolinari 316f49f25b feat(Worker): Adaptación integral para la API de Elecciones Nacionales
Este commit refactoriza por completo el sistema de recolección de datos para asegurar la compatibilidad con la nueva API nacional, pasando de un modelo de distrito único a uno multi-distrito.

Cambios principales:

- **Refactorización de `SondearResumenProvincialAsync`:**
  - Se elimina la dependencia del endpoint obsoleto `/getResumen`.
  - El método ahora itera sobre todas las provincias (`NivelId=10`) y categorías, utilizando `GetResultadosAsync` para obtener los datos agregados.

- **Expansión de `SondearResultadosMunicipalesAsync`:**
  - Se renombra a `SondearResultadosPorAmbitosAsync` para reflejar su nueva responsabilidad.
  - La lógica ahora sondea múltiples niveles jerárquicos (`NivelId` 10, 20, 30), capturando resultados detallados para Provincias, Secciones Electorales y Municipios.

- **Modificación del Modelo de Datos:**
  - Se añade la columna `CategoriaId` a la entidad y tabla `ResumenVoto`.
  - Se crea la migración de base de datos `AddCategoriaIdToResumenVoto` para aplicar el cambio.

- **Ajustes de Nulabilidad en API Service:**
  - Se actualizan las firmas de `GetResultadosAsync` en `IElectoralApiService` y `ElectoralApiService` para permitir que `seccionId` y `municipioId` sean nulables (`string?`), resolviendo errores de compilación CS8625.

- **Deshabilitación de Seeders de Ejemplo:**
  - Se introduce una bandera `generarDatosDeEjemplo` en `Program.cs` de la API, establecida en `false`, para prevenir la ejecución de código de simulación en entornos de producción o pruebas.
2025-10-14 16:00:55 -03:00

300 lines
13 KiB
C#

using Elecciones.Core.DTOs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.RateLimiting;
using System.Threading.Tasks;
namespace Elecciones.Infrastructure.Services;
public class ElectoralApiService : IElectoralApiService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly ILogger<ElectoralApiService> _logger;
private readonly RateLimiter _rateLimiter;
public ElectoralApiService(
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ILogger<ElectoralApiService> logger,
RateLimiter rateLimiter)
{
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_logger = logger;
_rateLimiter = rateLimiter;
}
public async Task<TokenResponse?> GetAuthTokenAsync()
{
using RateLimitLease lease = await _rateLimiter.AcquireAsync(1);
if (lease.IsAcquired)
{
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
var username = _configuration["ElectoralApi:Username"];
var password = _configuration["ElectoralApi:Password"];
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) return null;
var request = new HttpRequestMessage(HttpMethod.Get, "/api/createtoken");
request.Headers.Add("username", username);
request.Headers.Add("password", password);
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<TokenResponse>();
}
return null;
}
public async Task<CatalogoDto?> GetCatalogoAmbitosAsync(string authToken, 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 request = new HttpRequestMessage(HttpMethod.Get, $"/api/catalogo/getCatalogo?categoriaId={categoriaId}");
request.Headers.Add("Authorization", $"Bearer {authToken}");
var response = await client.SendAsync(request);
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<CatalogoDto>() : null;
/* }
// Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null.
return null;*/
}
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 requestUri = $"/api/catalogo/getAgrupaciones?distritoId={distritoId}&categoriaId={categoriaId}";
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<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)
{
using RateLimitLease lease = await _rateLimiter.AcquireAsync(1);
if (!lease.IsAcquired) return null;
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
var requestUri = $"/api/resultados/getResultados?distritoId={distritoId}&seccionId={seccionId}&categoriaId={categoriaId}";
if (!string.IsNullOrEmpty(municipioId))
{
requestUri += $"&municipioId={municipioId}";
}
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Add("Authorization", $"Bearer {authToken}");
try
{
var response = await client.SendAsync(request);
// --- APLICAMOS LA MISMA LÓGICA DEFENSIVA ---
if (response.IsSuccessStatusCode)
{
try
{
// Leemos el contenido como un string primero para poder loguearlo si falla.
var contentString = await response.Content.ReadAsStringAsync();
if (string.IsNullOrEmpty(contentString))
{
_logger.LogWarning("La API devolvió 200 OK pero con cuerpo vacío para getResultados. URI: {uri}", requestUri);
return null;
}
return JsonSerializer.Deserialize<ResultadosDto>(contentString);
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getResultados. URI: {uri}", requestUri);
return null;
}
}
_logger.LogWarning("La API devolvió un código de error {statusCode} para getResultados. URI: {uri}", response.StatusCode, requestUri);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "La petición HTTP a getResultados falló. URI: {uri}", requestUri);
return null;
}
}
public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId)
{
using RateLimitLease lease = await _rateLimiter.AcquireAsync(1);
if (!lease.IsAcquired) return null;
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}";
if (!string.IsNullOrEmpty(seccionProvincialId))
{
requestUri += $"&seccionProvincialId={seccionProvincialId}";
}
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Add("Authorization", $"Bearer {authToken}");
try
{
var response = await client.SendAsync(request);
// --- CORRECCIÓN FINAL ---
// Eliminamos la comprobación de ContentLength. Confiamos en el try-catch.
if (response.IsSuccessStatusCode)
{
try
{
return await response.Content.ReadFromJsonAsync<RepartoBancasDto>();
}
catch (JsonException ex)
{
// Esto se activará si el cuerpo está vacío o no es un JSON válido.
_logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getBancas. URI: {uri}", requestUri);
return null;
}
}
_logger.LogWarning("La API devolvió un código de error {statusCode} para getBancas. URI: {uri}", response.StatusCode, requestUri);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "La petición HTTP a getBancas falló. URI: {uri}", requestUri);
return null;
}
}
public async Task<List<string>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = null)
{
// "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/resultados/getTelegramasTotalizados?distritoId={distritoId}&seccionId={seccionId}";
if (categoriaId.HasValue)
{
requestUri += $"&categoriaId={categoriaId.Value}";
}
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Add("Authorization", $"Bearer {authToken}");
var response = await client.SendAsync(request);
// Ahora deserializamos al tipo correcto: List<string>
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<string>>() : null;
/* }
// Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null.
return null;*/
}
public async Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId)
{
using RateLimitLease lease = await _rateLimiter.AcquireAsync(1);
if (!lease.IsAcquired) return null;
var client = _httpClientFactory.CreateClient("ElectoralApiClient");
var requestUri = $"/api/resultados/getFile?mesaId={mesaId}";
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Add("Authorization", $"Bearer {authToken}");
try
{
var response = await client.SendAsync(request);
// --- APLICAMOS LA MISMA CORRECCIÓN AQUÍ POR CONSISTENCIA ---
if (response.IsSuccessStatusCode)
{
try
{
return await response.Content.ReadFromJsonAsync<TelegramaFileDto>();
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getFile para la mesa {mesaId}", mesaId);
return null;
}
}
_logger.LogWarning("La API devolvió un código de error {statusCode} para la mesa {mesaId}", response.StatusCode, mesaId);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "La petición HTTP a getFile falló para la mesa {mesaId}", mesaId);
return null;
}
}
public async Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(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");
// La URL ahora usa el parámetro 'categoriaId' que se recibe
var requestUri = $"/api/estados/estadoRecuento?distritoId={distritoId}&categoriaId={categoriaId}";
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<EstadoRecuentoGeneralDto>() : null;
/* }
// Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null.
return null;*/
}
public async Task<List<CategoriaDto>?> GetCategoriasAsync(string authToken)
{
// "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 request = new HttpRequestMessage(HttpMethod.Get, "/api/catalogo/getCategorias");
request.Headers.Add("Authorization", $"Bearer {authToken}");
var response = await client.SendAsync(request);
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<CategoriaDto>>() : null;
/* }
// Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null.
return null;*/
}
}