166 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			166 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using AngleSharp;
 | |
| using Mercados.Core.Entities;
 | |
| using Mercados.Infrastructure.Persistence.Repositories;
 | |
| using Microsoft.Extensions.Logging;
 | |
| using System.Globalization;
 | |
| 
 | |
| namespace Mercados.Infrastructure.DataFetchers
 | |
| {
 | |
|     public class MercadoAgroFetcher : IDataFetcher
 | |
|     {
 | |
|         public string SourceName => "MercadoAgroganadero";
 | |
|         private const string DataUrl = "https://www.mercadoagroganadero.com.ar/dll/hacienda6.dll/haciinfo000225";
 | |
|         private readonly IHttpClientFactory _httpClientFactory;
 | |
|         private readonly ICotizacionGanadoRepository _cotizacionRepository;
 | |
|         private readonly IFuenteDatoRepository _fuenteDatoRepository;
 | |
|         private readonly ILogger<MercadoAgroFetcher> _logger;
 | |
| 
 | |
|         public MercadoAgroFetcher(
 | |
|             IHttpClientFactory httpClientFactory,
 | |
|             ICotizacionGanadoRepository cotizacionRepository,
 | |
|             IFuenteDatoRepository fuenteDatoRepository,
 | |
|             ILogger<MercadoAgroFetcher> logger)
 | |
|         {
 | |
|             _httpClientFactory = httpClientFactory;
 | |
|             _cotizacionRepository = cotizacionRepository;
 | |
|             _fuenteDatoRepository = fuenteDatoRepository;
 | |
|             _logger = logger;
 | |
|         }
 | |
| 
 | |
|         public async Task<(bool Success, string Message)> FetchDataAsync()
 | |
|         {
 | |
|             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
 | |
| 
 | |
|             try
 | |
|             {
 | |
|                 var htmlContent = await GetHtmlContentAsync();
 | |
|                 if (string.IsNullOrEmpty(htmlContent))
 | |
|                 {
 | |
|                     // Esto sigue siendo un fallo, no se pudo obtener la página
 | |
|                     return (false, "No se pudo obtener el contenido HTML.");
 | |
|                 }
 | |
| 
 | |
|                 var cotizaciones = ParseHtmlToEntities(htmlContent);
 | |
| 
 | |
|                 if (!cotizaciones.Any())
 | |
|                 {
 | |
|                     // La conexión fue exitosa, pero no se encontraron datos válidos.
 | |
|                     // Esto NO es un error crítico, es un estado informativo.
 | |
|                     _logger.LogInformation("La conexión con {SourceName} fue exitosa, pero no se encontraron datos de cotizaciones para procesar.", SourceName);
 | |
|                     return (true, "Conexión exitosa, pero no se encontraron nuevos datos.");
 | |
|                 }
 | |
| 
 | |
|                 await _cotizacionRepository.GuardarMuchosAsync(cotizaciones);
 | |
|                 await UpdateSourceInfoAsync();
 | |
| 
 | |
|                 _logger.LogInformation("Fetch para {SourceName} completado exitosamente. Se guardaron {Count} registros.", SourceName, cotizaciones.Count);
 | |
|                 return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros.");
 | |
|             }
 | |
|             catch (Exception ex)
 | |
|             {
 | |
|                 // Un catch aquí sí es un error real (ej. 404, timeout, etc.)
 | |
|                 _logger.LogError(ex, "Ocurrió un error durante el fetch para {SourceName}.", SourceName);
 | |
|                 return (false, $"Error: {ex.Message}");
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         private async Task<string> GetHtmlContentAsync()
 | |
|         {
 | |
|             // Pedimos el cliente HTTP con el nombre específico que tiene la política de Polly
 | |
|             var client = _httpClientFactory.CreateClient("MercadoAgroFetcher");
 | |
|             // Es importante simular un navegador para evitar bloqueos.
 | |
|             client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
 | |
| 
 | |
|             var response = await client.GetAsync(DataUrl);
 | |
|             response.EnsureSuccessStatusCode();
 | |
|             // El sitio usa una codificación específica, hay que decodificarla correctamente.
 | |
|             var stream = await response.Content.ReadAsStreamAsync();
 | |
|             using var reader = new StreamReader(stream, System.Text.Encoding.GetEncoding("windows-1252"));
 | |
|             return await reader.ReadToEndAsync();
 | |
|         }
 | |
| 
 | |
|         private List<CotizacionGanado> ParseHtmlToEntities(string html)
 | |
|         {
 | |
|             var config = Configuration.Default;
 | |
|             var context = BrowsingContext.New(config);
 | |
|             var document = context.OpenAsync(req => req.Content(html)).Result;
 | |
| 
 | |
|             var tabla = document.QuerySelector("table.table.table-striped");
 | |
|             if (tabla == null)
 | |
|             {
 | |
|                 _logger.LogWarning("No se encontró la tabla de cotizaciones en el HTML.");
 | |
|                 return new List<CotizacionGanado>();
 | |
|             }
 | |
| 
 | |
|             var cotizaciones = new List<CotizacionGanado>();
 | |
|             // Omitimos las primeras 2 filas (cabeceras) y las últimas 2 (totales/pies).
 | |
|             var filas = tabla.QuerySelectorAll("tr").Skip(2).SkipLast(2);
 | |
| 
 | |
|             foreach (var fila in filas)
 | |
|             {
 | |
|                 var celdas = fila.QuerySelectorAll("td").Select(c => c.TextContent.Trim()).ToList();
 | |
| 
 | |
|                 if (celdas.Count < 12) continue; // Fila inválida
 | |
| 
 | |
|                 try
 | |
|                 {
 | |
|                     var cotizacion = new CotizacionGanado
 | |
|                     {
 | |
|                         Categoria = celdas[1],
 | |
|                         Especificaciones = $"{celdas[2]} - {celdas[3]}",
 | |
|                         Maximo = ParseDecimal(celdas[4]),
 | |
|                         Minimo = ParseDecimal(celdas[5]),
 | |
|                         Promedio = ParseDecimal(celdas[6]),
 | |
|                         Mediano = ParseDecimal(celdas[7]),
 | |
|                         Cabezas = ParseInt(celdas[8]),
 | |
|                         KilosTotales = ParseInt(celdas[9]),
 | |
|                         KilosPorCabeza = ParseInt(celdas[10]),
 | |
|                         ImporteTotal = ParseDecimal(celdas[11]),
 | |
|                         FechaRegistro = DateTime.UtcNow
 | |
|                     };
 | |
|                     cotizaciones.Add(cotizacion);
 | |
|                 }
 | |
|                 catch (Exception ex)
 | |
|                 {
 | |
|                     _logger.LogWarning(ex, "No se pudo parsear una fila de la tabla. Contenido: {RowContent}", string.Join(" | ", celdas));
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             return cotizaciones;
 | |
|         }
 | |
| 
 | |
|         private async Task UpdateSourceInfoAsync()
 | |
|         {
 | |
|             var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
 | |
|             if (fuente == null)
 | |
|             {
 | |
|                 await _fuenteDatoRepository.CrearAsync(new FuenteDato
 | |
|                 {
 | |
|                     Nombre = SourceName,
 | |
|                     Url = DataUrl,
 | |
|                     UltimaEjecucionExitosa = DateTime.UtcNow
 | |
|                 });
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 fuente.Url = DataUrl;
 | |
|                 fuente.UltimaEjecucionExitosa = DateTime.UtcNow;
 | |
|                 await _fuenteDatoRepository.ActualizarAsync(fuente);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // --- Funciones de Ayuda para Parseo ---
 | |
|         private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR");
 | |
|         private decimal ParseDecimal(string value)
 | |
|         {
 | |
|             // El sitio usa '.' como separador de miles y ',' como decimal.
 | |
|             // Primero quitamos el de miles, luego reemplazamos la coma decimal por un punto.
 | |
|             var cleanValue = value.Replace("$", "").Replace(".", "").Trim();
 | |
|             return decimal.Parse(cleanValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, _cultureInfo);
 | |
|         }
 | |
|         private int ParseInt(string value)
 | |
|         {
 | |
|             return int.Parse(value.Replace(".", ""), CultureInfo.InvariantCulture);
 | |
|         }
 | |
|     }
 | |
| } |