Fix Goteo solo para Telegramas
This commit is contained in:
		| @@ -7,7 +7,7 @@ using System.Text.Json; | |||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using static Elecciones.Core.DTOs.BancaDto; | using static Elecciones.Core.DTOs.BancaDto; | ||||||
| using System.Threading.RateLimiting; | //using System.Threading.RateLimiting; | ||||||
|  |  | ||||||
| namespace Elecciones.Infrastructure.Services; | namespace Elecciones.Infrastructure.Services; | ||||||
|  |  | ||||||
| @@ -16,312 +16,321 @@ public class ElectoralApiService : IElectoralApiService | |||||||
|     private readonly IHttpClientFactory _httpClientFactory; |     private readonly IHttpClientFactory _httpClientFactory; | ||||||
|     private readonly IConfiguration _configuration; |     private readonly IConfiguration _configuration; | ||||||
|     private readonly ILogger<ElectoralApiService> _logger; |     private readonly ILogger<ElectoralApiService> _logger; | ||||||
|     private readonly RateLimiter _rateLimiter; |     //private readonly RateLimiter _rateLimiter; | ||||||
|  |  | ||||||
|     public ElectoralApiService(IHttpClientFactory httpClientFactory, |     public ElectoralApiService(IHttpClientFactory httpClientFactory, | ||||||
|         IConfiguration configuration, |         IConfiguration configuration, | ||||||
|         ILogger<ElectoralApiService> logger, |         ILogger<ElectoralApiService> logger) | ||||||
|         RateLimiter rateLimiter) |         //RateLimiter rateLimiter) | ||||||
|     { |     { | ||||||
|         _httpClientFactory = httpClientFactory; |         _httpClientFactory = httpClientFactory; | ||||||
|         _configuration = configuration; |         _configuration = configuration; | ||||||
|         _logger = logger; |         _logger = logger; | ||||||
|         _rateLimiter = rateLimiter; |         //_rateLimiter = rateLimiter; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<TokenResponse?> GetAuthTokenAsync() |     public async Task<TokenResponse?> GetAuthTokenAsync() | ||||||
|     { |     { | ||||||
|         // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 if (lease.IsAcquired) | ||||||
|         { |                 {*/ | ||||||
|             var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|             var username = _configuration["ElectoralApi:Username"]; |         var username = _configuration["ElectoralApi:Username"]; | ||||||
|             var password = _configuration["ElectoralApi:Password"]; |         var password = _configuration["ElectoralApi:Password"]; | ||||||
|             if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) return null; |         if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) return null; | ||||||
|  |  | ||||||
|             var request = new HttpRequestMessage(HttpMethod.Get, "/api/createtoken"); |         var request = new HttpRequestMessage(HttpMethod.Get, "/api/createtoken"); | ||||||
|             request.Headers.Add("username", username); |         request.Headers.Add("username", username); | ||||||
|             request.Headers.Add("password", password); |         request.Headers.Add("password", password); | ||||||
|  |  | ||||||
|             var response = await client.SendAsync(request); |         var response = await client.SendAsync(request); | ||||||
|             if (!response.IsSuccessStatusCode) return null; |         if (!response.IsSuccessStatusCode) return null; | ||||||
|  |  | ||||||
|             // Ahora esto es válido, porque el método devuelve Task<TokenResponse?> |         // Ahora esto es válido, porque el método devuelve Task<TokenResponse?> | ||||||
|             return await response.Content.ReadFromJsonAsync<TokenResponse>(); |         return await response.Content.ReadFromJsonAsync<TokenResponse>(); | ||||||
|         } |         /* } | ||||||
|  |          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||||
|         // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. |          return null;*/ | ||||||
|         return null; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<CatalogoDto?> GetCatalogoAmbitosAsync(string authToken, int categoriaId) |     public async Task<CatalogoDto?> GetCatalogoAmbitosAsync(string authToken, int categoriaId) | ||||||
|     { |     { | ||||||
|         // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 if (lease.IsAcquired) | ||||||
|         { |                 {*/ | ||||||
|             var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|             var request = new HttpRequestMessage(HttpMethod.Get, $"/api/catalogo/getCatalogo?categoriaId={categoriaId}"); |         var request = new HttpRequestMessage(HttpMethod.Get, $"/api/catalogo/getCatalogo?categoriaId={categoriaId}"); | ||||||
|             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<CatalogoDto>() : null; |         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<CatalogoDto>() : null; | ||||||
|         } |         /* } | ||||||
|         // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. |          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||||
|         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). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 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?distritoId={distritoId}&categoriaId={categoriaId}"; | ||||||
|             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. |          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||||
|         return 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) | ||||||
|     { |     { | ||||||
|         // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 if (lease.IsAcquired) | ||||||
|  |                 {*/ | ||||||
|  |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|  |  | ||||||
|  |         // Construimos la URL base | ||||||
|  |         var requestUri = $"/api/resultados/getResultados?distritoId={distritoId}&seccionId={seccionId}&categoriaId={categoriaId}"; | ||||||
|  |  | ||||||
|  |         // Añadimos el municipioId a la URL SÓLO si no es nulo o vacío | ||||||
|  |         if (!string.IsNullOrEmpty(municipioId)) | ||||||
|         { |         { | ||||||
|             var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |             requestUri += $"&municipioId={municipioId}"; | ||||||
|  |  | ||||||
|             // Construimos la URL base |  | ||||||
|             var requestUri = $"/api/resultados/getResultados?distritoId={distritoId}&seccionId={seccionId}&categoriaId={categoriaId}"; |  | ||||||
|  |  | ||||||
|             // Añadimos el municipioId a la URL SÓLO si no es nulo o vacío |  | ||||||
|             if (!string.IsNullOrEmpty(municipioId)) |  | ||||||
|             { |  | ||||||
|                 requestUri += $"&municipioId={municipioId}"; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             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<ResultadosDto>() : null; |  | ||||||
|         } |         } | ||||||
|         // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. |  | ||||||
|         return null; |         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<ResultadosDto>() : null; | ||||||
|  |         /* } | ||||||
|  |          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||||
|  |          return null;*/ | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId) |     public async Task<RepartoBancasDto?> GetBancasAsync(string authToken, string distritoId, string? seccionProvincialId, int categoriaId) | ||||||
|     { |     { | ||||||
|         // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 if (lease.IsAcquired) | ||||||
|  |                 {*/ | ||||||
|  |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|  |         var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}"; | ||||||
|  |  | ||||||
|  |         if (!string.IsNullOrEmpty(seccionProvincialId)) | ||||||
|         { |         { | ||||||
|             var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |             requestUri += $"&seccionProvincialId={seccionProvincialId}"; | ||||||
|             var requestUri = $"/api/resultados/getBancas?distritoId={distritoId}&categoriaId={categoriaId}"; |         } | ||||||
|  |  | ||||||
|             if (!string.IsNullOrEmpty(seccionProvincialId)) |         var request = new HttpRequestMessage(HttpMethod.Get, requestUri); | ||||||
|             { |         request.Headers.Add("Authorization", $"Bearer {authToken}"); | ||||||
|                 requestUri += $"&seccionProvincialId={seccionProvincialId}"; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             var request = new HttpRequestMessage(HttpMethod.Get, requestUri); |         HttpResponseMessage response; | ||||||
|             request.Headers.Add("Authorization", $"Bearer {authToken}"); |         try | ||||||
|  |         { | ||||||
|             HttpResponseMessage response; |             response = await client.SendAsync(request); | ||||||
|             try |         } | ||||||
|             { |         catch (Exception ex) | ||||||
|                 response = await client.SendAsync(request); |         { | ||||||
|             } |             // Captura errores de red (ej. la API se cae momentáneamente) | ||||||
|             catch (Exception ex) |             _logger.LogError(ex, "La petición HTTP a getBancas falló. URI: {requestUri}", requestUri); | ||||||
|             { |  | ||||||
|                 // Captura errores de red (ej. la API se cae momentáneamente) |  | ||||||
|                 _logger.LogError(ex, "La petición HTTP a getBancas falló. URI: {requestUri}", requestUri); |  | ||||||
|                 return null; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Comprobamos que la respuesta fue exitosa Y que contiene datos antes de intentar leerla. |  | ||||||
|             if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) |  | ||||||
|             { |  | ||||||
|                 try |  | ||||||
|                 { |  | ||||||
|                     // Solo si hay contenido, intentamos deserializar. |  | ||||||
|                     return await response.Content.ReadFromJsonAsync<RepartoBancasDto>(); |  | ||||||
|                 } |  | ||||||
|                 catch (JsonException ex) |  | ||||||
|                 { |  | ||||||
|                     // Si el contenido no es un JSON válido, lo registramos y devolvemos null. |  | ||||||
|                     _logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getBancas. URI: {requestUri}, Status: {statusCode}", requestUri, response.StatusCode); |  | ||||||
|                     return null; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             else if (!response.IsSuccessStatusCode) |  | ||||||
|             { |  | ||||||
|                 // Si la API devolvió un error HTTP, lo registramos. |  | ||||||
|                 _logger.LogWarning("La API devolvió un código de error {statusCode} para getBancas. URI: {requestUri}", response.StatusCode, requestUri); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Si la respuesta fue 200 OK pero con cuerpo vacío, o si fue un error HTTP, devolvemos null. |  | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|         // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. |  | ||||||
|  |         // Comprobamos que la respuesta fue exitosa Y que contiene datos antes de intentar leerla. | ||||||
|  |         if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 // Solo si hay contenido, intentamos deserializar. | ||||||
|  |                 return await response.Content.ReadFromJsonAsync<RepartoBancasDto>(); | ||||||
|  |             } | ||||||
|  |             catch (JsonException ex) | ||||||
|  |             { | ||||||
|  |                 // Si el contenido no es un JSON válido, lo registramos y devolvemos null. | ||||||
|  |                 _logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para getBancas. URI: {requestUri}, Status: {statusCode}", requestUri, response.StatusCode); | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else if (!response.IsSuccessStatusCode) | ||||||
|  |         { | ||||||
|  |             // Si la API devolvió un error HTTP, lo registramos. | ||||||
|  |             _logger.LogWarning("La API devolvió un código de error {statusCode} para getBancas. URI: {requestUri}", response.StatusCode, requestUri); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Si la respuesta fue 200 OK pero con cuerpo vacío, o si fue un error HTTP, devolvemos null. | ||||||
|         return null; |         return null; | ||||||
|  |         /* } | ||||||
|  |          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||||
|  |          return null;*/ | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<List<string>?> GetTelegramasTotalizadosAsync(string authToken, string distritoId, string seccionId, int? categoriaId = 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). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 if (lease.IsAcquired) | ||||||
|  |                 {*/ | ||||||
|  |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|  |         var requestUri = $"/api/resultados/getTelegramasTotalizados?distritoId={distritoId}&seccionId={seccionId}"; | ||||||
|  |  | ||||||
|  |         if (categoriaId.HasValue) | ||||||
|         { |         { | ||||||
|             var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |             requestUri += $"&categoriaId={categoriaId.Value}"; | ||||||
|             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; |         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) |     public async Task<TelegramaFileDto?> GetTelegramaFileAsync(string authToken, string mesaId) | ||||||
|     { |     { | ||||||
|         // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 if (lease.IsAcquired) | ||||||
|  |                 {*/ | ||||||
|  |         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}"); | ||||||
|  |  | ||||||
|  |         HttpResponseMessage response; | ||||||
|  |         try | ||||||
|         { |         { | ||||||
|             var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |             response = await client.SendAsync(request); | ||||||
|             var requestUri = $"/api/resultados/getFile?mesaId={mesaId}"; |         } | ||||||
|             var request = new HttpRequestMessage(HttpMethod.Get, requestUri); |         catch (Exception ex) | ||||||
|             request.Headers.Add("Authorization", $"Bearer {authToken}"); |         { | ||||||
|  |             _logger.LogError(ex, "La petición HTTP a getFile falló para la mesa {mesaId}", mesaId); | ||||||
|             HttpResponseMessage response; |  | ||||||
|             try |  | ||||||
|             { |  | ||||||
|                 response = await client.SendAsync(request); |  | ||||||
|             } |  | ||||||
|             catch (Exception ex) |  | ||||||
|             { |  | ||||||
|                 _logger.LogError(ex, "La petición HTTP a getFile falló para la mesa {mesaId}", mesaId); |  | ||||||
|                 return null; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) |  | ||||||
|             { |  | ||||||
|                 try |  | ||||||
|                 { |  | ||||||
|                     return await response.Content.ReadFromJsonAsync<TelegramaFileDto>(); |  | ||||||
|                 } |  | ||||||
|                 catch (JsonException ex) |  | ||||||
|                 { |  | ||||||
|                     // Si la deserialización falla, ahora lo sabremos exactamente. |  | ||||||
|                     _logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para la mesa {mesaId}. Status: {statusCode}", mesaId, response.StatusCode); |  | ||||||
|                     return null; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             else if (!response.IsSuccessStatusCode) |  | ||||||
|             { |  | ||||||
|                 _logger.LogWarning("La API devolvió un código de error {statusCode} para la mesa {mesaId}", response.StatusCode, mesaId); |  | ||||||
|             } |  | ||||||
|             // Si la respuesta fue exitosa pero sin contenido, no es necesario loguearlo como un error, |  | ||||||
|             // simplemente devolvemos null y el Worker lo ignorará. |  | ||||||
|  |  | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|         // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. |  | ||||||
|  |         if (response.IsSuccessStatusCode && response.Content?.Headers.ContentLength > 0) | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 return await response.Content.ReadFromJsonAsync<TelegramaFileDto>(); | ||||||
|  |             } | ||||||
|  |             catch (JsonException ex) | ||||||
|  |             { | ||||||
|  |                 // Si la deserialización falla, ahora lo sabremos exactamente. | ||||||
|  |                 _logger.LogWarning(ex, "La API devolvió una respuesta no-JSON para la mesa {mesaId}. Status: {statusCode}", mesaId, response.StatusCode); | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else if (!response.IsSuccessStatusCode) | ||||||
|  |         { | ||||||
|  |             _logger.LogWarning("La API devolvió un código de error {statusCode} para la mesa {mesaId}", response.StatusCode, mesaId); | ||||||
|  |         } | ||||||
|  |         // Si la respuesta fue exitosa pero sin contenido, no es necesario loguearlo como un error, | ||||||
|  |         // simplemente devolvemos null y el Worker lo ignorará. | ||||||
|  |  | ||||||
|         return null; |         return null; | ||||||
|  |         /* } | ||||||
|  |          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||||
|  |          return null;*/ | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<ResumenDto?> GetResumenAsync(string authToken, string distritoId) |     public async Task<ResumenDto?> GetResumenAsync(string authToken, string distritoId) | ||||||
|     { |     { | ||||||
|         // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 if (lease.IsAcquired) | ||||||
|         { |                 {*/ | ||||||
|             var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|             var requestUri = $"/api/resultados/getResumen?distritoId={distritoId}"; |         var requestUri = $"/api/resultados/getResumen?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<ResumenDto>() : null; |         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<ResumenDto>() : null; | ||||||
|         } |         /* } | ||||||
|         // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. |          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||||
|         return null; |          return null;*/ | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(string authToken, string distritoId, int categoriaId) |     public async Task<EstadoRecuentoGeneralDto?> GetEstadoRecuentoGeneralAsync(string authToken, string distritoId, int categoriaId) | ||||||
|     { |     { | ||||||
|         // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 if (lease.IsAcquired) | ||||||
|         { |                 {*/ | ||||||
|             var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|             // La URL ahora usa el parámetro 'categoriaId' que se recibe |         // La URL ahora usa el parámetro 'categoriaId' que se recibe | ||||||
|             var requestUri = $"/api/estados/estadoRecuento?distritoId={distritoId}&categoriaId={categoriaId}"; |         var requestUri = $"/api/estados/estadoRecuento?distritoId={distritoId}&categoriaId={categoriaId}"; | ||||||
|             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<EstadoRecuentoGeneralDto>() : null; |         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<EstadoRecuentoGeneralDto>() : null; | ||||||
|         } |         /* } | ||||||
|         // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. |          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||||
|         return null; |          return null;*/ | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<List<CategoriaDto>?> GetCategoriasAsync(string authToken) |     public async Task<List<CategoriaDto>?> GetCategoriasAsync(string authToken) | ||||||
|     { |     { | ||||||
|         // "Pedir una ficha". Este método ahora devuelve un "lease" (permiso). |         // "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. |         // Si no hay fichas, esperará aquí automáticamente hasta que se rellene el cubo. | ||||||
|         using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); |         /* | ||||||
|  |                 using RateLimitLease lease = await _rateLimiter.AcquireAsync(1); | ||||||
|  |  | ||||||
|         // Si se nos concede el permiso para proceder... |                 // Si se nos concede el permiso para proceder... | ||||||
|         if (lease.IsAcquired) |                 if (lease.IsAcquired) | ||||||
|         { |                 {*/ | ||||||
|             var client = _httpClientFactory.CreateClient("ElectoralApiClient"); |         var client = _httpClientFactory.CreateClient("ElectoralApiClient"); | ||||||
|             var request = new HttpRequestMessage(HttpMethod.Get, "/api/catalogo/getCategorias"); |         var request = new HttpRequestMessage(HttpMethod.Get, "/api/catalogo/getCategorias"); | ||||||
|             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<CategoriaDto>>() : null; |         return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<List<CategoriaDto>>() : null; | ||||||
|         } |         /* } | ||||||
|         // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. |          // Si no se pudo obtener un permiso (ej. la cola está llena), devolvemos null. | ||||||
|         return null; |          return null;*/ | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -83,7 +83,7 @@ builder.Services.AddHttpClient("ElectoralApiClient", client => | |||||||
| .AddPolicyHandler(GetRetryPolicy()); | .AddPolicyHandler(GetRetryPolicy()); | ||||||
|  |  | ||||||
| // --- LIMITADOR DE VELOCIDAD BASADO EN TOKEN BUCKET --- | // --- LIMITADOR DE VELOCIDAD BASADO EN TOKEN BUCKET --- | ||||||
| builder.Services.AddSingleton<RateLimiter>(sp =>  | /*builder.Services.AddSingleton<RateLimiter>(sp =>  | ||||||
|     new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions |     new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions | ||||||
|     { |     { | ||||||
|         // El tamaño máximo del cubo (la ráfaga máxima que permitimos). |         // El tamaño máximo del cubo (la ráfaga máxima que permitimos). | ||||||
| @@ -101,7 +101,7 @@ builder.Services.AddSingleton<RateLimiter>(sp => | |||||||
|         // Cómo se comporta cuando la cola está llena. |         // Cómo se comporta cuando la cola está llena. | ||||||
|         QueueProcessingOrder = QueueProcessingOrder.OldestFirst |         QueueProcessingOrder = QueueProcessingOrder.OldestFirst | ||||||
|     })); |     })); | ||||||
|  | */ | ||||||
| builder.Services.AddScoped<IElectoralApiService, ElectoralApiService>();  | builder.Services.AddScoped<IElectoralApiService, ElectoralApiService>();  | ||||||
|  |  | ||||||
| builder.Services.AddHostedService<Worker>(); | builder.Services.AddHostedService<Worker>(); | ||||||
|   | |||||||
| @@ -603,111 +603,85 @@ public class Worker : BackgroundService | |||||||
|     { |     { | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             // PASO 1: Obtener los datos base para las consultas. |             _logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas (modo de bajo perfil) ---"); | ||||||
|             // Usamos un DbContext inicial solo para leer los catálogos. |  | ||||||
|             using var initialScope = _serviceProvider.CreateScope(); |  | ||||||
|             var initialDbContext = initialScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |  | ||||||
|  |  | ||||||
|             var partidos = await initialDbContext.AmbitosGeograficos |             using var scope = _serviceProvider.CreateScope(); | ||||||
|  |             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|  |             var partidos = await dbContext.AmbitosGeograficos | ||||||
|                 .AsNoTracking() |                 .AsNoTracking() | ||||||
|                 .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) |                 .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) | ||||||
|                 .ToListAsync(stoppingToken); |                 .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|             var categorias = await initialDbContext.CategoriasElectorales |             var categorias = await dbContext.CategoriasElectorales | ||||||
|                 .AsNoTracking() |                 .AsNoTracking() | ||||||
|                 .ToListAsync(stoppingToken); |                 .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|             if (!partidos.Any() || !categorias.Any()) |             if (!partidos.Any() || !categorias.Any()) return; | ||||||
|  |  | ||||||
|  |             // --- LÓGICA DE GOTEO LENTO --- | ||||||
|  |             // Procesamos una combinación (partido/categoría) a la vez. | ||||||
|  |             foreach (var partido in partidos) | ||||||
|             { |             { | ||||||
|                 _logger.LogWarning("No se encontraron partidos o categorías en la BD para sondear telegramas."); |                 foreach (var categoria in categorias) | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Creamos una lista de todas las consultas que necesitamos hacer (135 partidos * 3 categorías = 405 consultas). |  | ||||||
|             var combinaciones = partidos.SelectMany(partido => categorias, (partido, categoria) => new { partido, categoria }); |  | ||||||
|  |  | ||||||
|             const int GRADO_DE_PARALELISMO = 3; |  | ||||||
|             var semaforo = new SemaphoreSlim(GRADO_DE_PARALELISMO); |  | ||||||
|  |  | ||||||
|             _logger.LogInformation("Iniciando sondeo de Telegramas para {count} combinaciones... con paralelismo de {degree}", combinaciones.Count(), GRADO_DE_PARALELISMO); |  | ||||||
|  |  | ||||||
|             // Usaremos un ConcurrentBag para recolectar de forma segura los telegramas nuevos desde múltiples hilos. |  | ||||||
|             var telegramasNuevosParaGuardar = new ConcurrentBag<Telegrama>(); |  | ||||||
|  |  | ||||||
|             var tareas = combinaciones.Select(async item => |  | ||||||
|             { |  | ||||||
|                 await semaforo.WaitAsync(stoppingToken); |  | ||||||
|                 try |  | ||||||
|                 { |                 { | ||||||
|                     var idsDeApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, item.partido.DistritoId!, item.partido.SeccionId!, item.categoria.Id); |                     // Si la aplicación se apaga, salimos inmediatamente. | ||||||
|  |                     if (stoppingToken.IsCancellationRequested) return; | ||||||
|  |  | ||||||
|                     if (idsDeApi is { Count: > 0 }) |                     // Obtenemos la lista de IDs. | ||||||
|  |                     var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id); | ||||||
|  |  | ||||||
|  |                     if (listaTelegramasApi is { Count: > 0 }) | ||||||
|                     { |                     { | ||||||
|  |                         // Usamos un DbContext propio para este bloque para asegurar que los cambios se guarden. | ||||||
|                         using var innerScope = _serviceProvider.CreateScope(); |                         using var innerScope = _serviceProvider.CreateScope(); | ||||||
|                         var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |                         var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||||
|  |  | ||||||
|                         var idsYaEnDb = await innerDbContext.Telegramas |                         var idsYaEnDb = await innerDbContext.Telegramas | ||||||
|                             .Where(t => idsDeApi.Contains(t.Id)) |                             .Where(t => listaTelegramasApi.Contains(t.Id)) | ||||||
|                             .Select(t => t.Id) |                             .Select(t => t.Id) | ||||||
|                             .ToListAsync(stoppingToken); |                             .ToListAsync(stoppingToken); | ||||||
|  |  | ||||||
|                         var nuevosTelegramasIds = idsDeApi.Except(idsYaEnDb).ToList(); |                         var nuevosTelegramasIds = listaTelegramasApi.Except(idsYaEnDb).ToList(); | ||||||
|  |  | ||||||
|                         if (!nuevosTelegramasIds.Any()) return; |                         if (nuevosTelegramasIds.Any()) | ||||||
|  |  | ||||||
|                         _logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando en paralelo...", nuevosTelegramasIds.Count, item.partido.Nombre, item.categoria.Nombre); |  | ||||||
|  |  | ||||||
|                         // --- NUEVA OPTIMIZACIÓN: Paralelizar la descarga de los archivos --- |  | ||||||
|                         await Task.WhenAll(nuevosTelegramasIds.Select(async mesaId => |  | ||||||
|                         { |                         { | ||||||
|                             var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); |                             _logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre); | ||||||
|                             if (telegramaFile != null) |  | ||||||
|  |                             // Descargamos los archivos de uno en uno, con una pausa entre cada uno. | ||||||
|  |                             foreach (var mesaId in nuevosTelegramasIds) | ||||||
|                             { |                             { | ||||||
|                                 var nuevoTelegrama = new Telegrama |                                 if (stoppingToken.IsCancellationRequested) return; | ||||||
|  |  | ||||||
|  |                                 var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); | ||||||
|  |                                 if (telegramaFile != null) | ||||||
|                                 { |                                 { | ||||||
|                                     Id = telegramaFile.NombreArchivo, |                                     var nuevoTelegrama = new Telegrama | ||||||
|                                     AmbitoGeograficoId = item.partido.Id, |                                     { | ||||||
|                                     ContenidoBase64 = telegramaFile.Imagen, |                                         Id = telegramaFile.NombreArchivo, | ||||||
|                                     FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), |                                         AmbitoGeograficoId = partido.Id, | ||||||
|                                     FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() |                                         ContenidoBase64 = telegramaFile.Imagen, | ||||||
|                                 }; |                                         FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), | ||||||
|                                 telegramasNuevosParaGuardar.Add(nuevoTelegrama); |                                         FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() | ||||||
|  |                                     }; | ||||||
|  |                                     await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken); | ||||||
|  |                                 } | ||||||
|  |                                 // PAUSA DELIBERADA: Esperamos un poco para no parecer un bot. | ||||||
|  |                                 await Task.Delay(250, stoppingToken); // 250ms de espera = 4 peticiones/segundo máximo. | ||||||
|                             } |                             } | ||||||
|                             // Si telegramaFile es null (por 403, 200 vacío, etc.), simplemente no hacemos nada. |                             await innerDbContext.SaveChangesAsync(stoppingToken); | ||||||
|                         })); |                         } | ||||||
|                     } |                     } | ||||||
|                 } |  | ||||||
|                 // --- NUEVA ROBUSTEZ: Capturar errores por tarea --- |  | ||||||
|                 catch (Exception ex) |  | ||||||
|                 { |  | ||||||
|                     // Si una combinación entera falla (ej. getTelegramasTotalizados da 500), lo logueamos |  | ||||||
|                     // pero NO relanzamos la excepción, para no cancelar el Task.WhenAll principal. |  | ||||||
|                     _logger.LogError(ex, "Falló el sondeo de telegramas para el partido '{partido}' y categoría '{cat}'", item.partido.Nombre, item.categoria.Nombre); |  | ||||||
|                 } |  | ||||||
|                 finally |  | ||||||
|                 { |  | ||||||
|                     semaforo.Release(); |  | ||||||
|                     // Añadir un pequeño retraso aleatorio |  | ||||||
|                     await Task.Delay(TimeSpan.FromMilliseconds(new Random().Next(50, 251)), stoppingToken); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             // Esperamos a que todas las tareas de sondeo y descarga terminen. |                     // PAUSA DELIBERADA: Esperamos un poco entre cada consulta de lista de telegramas. | ||||||
|             await Task.WhenAll(tareas); |                     await Task.Delay(100, stoppingToken); | ||||||
|  |                 } | ||||||
|             // --- Guardado Masivo Final --- |  | ||||||
|             // Después de que todo el paralelismo ha terminado, hacemos una única operación de escritura en la BD. |  | ||||||
|             if (!telegramasNuevosParaGuardar.IsEmpty) |  | ||||||
|             { |  | ||||||
|                 _logger.LogInformation("Guardando un total de {count} telegramas nuevos en la base de datos...", telegramasNuevosParaGuardar.Count); |  | ||||||
|                 using var finalScope = _serviceProvider.CreateScope(); |  | ||||||
|                 var finalDbContext = finalScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); |  | ||||||
|                 await finalDbContext.Telegramas.AddRangeAsync(telegramasNuevosParaGuardar, stoppingToken); |  | ||||||
|                 await finalDbContext.SaveChangesAsync(stoppingToken); |  | ||||||
|             } |             } | ||||||
|  |             _logger.LogInformation("Sondeo de Telegramas completado."); | ||||||
|             _logger.LogInformation( |         } | ||||||
|                 "Sondeo de Telegramas completado. Se guardaron {count} nuevos telegramas.", telegramasNuevosParaGuardar.Count); |         catch (OperationCanceledException) | ||||||
|  |         { | ||||||
|  |             _logger.LogInformation("Sondeo de telegramas cancelado."); | ||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user