Feat: Backend's documentation added
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -178,6 +178,8 @@ DocProject/Help/*.hhk | |||||||
| DocProject/Help/*.hhp | DocProject/Help/*.hhp | ||||||
| DocProject/Help/Html2 | DocProject/Help/Html2 | ||||||
| DocProject/Help/html | DocProject/Help/html | ||||||
|  | # DocFx | ||||||
|  | [Dd]oc/ | ||||||
|  |  | ||||||
| # Click-Once directory | # Click-Once directory | ||||||
| publish/ | publish/ | ||||||
|   | |||||||
| @@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Mvc; | |||||||
|  |  | ||||||
| namespace Mercados.Api.Controllers | namespace Mercados.Api.Controllers | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Controlador principal para exponer los datos de los mercados financieros. | ||||||
|  |     /// </summary> | ||||||
|     [ApiController] |     [ApiController] | ||||||
|     [Route("api/[controller]")] |     [Route("api/[controller]")] | ||||||
|     public class MercadosController : ControllerBase |     public class MercadosController : ControllerBase | ||||||
| @@ -15,7 +18,14 @@ namespace Mercados.Api.Controllers | |||||||
|         private readonly IHolidayService _holidayService; |         private readonly IHolidayService _holidayService; | ||||||
|         private readonly ILogger<MercadosController> _logger; |         private readonly ILogger<MercadosController> _logger; | ||||||
|  |  | ||||||
|         // Inyectamos TODOS los repositorios que necesita el controlador. |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia del controlador MercadosController. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="bolsaRepo">Repositorio para datos de la bolsa.</param> | ||||||
|  |         /// <param name="granoRepo">Repositorio para datos de granos.</param> | ||||||
|  |         /// <param name="ganadoRepo">Repositorio para datos de ganado.</param> | ||||||
|  |         /// <param name="holidayService">Servicio para consultar feriados.</param> | ||||||
|  |         /// <param name="logger">Servicio de logging.</param> | ||||||
|         public MercadosController( |         public MercadosController( | ||||||
|             ICotizacionBolsaRepository bolsaRepo, |             ICotizacionBolsaRepository bolsaRepo, | ||||||
|             ICotizacionGranoRepository granoRepo, |             ICotizacionGranoRepository granoRepo, | ||||||
| @@ -30,7 +40,10 @@ namespace Mercados.Api.Controllers | |||||||
|             _logger = logger; |             _logger = logger; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // --- Endpoint para Agroganadero --- |         /// <summary> | ||||||
|  |         /// Obtiene el último parte completo del mercado agroganadero. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una colección de objetos CotizacionGanado.</returns> | ||||||
|         [HttpGet("agroganadero")] |         [HttpGet("agroganadero")] | ||||||
|         [ProducesResponseType(typeof(IEnumerable<CotizacionGanado>), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(IEnumerable<CotizacionGanado>), StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
| @@ -48,7 +61,10 @@ namespace Mercados.Api.Controllers | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // --- Endpoint para Granos --- |         /// <summary> | ||||||
|  |         /// Obtiene las últimas cotizaciones para los principales granos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una colección de objetos CotizacionGrano.</returns> | ||||||
|         [HttpGet("granos")] |         [HttpGet("granos")] | ||||||
|         [ProducesResponseType(typeof(IEnumerable<CotizacionGrano>), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(IEnumerable<CotizacionGrano>), StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
| @@ -67,6 +83,10 @@ namespace Mercados.Api.Controllers | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // --- Endpoints de Bolsa --- |         // --- Endpoints de Bolsa --- | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene las últimas cotizaciones para el mercado de bolsa de EEUU. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una colección de objetos CotizacionBolsa.</returns> | ||||||
|         [HttpGet("bolsa/eeuu")] |         [HttpGet("bolsa/eeuu")] | ||||||
|         [ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
| @@ -84,6 +104,10 @@ namespace Mercados.Api.Controllers | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene las últimas cotizaciones para el mercado de bolsa local. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una colección de objetos CotizacionBolsa.</returns> | ||||||
|         [HttpGet("bolsa/local")] |         [HttpGet("bolsa/local")] | ||||||
|         [ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
| @@ -101,6 +125,13 @@ namespace Mercados.Api.Controllers | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene el historial de cotizaciones para un ticker específico en un mercado determinado. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="ticker">El identificador del ticker.</param> | ||||||
|  |         /// <param name="mercado">El nombre del mercado (por defecto "Local").</param> | ||||||
|  |         /// <param name="dias">Cantidad de días de historial a recuperar (por defecto 30).</param> | ||||||
|  |         /// <returns>Una colección de objetos CotizacionBolsa.</returns> | ||||||
|                 [HttpGet("bolsa/history/{ticker}")] |                 [HttpGet("bolsa/history/{ticker}")] | ||||||
|                 [ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)] |                 [ProducesResponseType(typeof(IEnumerable<CotizacionBolsa>), StatusCodes.Status200OK)] | ||||||
|                 [ProducesResponseType(StatusCodes.Status500InternalServerError)] |                 [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
| @@ -118,6 +149,13 @@ namespace Mercados.Api.Controllers | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene el historial de cotizaciones para una categoría y especificaciones de ganado en un rango de días. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="categoria">La categoría de ganado.</param> | ||||||
|  |         /// <param name="especificaciones">Las especificaciones del ganado.</param> | ||||||
|  |         /// <param name="dias">Cantidad de días de historial a recuperar (por defecto 30).</param> | ||||||
|  |         /// <returns>Una colección de objetos CotizacionGanado.</returns> | ||||||
|         [HttpGet("agroganadero/history")] |         [HttpGet("agroganadero/history")] | ||||||
|         [ProducesResponseType(typeof(IEnumerable<CotizacionGanado>), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(IEnumerable<CotizacionGanado>), StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
| @@ -135,6 +173,12 @@ namespace Mercados.Api.Controllers | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene el historial de cotizaciones para un grano específico en un rango de días. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="nombre">El nombre del grano.</param> | ||||||
|  |         /// <param name="dias">Cantidad de días de historial a recuperar (por defecto 30).</param> | ||||||
|  |         /// <returns>Una colección de objetos CotizacionGrano.</returns> | ||||||
|         [HttpGet("granos/history/{nombre}")] |         [HttpGet("granos/history/{nombre}")] | ||||||
|         [ProducesResponseType(typeof(IEnumerable<CotizacionGrano>), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(IEnumerable<CotizacionGrano>), StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
| @@ -152,6 +196,11 @@ namespace Mercados.Api.Controllers | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Verifica si la fecha actual es feriado para el mercado especificado. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="mercado">El nombre del mercado a consultar.</param> | ||||||
|  |         /// <returns>True si es feriado, false en caso contrario.</returns> | ||||||
|         [HttpGet("es-feriado/{mercado}")] |         [HttpGet("es-feriado/{mercado}")] | ||||||
|         [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] |         [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] | ||||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] |         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
|     <Nullable>enable</Nullable> |     <Nullable>enable</Nullable> | ||||||
|     <ImplicitUsings>enable</ImplicitUsings> |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|     <UserSecretsId>28c6a673-1f1e-4140-aa75-a0d894d1fbc4</UserSecretsId> |     <UserSecretsId>28c6a673-1f1e-4140-aa75-a0d894d1fbc4</UserSecretsId> | ||||||
|  |     <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|  |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|   | |||||||
| @@ -9,12 +9,25 @@ namespace Mercados.Api.Utils | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     public class UtcDateTimeConverter : JsonConverter<DateTime> |     public class UtcDateTimeConverter : JsonConverter<DateTime> | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Lee un valor DateTime desde el lector JSON y lo convierte a UTC. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="reader">El lector JSON.</param> | ||||||
|  |         /// <param name="typeToConvert">El tipo a convertir.</param> | ||||||
|  |         /// <param name="options">Las opciones de serialización JSON.</param> | ||||||
|  |         /// <returns>El valor DateTime en UTC.</returns> | ||||||
|         public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) |         public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||||||
|         { |         { | ||||||
|             // Al leer un string de fecha, nos aseguramos de que se interprete como UTC |             // Al leer un string de fecha, nos aseguramos de que se interprete como UTC | ||||||
|             return reader.GetDateTime().ToUniversalTime(); |             return reader.GetDateTime().ToUniversalTime(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Escribe un valor DateTime en formato UTC como una cadena en el escritor JSON. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="writer">El escritor JSON.</param> | ||||||
|  |         /// <param name="value">El valor DateTime a escribir.</param> | ||||||
|  |         /// <param name="options">Las opciones de serialización JSON.</param> | ||||||
|                 public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) |                 public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) | ||||||
|                 { |                 { | ||||||
|                     // Antes de escribir el string, especificamos que el 'Kind' es Utc. |                     // Antes de escribir el string, especificamos que el 'Kind' es Utc. | ||||||
|   | |||||||
| @@ -1,15 +1,53 @@ | |||||||
| namespace Mercados.Core.Entities | namespace Mercados.Core.Entities | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Representa una única captura de cotización para un activo de la bolsa de valores. | ||||||
|  |     /// </summary> | ||||||
|     public class CotizacionBolsa |     public class CotizacionBolsa | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Identificador único del registro en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         public long Id { get; set; } |         public long Id { get; set; } | ||||||
|     public string Ticker { get; set; } = string.Empty; // "AAPL", "GGAL.BA", etc. |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El símbolo o identificador del activo en el mercado (ej. "AAPL", "GGAL.BA"). | ||||||
|  |         /// </summary> | ||||||
|  |         public string Ticker { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El nombre completo de la empresa o del activo. | ||||||
|  |         /// </summary> | ||||||
|         public string? NombreEmpresa { get; set; } |         public string? NombreEmpresa { get; set; } | ||||||
|     public string Mercado { get; set; } = string.Empty; // "EEUU" o "Local" |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El mercado al que pertenece el activo (ej. "EEUU", "Local"). | ||||||
|  |         /// </summary> | ||||||
|  |         public string Mercado { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El último precio registrado para el activo. | ||||||
|  |         /// </summary> | ||||||
|         public decimal PrecioActual { get; set; } |         public decimal PrecioActual { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El precio del activo al inicio de la jornada de mercado. | ||||||
|  |         /// </summary> | ||||||
|         public decimal Apertura { get; set; } |         public decimal Apertura { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El precio de cierre del activo en la jornada anterior. | ||||||
|  |         /// </summary> | ||||||
|         public decimal CierreAnterior { get; set; } |         public decimal CierreAnterior { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El cambio porcentual del precio actual con respecto al cierre anterior. | ||||||
|  |         /// </summary> | ||||||
|         public decimal PorcentajeCambio { get; set; } |         public decimal PorcentajeCambio { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// La fecha y hora (en UTC) en que se registró esta cotización en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         public DateTime FechaRegistro { get; set; } |         public DateTime FechaRegistro { get; set; } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,18 +1,68 @@ | |||||||
| namespace Mercados.Core.Entities | namespace Mercados.Core.Entities | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Representa una cotización para una categoría de ganado en el Mercado Agroganadero. | ||||||
|  |     /// </summary> | ||||||
|     public class CotizacionGanado |     public class CotizacionGanado | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Identificador único del registro en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         public long Id { get; set; } |         public long Id { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// La categoría principal del ganado (ej. "NOVILLOS", "VACAS"). | ||||||
|  |         /// </summary> | ||||||
|         public string Categoria { get; set; } = string.Empty; |         public string Categoria { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Detalles adicionales sobre la categoría, como raza o peso. | ||||||
|  |         /// </summary> | ||||||
|         public string Especificaciones { get; set; } = string.Empty; |         public string Especificaciones { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El precio máximo alcanzado para esta categoría en la jornada. | ||||||
|  |         /// </summary> | ||||||
|         public decimal Maximo { get; set; } |         public decimal Maximo { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El precio mínimo alcanzado para esta categoría en la jornada. | ||||||
|  |         /// </summary> | ||||||
|         public decimal Minimo { get; set; } |         public decimal Minimo { get; set; } | ||||||
|  |          | ||||||
|  |         /// <summary> | ||||||
|  |         /// El precio promedio ponderado para la categoría. | ||||||
|  |         /// </summary> | ||||||
|         public decimal Promedio { get; set; } |         public decimal Promedio { get; set; } | ||||||
|  |          | ||||||
|  |         /// <summary> | ||||||
|  |         /// El precio mediano (valor central) registrado para la categoría. | ||||||
|  |         /// </summary> | ||||||
|         public decimal Mediano { get; set; } |         public decimal Mediano { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El número total de cabezas de ganado comercializadas en esta categoría. | ||||||
|  |         /// </summary> | ||||||
|         public int Cabezas { get; set; } |         public int Cabezas { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El peso total en kilogramos de todo el ganado comercializado. | ||||||
|  |         /// </summary> | ||||||
|         public int KilosTotales { get; set; } |         public int KilosTotales { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El peso promedio por cabeza de ganado. | ||||||
|  |         /// </summary> | ||||||
|         public int KilosPorCabeza { get; set; } |         public int KilosPorCabeza { get; set; } | ||||||
|  |          | ||||||
|  |         /// <summary> | ||||||
|  |         /// El importe total monetario de las transacciones para esta categoría. | ||||||
|  |         /// </summary> | ||||||
|         public decimal ImporteTotal { get; set; } |         public decimal ImporteTotal { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// La fecha y hora (en UTC) en que se registró esta cotización en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         public DateTime FechaRegistro { get; set; } |         public DateTime FechaRegistro { get; set; } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,12 +1,38 @@ | |||||||
| namespace Mercados.Core.Entities | namespace Mercados.Core.Entities | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Representa una cotización para un tipo de grano específico. | ||||||
|  |     /// </summary> | ||||||
|     public class CotizacionGrano |     public class CotizacionGrano | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Identificador único del registro en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         public long Id { get; set; } |         public long Id { get; set; } | ||||||
|     public string Nombre { get; set; } = string.Empty; // "Soja", "Trigo", etc. |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El nombre del grano (ej. "Soja", "Trigo", "Maíz"). | ||||||
|  |         /// </summary> | ||||||
|  |         public string Nombre { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El precio de cotización, generalmente por tonelada. | ||||||
|  |         /// </summary> | ||||||
|         public decimal Precio { get; set; } |         public decimal Precio { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// La variación del precio con respecto a la cotización anterior. | ||||||
|  |         /// </summary> | ||||||
|         public decimal VariacionPrecio { get; set; } |         public decimal VariacionPrecio { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// La fecha en que se concertó la operación de la cotización. | ||||||
|  |         /// </summary> | ||||||
|         public DateTime FechaOperacion { get; set; } |         public DateTime FechaOperacion { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// La fecha y hora (en UTC) en que se registró esta cotización en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         public DateTime FechaRegistro { get; set; } |         public DateTime FechaRegistro { get; set; } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,10 +1,31 @@ | |||||||
| namespace Mercados.Core.Entities | namespace Mercados.Core.Entities | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Representa una fuente de datos externa desde la cual se obtiene información. | ||||||
|  |     /// Esta entidad se utiliza para auditar y monitorear la salud de los Data Fetchers. | ||||||
|  |     /// </summary> | ||||||
|     public class FuenteDato |     public class FuenteDato | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Identificador único del registro en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         public long Id { get; set; } |         public long Id { get; set; } | ||||||
|     public string Nombre { get; set; } = string.Empty; // "BCR", "Finnhub", "YahooFinance", "MercadoAgroganadero" |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El nombre único que identifica a la fuente de datos (ej. "BCR", "Finnhub", "YahooFinance", "MercadoAgroganadero"). | ||||||
|  |         /// Este nombre coincide con la propiedad SourceName de la interfaz IDataFetcher. | ||||||
|  |         /// </summary> | ||||||
|  |         public string Nombre { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// La fecha y hora (en UTC) de la última vez que el Data Fetcher correspondiente | ||||||
|  |         /// se ejecutó y completó su tarea exitosamente. | ||||||
|  |         /// </summary> | ||||||
|         public DateTime UltimaEjecucionExitosa { get; set; } |         public DateTime UltimaEjecucionExitosa { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// La URL base o principal de la fuente de datos, para referencia. | ||||||
|  |         /// </summary> | ||||||
|         public string? Url { get; set; } |         public string? Url { get; set; } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,10 +1,28 @@ | |||||||
| namespace Mercados.Core.Entities | namespace Mercados.Core.Entities | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Representa un único día feriado para un mercado bursátil específico. | ||||||
|  |     /// </summary> | ||||||
|     public class MercadoFeriado |     public class MercadoFeriado | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Identificador único del registro en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         public long Id { get; set; } |         public long Id { get; set; } | ||||||
|         public string CodigoMercado { get; set; } = string.Empty; // "US" o "BA" |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El código del mercado al que pertenece el feriado (ej. "US", "BA"). | ||||||
|  |         /// </summary> | ||||||
|  |         public string CodigoMercado { get; set; } = string.Empty; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// La fecha exacta del feriado (la hora no es relevante). | ||||||
|  |         /// </summary> | ||||||
|         public DateTime Fecha { get; set; } |         public DateTime Fecha { get; set; } | ||||||
|         public string? Nombre { get; set; } // Nombre del feriado, si la API lo provee |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// El nombre o la descripción del feriado (si está disponible). | ||||||
|  |         /// </summary> | ||||||
|  |         public string? Nombre { get; set; } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -4,6 +4,7 @@ | |||||||
|     <TargetFramework>net9.0</TargetFramework> |     <TargetFramework>net9.0</TargetFramework> | ||||||
|     <ImplicitUsings>enable</ImplicitUsings> |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|     <Nullable>enable</Nullable> |     <Nullable>enable</Nullable> | ||||||
|  |     <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|  |  | ||||||
| </Project> | </Project> | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
|     <TargetFramework>net9.0</TargetFramework> |     <TargetFramework>net9.0</TargetFramework> | ||||||
|     <ImplicitUsings>enable</ImplicitUsings> |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|     <Nullable>enable</Nullable> |     <Nullable>enable</Nullable> | ||||||
|  |     <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|  |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|   | |||||||
| @@ -2,8 +2,10 @@ using FluentMigrator; | |||||||
|  |  | ||||||
| namespace Mercados.Database.Migrations | namespace Mercados.Database.Migrations | ||||||
| { | { | ||||||
|     // El número es la versión única de esta migración. |     /// <summary> | ||||||
|     // Usar un timestamp es una práctica común y segura. |     /// Migración inicial que crea las tablas necesarias para almacenar | ||||||
|  |     /// las cotizaciones de ganado, granos, bolsa y fuentes de datos. | ||||||
|  |     /// </summary> | ||||||
|     [Migration(20250701113000)] |     [Migration(20250701113000)] | ||||||
|     public class CreateInitialTables : Migration |     public class CreateInitialTables : Migration | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -2,15 +2,25 @@ using FluentMigrator; | |||||||
|  |  | ||||||
| namespace Mercados.Database.Migrations | namespace Mercados.Database.Migrations | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Migración que añade la columna 'NombreEmpresa' a la tabla 'CotizacionesBolsa' | ||||||
|  |     /// para almacenar el nombre descriptivo de la acción. | ||||||
|  |     /// </summary> | ||||||
|     [Migration(20250702133000)] |     [Migration(20250702133000)] | ||||||
|     public class AddNameToStocks : Migration |     public class AddNameToStocks : Migration | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Aplica la migración, añadiendo la columna 'NombreEmpresa'. | ||||||
|  |         /// </summary> | ||||||
|         public override void Up() |         public override void Up() | ||||||
|         { |         { | ||||||
|             Alter.Table("CotizacionesBolsa") |             Alter.Table("CotizacionesBolsa") | ||||||
|                 .AddColumn("NombreEmpresa").AsString(255).Nullable(); |                 .AddColumn("NombreEmpresa").AsString(255).Nullable(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Revierte la migración, eliminando la columna 'NombreEmpresa'. | ||||||
|  |         /// </summary> | ||||||
|         public override void Down() |         public override void Down() | ||||||
|         { |         { | ||||||
|             Delete.Column("NombreEmpresa").FromTable("CotizacionesBolsa"); |             Delete.Column("NombreEmpresa").FromTable("CotizacionesBolsa"); | ||||||
|   | |||||||
| @@ -2,11 +2,18 @@ using FluentMigrator; | |||||||
|  |  | ||||||
| namespace Mercados.Database.Migrations | namespace Mercados.Database.Migrations | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Migración para crear la tabla 'MercadosFeriados', que almacenará los días no laborables | ||||||
|  |     /// para diferentes mercados bursátiles. | ||||||
|  |     /// </summary> | ||||||
|     [Migration(20250714150000)] |     [Migration(20250714150000)] | ||||||
|     public class CreateMercadoFeriadoTable : Migration |     public class CreateMercadoFeriadoTable : Migration | ||||||
|     { |     { | ||||||
|         private const string TableName = "MercadosFeriados"; |         private const string TableName = "MercadosFeriados"; | ||||||
|          |          | ||||||
|  |         /// <summary> | ||||||
|  |         /// Define la estructura de la tabla 'MercadosFeriados' y crea un índice único. | ||||||
|  |         /// </summary> | ||||||
|         public override void Up() |         public override void Up() | ||||||
|         { |         { | ||||||
|             Create.Table(TableName) |             Create.Table(TableName) | ||||||
| @@ -23,6 +30,9 @@ namespace Mercados.Database.Migrations | |||||||
|                 .WithOptions().Unique(); |                 .WithOptions().Unique(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Revierte la migración eliminando la tabla 'MercadosFeriados'. | ||||||
|  |         /// </summary> | ||||||
|         public override void Down() |         public override void Down() | ||||||
|         { |         { | ||||||
|             Delete.Table(TableName); |             Delete.Table(TableName); | ||||||
|   | |||||||
| @@ -8,37 +8,83 @@ using System.Text.Json.Serialization; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.DataFetchers | namespace Mercados.Infrastructure.DataFetchers | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Implementación de <see cref="IDataFetcher"/> para obtener datos de cotizaciones de granos | ||||||
|  |     /// desde la API de la Bolsa de Comercio de Rosario (BCR). | ||||||
|  |     /// </summary> | ||||||
|     public class BcrDataFetcher : IDataFetcher |     public class BcrDataFetcher : IDataFetcher | ||||||
|     { |     { | ||||||
|         #region Clases DTO para la respuesta de la API de BCR |         #region Clases DTO para la respuesta de la API de BCR | ||||||
|  |         /// <summary> | ||||||
|  |         /// DTO para la respuesta del endpoint de autenticación de BCR. | ||||||
|  |         /// </summary> | ||||||
|         private class BcrTokenResponse |         private class BcrTokenResponse | ||||||
|         { |         { | ||||||
|  |             /// <summary> | ||||||
|  |             /// Contenedor de datos del token. | ||||||
|  |             /// </summary> | ||||||
|             [JsonPropertyName("data")] |             [JsonPropertyName("data")] | ||||||
|             public TokenData? Data { get; set; } |             public TokenData? Data { get; set; } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Contiene el token de autenticación. | ||||||
|  |         /// </summary> | ||||||
|         private class TokenData |         private class TokenData | ||||||
|         { |         { | ||||||
|  |             /// <summary> | ||||||
|  |             /// El token JWT para autenticar las solicitudes. | ||||||
|  |             /// </summary> | ||||||
|             [JsonPropertyName("token")] |             [JsonPropertyName("token")] | ||||||
|             public string? Token { get; set; } |             public string? Token { get; set; } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// DTO para la respuesta del endpoint de precios de BCR. | ||||||
|  |         /// </summary> | ||||||
|         private class BcrPreciosResponse |         private class BcrPreciosResponse | ||||||
|         { |         { | ||||||
|  |             /// <summary> | ||||||
|  |             /// Lista de precios de granos. | ||||||
|  |             /// </summary> | ||||||
|             [JsonPropertyName("data")] |             [JsonPropertyName("data")] | ||||||
|             public List<BcrPrecioItem>? Data { get; set; } |             public List<BcrPrecioItem>? Data { get; set; } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Representa un ítem individual de precio en la respuesta de la API de BCR. | ||||||
|  |         /// </summary> | ||||||
|         private class BcrPrecioItem |         private class BcrPrecioItem | ||||||
|         { |         { | ||||||
|  |             /// <summary> | ||||||
|  |             /// El precio de cotización del grano. | ||||||
|  |             /// </summary> | ||||||
|             [JsonPropertyName("precio_Cotizacion")] |             [JsonPropertyName("precio_Cotizacion")] | ||||||
|             public decimal PrecioCotizacion { get; set; } |             public decimal PrecioCotizacion { get; set; } | ||||||
|  |             /// <summary> | ||||||
|  |             /// La variación del precio respecto a la cotización anterior. | ||||||
|  |             /// </summary> | ||||||
|             [JsonPropertyName("variacion_Precio_Cotizacion")] |             [JsonPropertyName("variacion_Precio_Cotizacion")] | ||||||
|             public decimal VariacionPrecioCotizacion { get; set; } |             public decimal VariacionPrecioCotizacion { get; set; } | ||||||
|  |             /// <summary> | ||||||
|  |             /// La fecha en que se realizó la operación. | ||||||
|  |             /// </summary> | ||||||
|             [JsonPropertyName("fecha_Operacion_Pizarra")] |             [JsonPropertyName("fecha_Operacion_Pizarra")] | ||||||
|             public DateTime FechaOperacionPizarra { get; set; } |             public DateTime FechaOperacionPizarra { get; set; } | ||||||
|         } |         } | ||||||
|         #endregion |         #endregion | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public string SourceName => "BCR"; |         public string SourceName => "BCR"; | ||||||
|  |          | ||||||
|  |         /// <summary> | ||||||
|  |         /// URL base de la API de BCR. | ||||||
|  |         /// </summary> | ||||||
|         private const string BaseUrl = "https://api.bcr.com.ar/gix/v1.0"; |         private const string BaseUrl = "https://api.bcr.com.ar/gix/v1.0"; | ||||||
|  |          | ||||||
|  |         /// <summary> | ||||||
|  |         /// Mapeo de nombres de granos a sus IDs correspondientes en la API de BCR. | ||||||
|  |         /// </summary> | ||||||
|         private readonly Dictionary<string, int> _grainIds = new() |         private readonly Dictionary<string, int> _grainIds = new() | ||||||
|         { |         { | ||||||
|             { "Trigo", 1 }, { "Maiz", 2 }, { "Sorgo", 3 }, { "Girasol", 20 }, { "Soja", 21 } |             { "Trigo", 1 }, { "Maiz", 2 }, { "Sorgo", 3 }, { "Girasol", 20 }, { "Soja", 21 } | ||||||
| @@ -50,6 +96,14 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|         private readonly IConfiguration _configuration; |         private readonly IConfiguration _configuration; | ||||||
|         private readonly ILogger<BcrDataFetcher> _logger; |         private readonly ILogger<BcrDataFetcher> _logger; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="BcrDataFetcher"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="httpClientFactory">Fábrica para crear instancias de HttpClient.</param> | ||||||
|  |         /// <param name="cotizacionRepository">Repositorio para guardar las cotizaciones de granos.</param> | ||||||
|  |         /// <param name="fuenteDatoRepository">Repositorio para gestionar la información de la fuente de datos.</param> | ||||||
|  |         /// <param name="configuration">Configuración de la aplicación para acceder a las claves de API.</param> | ||||||
|  |         /// <param name="logger">Logger para registrar información y errores.</param> | ||||||
|         public BcrDataFetcher( |         public BcrDataFetcher( | ||||||
|             IHttpClientFactory httpClientFactory, |             IHttpClientFactory httpClientFactory, | ||||||
|             ICotizacionGranoRepository cotizacionRepository, |             ICotizacionGranoRepository cotizacionRepository, | ||||||
| @@ -64,6 +118,7 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             _logger = logger; |             _logger = logger; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<(bool Success, string Message)> FetchDataAsync() |         public async Task<(bool Success, string Message)> FetchDataAsync() | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); |             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); | ||||||
| @@ -124,6 +179,11 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene un token de autenticación de la API de BCR. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="client">El cliente HTTP a utilizar para la solicitud.</param> | ||||||
|  |         /// <returns>El token de autenticación como una cadena de texto, o null si la operación falla.</returns> | ||||||
|         private async Task<string?> GetAuthTokenAsync(HttpClient client) |         private async Task<string?> GetAuthTokenAsync(HttpClient client) | ||||||
|         { |         { | ||||||
|             var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/Login"); |             var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/Login"); | ||||||
| @@ -137,6 +197,9 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             return tokenResponse?.Data?.Token; |             return tokenResponse?.Data?.Token; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Actualiza la información de la fuente de datos en la base de datos, registrando la última ejecución exitosa. | ||||||
|  |         /// </summary> | ||||||
|         private async Task UpdateSourceInfoAsync() |         private async Task UpdateSourceInfoAsync() | ||||||
|         { |         { | ||||||
|             var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); |             var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); | ||||||
|   | |||||||
| @@ -7,8 +7,18 @@ using System.Net.Http; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.DataFetchers | namespace Mercados.Infrastructure.DataFetchers | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Implementación de <see cref="IDataFetcher"/> para obtener datos de cotizaciones de bolsa | ||||||
|  |     /// desde la API de Finnhub. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <remarks> | ||||||
|  |     /// Utiliza la librería ThreeFourteen.Finnhub.Client para interactuar con la API. | ||||||
|  |     /// </remarks> | ||||||
|     public class FinnhubDataFetcher : IDataFetcher |     public class FinnhubDataFetcher : IDataFetcher | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Nombre de la fuente de datos utilizada por este fetcher. | ||||||
|  |         /// </summary> | ||||||
|         public string SourceName => "Finnhub"; |         public string SourceName => "Finnhub"; | ||||||
|         private readonly List<string> _tickers = new() { |         private readonly List<string> _tickers = new() { | ||||||
|             // Tecnológicas y ETFs |             // Tecnológicas y ETFs | ||||||
| @@ -24,6 +34,17 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|         private readonly IFuenteDatoRepository _fuenteDatoRepository; |         private readonly IFuenteDatoRepository _fuenteDatoRepository; | ||||||
|         private readonly ILogger<FinnhubDataFetcher> _logger; |         private readonly ILogger<FinnhubDataFetcher> _logger; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="FinnhubDataFetcher"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="configuration">Configuración de la aplicación para acceder a la clave de API de Finnhub.</param> | ||||||
|  |         /// <param name="httpClientFactory">Fábrica para crear instancias de HttpClient.</param> | ||||||
|  |         /// <param name="cotizacionRepository">Repositorio para guardar las cotizaciones de bolsa obtenidas.</param> | ||||||
|  |         /// <param name="fuenteDatoRepository">Repositorio para gestionar la información de la fuente de datos (Finnhub).</param> | ||||||
|  |         /// <param name="logger">Logger para registrar información y errores durante la ejecución.</param> | ||||||
|  |         /// <exception cref="InvalidOperationException"> | ||||||
|  |         /// Se lanza si la clave de API de Finnhub no está configurada en la aplicación. | ||||||
|  |         /// </exception> | ||||||
|         public FinnhubDataFetcher( |         public FinnhubDataFetcher( | ||||||
|             IConfiguration configuration, |             IConfiguration configuration, | ||||||
|             IHttpClientFactory httpClientFactory, |             IHttpClientFactory httpClientFactory, | ||||||
| @@ -42,6 +63,12 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             _logger = logger; |             _logger = logger; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene los datos de cotizaciones de bolsa desde la API de Finnhub para los tickers configurados | ||||||
|  |         /// y los guarda en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado.</returns> | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<(bool Success, string Message)> FetchDataAsync() |         public async Task<(bool Success, string Message)> FetchDataAsync() | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); |             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); | ||||||
| @@ -88,6 +115,9 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros."); |             return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros."); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Actualiza la información de la fuente de datos (Finnhub) en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         private async Task UpdateSourceInfoAsync() |         private async Task UpdateSourceInfoAsync() | ||||||
|         { |         { | ||||||
|             var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); |             var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); | ||||||
|   | |||||||
| @@ -7,24 +7,51 @@ using System.Text.Json.Serialization; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.DataFetchers | namespace Mercados.Infrastructure.DataFetchers | ||||||
| { | { | ||||||
|     // Añadimos las clases DTO necesarias para deserializar la respuesta de Finnhub |     /// <summary> | ||||||
|  |     /// DTO para deserializar la respuesta de la API de Finnhub al obtener feriados de mercado. | ||||||
|  |     /// </summary> | ||||||
|     public class MarketHolidayResponse |     public class MarketHolidayResponse | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Lista de feriados del mercado. | ||||||
|  |         /// </summary> | ||||||
|         [JsonPropertyName("data")] |         [JsonPropertyName("data")] | ||||||
|         public List<MarketHoliday>? Data { get; set; } |         public List<MarketHoliday>? Data { get; set; } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Representa un feriado de mercado individual en la respuesta de la API de Finnhub. | ||||||
|  |     /// </summary> | ||||||
|     public class MarketHoliday |     public class MarketHoliday | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Fecha del feriado en formato de cadena (YYYY-MM-DD). | ||||||
|  |         /// </summary> | ||||||
|         [JsonPropertyName("at")] |         [JsonPropertyName("at")] | ||||||
|         public string? At { get; set; } |         public string? At { get; set; } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Fecha del feriado como <see cref="DateOnly"/>. | ||||||
|  |         /// </summary> | ||||||
|         [JsonIgnore] |         [JsonIgnore] | ||||||
|         public DateOnly Date => DateOnly.FromDateTime(DateTime.Parse(At!)); |         public DateOnly Date => DateOnly.FromDateTime(DateTime.Parse(At!)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Implementación de <see cref="IDataFetcher"/> para obtener datos de feriados de mercado | ||||||
|  |     /// desde la API de Finnhub. | ||||||
|  |     /// </summary> | ||||||
|     public class HolidayDataFetcher : IDataFetcher |     public class HolidayDataFetcher : IDataFetcher | ||||||
|     { |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public string SourceName => "Holidays"; |         public string SourceName => "Holidays"; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Códigos de mercado para los cuales se obtendrán los feriados. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <remarks> | ||||||
|  |         /// "US" para Estados Unidos, "BA" para Argentina (Bolsa de Comercio de Buenos Aires). | ||||||
|  |         /// </remarks> | ||||||
|         private readonly string[] _marketCodes = { "US", "BA" }; |         private readonly string[] _marketCodes = { "US", "BA" }; | ||||||
|  |  | ||||||
|         private readonly IHttpClientFactory _httpClientFactory; |         private readonly IHttpClientFactory _httpClientFactory; | ||||||
| @@ -32,6 +59,16 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|         private readonly IConfiguration _configuration; |         private readonly IConfiguration _configuration; | ||||||
|         private readonly ILogger<HolidayDataFetcher> _logger; |         private readonly ILogger<HolidayDataFetcher> _logger; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="HolidayDataFetcher"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="httpClientFactory">Fábrica para crear instancias de HttpClient.</param> | ||||||
|  |         /// <param name="feriadoRepository">Repositorio para gestionar los feriados de mercado en la base de datos.</param> | ||||||
|  |         /// <param name="configuration">Configuración de la aplicación para acceder a la clave de API de Finnhub.</param> | ||||||
|  |         /// <param name="logger">Logger para registrar información y errores durante la ejecución.</param> | ||||||
|  |         /// <exception cref="InvalidOperationException"> | ||||||
|  |         /// Se lanza si la clave de API de Finnhub no está configurada en la aplicación. | ||||||
|  |         /// </exception> | ||||||
|         public HolidayDataFetcher( |         public HolidayDataFetcher( | ||||||
|             IHttpClientFactory httpClientFactory, |             IHttpClientFactory httpClientFactory, | ||||||
|             IMercadoFeriadoRepository feriadoRepository, |             IMercadoFeriadoRepository feriadoRepository, | ||||||
| @@ -44,14 +81,24 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             _logger = logger; |             _logger = logger; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene los datos de feriados de mercado desde la API de Finnhub y los guarda en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado.</returns> | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<(bool Success, string Message)> FetchDataAsync() |         public async Task<(bool Success, string Message)> FetchDataAsync() | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("Iniciando actualización de feriados."); |             _logger.LogInformation("Iniciando actualización de feriados."); | ||||||
|  |  | ||||||
|  |             // Verificamos que la API Key de Finnhub esté configurada | ||||||
|             var apiKey = _configuration["ApiKeys:Finnhub"]; |             var apiKey = _configuration["ApiKeys:Finnhub"]; | ||||||
|             if (string.IsNullOrEmpty(apiKey)) return (false, "API Key de Finnhub no configurada."); |             if (string.IsNullOrEmpty(apiKey)) | ||||||
|  |             { | ||||||
|  |                 return (false, "API Key de Finnhub no configurada."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             var client = _httpClientFactory.CreateClient("FinnhubDataFetcher"); |             var client = _httpClientFactory.CreateClient("FinnhubDataFetcher"); | ||||||
|  |             // Iteramos sobre cada código de mercado configurado | ||||||
|             foreach (var marketCode in _marketCodes) |             foreach (var marketCode in _marketCodes) | ||||||
|             { |             { | ||||||
|                 try |                 try | ||||||
| @@ -60,8 +107,10 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|                     // Ahora la deserialización funcionará porque la clase existe |                     // Ahora la deserialización funcionará porque la clase existe | ||||||
|                     var response = await client.GetFromJsonAsync<MarketHolidayResponse>(apiUrl); |                     var response = await client.GetFromJsonAsync<MarketHolidayResponse>(apiUrl); | ||||||
|                      |                      | ||||||
|  |                     // Si obtuvimos datos en la respuesta | ||||||
|                     if (response?.Data != null) |                     if (response?.Data != null) | ||||||
|                     { |                     { | ||||||
|  |                         // Convertimos los datos de la API al formato de nuestra entidad MercadoFeriado | ||||||
|                         var nuevosFeriados = response.Data.Select(h => new MercadoFeriado |                         var nuevosFeriados = response.Data.Select(h => new MercadoFeriado | ||||||
|                         { |                         { | ||||||
|                             CodigoMercado = marketCode, |                             CodigoMercado = marketCode, | ||||||
| @@ -69,8 +118,12 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|                             Nombre = "Feriado Bursátil" |                             Nombre = "Feriado Bursátil" | ||||||
|                         }).ToList(); |                         }).ToList(); | ||||||
|                          |                          | ||||||
|  |                         // Guardamos los feriados en la base de datos, reemplazando los existentes para ese mercado | ||||||
|                         await _feriadoRepository.ReemplazarFeriadosPorMercadoAsync(marketCode, nuevosFeriados); |                         await _feriadoRepository.ReemplazarFeriadosPorMercadoAsync(marketCode, nuevosFeriados); | ||||||
|                         _logger.LogInformation("Feriados para {MarketCode} actualizados exitosamente: {Count} registros.", marketCode, nuevosFeriados.Count); |                         _logger.LogInformation( | ||||||
|  |                             "Feriados para {MarketCode} actualizados exitosamente: {Count} registros.",  | ||||||
|  |                             marketCode,  | ||||||
|  |                             nuevosFeriados.Count); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 catch (Exception ex) |                 catch (Exception ex) | ||||||
| @@ -78,6 +131,7 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|                     _logger.LogError(ex, "Falló la obtención de feriados para {MarketCode}.", marketCode); |                     _logger.LogError(ex, "Falló la obtención de feriados para {MarketCode}.", marketCode); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |             // Retornamos éxito si el proceso completo se ejecutó sin errores irrecuperables | ||||||
|             return (true, "Actualización de feriados completada."); |             return (true, "Actualización de feriados completada."); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -6,15 +6,39 @@ using System.Globalization; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.DataFetchers | namespace Mercados.Infrastructure.DataFetchers | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Implementación de <see cref="IDataFetcher"/> para obtener datos de cotizaciones de ganado | ||||||
|  |     /// desde el sitio web de Mercado Agro Ganadero. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <remarks> | ||||||
|  |     /// Utiliza AngleSharp para el parsing del HTML. | ||||||
|  |     /// </remarks> | ||||||
|     public class MercadoAgroFetcher : IDataFetcher |     public class MercadoAgroFetcher : IDataFetcher | ||||||
|     { |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public string SourceName => "MercadoAgroganadero"; |         public string SourceName => "MercadoAgroganadero"; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// URL del sitio web de Mercado Agro Ganadero donde se encuentran las cotizaciones. | ||||||
|  |         /// </summary> | ||||||
|         private const string DataUrl = "https://www.mercadoagroganadero.com.ar/dll/hacienda6.dll/haciinfo000225"; |         private const string DataUrl = "https://www.mercadoagroganadero.com.ar/dll/hacienda6.dll/haciinfo000225"; | ||||||
|  |  | ||||||
|         private readonly IHttpClientFactory _httpClientFactory; |         private readonly IHttpClientFactory _httpClientFactory; | ||||||
|         private readonly ICotizacionGanadoRepository _cotizacionRepository; |         private readonly ICotizacionGanadoRepository _cotizacionRepository; | ||||||
|         private readonly IFuenteDatoRepository _fuenteDatoRepository; |         private readonly IFuenteDatoRepository _fuenteDatoRepository; | ||||||
|         private readonly ILogger<MercadoAgroFetcher> _logger; |         private readonly ILogger<MercadoAgroFetcher> _logger; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="MercadoAgroFetcher"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="httpClientFactory">Fábrica para crear instancias de HttpClient, configuradas con políticas de reintento.</param> | ||||||
|  |         /// <param name="cotizacionRepository">Repositorio para guardar las cotizaciones de ganado obtenidas.</param> | ||||||
|  |         /// <param name="fuenteDatoRepository">Repositorio para gestionar la información de la fuente de datos (Mercado Agro Ganadero).</param> | ||||||
|  |         /// <param name="logger">Logger para registrar información y errores durante la ejecución.</param> | ||||||
|  |         /// <remarks> | ||||||
|  |         /// El constructor requiere una <see cref="IHttpClientFactory"/> que debe tener configurado un cliente HTTP | ||||||
|  |         /// con el nombre "MercadoAgroFetcher", y este cliente debe tener aplicada una política de reintentos (ej. con Polly). | ||||||
|  |         /// </remarks> | ||||||
|         public MercadoAgroFetcher( |         public MercadoAgroFetcher( | ||||||
|             IHttpClientFactory httpClientFactory, |             IHttpClientFactory httpClientFactory, | ||||||
|             ICotizacionGanadoRepository cotizacionRepository, |             ICotizacionGanadoRepository cotizacionRepository, | ||||||
| @@ -27,6 +51,12 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             _logger = logger; |             _logger = logger; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene los datos de cotizaciones de ganado desde el sitio web de Mercado Agro Ganadero, | ||||||
|  |         /// los parsea y los guarda en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado.</returns> | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<(bool Success, string Message)> FetchDataAsync() |         public async Task<(bool Success, string Message)> FetchDataAsync() | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); |             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); | ||||||
| @@ -36,15 +66,12 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|                 var htmlContent = await GetHtmlContentAsync(); |                 var htmlContent = await GetHtmlContentAsync(); | ||||||
|                 if (string.IsNullOrEmpty(htmlContent)) |                 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."); |                     return (false, "No se pudo obtener el contenido HTML."); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 var cotizaciones = ParseHtmlToEntities(htmlContent); |                 var cotizaciones = ParseHtmlToEntities(htmlContent); | ||||||
|  |  | ||||||
|                 if (!cotizaciones.Any()) |                 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. |                     // 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); |                     _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."); |                     return (true, "Conexión exitosa, pero no se encontraron nuevos datos."); | ||||||
| @@ -64,21 +91,34 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene el contenido HTML de la página de cotizaciones. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>El contenido HTML como una cadena.</returns> | ||||||
|  |         /// <exception cref="HttpRequestException"> | ||||||
|  |         /// Se lanza si la solicitud HTTP no es exitosa. | ||||||
|  |         /// </exception> | ||||||
|         private async Task<string> GetHtmlContentAsync() |         private async Task<string> GetHtmlContentAsync() | ||||||
|         { |         { | ||||||
|             // Pedimos el cliente HTTP con el nombre específico que tiene la política de Polly |             // Pedimos el cliente HTTP con el nombre específico que tiene la política de Polly | ||||||
|             var client = _httpClientFactory.CreateClient("MercadoAgroFetcher"); |             var client = _httpClientFactory.CreateClient("MercadoAgroFetcher"); | ||||||
|  |  | ||||||
|             // Es importante simular un navegador para evitar bloqueos. |             // 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"); |             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); |             var response = await client.GetAsync(DataUrl); | ||||||
|             response.EnsureSuccessStatusCode(); |             response.EnsureSuccessStatusCode(); | ||||||
|  |  | ||||||
|             // El sitio usa una codificación específica, hay que decodificarla correctamente. |             // El sitio usa una codificación específica, hay que decodificarla correctamente. | ||||||
|             var stream = await response.Content.ReadAsStreamAsync(); |             var stream = await response.Content.ReadAsStreamAsync(); | ||||||
|             using var reader = new StreamReader(stream, System.Text.Encoding.GetEncoding("windows-1252")); |             using var reader = new StreamReader(stream, System.Text.Encoding.GetEncoding("windows-1252")); | ||||||
|             return await reader.ReadToEndAsync(); |             return await reader.ReadToEndAsync(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Parsea el contenido HTML para extraer las cotizaciones de ganado. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="html">El HTML a parsear.</param> | ||||||
|  |         /// <returns>Una lista de entidades <see cref="CotizacionGanado"/>.</returns> | ||||||
|         private List<CotizacionGanado> ParseHtmlToEntities(string html) |         private List<CotizacionGanado> ParseHtmlToEntities(string html) | ||||||
|         { |         { | ||||||
|             var config = Configuration.Default; |             var config = Configuration.Default; | ||||||
| @@ -122,13 +162,16 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|                 } |                 } | ||||||
|                 catch (Exception ex) |                 catch (Exception ex) | ||||||
|                 { |                 { | ||||||
|                     _logger.LogWarning(ex, "No se pudo parsear una fila de la tabla. Contenido: {RowContent}", string.Join(" | ", celdas)); |                     _logger.LogWarning(ex, "No se pudo parsear una fila de la tabla. Contenido: {RowContent}", | ||||||
|  |                         string.Join(" | ", celdas)); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             return cotizaciones; |             return cotizaciones; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Actualiza la información de la fuente de datos (Mercado Agro Ganadero) en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         private async Task UpdateSourceInfoAsync() |         private async Task UpdateSourceInfoAsync() | ||||||
|         { |         { | ||||||
|             var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); |             var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); | ||||||
| @@ -150,7 +193,17 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // --- Funciones de Ayuda para Parseo --- |         // --- Funciones de Ayuda para Parseo --- | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// <see cref="CultureInfo"/> para el parseo de números en formato "es-AR". | ||||||
|  |         /// </summary> | ||||||
|         private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR"); |         private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR"); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Parsea una cadena a decimal, considerando el formato numérico de Argentina. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="value">La cadena a parsear.</param> | ||||||
|  |         /// <returns>El valor decimal parseado.</returns> | ||||||
|         private decimal ParseDecimal(string value) |         private decimal ParseDecimal(string value) | ||||||
|         { |         { | ||||||
|             // El sitio usa '.' como separador de miles y ',' como decimal. |             // El sitio usa '.' como separador de miles y ',' como decimal. | ||||||
| @@ -158,6 +211,12 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             var cleanValue = value.Replace("$", "").Replace(".", "").Trim(); |             var cleanValue = value.Replace("$", "").Replace(".", "").Trim(); | ||||||
|             return decimal.Parse(cleanValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, _cultureInfo); |             return decimal.Parse(cleanValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, _cultureInfo); | ||||||
|          } |          } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Parsea una cadena a entero, quitando separadores de miles. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="value">La cadena a parsear.</param> | ||||||
|  |         /// <returns>El valor entero parseado.</returns> | ||||||
|         private int ParseInt(string value) |         private int ParseInt(string value) | ||||||
|          { |          { | ||||||
|              return int.Parse(value.Replace(".", ""), CultureInfo.InvariantCulture); |              return int.Parse(value.Replace(".", ""), CultureInfo.InvariantCulture); | ||||||
|   | |||||||
| @@ -1,13 +1,19 @@ | |||||||
| namespace Mercados.Infrastructure.DataFetchers | namespace Mercados.Infrastructure.DataFetchers | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Clase estática que proporciona un mapeo entre los tickers de acciones y sus nombres descriptivos. | ||||||
|  |     /// </summary> | ||||||
|     public static class TickerNameMapping |     public static class TickerNameMapping | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Diccionario privado que almacena los tickers como claves y los nombres de las empresas como valores. | ||||||
|  |         /// La comparación de claves no distingue entre mayúsculas y minúsculas. | ||||||
|  |         /// </summary> | ||||||
|         private static readonly Dictionary<string, string> Names = new(StringComparer.OrdinalIgnoreCase) |         private static readonly Dictionary<string, string> Names = new(StringComparer.OrdinalIgnoreCase) | ||||||
|         { |         { | ||||||
|           // USA |           // USA | ||||||
|             { "SPY", "S&P 500 ETF" }, // Cambiado de GSPC a SPY para Finnhub |             { "SPY", "S&P 500 ETF" }, | ||||||
|             { "AAPL", "Apple Inc." }, |             { "AAPL", "Apple Inc." }, | ||||||
|             { "MSFT", "Microsoft Corp." }, |  | ||||||
|             { "AMZN", "Amazon.com, Inc." }, |             { "AMZN", "Amazon.com, Inc." }, | ||||||
|             { "NVDA", "NVIDIA Corp." }, |             { "NVDA", "NVIDIA Corp." }, | ||||||
|             { "AMD", "Advanced Micro Devices" }, |             { "AMD", "Advanced Micro Devices" }, | ||||||
| @@ -19,6 +25,7 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             { "XLE", "Energy Select Sector SPDR" }, |             { "XLE", "Energy Select Sector SPDR" }, | ||||||
|             { "XLK", "Technology Select Sector SPDR" }, |             { "XLK", "Technology Select Sector SPDR" }, | ||||||
|             { "MELI", "MercadoLibre, Inc." }, |             { "MELI", "MercadoLibre, Inc." }, | ||||||
|  |             { "MSFT", "Microsoft Corp." }, | ||||||
|             { "GLOB", "Globant" }, |             { "GLOB", "Globant" }, | ||||||
|              |              | ||||||
|             // ADRs Argentinos que cotizan en EEUU |             // ADRs Argentinos que cotizan en EEUU | ||||||
| @@ -53,9 +60,15 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             { "MELI.BA", "MercadoLibre (CEDEAR)" }, // Aclaramos que es el CEDEAR |             { "MELI.BA", "MercadoLibre (CEDEAR)" }, // Aclaramos que es el CEDEAR | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene el nombre descriptivo asociado a un ticker. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="ticker">El ticker de la acción (ej. "AAPL").</param> | ||||||
|  |         /// <returns>El nombre completo de la empresa si se encuentra en el mapeo; de lo contrario, null.</returns> | ||||||
|         public static string? GetName(string ticker) |         public static string? GetName(string ticker) | ||||||
|         { |         { | ||||||
|             return Names.GetValueOrDefault(ticker); |             // Devuelve el nombre si existe, o null si no se encuentra la clave. | ||||||
|  |             return Names.TryGetValue(ticker, out var name) ? name : $"Ticker no reconocido: {ticker}"; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -5,9 +5,24 @@ using YahooFinanceApi; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.DataFetchers | namespace Mercados.Infrastructure.DataFetchers | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Implementación de <see cref="IDataFetcher"/> para obtener datos de cotizaciones de bolsa | ||||||
|  |     /// desde la API de Yahoo Finance. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <remarks> | ||||||
|  |     /// Utiliza la librería YahooFinanceApi para interactuar con la API. | ||||||
|  |     /// </remarks> | ||||||
|     public class YahooFinanceDataFetcher : IDataFetcher |     public class YahooFinanceDataFetcher : IDataFetcher | ||||||
|     { |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public string SourceName => "YahooFinance"; |         public string SourceName => "YahooFinance"; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Lista de tickers a obtener de Yahoo Finance. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <remarks> | ||||||
|  |         /// Incluye el índice S&P 500, acciones del Merval argentino y algunos CEDEARs. | ||||||
|  |         /// </remarks> | ||||||
|         private readonly List<string> _tickers = new() { |         private readonly List<string> _tickers = new() { | ||||||
|             "^GSPC", // Índice S&P 500 |             "^GSPC", // Índice S&P 500 | ||||||
|             "^MERV", "GGAL.BA", "YPFD.BA", "PAMP.BA", "BMA.BA", "COME.BA", |             "^MERV", "GGAL.BA", "YPFD.BA", "PAMP.BA", "BMA.BA", "COME.BA", | ||||||
| @@ -15,10 +30,21 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             "CEPU.BA", "LOMA.BA", "VALO.BA", "MELI.BA" |             "CEPU.BA", "LOMA.BA", "VALO.BA", "MELI.BA" | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Diccionario para almacenar el mapeo de tickers con su información de mercado (Local o EEUU). | ||||||
|  |         /// </summary> | ||||||
|  |         private readonly Dictionary<string, string> _tickerMarketMapping = new Dictionary<string, string>(); | ||||||
|  |  | ||||||
|         private readonly ICotizacionBolsaRepository _cotizacionRepository; |         private readonly ICotizacionBolsaRepository _cotizacionRepository; | ||||||
|         private readonly IFuenteDatoRepository _fuenteDatoRepository; |         private readonly IFuenteDatoRepository _fuenteDatoRepository; | ||||||
|         private readonly ILogger<YahooFinanceDataFetcher> _logger; |         private readonly ILogger<YahooFinanceDataFetcher> _logger; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="YahooFinanceDataFetcher"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="cotizacionRepository">Repositorio para guardar las cotizaciones de bolsa obtenidas.</param> | ||||||
|  |         /// <param name="fuenteDatoRepository">Repositorio para gestionar la información de la fuente de datos (Yahoo Finance).</param> | ||||||
|  |         /// <param name="logger">Logger para registrar información y errores durante la ejecución.</param> | ||||||
|                 public YahooFinanceDataFetcher( |                 public YahooFinanceDataFetcher( | ||||||
|                     ICotizacionBolsaRepository cotizacionRepository, |                     ICotizacionBolsaRepository cotizacionRepository, | ||||||
|                     IFuenteDatoRepository fuenteDatoRepository, |                     IFuenteDatoRepository fuenteDatoRepository, | ||||||
| @@ -29,6 +55,11 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|                     _logger = logger; |                     _logger = logger; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene los datos de cotizaciones de bolsa desde la API de Yahoo Finance para los tickers configurados | ||||||
|  |         /// y los guarda en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado.</returns> | ||||||
|         public async Task<(bool Success, string Message)> FetchDataAsync() |         public async Task<(bool Success, string Message)> FetchDataAsync() | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); |             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); | ||||||
| @@ -41,7 +72,7 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|                 { |                 { | ||||||
|                     if (sec.RegularMarketPrice == 0 || sec.RegularMarketPreviousClose == 0) continue; |                     if (sec.RegularMarketPrice == 0 || sec.RegularMarketPreviousClose == 0) continue; | ||||||
|  |  | ||||||
|                     string mercado = sec.Symbol.EndsWith(".BA") || sec.Symbol == "^MERV" ? "Local" : "EEUU"; |                     string mercado = DetermineMarket(sec.Symbol); | ||||||
|  |  | ||||||
|                     cotizaciones.Add(new CotizacionBolsa |                     cotizaciones.Add(new CotizacionBolsa | ||||||
|                     { |                     { | ||||||
| @@ -75,6 +106,27 @@ namespace Mercados.Infrastructure.DataFetchers | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Determina el mercado (Local o EEUU) para un ticker específico. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="symbol">El ticker de la acción.</param> | ||||||
|  |         /// <returns>El mercado al que pertenece el ticker.</returns> | ||||||
|  |         private string DetermineMarket(string symbol) | ||||||
|  |         { | ||||||
|  |             if (_tickerMarketMapping.TryGetValue(symbol, out string? market)) | ||||||
|  |             { | ||||||
|  |                 return market; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Si no existe en el mapping, determinamos y lo agregamos. | ||||||
|  |             market = symbol.EndsWith(".BA") || symbol == "^MERV" ? "Local" : "EEUU"; | ||||||
|  |             _tickerMarketMapping[symbol] = market; | ||||||
|  |             return market; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Actualiza la información de la fuente de datos (Yahoo Finance) en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|         private async Task UpdateSourceInfoAsync() |         private async Task UpdateSourceInfoAsync() | ||||||
|         { |         { | ||||||
|             var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); |             var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ | |||||||
|     <TargetFramework>net9.0</TargetFramework> |     <TargetFramework>net9.0</TargetFramework> | ||||||
|     <ImplicitUsings>enable</ImplicitUsings> |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|     <Nullable>enable</Nullable> |     <Nullable>enable</Nullable> | ||||||
|  |     <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|  |  | ||||||
| </Project> | </Project> | ||||||
|   | |||||||
| @@ -2,8 +2,15 @@ using System.Data; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence | namespace Mercados.Infrastructure.Persistence | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Define una interfaz para una fábrica de conexiones a la base de datos. | ||||||
|  |     /// </summary> | ||||||
|     public interface IDbConnectionFactory |     public interface IDbConnectionFactory | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Crea y abre una nueva conexión a la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Un objeto <see cref="IDbConnection"/> representando la conexión abierta.</returns> | ||||||
|         IDbConnection CreateConnection(); |         IDbConnection CreateConnection(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -4,26 +4,34 @@ using System.Data; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <inheritdoc cref="ICotizacionBolsaRepository"/> | ||||||
|     public class CotizacionBolsaRepository : ICotizacionBolsaRepository |     public class CotizacionBolsaRepository : ICotizacionBolsaRepository | ||||||
|     { |     { | ||||||
|         private readonly IDbConnectionFactory _connectionFactory; |         private readonly IDbConnectionFactory _connectionFactory; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="CotizacionBolsaRepository"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param> | ||||||
|         public CotizacionBolsaRepository(IDbConnectionFactory connectionFactory) |         public CotizacionBolsaRepository(IDbConnectionFactory connectionFactory) | ||||||
|         { |         { | ||||||
|             _connectionFactory = connectionFactory; |             _connectionFactory = connectionFactory; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task GuardarMuchosAsync(IEnumerable<CotizacionBolsa> cotizaciones) |         public async Task GuardarMuchosAsync(IEnumerable<CotizacionBolsa> cotizaciones) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|  |  | ||||||
|             const string sql = @" |             const string sql = @"INSERT INTO  | ||||||
|                 INSERT INTO CotizacionesBolsa (Ticker, NombreEmpresa, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro)  |                     CotizacionesBolsa (Ticker, NombreEmpresa, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro)  | ||||||
|                 VALUES (@Ticker, @NombreEmpresa, @Mercado, @PrecioActual, @Apertura, @CierreAnterior, @PorcentajeCambio, @FechaRegistro);"; |                 VALUES  | ||||||
|  |                     (@Ticker, @NombreEmpresa, @Mercado, @PrecioActual, @Apertura, @CierreAnterior, @PorcentajeCambio, @FechaRegistro);"; | ||||||
|  |  | ||||||
|             await connection.ExecuteAsync(sql, cotizaciones); |             await connection.ExecuteAsync(sql, cotizaciones); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado) |         public async Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
| @@ -48,6 +56,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories | |||||||
|             return await connection.QueryAsync<CotizacionBolsa>(sql, new { Mercado = mercado }); |             return await connection.QueryAsync<CotizacionBolsa>(sql, new { Mercado = mercado }); | ||||||
|         } |         } | ||||||
|          |          | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<IEnumerable<CotizacionBolsa>> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias) |         public async Task<IEnumerable<CotizacionBolsa>> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|   | |||||||
| @@ -4,22 +4,29 @@ using System.Data; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <inheritdoc cref="ICotizacionGanadoRepository"/> | ||||||
|     public class CotizacionGanadoRepository : ICotizacionGanadoRepository |     public class CotizacionGanadoRepository : ICotizacionGanadoRepository | ||||||
|     { |     { | ||||||
|         private readonly IDbConnectionFactory _connectionFactory; |         private readonly IDbConnectionFactory _connectionFactory; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="CotizacionGanadoRepository"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param> | ||||||
|         public CotizacionGanadoRepository(IDbConnectionFactory connectionFactory) |         public CotizacionGanadoRepository(IDbConnectionFactory connectionFactory) | ||||||
|         { |         { | ||||||
|             _connectionFactory = connectionFactory; |             _connectionFactory = connectionFactory; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task GuardarMuchosAsync(IEnumerable<CotizacionGanado> cotizaciones) |         public async Task GuardarMuchosAsync(IEnumerable<CotizacionGanado> cotizaciones) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|  |  | ||||||
|             // Dapper puede insertar una colección de objetos de una sola vez, ¡muy eficiente! |             // Dapper puede insertar una colección de objetos de una sola vez, ¡muy eficiente! | ||||||
|             const string sql = @" |             const string sql = @" | ||||||
|                 INSERT INTO CotizacionesGanado ( |                 INSERT INTO  | ||||||
|  |                     CotizacionesGanado ( | ||||||
|                     Categoria, Especificaciones, Maximo, Minimo, Promedio, Mediano,  |                     Categoria, Especificaciones, Maximo, Minimo, Promedio, Mediano,  | ||||||
|                     Cabezas, KilosTotales, KilosPorCabeza, ImporteTotal, FechaRegistro |                     Cabezas, KilosTotales, KilosPorCabeza, ImporteTotal, FechaRegistro | ||||||
|                 )  |                 )  | ||||||
| @@ -30,6 +37,8 @@ namespace Mercados.Infrastructure.Persistence.Repositories | |||||||
|  |  | ||||||
|             await connection.ExecuteAsync(sql, cotizaciones); |             await connection.ExecuteAsync(sql, cotizaciones); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<IEnumerable<CotizacionGanado>> ObtenerUltimaTandaAsync() |         public async Task<IEnumerable<CotizacionGanado>> ObtenerUltimaTandaAsync() | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
| @@ -44,6 +53,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories | |||||||
|             return await connection.QueryAsync<CotizacionGanado>(sql); |             return await connection.QueryAsync<CotizacionGanado>(sql); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias) |         public async Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|   | |||||||
| @@ -4,25 +4,34 @@ using System.Data; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <inheritdoc cref="ICotizacionGranoRepository"/> | ||||||
|     public class CotizacionGranoRepository : ICotizacionGranoRepository |     public class CotizacionGranoRepository : ICotizacionGranoRepository | ||||||
|     { |     { | ||||||
|         private readonly IDbConnectionFactory _connectionFactory; |         private readonly IDbConnectionFactory _connectionFactory; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="CotizacionGranoRepository"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param> | ||||||
|         public CotizacionGranoRepository(IDbConnectionFactory connectionFactory) |         public CotizacionGranoRepository(IDbConnectionFactory connectionFactory) | ||||||
|         { |         { | ||||||
|             _connectionFactory = connectionFactory; |             _connectionFactory = connectionFactory; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task GuardarMuchosAsync(IEnumerable<CotizacionGrano> cotizaciones) |         public async Task GuardarMuchosAsync(IEnumerable<CotizacionGrano> cotizaciones) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|  |  | ||||||
|             const string sql = @" |             const string sql = @"INSERT INTO  | ||||||
|                 INSERT INTO CotizacionesGranos (Nombre, Precio, VariacionPrecio, FechaOperacion, FechaRegistro)  |                     CotizacionesGranos (Nombre, Precio, VariacionPrecio, FechaOperacion, FechaRegistro)  | ||||||
|                 VALUES (@Nombre, @Precio, @VariacionPrecio, @FechaOperacion, @FechaRegistro);"; |                 VALUES  | ||||||
|  |                     (@Nombre, @Precio, @VariacionPrecio, @FechaOperacion, @FechaRegistro);"; | ||||||
|  |  | ||||||
|             await connection.ExecuteAsync(sql, cotizaciones); |             await connection.ExecuteAsync(sql, cotizaciones); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<IEnumerable<CotizacionGrano>> ObtenerUltimasAsync() |         public async Task<IEnumerable<CotizacionGrano>> ObtenerUltimasAsync() | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
| @@ -45,6 +54,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories | |||||||
|             return await connection.QueryAsync<CotizacionGrano>(sql); |             return await connection.QueryAsync<CotizacionGrano>(sql); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias) |         public async Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|   | |||||||
| @@ -4,36 +4,41 @@ using System.Data; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <inheritdoc cref="IFuenteDatoRepository"/> | ||||||
|     public class FuenteDatoRepository : IFuenteDatoRepository |     public class FuenteDatoRepository : IFuenteDatoRepository | ||||||
|     { |     { | ||||||
|         private readonly IDbConnectionFactory _connectionFactory; |         private readonly IDbConnectionFactory _connectionFactory; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="FuenteDatoRepository"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param> | ||||||
|         public FuenteDatoRepository(IDbConnectionFactory connectionFactory) |         public FuenteDatoRepository(IDbConnectionFactory connectionFactory) | ||||||
|         { |         { | ||||||
|             _connectionFactory = connectionFactory; |             _connectionFactory = connectionFactory; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<FuenteDato?> ObtenerPorNombreAsync(string nombre) |         public async Task<FuenteDato?> ObtenerPorNombreAsync(string nombre) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|             const string sql = "SELECT * FROM FuentesDatos WHERE Nombre = @Nombre;"; |             const string sql = "SELECT * FROM FuentesDatos WHERE Nombre = @Nombre;"; | ||||||
|             return await connection.QuerySingleOrDefaultAsync<FuenteDato>(sql, new { Nombre = nombre }); |             return await connection.QuerySingleOrDefaultAsync<FuenteDato>(sql, new { Nombre = nombre }); | ||||||
|         } |         } | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task CrearAsync(FuenteDato fuenteDato) |         public async Task CrearAsync(FuenteDato fuenteDato) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|             const string sql = @" |             const string sql = @"INSERT INTO FuentesDatos (Nombre, UltimaEjecucionExitosa, Url)  | ||||||
|                 INSERT INTO FuentesDatos (Nombre, UltimaEjecucionExitosa, Url)  |  | ||||||
|                 VALUES (@Nombre, @UltimaEjecucionExitosa, @Url);"; |                 VALUES (@Nombre, @UltimaEjecucionExitosa, @Url);"; | ||||||
|             await connection.ExecuteAsync(sql, fuenteDato); |             await connection.ExecuteAsync(sql, fuenteDato); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task ActualizarAsync(FuenteDato fuenteDato) |         public async Task ActualizarAsync(FuenteDato fuenteDato) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|             const string sql = @" |             const string sql = @"UPDATE FuentesDatos  | ||||||
|                 UPDATE FuentesDatos  |  | ||||||
|                 SET UltimaEjecucionExitosa = @UltimaEjecucionExitosa, Url = @Url |                 SET UltimaEjecucionExitosa = @UltimaEjecucionExitosa, Url = @Url | ||||||
|                 WHERE Id = @Id;"; |                 WHERE Id = @Id;"; | ||||||
|             await connection.ExecuteAsync(sql, fuenteDato); |             await connection.ExecuteAsync(sql, fuenteDato); | ||||||
|   | |||||||
| @@ -1,6 +1,9 @@ | |||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|     // Esta interfaz no es estrictamente necesaria ahora, pero es útil para futuras abstracciones. |     /// <summary> | ||||||
|  |     /// Interfaz base marcadora para todos los repositorios. | ||||||
|  |     /// No define miembros, pero sirve para la abstracción y la inyección de dependencias. | ||||||
|  |     /// </summary> | ||||||
|     public interface IBaseRepository |     public interface IBaseRepository | ||||||
|     { |     { | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -2,10 +2,31 @@ using Mercados.Core.Entities; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Define el contrato para el repositorio que gestiona las cotizaciones de la bolsa. | ||||||
|  |     /// </summary> | ||||||
|     public interface ICotizacionBolsaRepository : IBaseRepository |     public interface ICotizacionBolsaRepository : IBaseRepository | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Guarda una colección de cotizaciones de bolsa en la base de datos de forma masiva. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="cotizaciones">La colección de entidades CotizacionBolsa a guardar.</param> | ||||||
|         Task GuardarMuchosAsync(IEnumerable<CotizacionBolsa> cotizaciones); |         Task GuardarMuchosAsync(IEnumerable<CotizacionBolsa> cotizaciones); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene la última cotización registrada para cada ticker de un mercado específico. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="mercado">El código del mercado a consultar (ej. "US", "Local").</param> | ||||||
|  |         /// <returns>Una colección con la última cotización de cada activo de ese mercado.</returns> | ||||||
|         Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado); |         Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene el historial de cotizaciones para un ticker específico durante un período determinado. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="ticker">El símbolo del activo (ej. "AAPL", "^MERV").</param> | ||||||
|  |         /// <param name="mercado">El mercado al que pertenece el ticker.</param> | ||||||
|  |         /// <param name="dias">El número de días hacia atrás desde hoy para obtener el historial.</param> | ||||||
|  |         /// <returns>Una colección de cotizaciones ordenadas por fecha de forma ascendente.</returns> | ||||||
|         Task<IEnumerable<CotizacionBolsa>> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias); |         Task<IEnumerable<CotizacionBolsa>> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -2,10 +2,30 @@ using Mercados.Core.Entities; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Define el contrato para el repositorio que gestiona las cotizaciones del mercado de ganado. | ||||||
|  |     /// </summary> | ||||||
|     public interface ICotizacionGanadoRepository : IBaseRepository |     public interface ICotizacionGanadoRepository : IBaseRepository | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Guarda una colección de cotizaciones de ganado en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="cotizaciones">La colección de entidades CotizacionGanado a guardar.</param> | ||||||
|         Task GuardarMuchosAsync(IEnumerable<CotizacionGanado> cotizaciones); |         Task GuardarMuchosAsync(IEnumerable<CotizacionGanado> cotizaciones); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene el último parte completo de cotizaciones del mercado de ganado. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una colección de todas las cotizaciones de la última tanda registrada.</returns> | ||||||
|         Task<IEnumerable<CotizacionGanado>> ObtenerUltimaTandaAsync(); |         Task<IEnumerable<CotizacionGanado>> ObtenerUltimaTandaAsync(); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene el historial de cotizaciones para una categoría y especificación de ganado. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="categoria">La categoría principal del ganado (ej. "NOVILLOS").</param> | ||||||
|  |         /// <param name="especificaciones">La especificación detallada del ganado.</param> | ||||||
|  |         /// <param name="dias">El número de días de historial a recuperar.</param> | ||||||
|  |         /// <returns>Una colección de cotizaciones históricas para esa categoría.</returns> | ||||||
|         Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias); |         Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -2,10 +2,29 @@ using Mercados.Core.Entities; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Define el contrato para el repositorio que gestiona las cotizaciones del mercado de granos. | ||||||
|  |     /// </summary> | ||||||
|     public interface ICotizacionGranoRepository : IBaseRepository |     public interface ICotizacionGranoRepository : IBaseRepository | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Guarda una colección de cotizaciones de granos en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="cotizaciones">La colección de entidades CotizacionGrano a guardar.</param> | ||||||
|         Task GuardarMuchosAsync(IEnumerable<CotizacionGrano> cotizaciones); |         Task GuardarMuchosAsync(IEnumerable<CotizacionGrano> cotizaciones); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene las últimas cotizaciones disponibles para los granos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Una colección de las últimas cotizaciones de granos registradas.</returns> | ||||||
|         Task<IEnumerable<CotizacionGrano>> ObtenerUltimasAsync(); |         Task<IEnumerable<CotizacionGrano>> ObtenerUltimasAsync(); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene el historial de cotizaciones para un grano específico. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="nombre">El nombre del grano (ej. "Soja").</param> | ||||||
|  |         /// <param name="dias">El número de días de historial a recuperar.</param> | ||||||
|  |         /// <returns>Una colección de cotizaciones históricas para el grano especificado.</returns> | ||||||
|         Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias); |         Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -2,10 +2,28 @@ using Mercados.Core.Entities; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Define el contrato para el repositorio que gestiona las fuentes de datos. | ||||||
|  |     /// </summary> | ||||||
|     public interface IFuenteDatoRepository : IBaseRepository |     public interface IFuenteDatoRepository : IBaseRepository | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene una entidad FuenteDato por su nombre único. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="nombre">El nombre de la fuente de datos a buscar.</param> | ||||||
|  |         /// <returns>La entidad FuenteDato si se encuentra; de lo contrario, null.</returns> | ||||||
|         Task<FuenteDato?> ObtenerPorNombreAsync(string nombre); |         Task<FuenteDato?> ObtenerPorNombreAsync(string nombre); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Actualiza una entidad FuenteDato existente en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="fuenteDato">La entidad FuenteDato con los datos actualizados.</param> | ||||||
|         Task ActualizarAsync(FuenteDato fuenteDato); |         Task ActualizarAsync(FuenteDato fuenteDato); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Crea una nueva entidad FuenteDato en la base de datos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="fuenteDato">La entidad FuenteDato a crear.</param> | ||||||
|         Task CrearAsync(FuenteDato fuenteDato); |         Task CrearAsync(FuenteDato fuenteDato); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -2,9 +2,23 @@ using Mercados.Core.Entities; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Define el contrato para el repositorio que gestiona los feriados de los mercados. | ||||||
|  |     /// </summary> | ||||||
|     public interface IMercadoFeriadoRepository : IBaseRepository |     public interface IMercadoFeriadoRepository : IBaseRepository | ||||||
|     { |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Obtiene todos los feriados para un mercado y año específicos. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="codigoMercado">El código del mercado para el cual se buscan los feriados.</param> | ||||||
|  |         /// <param name="anio">El año para el cual se desean obtener los feriados.</param> | ||||||
|  |         /// <returns>Una colección de entidades MercadoFeriado para el mercado y año especificados.</returns> | ||||||
|         Task<IEnumerable<MercadoFeriado>> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio); |         Task<IEnumerable<MercadoFeriado>> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio); | ||||||
|  |         /// <summary> | ||||||
|  |         /// Reemplaza todos los feriados existentes para un mercado con una nueva lista. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="codigoMercado">El código del mercado cuyos feriados serán reemplazados.</param> | ||||||
|  |         /// <param name="nuevosFeriados">La nueva colección de feriados que se guardará.</param> | ||||||
|         Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable<MercadoFeriado> nuevosFeriados); |         Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable<MercadoFeriado> nuevosFeriados); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -4,24 +4,35 @@ using System.Data; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Persistence.Repositories | namespace Mercados.Infrastructure.Persistence.Repositories | ||||||
| { | { | ||||||
|  |     /// <inheritdoc cref="IMercadoFeriadoRepository"/> | ||||||
|     public class MercadoFeriadoRepository : IMercadoFeriadoRepository |     public class MercadoFeriadoRepository : IMercadoFeriadoRepository | ||||||
|     { |     { | ||||||
|         private readonly IDbConnectionFactory _connectionFactory; |         private readonly IDbConnectionFactory _connectionFactory; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="MercadoFeriadoRepository"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="connectionFactory">Fábrica para crear conexiones a la base de datos.</param> | ||||||
|         public MercadoFeriadoRepository(IDbConnectionFactory connectionFactory) |         public MercadoFeriadoRepository(IDbConnectionFactory connectionFactory) | ||||||
|         { |         { | ||||||
|             _connectionFactory = connectionFactory; |             _connectionFactory = connectionFactory; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task<IEnumerable<MercadoFeriado>> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio) |         public async Task<IEnumerable<MercadoFeriado>> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
|             const string sql = @" |             const string sql = @"SELECT *  | ||||||
|                 SELECT * FROM MercadosFeriados  |                 FROM MercadosFeriados  | ||||||
|                 WHERE CodigoMercado = @CodigoMercado AND YEAR(Fecha) = @Anio;"; |                 WHERE CodigoMercado = @CodigoMercado AND YEAR(Fecha) = @Anio;"; | ||||||
|             return await connection.QueryAsync<MercadoFeriado>(sql, new { CodigoMercado = codigoMercado, Anio = anio }); |             return await connection.QueryAsync<MercadoFeriado>(sql, new  | ||||||
|  |             {  | ||||||
|  |                 CodigoMercado = codigoMercado,  | ||||||
|  |                 Anio = anio  | ||||||
|  |             }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable<MercadoFeriado> nuevosFeriados) |         public async Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable<MercadoFeriado> nuevosFeriados) | ||||||
|         { |         { | ||||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); |             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||||
| @@ -30,25 +41,31 @@ namespace Mercados.Infrastructure.Persistence.Repositories | |||||||
|  |  | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
|                 // Borramos todos los feriados del año en curso para ese mercado |                 // Obtenemos el año del primer feriado (asumimos que todos son del mismo año) | ||||||
|                 var anio = nuevosFeriados.FirstOrDefault()?.Fecha.Year; |                 var anio = nuevosFeriados.FirstOrDefault()?.Fecha.Year; | ||||||
|                 if (anio.HasValue) |                 if (!anio.HasValue) return; // Si no hay feriados, no hay nada que hacer | ||||||
|                 { |  | ||||||
|                     const string deleteSql = "DELETE FROM MercadosFeriados WHERE CodigoMercado = @CodigoMercado AND YEAR(Fecha) = @Anio;"; |  | ||||||
|                     await connection.ExecuteAsync(deleteSql, new { CodigoMercado = codigoMercado, Anio = anio.Value }, transaction); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 // Insertamos los nuevos |                 // 1. Borrar los feriados existentes para ese mercado | ||||||
|                 const string insertSql = @" |                 const string deleteSql = "DELETE FROM MercadosFeriados WHERE CodigoMercado = @CodigoMercado;"; | ||||||
|                     INSERT INTO MercadosFeriados (CodigoMercado, Fecha, Nombre)  |                 await connection.ExecuteAsync(deleteSql, new { CodigoMercado = codigoMercado }, transaction); | ||||||
|  |  | ||||||
|  |                 // 2. Insertar los nuevos feriados | ||||||
|  |                 if (nuevosFeriados.Any()) | ||||||
|  |                 { | ||||||
|  |                     const string insertSql = @"INSERT INTO MercadosFeriados (CodigoMercado, Fecha, Nombre) | ||||||
|                         VALUES (@CodigoMercado, @Fecha, @Nombre);"; |                         VALUES (@CodigoMercado, @Fecha, @Nombre);"; | ||||||
|                     await connection.ExecuteAsync(insertSql, nuevosFeriados, transaction); |                     await connection.ExecuteAsync(insertSql, nuevosFeriados, transaction); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // Si todo sale bien, confirmar la transacción | ||||||
|                 transaction.Commit(); |                 transaction.Commit(); | ||||||
|             } |             } | ||||||
|             catch |             catch | ||||||
|             { |             { | ||||||
|  |                 // Si hay algún error, deshacer la transacción para no dejar datos inconsistentes | ||||||
|                 transaction.Rollback(); |                 transaction.Rollback(); | ||||||
|  |  | ||||||
|  |                 // Relanzar la excepción para que el llamador sepa que algo falló | ||||||
|                 throw; |                 throw; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -5,10 +5,17 @@ using System.Data; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure | namespace Mercados.Infrastructure | ||||||
| { | { | ||||||
|  |   /// <summary> | ||||||
|  |   /// Proporciona una fábrica para crear conexiones a la base de datos SQL. | ||||||
|  |   /// </summary> | ||||||
|   public class SqlConnectionFactory : IDbConnectionFactory |   public class SqlConnectionFactory : IDbConnectionFactory | ||||||
|   { |   { | ||||||
|     private readonly string _connectionString; |     private readonly string _connectionString; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Inicializa una nueva instancia de la clase <see cref="SqlConnectionFactory"/>. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="configuration">La configuración de la aplicación desde donde se obtiene la cadena de conexión.</param> | ||||||
|     public SqlConnectionFactory(IConfiguration configuration) |     public SqlConnectionFactory(IConfiguration configuration) | ||||||
|     { |     { | ||||||
|       // Variable de entorno 'DB_CONNECTION_STRING' si está disponible, |       // Variable de entorno 'DB_CONNECTION_STRING' si está disponible, | ||||||
| @@ -17,6 +24,7 @@ namespace Mercados.Infrastructure | |||||||
|           ?? throw new ArgumentNullException(nameof(configuration), "La cadena de conexión 'DefaultConnection' no fue encontrada."); |           ?? throw new ArgumentNullException(nameof(configuration), "La cadena de conexión 'DefaultConnection' no fue encontrada."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public IDbConnection CreateConnection() |         public IDbConnection CreateConnection() | ||||||
|     { |     { | ||||||
|       return new SqlConnection(_connectionString); |       return new SqlConnection(_connectionString); | ||||||
|   | |||||||
| @@ -6,17 +6,26 @@ using MimeKit; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Services | namespace Mercados.Infrastructure.Services | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Servicio que gestiona el envío de notificaciones por correo electrónico. | ||||||
|  |     /// </summary> | ||||||
|     public class EmailNotificationService : INotificationService |     public class EmailNotificationService : INotificationService | ||||||
|     { |     { | ||||||
|         private readonly ILogger<EmailNotificationService> _logger; |         private readonly ILogger<EmailNotificationService> _logger; | ||||||
|         private readonly IConfiguration _configuration; |         private readonly IConfiguration _configuration; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="EmailNotificationService"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="logger">Logger para registrar información y errores.</param> | ||||||
|  |         /// <param name="configuration">Configuración de la aplicación para obtener los ajustes SMTP.</param> | ||||||
|         public EmailNotificationService(ILogger<EmailNotificationService> logger, IConfiguration configuration) |         public EmailNotificationService(ILogger<EmailNotificationService> logger, IConfiguration configuration) | ||||||
|         { |         { | ||||||
|             _logger = logger; |             _logger = logger; | ||||||
|             _configuration = configuration; |             _configuration = configuration; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|         public async Task SendFailureAlertAsync(string subject, string message, DateTime? eventTimeUtc = null) |         public async Task SendFailureAlertAsync(string subject, string message, DateTime? eventTimeUtc = null) | ||||||
|         { |         { | ||||||
|             // Leemos la configuración de forma segura desde IConfiguration (que a su vez lee el .env) |             // Leemos la configuración de forma segura desde IConfiguration (que a su vez lee el .env) | ||||||
|   | |||||||
| @@ -5,12 +5,21 @@ using Microsoft.Extensions.Logging; | |||||||
|  |  | ||||||
| namespace Mercados.Infrastructure.Services | namespace Mercados.Infrastructure.Services | ||||||
| { | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Servicio para consultar si una fecha es feriado de mercado utilizando la base de datos interna. | ||||||
|  |     /// </summary> | ||||||
|         public class FinnhubHolidayService : IHolidayService |         public class FinnhubHolidayService : IHolidayService | ||||||
|     { |     { | ||||||
|         private readonly IMercadoFeriadoRepository _feriadoRepository; |         private readonly IMercadoFeriadoRepository _feriadoRepository; | ||||||
|         private readonly IMemoryCache _cache; |         private readonly IMemoryCache _cache; | ||||||
|         private readonly ILogger<FinnhubHolidayService> _logger; |         private readonly ILogger<FinnhubHolidayService> _logger; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="FinnhubHolidayService"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="feriadoRepository">Repositorio para acceder a los feriados de mercado.</param> | ||||||
|  |         /// <param name="cache">Caché en memoria para almacenar los feriados.</param> | ||||||
|  |         /// <param name="logger">Logger para registrar información y errores.</param> | ||||||
|                 public FinnhubHolidayService( |                 public FinnhubHolidayService( | ||||||
|                     IMercadoFeriadoRepository feriadoRepository, |                     IMercadoFeriadoRepository feriadoRepository, | ||||||
|                     IMemoryCache cache, |                     IMemoryCache cache, | ||||||
| @@ -21,6 +30,12 @@ namespace Mercados.Infrastructure.Services | |||||||
|                     _logger = logger; |                     _logger = logger; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Determina si una fecha específica es feriado de mercado para el código de mercado proporcionado. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="marketCode">Código del mercado a consultar.</param> | ||||||
|  |         /// <param name="date">Fecha a verificar.</param> | ||||||
|  |         /// <returns>True si la fecha es feriado de mercado; de lo contrario, false.</returns> | ||||||
|                 public async Task<bool> IsMarketHolidayAsync(string marketCode, DateTime date) |                 public async Task<bool> IsMarketHolidayAsync(string marketCode, DateTime date) | ||||||
|                 { |                 { | ||||||
|                     var dateOnly = DateOnly.FromDateTime(date); |                     var dateOnly = DateOnly.FromDateTime(date); | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ namespace Mercados.Infrastructure.Services | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="subject">El título de la alerta.</param> |         /// <param name="subject">El título de la alerta.</param> | ||||||
|         /// <param name="message">El mensaje detallado del error.</param> |         /// <param name="message">El mensaje detallado del error.</param> | ||||||
|  |         /// <param name="eventTimeUtc">La fecha y hora UTC del evento (opcional).</param> | ||||||
|         Task SendFailureAlertAsync(string subject, string message, DateTime? eventTimeUtc = null); |         Task SendFailureAlertAsync(string subject, string message, DateTime? eventTimeUtc = null); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -15,22 +15,47 @@ namespace Mercados.Worker | |||||||
|         private readonly IServiceProvider _serviceProvider; |         private readonly IServiceProvider _serviceProvider; | ||||||
|         private readonly TimeZoneInfo _argentinaTimeZone; |         private readonly TimeZoneInfo _argentinaTimeZone; | ||||||
|  |  | ||||||
|         // Expresiones Cron |         /// <summary> | ||||||
|  |         /// Expresión Cron para la tarea de Mercado Agroganadero. | ||||||
|  |         /// </summary> | ||||||
|         private readonly CronExpression _agroSchedule; |         private readonly CronExpression _agroSchedule; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Expresión Cron para la tarea de la Bolsa de Comercio de Rosario (BCR). | ||||||
|  |         /// </summary> | ||||||
|         private readonly CronExpression _bcrSchedule; |         private readonly CronExpression _bcrSchedule; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Expresión Cron para la tarea de las Bolsas (Finnhub y Yahoo Finance). | ||||||
|  |         /// </summary> | ||||||
|         private readonly CronExpression _bolsasSchedule; |         private readonly CronExpression _bolsasSchedule; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Expresión Cron para la tarea de actualización de feriados. | ||||||
|  |         /// </summary> | ||||||
|         private readonly CronExpression _holidaysSchedule; |         private readonly CronExpression _holidaysSchedule; | ||||||
|  |  | ||||||
|         // Próximas ejecuciones |         /// <summary>Próxima hora de ejecución programada para la tarea de Mercado Agroganadero.</summary> | ||||||
|         private DateTime? _nextAgroRun; |         private DateTime? _nextAgroRun; | ||||||
|  |         /// <summary>Próxima hora de ejecución programada para la tarea de BCR.</summary> | ||||||
|         private DateTime? _nextBcrRun; |         private DateTime? _nextBcrRun; | ||||||
|  |         /// <summary>Próxima hora de ejecución programada para la tarea de Bolsas.</summary> | ||||||
|         private DateTime? _nextBolsasRun; |         private DateTime? _nextBolsasRun; | ||||||
|  |         /// <summary>Próxima hora de ejecución programada para la tarea de Feriados.</summary> | ||||||
|         private DateTime? _nextHolidaysRun; |         private DateTime? _nextHolidaysRun; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Almacena la última vez que se envió una alerta para una tarea específica, para evitar spam. | ||||||
|  |         /// </summary> | ||||||
|         private readonly Dictionary<string, DateTime> _lastAlertSent = new(); |         private readonly Dictionary<string, DateTime> _lastAlertSent = new(); | ||||||
|  |         /// <summary> | ||||||
|  |         /// Período de tiempo durante el cual no se enviarán alertas repetidas para la misma tarea. | ||||||
|  |         /// </summary> | ||||||
|         private readonly TimeSpan _alertSilencePeriod = TimeSpan.FromHours(4); |         private readonly TimeSpan _alertSilencePeriod = TimeSpan.FromHours(4); | ||||||
|  |  | ||||||
|         // Eliminamos IHolidayService del constructor |         /// <summary> | ||||||
|  |         /// Inicializa una nueva instancia de la clase <see cref="DataFetchingService"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="logger">Logger para registrar información y eventos.</param> | ||||||
|  |         /// <param name="serviceProvider">Proveedor de servicios para la inyección de dependencias con scope.</param> | ||||||
|  |         /// <param name="configuration">Configuración de la aplicación para obtener los schedules de Cron.</param> | ||||||
|         public DataFetchingService( |         public DataFetchingService( | ||||||
|             ILogger<DataFetchingService> logger, |             ILogger<DataFetchingService> logger, | ||||||
|             IServiceProvider serviceProvider, |             IServiceProvider serviceProvider, | ||||||
| @@ -60,8 +85,10 @@ namespace Mercados.Worker | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Método principal del servicio. Se ejecuta una vez cuando el servicio arranca. |         /// Método principal del servicio que se ejecuta en segundo plano. Contiene el bucle | ||||||
|  |         /// principal que verifica periódicamente si se debe ejecutar alguna tarea programada. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|  |         /// <param name="stoppingToken">Token de cancelación para detener el servicio de forma segura.</param> | ||||||
|         protected override async Task ExecuteAsync(CancellationToken stoppingToken) |         protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now); |             _logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now); | ||||||
| @@ -154,6 +181,9 @@ namespace Mercados.Worker | |||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Ejecuta un fetcher específico por su nombre, gestionando el scope de DI y las notificaciones. |         /// Ejecuta un fetcher específico por su nombre, gestionando el scope de DI y las notificaciones. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|  |         /// <param name="sourceName">El nombre del <see cref="IDataFetcher"/> a ejecutar.</param> | ||||||
|  |         /// <param name="stoppingToken">Token de cancelación para detener la operación si el servicio se está parando.</param> | ||||||
|  |         /// <remarks>Este método crea un nuevo scope de DI para resolver los servicios necesarios.</remarks> | ||||||
|         private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken) |         private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken) | ||||||
|         { |         { | ||||||
|             if (stoppingToken.IsCancellationRequested) return; |             if (stoppingToken.IsCancellationRequested) return; | ||||||
| @@ -193,6 +223,8 @@ namespace Mercados.Worker | |||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Ejecuta todos los fetchers en paralelo al iniciar el servicio. |         /// Ejecuta todos los fetchers en paralelo al iniciar el servicio. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|  |         /// <param name="stoppingToken">Token de cancelación para detener la operación si el servicio se está parando.</param> | ||||||
|  |         /// <remarks>Esta función se usa principalmente para una ejecución de prueba al arrancar.</remarks> | ||||||
|         private async Task RunAllFetchersAsync(CancellationToken stoppingToken) |         private async Task RunAllFetchersAsync(CancellationToken stoppingToken) | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("Ejecutando todos los fetchers al iniciar en paralelo..."); |             _logger.LogInformation("Ejecutando todos los fetchers al iniciar en paralelo..."); | ||||||
| @@ -211,6 +243,8 @@ namespace Mercados.Worker | |||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Determina si se debe enviar una alerta o si está en período de silencio. |         /// Determina si se debe enviar una alerta o si está en período de silencio. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|  |         /// <param name="taskName">El nombre de la tarea que podría generar la alerta.</param> | ||||||
|  |         /// <returns>True si se debe enviar la alerta; de lo contrario, false.</returns> | ||||||
|         private bool ShouldSendAlert(string taskName) |         private bool ShouldSendAlert(string taskName) | ||||||
|         { |         { | ||||||
|             if (!_lastAlertSent.ContainsKey(taskName)) |             if (!_lastAlertSent.ContainsKey(taskName)) | ||||||
| @@ -224,8 +258,13 @@ namespace Mercados.Worker | |||||||
|  |  | ||||||
|         #endregion |         #endregion | ||||||
|  |  | ||||||
|         // Creamos una única función para comprobar feriados que obtiene el servicio |         /// <summary> | ||||||
|         // desde un scope. |         /// Comprueba si una fecha dada es feriado para un mercado específico. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="marketCode">El código del mercado (ej. "US", "BA").</param> | ||||||
|  |         /// <param name="date">La fecha a comprobar.</param> | ||||||
|  |         /// <returns>True si es feriado, false si no lo es o si ocurre un error.</returns> | ||||||
|  |         /// <remarks>Este método resuelve el <see cref="IHolidayService"/> desde un nuevo scope de DI para cada llamada.</remarks> | ||||||
|         private async Task<bool> IsMarketHolidayAsync(string marketCode, DateTime date) |         private async Task<bool> IsMarketHolidayAsync(string marketCode, DateTime date) | ||||||
|         { |         { | ||||||
|             using var scope = _serviceProvider.CreateScope(); |             using var scope = _serviceProvider.CreateScope(); | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
|     <Nullable>enable</Nullable> |     <Nullable>enable</Nullable> | ||||||
|     <ImplicitUsings>enable</ImplicitUsings> |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|     <UserSecretsId>dotnet-Mercados.Worker-0e9a6e84-c8fb-400b-ba7b-193d9981a046</UserSecretsId> |     <UserSecretsId>dotnet-Mercados.Worker-0e9a6e84-c8fb-400b-ba7b-193d9981a046</UserSecretsId> | ||||||
|  |     <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|  |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user