From 88f245a80d746020be2ebe68f5507122c7b81edb Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 16 Jul 2025 23:12:07 -0300 Subject: [PATCH] Feat: Backend's documentation added --- .gitignore | 2 + .../Controllers/MercadosController.cs | 87 ++++++++++++++---- src/Mercados.Api/Mercados.Api.csproj | 1 + .../Utils/UtcDateTimeConverter.cs | 29 ++++-- src/Mercados.Core/Entities/CotizacionBolsa.cs | 62 ++++++++++--- .../Entities/CotizacionGanado.cs | 80 +++++++++++++--- src/Mercados.Core/Entities/CotizacionGrano.cs | 44 +++++++-- src/Mercados.Core/Entities/FuenteDato.cs | 35 +++++-- src/Mercados.Core/Entities/MercadoFeriado.cs | 22 ++++- src/Mercados.Core/Mercados.Core.csproj | 1 + .../Mercados.Database.csproj | 1 + .../20250701113000_CreateInitialTables.cs | 6 +- .../20250702133000_AddNameToStocks.cs | 10 ++ ...0250714150000_CreateMercadoFeriadoTable.cs | 10 ++ .../DataFetchers/BcrDataFetcher.cs | 63 +++++++++++++ .../DataFetchers/FinnhubDataFetcher.cs | 30 ++++++ .../DataFetchers/HolidayDataFetcher.cs | 66 ++++++++++++-- .../DataFetchers/MercadoAgroFetcher.cs | 91 +++++++++++++++---- .../DataFetchers/TickerNameMapping.cs | 19 +++- .../DataFetchers/YahooFinanceDataFetcher.cs | 72 +++++++++++++-- .../Mercados.Infrastructure.csproj | 1 + .../Persistence/IDbConnectionFactory.cs | 15 ++- .../Repositories/CotizacionBolsaRepository.cs | 15 ++- .../CotizacionGanadoRepository.cs | 12 ++- .../Repositories/CotizacionGranoRepository.cs | 16 +++- .../Repositories/FuenteDatoRepository.cs | 19 ++-- .../Repositories/IBaseRepository.cs | 5 +- .../ICotizacionBolsaRepository.cs | 21 +++++ .../ICotizacionGanadoRepository.cs | 20 ++++ .../ICotizacionGranoRepository.cs | 19 ++++ .../Repositories/IFuenteDatoRepository.cs | 18 ++++ .../Repositories/IMercadoFeriadoRepository.cs | 14 +++ .../Repositories/MercadoFeriadoRepository.cs | 43 ++++++--- .../Persistence/SqlConnectionFactory.cs | 12 ++- .../Services/EmailNotificationService.cs | 9 ++ .../Services/FinnhubHolidayService.cs | 43 ++++++--- .../Services/INotificationService.cs | 1 + src/Mercados.Worker/DataFetchingService.cs | 53 +++++++++-- src/Mercados.Worker/Mercados.Worker.csproj | 1 + 39 files changed, 904 insertions(+), 164 deletions(-) diff --git a/.gitignore b/.gitignore index 7e2e97c..a03a208 100644 --- a/.gitignore +++ b/.gitignore @@ -178,6 +178,8 @@ DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html +# DocFx +[Dd]oc/ # Click-Once directory publish/ diff --git a/src/Mercados.Api/Controllers/MercadosController.cs b/src/Mercados.Api/Controllers/MercadosController.cs index f3dfd37..831e066 100644 --- a/src/Mercados.Api/Controllers/MercadosController.cs +++ b/src/Mercados.Api/Controllers/MercadosController.cs @@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Mvc; namespace Mercados.Api.Controllers { + /// + /// Controlador principal para exponer los datos de los mercados financieros. + /// [ApiController] [Route("api/[controller]")] public class MercadosController : ControllerBase @@ -15,7 +18,14 @@ namespace Mercados.Api.Controllers private readonly IHolidayService _holidayService; private readonly ILogger _logger; - // Inyectamos TODOS los repositorios que necesita el controlador. + /// + /// Inicializa una nueva instancia del controlador MercadosController. + /// + /// Repositorio para datos de la bolsa. + /// Repositorio para datos de granos. + /// Repositorio para datos de ganado. + /// Servicio para consultar feriados. + /// Servicio de logging. public MercadosController( ICotizacionBolsaRepository bolsaRepo, ICotizacionGranoRepository granoRepo, @@ -30,7 +40,10 @@ namespace Mercados.Api.Controllers _logger = logger; } - // --- Endpoint para Agroganadero --- + /// + /// Obtiene el último parte completo del mercado agroganadero. + /// + /// Una colección de objetos CotizacionGanado. [HttpGet("agroganadero")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -48,7 +61,10 @@ namespace Mercados.Api.Controllers } } - // --- Endpoint para Granos --- + /// + /// Obtiene las últimas cotizaciones para los principales granos. + /// + /// Una colección de objetos CotizacionGrano. [HttpGet("granos")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -67,6 +83,10 @@ namespace Mercados.Api.Controllers } // --- Endpoints de Bolsa --- + /// + /// Obtiene las últimas cotizaciones para el mercado de bolsa de EEUU. + /// + /// Una colección de objetos CotizacionBolsa. [HttpGet("bolsa/eeuu")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -84,6 +104,10 @@ namespace Mercados.Api.Controllers } } + /// + /// Obtiene las últimas cotizaciones para el mercado de bolsa local. + /// + /// Una colección de objetos CotizacionBolsa. [HttpGet("bolsa/local")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -101,23 +125,37 @@ namespace Mercados.Api.Controllers } } - [HttpGet("bolsa/history/{ticker}")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetBolsaHistory(string ticker, [FromQuery] string mercado = "Local", [FromQuery] int dias = 30) - { - try - { - var data = await _bolsaRepo.ObtenerHistorialPorTickerAsync(ticker, mercado, dias); - return Ok(data); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error al obtener historial para el ticker {Ticker}.", ticker); - return StatusCode(500, "Ocurrió un error interno en el servidor."); - } - } + /// + /// Obtiene el historial de cotizaciones para un ticker específico en un mercado determinado. + /// + /// El identificador del ticker. + /// El nombre del mercado (por defecto "Local"). + /// Cantidad de días de historial a recuperar (por defecto 30). + /// Una colección de objetos CotizacionBolsa. + [HttpGet("bolsa/history/{ticker}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetBolsaHistory(string ticker, [FromQuery] string mercado = "Local", [FromQuery] int dias = 30) + { + try + { + var data = await _bolsaRepo.ObtenerHistorialPorTickerAsync(ticker, mercado, dias); + return Ok(data); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener historial para el ticker {Ticker}.", ticker); + return StatusCode(500, "Ocurrió un error interno en el servidor."); + } + } + /// + /// Obtiene el historial de cotizaciones para una categoría y especificaciones de ganado en un rango de días. + /// + /// La categoría de ganado. + /// Las especificaciones del ganado. + /// Cantidad de días de historial a recuperar (por defecto 30). + /// Una colección de objetos CotizacionGanado. [HttpGet("agroganadero/history")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -135,6 +173,12 @@ namespace Mercados.Api.Controllers } } + /// + /// Obtiene el historial de cotizaciones para un grano específico en un rango de días. + /// + /// El nombre del grano. + /// Cantidad de días de historial a recuperar (por defecto 30). + /// Una colección de objetos CotizacionGrano. [HttpGet("granos/history/{nombre}")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -152,6 +196,11 @@ namespace Mercados.Api.Controllers } } + /// + /// Verifica si la fecha actual es feriado para el mercado especificado. + /// + /// El nombre del mercado a consultar. + /// True si es feriado, false en caso contrario. [HttpGet("es-feriado/{mercado}")] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] diff --git a/src/Mercados.Api/Mercados.Api.csproj b/src/Mercados.Api/Mercados.Api.csproj index 1942c04..d2b4dca 100644 --- a/src/Mercados.Api/Mercados.Api.csproj +++ b/src/Mercados.Api/Mercados.Api.csproj @@ -5,6 +5,7 @@ enable enable 28c6a673-1f1e-4140-aa75-a0d894d1fbc4 + true diff --git a/src/Mercados.Api/Utils/UtcDateTimeConverter.cs b/src/Mercados.Api/Utils/UtcDateTimeConverter.cs index ac37274..1dc9916 100644 --- a/src/Mercados.Api/Utils/UtcDateTimeConverter.cs +++ b/src/Mercados.Api/Utils/UtcDateTimeConverter.cs @@ -9,19 +9,32 @@ namespace Mercados.Api.Utils /// public class UtcDateTimeConverter : JsonConverter { + /// + /// Lee un valor DateTime desde el lector JSON y lo convierte a UTC. + /// + /// El lector JSON. + /// El tipo a convertir. + /// Las opciones de serialización JSON. + /// El valor DateTime en UTC. 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 return reader.GetDateTime().ToUniversalTime(); } - public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) - { - // Antes de escribir el string, especificamos que el 'Kind' es Utc. - // Si ya es Utc, no hace nada. Si es Local o Unspecified, lo trata como si fuera Utc. - // Esto es seguro porque sabemos que todas nuestras fechas en la BD son UTC. - var utcValue = DateTime.SpecifyKind(value, DateTimeKind.Utc); - writer.WriteStringValue(utcValue); - } + /// + /// Escribe un valor DateTime en formato UTC como una cadena en el escritor JSON. + /// + /// El escritor JSON. + /// El valor DateTime a escribir. + /// Las opciones de serialización JSON. + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + // Antes de escribir el string, especificamos que el 'Kind' es Utc. + // Si ya es Utc, no hace nada. Si es Local o Unspecified, lo trata como si fuera Utc. + // Esto es seguro porque sabemos que todas nuestras fechas en la BD son UTC. + var utcValue = DateTime.SpecifyKind(value, DateTimeKind.Utc); + writer.WriteStringValue(utcValue); + } } } \ No newline at end of file diff --git a/src/Mercados.Core/Entities/CotizacionBolsa.cs b/src/Mercados.Core/Entities/CotizacionBolsa.cs index a8f0a82..8a06d41 100644 --- a/src/Mercados.Core/Entities/CotizacionBolsa.cs +++ b/src/Mercados.Core/Entities/CotizacionBolsa.cs @@ -1,15 +1,53 @@ namespace Mercados.Core.Entities { - public class CotizacionBolsa - { - public long Id { get; set; } - public string Ticker { get; set; } = string.Empty; // "AAPL", "GGAL.BA", etc. - public string? NombreEmpresa { get; set; } - public string Mercado { get; set; } = string.Empty; // "EEUU" o "Local" - public decimal PrecioActual { get; set; } - public decimal Apertura { get; set; } - public decimal CierreAnterior { get; set; } - public decimal PorcentajeCambio { get; set; } - public DateTime FechaRegistro { get; set; } - } + /// + /// Representa una única captura de cotización para un activo de la bolsa de valores. + /// + public class CotizacionBolsa + { + /// + /// Identificador único del registro en la base de datos. + /// + public long Id { get; set; } + + /// + /// El símbolo o identificador del activo en el mercado (ej. "AAPL", "GGAL.BA"). + /// + public string Ticker { get; set; } = string.Empty; + + /// + /// El nombre completo de la empresa o del activo. + /// + public string? NombreEmpresa { get; set; } + + /// + /// El mercado al que pertenece el activo (ej. "EEUU", "Local"). + /// + public string Mercado { get; set; } = string.Empty; + + /// + /// El último precio registrado para el activo. + /// + public decimal PrecioActual { get; set; } + + /// + /// El precio del activo al inicio de la jornada de mercado. + /// + public decimal Apertura { get; set; } + + /// + /// El precio de cierre del activo en la jornada anterior. + /// + public decimal CierreAnterior { get; set; } + + /// + /// El cambio porcentual del precio actual con respecto al cierre anterior. + /// + public decimal PorcentajeCambio { get; set; } + + /// + /// La fecha y hora (en UTC) en que se registró esta cotización en la base de datos. + /// + public DateTime FechaRegistro { get; set; } + } } \ No newline at end of file diff --git a/src/Mercados.Core/Entities/CotizacionGanado.cs b/src/Mercados.Core/Entities/CotizacionGanado.cs index 0950392..5863231 100644 --- a/src/Mercados.Core/Entities/CotizacionGanado.cs +++ b/src/Mercados.Core/Entities/CotizacionGanado.cs @@ -1,18 +1,68 @@ namespace Mercados.Core.Entities { - public class CotizacionGanado - { - public long Id { get; set; } - public string Categoria { get; set; } = string.Empty; - public string Especificaciones { get; set; } = string.Empty; - public decimal Maximo { get; set; } - public decimal Minimo { get; set; } - public decimal Promedio { get; set; } - public decimal Mediano { get; set; } - public int Cabezas { get; set; } - public int KilosTotales { get; set; } - public int KilosPorCabeza { get; set; } - public decimal ImporteTotal { get; set; } - public DateTime FechaRegistro { get; set; } - } + /// + /// Representa una cotización para una categoría de ganado en el Mercado Agroganadero. + /// + public class CotizacionGanado + { + /// + /// Identificador único del registro en la base de datos. + /// + public long Id { get; set; } + + /// + /// La categoría principal del ganado (ej. "NOVILLOS", "VACAS"). + /// + public string Categoria { get; set; } = string.Empty; + + /// + /// Detalles adicionales sobre la categoría, como raza o peso. + /// + public string Especificaciones { get; set; } = string.Empty; + + /// + /// El precio máximo alcanzado para esta categoría en la jornada. + /// + public decimal Maximo { get; set; } + + /// + /// El precio mínimo alcanzado para esta categoría en la jornada. + /// + public decimal Minimo { get; set; } + + /// + /// El precio promedio ponderado para la categoría. + /// + public decimal Promedio { get; set; } + + /// + /// El precio mediano (valor central) registrado para la categoría. + /// + public decimal Mediano { get; set; } + + /// + /// El número total de cabezas de ganado comercializadas en esta categoría. + /// + public int Cabezas { get; set; } + + /// + /// El peso total en kilogramos de todo el ganado comercializado. + /// + public int KilosTotales { get; set; } + + /// + /// El peso promedio por cabeza de ganado. + /// + public int KilosPorCabeza { get; set; } + + /// + /// El importe total monetario de las transacciones para esta categoría. + /// + public decimal ImporteTotal { get; set; } + + /// + /// La fecha y hora (en UTC) en que se registró esta cotización en la base de datos. + /// + public DateTime FechaRegistro { get; set; } + } } \ No newline at end of file diff --git a/src/Mercados.Core/Entities/CotizacionGrano.cs b/src/Mercados.Core/Entities/CotizacionGrano.cs index 60ce45f..97be5c0 100644 --- a/src/Mercados.Core/Entities/CotizacionGrano.cs +++ b/src/Mercados.Core/Entities/CotizacionGrano.cs @@ -1,12 +1,38 @@ namespace Mercados.Core.Entities { - public class CotizacionGrano - { - public long Id { get; set; } - public string Nombre { get; set; } = string.Empty; // "Soja", "Trigo", etc. - public decimal Precio { get; set; } - public decimal VariacionPrecio { get; set; } - public DateTime FechaOperacion { get; set; } - public DateTime FechaRegistro { get; set; } - } + /// + /// Representa una cotización para un tipo de grano específico. + /// + public class CotizacionGrano + { + /// + /// Identificador único del registro en la base de datos. + /// + public long Id { get; set; } + + /// + /// El nombre del grano (ej. "Soja", "Trigo", "Maíz"). + /// + public string Nombre { get; set; } = string.Empty; + + /// + /// El precio de cotización, generalmente por tonelada. + /// + public decimal Precio { get; set; } + + /// + /// La variación del precio con respecto a la cotización anterior. + /// + public decimal VariacionPrecio { get; set; } + + /// + /// La fecha en que se concertó la operación de la cotización. + /// + public DateTime FechaOperacion { get; set; } + + /// + /// La fecha y hora (en UTC) en que se registró esta cotización en la base de datos. + /// + public DateTime FechaRegistro { get; set; } + } } \ No newline at end of file diff --git a/src/Mercados.Core/Entities/FuenteDato.cs b/src/Mercados.Core/Entities/FuenteDato.cs index 780f78e..697c3ae 100644 --- a/src/Mercados.Core/Entities/FuenteDato.cs +++ b/src/Mercados.Core/Entities/FuenteDato.cs @@ -1,10 +1,31 @@ namespace Mercados.Core.Entities { - public class FuenteDato - { - public long Id { get; set; } - public string Nombre { get; set; } = string.Empty; // "BCR", "Finnhub", "YahooFinance", "MercadoAgroganadero" - public DateTime UltimaEjecucionExitosa { get; set; } - public string? Url { get; set; } - } + /// + /// 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. + /// + public class FuenteDato + { + /// + /// Identificador único del registro en la base de datos. + /// + public long Id { get; set; } + + /// + /// 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. + /// + public string Nombre { get; set; } = string.Empty; + + /// + /// La fecha y hora (en UTC) de la última vez que el Data Fetcher correspondiente + /// se ejecutó y completó su tarea exitosamente. + /// + public DateTime UltimaEjecucionExitosa { get; set; } + + /// + /// La URL base o principal de la fuente de datos, para referencia. + /// + public string? Url { get; set; } + } } \ No newline at end of file diff --git a/src/Mercados.Core/Entities/MercadoFeriado.cs b/src/Mercados.Core/Entities/MercadoFeriado.cs index 0e471cb..6592852 100644 --- a/src/Mercados.Core/Entities/MercadoFeriado.cs +++ b/src/Mercados.Core/Entities/MercadoFeriado.cs @@ -1,10 +1,28 @@ namespace Mercados.Core.Entities { + /// + /// Representa un único día feriado para un mercado bursátil específico. + /// public class MercadoFeriado { + /// + /// Identificador único del registro en la base de datos. + /// public long Id { get; set; } - public string CodigoMercado { get; set; } = string.Empty; // "US" o "BA" + + /// + /// El código del mercado al que pertenece el feriado (ej. "US", "BA"). + /// + public string CodigoMercado { get; set; } = string.Empty; + + /// + /// La fecha exacta del feriado (la hora no es relevante). + /// public DateTime Fecha { get; set; } - public string? Nombre { get; set; } // Nombre del feriado, si la API lo provee + + /// + /// El nombre o la descripción del feriado (si está disponible). + /// + public string? Nombre { get; set; } } } \ No newline at end of file diff --git a/src/Mercados.Core/Mercados.Core.csproj b/src/Mercados.Core/Mercados.Core.csproj index 125f4c9..c964f57 100644 --- a/src/Mercados.Core/Mercados.Core.csproj +++ b/src/Mercados.Core/Mercados.Core.csproj @@ -4,6 +4,7 @@ net9.0 enable enable + true diff --git a/src/Mercados.Database/Mercados.Database.csproj b/src/Mercados.Database/Mercados.Database.csproj index 5ad11ce..2aaa196 100644 --- a/src/Mercados.Database/Mercados.Database.csproj +++ b/src/Mercados.Database/Mercados.Database.csproj @@ -4,6 +4,7 @@ net9.0 enable enable + true diff --git a/src/Mercados.Database/Migrations/20250701113000_CreateInitialTables.cs b/src/Mercados.Database/Migrations/20250701113000_CreateInitialTables.cs index cf2faf4..b78fd3e 100644 --- a/src/Mercados.Database/Migrations/20250701113000_CreateInitialTables.cs +++ b/src/Mercados.Database/Migrations/20250701113000_CreateInitialTables.cs @@ -2,8 +2,10 @@ using FluentMigrator; namespace Mercados.Database.Migrations { - // El número es la versión única de esta migración. - // 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. + /// [Migration(20250701113000)] public class CreateInitialTables : Migration { diff --git a/src/Mercados.Database/Migrations/20250702133000_AddNameToStocks.cs b/src/Mercados.Database/Migrations/20250702133000_AddNameToStocks.cs index 9844edb..48f36d9 100644 --- a/src/Mercados.Database/Migrations/20250702133000_AddNameToStocks.cs +++ b/src/Mercados.Database/Migrations/20250702133000_AddNameToStocks.cs @@ -2,15 +2,25 @@ using FluentMigrator; namespace Mercados.Database.Migrations { + /// + /// Migración que añade la columna 'NombreEmpresa' a la tabla 'CotizacionesBolsa' + /// para almacenar el nombre descriptivo de la acción. + /// [Migration(20250702133000)] public class AddNameToStocks : Migration { + /// + /// Aplica la migración, añadiendo la columna 'NombreEmpresa'. + /// public override void Up() { Alter.Table("CotizacionesBolsa") .AddColumn("NombreEmpresa").AsString(255).Nullable(); } + /// + /// Revierte la migración, eliminando la columna 'NombreEmpresa'. + /// public override void Down() { Delete.Column("NombreEmpresa").FromTable("CotizacionesBolsa"); diff --git a/src/Mercados.Database/Migrations/20250714150000_CreateMercadoFeriadoTable.cs b/src/Mercados.Database/Migrations/20250714150000_CreateMercadoFeriadoTable.cs index 078704a..130fc5f 100644 --- a/src/Mercados.Database/Migrations/20250714150000_CreateMercadoFeriadoTable.cs +++ b/src/Mercados.Database/Migrations/20250714150000_CreateMercadoFeriadoTable.cs @@ -2,11 +2,18 @@ using FluentMigrator; namespace Mercados.Database.Migrations { + /// + /// Migración para crear la tabla 'MercadosFeriados', que almacenará los días no laborables + /// para diferentes mercados bursátiles. + /// [Migration(20250714150000)] public class CreateMercadoFeriadoTable : Migration { private const string TableName = "MercadosFeriados"; + /// + /// Define la estructura de la tabla 'MercadosFeriados' y crea un índice único. + /// public override void Up() { Create.Table(TableName) @@ -23,6 +30,9 @@ namespace Mercados.Database.Migrations .WithOptions().Unique(); } + /// + /// Revierte la migración eliminando la tabla 'MercadosFeriados'. + /// public override void Down() { Delete.Table(TableName); diff --git a/src/Mercados.Infrastructure/DataFetchers/BcrDataFetcher.cs b/src/Mercados.Infrastructure/DataFetchers/BcrDataFetcher.cs index aad2d76..ab89c8e 100644 --- a/src/Mercados.Infrastructure/DataFetchers/BcrDataFetcher.cs +++ b/src/Mercados.Infrastructure/DataFetchers/BcrDataFetcher.cs @@ -8,37 +8,83 @@ using System.Text.Json.Serialization; namespace Mercados.Infrastructure.DataFetchers { + /// + /// Implementación de para obtener datos de cotizaciones de granos + /// desde la API de la Bolsa de Comercio de Rosario (BCR). + /// public class BcrDataFetcher : IDataFetcher { #region Clases DTO para la respuesta de la API de BCR + /// + /// DTO para la respuesta del endpoint de autenticación de BCR. + /// private class BcrTokenResponse { + /// + /// Contenedor de datos del token. + /// [JsonPropertyName("data")] public TokenData? Data { get; set; } } + + /// + /// Contiene el token de autenticación. + /// private class TokenData { + /// + /// El token JWT para autenticar las solicitudes. + /// [JsonPropertyName("token")] public string? Token { get; set; } } + + /// + /// DTO para la respuesta del endpoint de precios de BCR. + /// private class BcrPreciosResponse { + /// + /// Lista de precios de granos. + /// [JsonPropertyName("data")] public List? Data { get; set; } } + + /// + /// Representa un ítem individual de precio en la respuesta de la API de BCR. + /// private class BcrPrecioItem { + /// + /// El precio de cotización del grano. + /// [JsonPropertyName("precio_Cotizacion")] public decimal PrecioCotizacion { get; set; } + /// + /// La variación del precio respecto a la cotización anterior. + /// [JsonPropertyName("variacion_Precio_Cotizacion")] public decimal VariacionPrecioCotizacion { get; set; } + /// + /// La fecha en que se realizó la operación. + /// [JsonPropertyName("fecha_Operacion_Pizarra")] public DateTime FechaOperacionPizarra { get; set; } } #endregion + /// public string SourceName => "BCR"; + + /// + /// URL base de la API de BCR. + /// private const string BaseUrl = "https://api.bcr.com.ar/gix/v1.0"; + + /// + /// Mapeo de nombres de granos a sus IDs correspondientes en la API de BCR. + /// private readonly Dictionary _grainIds = new() { { "Trigo", 1 }, { "Maiz", 2 }, { "Sorgo", 3 }, { "Girasol", 20 }, { "Soja", 21 } @@ -50,6 +96,14 @@ namespace Mercados.Infrastructure.DataFetchers private readonly IConfiguration _configuration; private readonly ILogger _logger; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Fábrica para crear instancias de HttpClient. + /// Repositorio para guardar las cotizaciones de granos. + /// Repositorio para gestionar la información de la fuente de datos. + /// Configuración de la aplicación para acceder a las claves de API. + /// Logger para registrar información y errores. public BcrDataFetcher( IHttpClientFactory httpClientFactory, ICotizacionGranoRepository cotizacionRepository, @@ -64,6 +118,7 @@ namespace Mercados.Infrastructure.DataFetchers _logger = logger; } + /// public async Task<(bool Success, string Message)> FetchDataAsync() { _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); @@ -124,6 +179,11 @@ namespace Mercados.Infrastructure.DataFetchers } } + /// + /// Obtiene un token de autenticación de la API de BCR. + /// + /// El cliente HTTP a utilizar para la solicitud. + /// El token de autenticación como una cadena de texto, o null si la operación falla. private async Task GetAuthTokenAsync(HttpClient client) { var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/Login"); @@ -137,6 +197,9 @@ namespace Mercados.Infrastructure.DataFetchers return tokenResponse?.Data?.Token; } + /// + /// Actualiza la información de la fuente de datos en la base de datos, registrando la última ejecución exitosa. + /// private async Task UpdateSourceInfoAsync() { var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); diff --git a/src/Mercados.Infrastructure/DataFetchers/FinnhubDataFetcher.cs b/src/Mercados.Infrastructure/DataFetchers/FinnhubDataFetcher.cs index 5717160..14729d7 100644 --- a/src/Mercados.Infrastructure/DataFetchers/FinnhubDataFetcher.cs +++ b/src/Mercados.Infrastructure/DataFetchers/FinnhubDataFetcher.cs @@ -7,8 +7,18 @@ using System.Net.Http; namespace Mercados.Infrastructure.DataFetchers { + /// + /// Implementación de para obtener datos de cotizaciones de bolsa + /// desde la API de Finnhub. + /// + /// + /// Utiliza la librería ThreeFourteen.Finnhub.Client para interactuar con la API. + /// public class FinnhubDataFetcher : IDataFetcher { + /// + /// Nombre de la fuente de datos utilizada por este fetcher. + /// public string SourceName => "Finnhub"; private readonly List _tickers = new() { // Tecnológicas y ETFs @@ -24,6 +34,17 @@ namespace Mercados.Infrastructure.DataFetchers private readonly IFuenteDatoRepository _fuenteDatoRepository; private readonly ILogger _logger; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Configuración de la aplicación para acceder a la clave de API de Finnhub. + /// Fábrica para crear instancias de HttpClient. + /// Repositorio para guardar las cotizaciones de bolsa obtenidas. + /// Repositorio para gestionar la información de la fuente de datos (Finnhub). + /// Logger para registrar información y errores durante la ejecución. + /// + /// Se lanza si la clave de API de Finnhub no está configurada en la aplicación. + /// public FinnhubDataFetcher( IConfiguration configuration, IHttpClientFactory httpClientFactory, @@ -42,6 +63,12 @@ namespace Mercados.Infrastructure.DataFetchers _logger = logger; } + /// + /// Obtiene los datos de cotizaciones de bolsa desde la API de Finnhub para los tickers configurados + /// y los guarda en la base de datos. + /// + /// Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado. + /// public async Task<(bool Success, string Message)> FetchDataAsync() { _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); @@ -88,6 +115,9 @@ namespace Mercados.Infrastructure.DataFetchers return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros."); } + /// + /// Actualiza la información de la fuente de datos (Finnhub) en la base de datos. + /// private async Task UpdateSourceInfoAsync() { var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); diff --git a/src/Mercados.Infrastructure/DataFetchers/HolidayDataFetcher.cs b/src/Mercados.Infrastructure/DataFetchers/HolidayDataFetcher.cs index af1e71d..2c4b74d 100644 --- a/src/Mercados.Infrastructure/DataFetchers/HolidayDataFetcher.cs +++ b/src/Mercados.Infrastructure/DataFetchers/HolidayDataFetcher.cs @@ -7,24 +7,51 @@ using System.Text.Json.Serialization; namespace Mercados.Infrastructure.DataFetchers { - // Añadimos las clases DTO necesarias para deserializar la respuesta de Finnhub + /// + /// DTO para deserializar la respuesta de la API de Finnhub al obtener feriados de mercado. + /// public class MarketHolidayResponse { + /// + /// Lista de feriados del mercado. + /// [JsonPropertyName("data")] public List? Data { get; set; } } + + /// + /// Representa un feriado de mercado individual en la respuesta de la API de Finnhub. + /// public class MarketHoliday { + /// + /// Fecha del feriado en formato de cadena (YYYY-MM-DD). + /// [JsonPropertyName("at")] public string? At { get; set; } + /// + /// Fecha del feriado como . + /// [JsonIgnore] public DateOnly Date => DateOnly.FromDateTime(DateTime.Parse(At!)); } + /// + /// Implementación de para obtener datos de feriados de mercado + /// desde la API de Finnhub. + /// public class HolidayDataFetcher : IDataFetcher { + /// public string SourceName => "Holidays"; + + /// + /// Códigos de mercado para los cuales se obtendrán los feriados. + /// + /// + /// "US" para Estados Unidos, "BA" para Argentina (Bolsa de Comercio de Buenos Aires). + /// private readonly string[] _marketCodes = { "US", "BA" }; private readonly IHttpClientFactory _httpClientFactory; @@ -32,6 +59,16 @@ namespace Mercados.Infrastructure.DataFetchers private readonly IConfiguration _configuration; private readonly ILogger _logger; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Fábrica para crear instancias de HttpClient. + /// Repositorio para gestionar los feriados de mercado en la base de datos. + /// Configuración de la aplicación para acceder a la clave de API de Finnhub. + /// Logger para registrar información y errores durante la ejecución. + /// + /// Se lanza si la clave de API de Finnhub no está configurada en la aplicación. + /// public HolidayDataFetcher( IHttpClientFactory httpClientFactory, IMercadoFeriadoRepository feriadoRepository, @@ -44,14 +81,24 @@ namespace Mercados.Infrastructure.DataFetchers _logger = logger; } + /// + /// Obtiene los datos de feriados de mercado desde la API de Finnhub y los guarda en la base de datos. + /// + /// Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado. + /// public async Task<(bool Success, string Message)> FetchDataAsync() { _logger.LogInformation("Iniciando actualización de feriados."); + + // Verificamos que la API Key de Finnhub esté configurada 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"); - + // Iteramos sobre cada código de mercado configurado foreach (var marketCode in _marketCodes) { try @@ -59,18 +106,24 @@ namespace Mercados.Infrastructure.DataFetchers var apiUrl = $"https://finnhub.io/api/v1/stock/market-holiday?exchange={marketCode}&token={apiKey}"; // Ahora la deserialización funcionará porque la clase existe var response = await client.GetFromJsonAsync(apiUrl); - + + // Si obtuvimos datos en la respuesta if (response?.Data != null) { + // Convertimos los datos de la API al formato de nuestra entidad MercadoFeriado var nuevosFeriados = response.Data.Select(h => new MercadoFeriado { CodigoMercado = marketCode, Fecha = h.Date.ToDateTime(TimeOnly.MinValue), Nombre = "Feriado Bursátil" }).ToList(); - + + // Guardamos los feriados en la base de datos, reemplazando los existentes para ese mercado 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) @@ -78,6 +131,7 @@ namespace Mercados.Infrastructure.DataFetchers _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."); } } diff --git a/src/Mercados.Infrastructure/DataFetchers/MercadoAgroFetcher.cs b/src/Mercados.Infrastructure/DataFetchers/MercadoAgroFetcher.cs index 418e559..14d821f 100644 --- a/src/Mercados.Infrastructure/DataFetchers/MercadoAgroFetcher.cs +++ b/src/Mercados.Infrastructure/DataFetchers/MercadoAgroFetcher.cs @@ -6,15 +6,39 @@ using System.Globalization; namespace Mercados.Infrastructure.DataFetchers { + /// + /// Implementación de para obtener datos de cotizaciones de ganado + /// desde el sitio web de Mercado Agro Ganadero. + /// + /// + /// Utiliza AngleSharp para el parsing del HTML. + /// public class MercadoAgroFetcher : IDataFetcher { + /// public string SourceName => "MercadoAgroganadero"; + + /// + /// URL del sitio web de Mercado Agro Ganadero donde se encuentran las cotizaciones. + /// private const string DataUrl = "https://www.mercadoagroganadero.com.ar/dll/hacienda6.dll/haciinfo000225"; + private readonly IHttpClientFactory _httpClientFactory; private readonly ICotizacionGanadoRepository _cotizacionRepository; private readonly IFuenteDatoRepository _fuenteDatoRepository; private readonly ILogger _logger; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Fábrica para crear instancias de HttpClient, configuradas con políticas de reintento. + /// Repositorio para guardar las cotizaciones de ganado obtenidas. + /// Repositorio para gestionar la información de la fuente de datos (Mercado Agro Ganadero). + /// Logger para registrar información y errores durante la ejecución. + /// + /// El constructor requiere una 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). + /// public MercadoAgroFetcher( IHttpClientFactory httpClientFactory, ICotizacionGanadoRepository cotizacionRepository, @@ -27,6 +51,12 @@ namespace Mercados.Infrastructure.DataFetchers _logger = logger; } + /// + /// 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. + /// + /// Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado. + /// public async Task<(bool Success, string Message)> FetchDataAsync() { _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); @@ -36,15 +66,12 @@ namespace Mercados.Infrastructure.DataFetchers var htmlContent = await GetHtmlContentAsync(); if (string.IsNullOrEmpty(htmlContent)) { - // Esto sigue siendo un fallo, no se pudo obtener la página return (false, "No se pudo obtener el contenido HTML."); } var cotizaciones = ParseHtmlToEntities(htmlContent); - if (!cotizaciones.Any()) { - // La conexión fue exitosa, pero no se encontraron datos válidos. // Esto NO es un error crítico, es un estado informativo. _logger.LogInformation("La conexión con {SourceName} fue exitosa, pero no se encontraron datos de cotizaciones para procesar.", SourceName); return (true, "Conexión exitosa, pero no se encontraron nuevos datos."); @@ -64,21 +91,34 @@ namespace Mercados.Infrastructure.DataFetchers } } + /// + /// Obtiene el contenido HTML de la página de cotizaciones. + /// + /// El contenido HTML como una cadena. + /// + /// Se lanza si la solicitud HTTP no es exitosa. + /// private async Task GetHtmlContentAsync() { // Pedimos el cliente HTTP con el nombre específico que tiene la política de Polly var client = _httpClientFactory.CreateClient("MercadoAgroFetcher"); + // Es importante simular un navegador para evitar bloqueos. client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); - var response = await client.GetAsync(DataUrl); response.EnsureSuccessStatusCode(); + // El sitio usa una codificación específica, hay que decodificarla correctamente. var stream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream, System.Text.Encoding.GetEncoding("windows-1252")); return await reader.ReadToEndAsync(); } + /// + /// Parsea el contenido HTML para extraer las cotizaciones de ganado. + /// + /// El HTML a parsear. + /// Una lista de entidades . private List ParseHtmlToEntities(string html) { var config = Configuration.Default; @@ -109,7 +149,7 @@ namespace Mercados.Infrastructure.DataFetchers Categoria = celdas[1], Especificaciones = $"{celdas[2]} - {celdas[3]}", Maximo = ParseDecimal(celdas[4]), - Minimo = ParseDecimal(celdas[5]), + Minimo = ParseDecimal(celdas[5]), Promedio = ParseDecimal(celdas[6]), Mediano = ParseDecimal(celdas[7]), Cabezas = ParseInt(celdas[8]), @@ -122,21 +162,24 @@ namespace Mercados.Infrastructure.DataFetchers } 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; } + /// + /// Actualiza la información de la fuente de datos (Mercado Agro Ganadero) en la base de datos. + /// private async Task UpdateSourceInfoAsync() { var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); if (fuente == null) { - await _fuenteDatoRepository.CrearAsync(new FuenteDato - { - Nombre = SourceName, + await _fuenteDatoRepository.CrearAsync(new FuenteDato + { + Nombre = SourceName, Url = DataUrl, UltimaEjecucionExitosa = DateTime.UtcNow }); @@ -150,17 +193,33 @@ namespace Mercados.Infrastructure.DataFetchers } // --- Funciones de Ayuda para Parseo --- + + /// + /// para el parseo de números en formato "es-AR". + /// private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR"); + + /// + /// Parsea una cadena a decimal, considerando el formato numérico de Argentina. + /// + /// La cadena a parsear. + /// El valor decimal parseado. private decimal ParseDecimal(string value) { // El sitio usa '.' como separador de miles y ',' como decimal. // Primero quitamos el de miles, luego reemplazamos la coma decimal por un punto. var cleanValue = value.Replace("$", "").Replace(".", "").Trim(); return decimal.Parse(cleanValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, _cultureInfo); - } + } + + /// + /// Parsea una cadena a entero, quitando separadores de miles. + /// + /// La cadena a parsear. + /// El valor entero parseado. private int ParseInt(string value) - { - return int.Parse(value.Replace(".", ""), CultureInfo.InvariantCulture); - } - } -} \ No newline at end of file + { + return int.Parse(value.Replace(".", ""), CultureInfo.InvariantCulture); + } + } + } \ No newline at end of file diff --git a/src/Mercados.Infrastructure/DataFetchers/TickerNameMapping.cs b/src/Mercados.Infrastructure/DataFetchers/TickerNameMapping.cs index a80c191..c48279c 100644 --- a/src/Mercados.Infrastructure/DataFetchers/TickerNameMapping.cs +++ b/src/Mercados.Infrastructure/DataFetchers/TickerNameMapping.cs @@ -1,13 +1,19 @@ namespace Mercados.Infrastructure.DataFetchers { + /// + /// Clase estática que proporciona un mapeo entre los tickers de acciones y sus nombres descriptivos. + /// public static class TickerNameMapping { + /// + /// 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. + /// private static readonly Dictionary Names = new(StringComparer.OrdinalIgnoreCase) { // USA - { "SPY", "S&P 500 ETF" }, // Cambiado de GSPC a SPY para Finnhub + { "SPY", "S&P 500 ETF" }, { "AAPL", "Apple Inc." }, - { "MSFT", "Microsoft Corp." }, { "AMZN", "Amazon.com, Inc." }, { "NVDA", "NVIDIA Corp." }, { "AMD", "Advanced Micro Devices" }, @@ -19,6 +25,7 @@ namespace Mercados.Infrastructure.DataFetchers { "XLE", "Energy Select Sector SPDR" }, { "XLK", "Technology Select Sector SPDR" }, { "MELI", "MercadoLibre, Inc." }, + { "MSFT", "Microsoft Corp." }, { "GLOB", "Globant" }, // ADRs Argentinos que cotizan en EEUU @@ -53,9 +60,15 @@ namespace Mercados.Infrastructure.DataFetchers { "MELI.BA", "MercadoLibre (CEDEAR)" }, // Aclaramos que es el CEDEAR }; + /// + /// Obtiene el nombre descriptivo asociado a un ticker. + /// + /// El ticker de la acción (ej. "AAPL"). + /// El nombre completo de la empresa si se encuentra en el mapeo; de lo contrario, null. 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}"; } } } \ No newline at end of file diff --git a/src/Mercados.Infrastructure/DataFetchers/YahooFinanceDataFetcher.cs b/src/Mercados.Infrastructure/DataFetchers/YahooFinanceDataFetcher.cs index 1401015..42b4bba 100644 --- a/src/Mercados.Infrastructure/DataFetchers/YahooFinanceDataFetcher.cs +++ b/src/Mercados.Infrastructure/DataFetchers/YahooFinanceDataFetcher.cs @@ -5,9 +5,24 @@ using YahooFinanceApi; namespace Mercados.Infrastructure.DataFetchers { + /// + /// Implementación de para obtener datos de cotizaciones de bolsa + /// desde la API de Yahoo Finance. + /// + /// + /// Utiliza la librería YahooFinanceApi para interactuar con la API. + /// public class YahooFinanceDataFetcher : IDataFetcher { + /// public string SourceName => "YahooFinance"; + + /// + /// Lista de tickers a obtener de Yahoo Finance. + /// + /// + /// Incluye el índice S&P 500, acciones del Merval argentino y algunos CEDEARs. + /// private readonly List _tickers = new() { "^GSPC", // Índice S&P 500 "^MERV", "GGAL.BA", "YPFD.BA", "PAMP.BA", "BMA.BA", "COME.BA", @@ -15,20 +30,36 @@ namespace Mercados.Infrastructure.DataFetchers "CEPU.BA", "LOMA.BA", "VALO.BA", "MELI.BA" }; + /// + /// Diccionario para almacenar el mapeo de tickers con su información de mercado (Local o EEUU). + /// + private readonly Dictionary _tickerMarketMapping = new Dictionary(); + private readonly ICotizacionBolsaRepository _cotizacionRepository; private readonly IFuenteDatoRepository _fuenteDatoRepository; private readonly ILogger _logger; - public YahooFinanceDataFetcher( - ICotizacionBolsaRepository cotizacionRepository, - IFuenteDatoRepository fuenteDatoRepository, - ILogger logger) - { - _cotizacionRepository = cotizacionRepository; - _fuenteDatoRepository = fuenteDatoRepository; - _logger = logger; - } + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Repositorio para guardar las cotizaciones de bolsa obtenidas. + /// Repositorio para gestionar la información de la fuente de datos (Yahoo Finance). + /// Logger para registrar información y errores durante la ejecución. + public YahooFinanceDataFetcher( + ICotizacionBolsaRepository cotizacionRepository, + IFuenteDatoRepository fuenteDatoRepository, + ILogger logger) + { + _cotizacionRepository = cotizacionRepository; + _fuenteDatoRepository = fuenteDatoRepository; + _logger = logger; + } + /// + /// 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. + /// + /// Una tupla que indica si la operación fue exitosa y un mensaje descriptivo del resultado. public async Task<(bool Success, string Message)> FetchDataAsync() { _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); @@ -41,7 +72,7 @@ namespace Mercados.Infrastructure.DataFetchers { 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 { @@ -75,6 +106,27 @@ namespace Mercados.Infrastructure.DataFetchers } } + /// + /// Determina el mercado (Local o EEUU) para un ticker específico. + /// + /// El ticker de la acción. + /// El mercado al que pertenece el ticker. + 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; + } + + /// + /// Actualiza la información de la fuente de datos (Yahoo Finance) en la base de datos. + /// private async Task UpdateSourceInfoAsync() { var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); diff --git a/src/Mercados.Infrastructure/Mercados.Infrastructure.csproj b/src/Mercados.Infrastructure/Mercados.Infrastructure.csproj index f94cbc5..dcc329a 100644 --- a/src/Mercados.Infrastructure/Mercados.Infrastructure.csproj +++ b/src/Mercados.Infrastructure/Mercados.Infrastructure.csproj @@ -21,6 +21,7 @@ net9.0 enable enable + true diff --git a/src/Mercados.Infrastructure/Persistence/IDbConnectionFactory.cs b/src/Mercados.Infrastructure/Persistence/IDbConnectionFactory.cs index 579b5b8..a6e6b12 100644 --- a/src/Mercados.Infrastructure/Persistence/IDbConnectionFactory.cs +++ b/src/Mercados.Infrastructure/Persistence/IDbConnectionFactory.cs @@ -2,8 +2,15 @@ using System.Data; namespace Mercados.Infrastructure.Persistence { - public interface IDbConnectionFactory - { - IDbConnection CreateConnection(); - } + /// + /// Define una interfaz para una fábrica de conexiones a la base de datos. + /// + public interface IDbConnectionFactory + { + /// + /// Crea y abre una nueva conexión a la base de datos. + /// + /// Un objeto representando la conexión abierta. + IDbConnection CreateConnection(); + } } \ No newline at end of file diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionBolsaRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionBolsaRepository.cs index f453456..f62ecfb 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionBolsaRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionBolsaRepository.cs @@ -4,26 +4,34 @@ using System.Data; namespace Mercados.Infrastructure.Persistence.Repositories { + /// public class CotizacionBolsaRepository : ICotizacionBolsaRepository { private readonly IDbConnectionFactory _connectionFactory; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Fábrica para crear conexiones a la base de datos. public CotizacionBolsaRepository(IDbConnectionFactory connectionFactory) { _connectionFactory = connectionFactory; } + /// public async Task GuardarMuchosAsync(IEnumerable cotizaciones) { using IDbConnection connection = _connectionFactory.CreateConnection(); - const string sql = @" - INSERT INTO CotizacionesBolsa (Ticker, NombreEmpresa, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro) - VALUES (@Ticker, @NombreEmpresa, @Mercado, @PrecioActual, @Apertura, @CierreAnterior, @PorcentajeCambio, @FechaRegistro);"; + const string sql = @"INSERT INTO + CotizacionesBolsa (Ticker, NombreEmpresa, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro) + VALUES + (@Ticker, @NombreEmpresa, @Mercado, @PrecioActual, @Apertura, @CierreAnterior, @PorcentajeCambio, @FechaRegistro);"; await connection.ExecuteAsync(sql, cotizaciones); } + /// public async Task> ObtenerUltimasPorMercadoAsync(string mercado) { using IDbConnection connection = _connectionFactory.CreateConnection(); @@ -48,6 +56,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories return await connection.QueryAsync(sql, new { Mercado = mercado }); } + /// public async Task> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias) { using IDbConnection connection = _connectionFactory.CreateConnection(); diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionGanadoRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionGanadoRepository.cs index 5257f69..7c84dd9 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionGanadoRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionGanadoRepository.cs @@ -4,22 +4,29 @@ using System.Data; namespace Mercados.Infrastructure.Persistence.Repositories { + /// public class CotizacionGanadoRepository : ICotizacionGanadoRepository { private readonly IDbConnectionFactory _connectionFactory; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Fábrica para crear conexiones a la base de datos. public CotizacionGanadoRepository(IDbConnectionFactory connectionFactory) { _connectionFactory = connectionFactory; } + /// public async Task GuardarMuchosAsync(IEnumerable cotizaciones) { using IDbConnection connection = _connectionFactory.CreateConnection(); // Dapper puede insertar una colección de objetos de una sola vez, ¡muy eficiente! const string sql = @" - INSERT INTO CotizacionesGanado ( + INSERT INTO + CotizacionesGanado ( Categoria, Especificaciones, Maximo, Minimo, Promedio, Mediano, Cabezas, KilosTotales, KilosPorCabeza, ImporteTotal, FechaRegistro ) @@ -30,6 +37,8 @@ namespace Mercados.Infrastructure.Persistence.Repositories await connection.ExecuteAsync(sql, cotizaciones); } + + /// public async Task> ObtenerUltimaTandaAsync() { using IDbConnection connection = _connectionFactory.CreateConnection(); @@ -44,6 +53,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories return await connection.QueryAsync(sql); } + /// public async Task> ObtenerHistorialAsync(string categoria, string especificaciones, int dias) { using IDbConnection connection = _connectionFactory.CreateConnection(); diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionGranoRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionGranoRepository.cs index bc68297..c2fe17d 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionGranoRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/CotizacionGranoRepository.cs @@ -4,25 +4,34 @@ using System.Data; namespace Mercados.Infrastructure.Persistence.Repositories { + /// public class CotizacionGranoRepository : ICotizacionGranoRepository { private readonly IDbConnectionFactory _connectionFactory; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Fábrica para crear conexiones a la base de datos. public CotizacionGranoRepository(IDbConnectionFactory connectionFactory) { _connectionFactory = connectionFactory; } + /// public async Task GuardarMuchosAsync(IEnumerable cotizaciones) { using IDbConnection connection = _connectionFactory.CreateConnection(); - const string sql = @" - INSERT INTO CotizacionesGranos (Nombre, Precio, VariacionPrecio, FechaOperacion, FechaRegistro) - VALUES (@Nombre, @Precio, @VariacionPrecio, @FechaOperacion, @FechaRegistro);"; + const string sql = @"INSERT INTO + CotizacionesGranos (Nombre, Precio, VariacionPrecio, FechaOperacion, FechaRegistro) + VALUES + (@Nombre, @Precio, @VariacionPrecio, @FechaOperacion, @FechaRegistro);"; await connection.ExecuteAsync(sql, cotizaciones); } + + /// public async Task> ObtenerUltimasAsync() { using IDbConnection connection = _connectionFactory.CreateConnection(); @@ -45,6 +54,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories return await connection.QueryAsync(sql); } + /// public async Task> ObtenerHistorialAsync(string nombre, int dias) { using IDbConnection connection = _connectionFactory.CreateConnection(); diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/FuenteDatoRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/FuenteDatoRepository.cs index 469e899..c1dcda6 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/FuenteDatoRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/FuenteDatoRepository.cs @@ -4,37 +4,42 @@ using System.Data; namespace Mercados.Infrastructure.Persistence.Repositories { + /// public class FuenteDatoRepository : IFuenteDatoRepository { private readonly IDbConnectionFactory _connectionFactory; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Fábrica para crear conexiones a la base de datos. public FuenteDatoRepository(IDbConnectionFactory connectionFactory) { _connectionFactory = connectionFactory; } - + + /// public async Task ObtenerPorNombreAsync(string nombre) { using IDbConnection connection = _connectionFactory.CreateConnection(); const string sql = "SELECT * FROM FuentesDatos WHERE Nombre = @Nombre;"; return await connection.QuerySingleOrDefaultAsync(sql, new { Nombre = nombre }); } - + /// public async Task CrearAsync(FuenteDato fuenteDato) { using IDbConnection connection = _connectionFactory.CreateConnection(); - const string sql = @" - INSERT INTO FuentesDatos (Nombre, UltimaEjecucionExitosa, Url) + const string sql = @"INSERT INTO FuentesDatos (Nombre, UltimaEjecucionExitosa, Url) VALUES (@Nombre, @UltimaEjecucionExitosa, @Url);"; await connection.ExecuteAsync(sql, fuenteDato); } + /// public async Task ActualizarAsync(FuenteDato fuenteDato) { using IDbConnection connection = _connectionFactory.CreateConnection(); - const string sql = @" - UPDATE FuentesDatos - SET UltimaEjecucionExitosa = @UltimaEjecucionExitosa, Url = @Url + const string sql = @"UPDATE FuentesDatos + SET UltimaEjecucionExitosa = @UltimaEjecucionExitosa, Url = @Url WHERE Id = @Id;"; await connection.ExecuteAsync(sql, fuenteDato); } diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/IBaseRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/IBaseRepository.cs index c1f3474..78c161e 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/IBaseRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/IBaseRepository.cs @@ -1,6 +1,9 @@ namespace Mercados.Infrastructure.Persistence.Repositories { - // Esta interfaz no es estrictamente necesaria ahora, pero es útil para futuras abstracciones. + /// + /// Interfaz base marcadora para todos los repositorios. + /// No define miembros, pero sirve para la abstracción y la inyección de dependencias. + /// public interface IBaseRepository { } diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionBolsaRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionBolsaRepository.cs index b225b75..c7deb3b 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionBolsaRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionBolsaRepository.cs @@ -2,10 +2,31 @@ using Mercados.Core.Entities; namespace Mercados.Infrastructure.Persistence.Repositories { + /// + /// Define el contrato para el repositorio que gestiona las cotizaciones de la bolsa. + /// public interface ICotizacionBolsaRepository : IBaseRepository { + /// + /// Guarda una colección de cotizaciones de bolsa en la base de datos de forma masiva. + /// + /// La colección de entidades CotizacionBolsa a guardar. Task GuardarMuchosAsync(IEnumerable cotizaciones); + + /// + /// Obtiene la última cotización registrada para cada ticker de un mercado específico. + /// + /// El código del mercado a consultar (ej. "US", "Local"). + /// Una colección con la última cotización de cada activo de ese mercado. Task> ObtenerUltimasPorMercadoAsync(string mercado); + + /// + /// Obtiene el historial de cotizaciones para un ticker específico durante un período determinado. + /// + /// El símbolo del activo (ej. "AAPL", "^MERV"). + /// El mercado al que pertenece el ticker. + /// El número de días hacia atrás desde hoy para obtener el historial. + /// Una colección de cotizaciones ordenadas por fecha de forma ascendente. Task> ObtenerHistorialPorTickerAsync(string ticker, string mercado, int dias); } } \ No newline at end of file diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionGanadoRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionGanadoRepository.cs index 21f6053..ca5d363 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionGanadoRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionGanadoRepository.cs @@ -2,10 +2,30 @@ using Mercados.Core.Entities; namespace Mercados.Infrastructure.Persistence.Repositories { + /// + /// Define el contrato para el repositorio que gestiona las cotizaciones del mercado de ganado. + /// public interface ICotizacionGanadoRepository : IBaseRepository { + /// + /// Guarda una colección de cotizaciones de ganado en la base de datos. + /// + /// La colección de entidades CotizacionGanado a guardar. Task GuardarMuchosAsync(IEnumerable cotizaciones); + + /// + /// Obtiene el último parte completo de cotizaciones del mercado de ganado. + /// + /// Una colección de todas las cotizaciones de la última tanda registrada. Task> ObtenerUltimaTandaAsync(); + + /// + /// Obtiene el historial de cotizaciones para una categoría y especificación de ganado. + /// + /// La categoría principal del ganado (ej. "NOVILLOS"). + /// La especificación detallada del ganado. + /// El número de días de historial a recuperar. + /// Una colección de cotizaciones históricas para esa categoría. Task> ObtenerHistorialAsync(string categoria, string especificaciones, int dias); } } \ No newline at end of file diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionGranoRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionGranoRepository.cs index 6b24bbe..642c95b 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionGranoRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/ICotizacionGranoRepository.cs @@ -2,10 +2,29 @@ using Mercados.Core.Entities; namespace Mercados.Infrastructure.Persistence.Repositories { + /// + /// Define el contrato para el repositorio que gestiona las cotizaciones del mercado de granos. + /// public interface ICotizacionGranoRepository : IBaseRepository { + /// + /// Guarda una colección de cotizaciones de granos en la base de datos. + /// + /// La colección de entidades CotizacionGrano a guardar. Task GuardarMuchosAsync(IEnumerable cotizaciones); + + /// + /// Obtiene las últimas cotizaciones disponibles para los granos. + /// + /// Una colección de las últimas cotizaciones de granos registradas. Task> ObtenerUltimasAsync(); + + /// + /// Obtiene el historial de cotizaciones para un grano específico. + /// + /// El nombre del grano (ej. "Soja"). + /// El número de días de historial a recuperar. + /// Una colección de cotizaciones históricas para el grano especificado. Task> ObtenerHistorialAsync(string nombre, int dias); } } \ No newline at end of file diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/IFuenteDatoRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/IFuenteDatoRepository.cs index 5046afa..3545c63 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/IFuenteDatoRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/IFuenteDatoRepository.cs @@ -2,10 +2,28 @@ using Mercados.Core.Entities; namespace Mercados.Infrastructure.Persistence.Repositories { + /// + /// Define el contrato para el repositorio que gestiona las fuentes de datos. + /// public interface IFuenteDatoRepository : IBaseRepository { + /// + /// Obtiene una entidad FuenteDato por su nombre único. + /// + /// El nombre de la fuente de datos a buscar. + /// La entidad FuenteDato si se encuentra; de lo contrario, null. Task ObtenerPorNombreAsync(string nombre); + + /// + /// Actualiza una entidad FuenteDato existente en la base de datos. + /// + /// La entidad FuenteDato con los datos actualizados. Task ActualizarAsync(FuenteDato fuenteDato); + + /// + /// Crea una nueva entidad FuenteDato en la base de datos. + /// + /// La entidad FuenteDato a crear. Task CrearAsync(FuenteDato fuenteDato); } } \ No newline at end of file diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/IMercadoFeriadoRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/IMercadoFeriadoRepository.cs index 47996f5..c30a3f2 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/IMercadoFeriadoRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/IMercadoFeriadoRepository.cs @@ -2,9 +2,23 @@ using Mercados.Core.Entities; namespace Mercados.Infrastructure.Persistence.Repositories { + /// + /// Define el contrato para el repositorio que gestiona los feriados de los mercados. + /// public interface IMercadoFeriadoRepository : IBaseRepository { + /// + /// Obtiene todos los feriados para un mercado y año específicos. + /// + /// El código del mercado para el cual se buscan los feriados. + /// El año para el cual se desean obtener los feriados. + /// Una colección de entidades MercadoFeriado para el mercado y año especificados. Task> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio); + /// + /// Reemplaza todos los feriados existentes para un mercado con una nueva lista. + /// + /// El código del mercado cuyos feriados serán reemplazados. + /// La nueva colección de feriados que se guardará. Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable nuevosFeriados); } } \ No newline at end of file diff --git a/src/Mercados.Infrastructure/Persistence/Repositories/MercadoFeriadoRepository.cs b/src/Mercados.Infrastructure/Persistence/Repositories/MercadoFeriadoRepository.cs index a2fcd2f..a04607e 100644 --- a/src/Mercados.Infrastructure/Persistence/Repositories/MercadoFeriadoRepository.cs +++ b/src/Mercados.Infrastructure/Persistence/Repositories/MercadoFeriadoRepository.cs @@ -4,24 +4,35 @@ using System.Data; namespace Mercados.Infrastructure.Persistence.Repositories { + /// public class MercadoFeriadoRepository : IMercadoFeriadoRepository { private readonly IDbConnectionFactory _connectionFactory; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Fábrica para crear conexiones a la base de datos. public MercadoFeriadoRepository(IDbConnectionFactory connectionFactory) { _connectionFactory = connectionFactory; } + /// public async Task> ObtenerPorMercadoYAnioAsync(string codigoMercado, int anio) { using IDbConnection connection = _connectionFactory.CreateConnection(); - const string sql = @" - SELECT * FROM MercadosFeriados + const string sql = @"SELECT * + FROM MercadosFeriados WHERE CodigoMercado = @CodigoMercado AND YEAR(Fecha) = @Anio;"; - return await connection.QueryAsync(sql, new { CodigoMercado = codigoMercado, Anio = anio }); + return await connection.QueryAsync(sql, new + { + CodigoMercado = codigoMercado, + Anio = anio + }); } + /// public async Task ReemplazarFeriadosPorMercadoAsync(string codigoMercado, IEnumerable nuevosFeriados) { using IDbConnection connection = _connectionFactory.CreateConnection(); @@ -30,25 +41,31 @@ namespace Mercados.Infrastructure.Persistence.Repositories 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; - if (anio.HasValue) + if (!anio.HasValue) return; // Si no hay feriados, no hay nada que hacer + + // 1. Borrar los feriados existentes para ese mercado + const string deleteSql = "DELETE FROM MercadosFeriados WHERE CodigoMercado = @CodigoMercado;"; + await connection.ExecuteAsync(deleteSql, new { CodigoMercado = codigoMercado }, transaction); + + // 2. Insertar los nuevos feriados + if (nuevosFeriados.Any()) { - const string deleteSql = "DELETE FROM MercadosFeriados WHERE CodigoMercado = @CodigoMercado AND YEAR(Fecha) = @Anio;"; - await connection.ExecuteAsync(deleteSql, new { CodigoMercado = codigoMercado, Anio = anio.Value }, transaction); + const string insertSql = @"INSERT INTO MercadosFeriados (CodigoMercado, Fecha, Nombre) + VALUES (@CodigoMercado, @Fecha, @Nombre);"; + await connection.ExecuteAsync(insertSql, nuevosFeriados, transaction); } - // Insertamos los nuevos - const string insertSql = @" - INSERT INTO MercadosFeriados (CodigoMercado, Fecha, Nombre) - VALUES (@CodigoMercado, @Fecha, @Nombre);"; - await connection.ExecuteAsync(insertSql, nuevosFeriados, transaction); - + // Si todo sale bien, confirmar la transacción transaction.Commit(); } catch { + // Si hay algún error, deshacer la transacción para no dejar datos inconsistentes transaction.Rollback(); + + // Relanzar la excepción para que el llamador sepa que algo falló throw; } } diff --git a/src/Mercados.Infrastructure/Persistence/SqlConnectionFactory.cs b/src/Mercados.Infrastructure/Persistence/SqlConnectionFactory.cs index 334c3c4..06157af 100644 --- a/src/Mercados.Infrastructure/Persistence/SqlConnectionFactory.cs +++ b/src/Mercados.Infrastructure/Persistence/SqlConnectionFactory.cs @@ -5,10 +5,17 @@ using System.Data; namespace Mercados.Infrastructure { + /// + /// Proporciona una fábrica para crear conexiones a la base de datos SQL. + /// public class SqlConnectionFactory : IDbConnectionFactory { private readonly string _connectionString; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// La configuración de la aplicación desde donde se obtiene la cadena de conexión. public SqlConnectionFactory(IConfiguration configuration) { // Variable de entorno 'DB_CONNECTION_STRING' si está disponible, @@ -17,9 +24,10 @@ namespace Mercados.Infrastructure ?? throw new ArgumentNullException(nameof(configuration), "La cadena de conexión 'DefaultConnection' no fue encontrada."); } - public IDbConnection CreateConnection() + /// + public IDbConnection CreateConnection() { return new SqlConnection(_connectionString); } - } +} } \ No newline at end of file diff --git a/src/Mercados.Infrastructure/Services/EmailNotificationService.cs b/src/Mercados.Infrastructure/Services/EmailNotificationService.cs index 747c78d..1cc0d20 100644 --- a/src/Mercados.Infrastructure/Services/EmailNotificationService.cs +++ b/src/Mercados.Infrastructure/Services/EmailNotificationService.cs @@ -6,17 +6,26 @@ using MimeKit; namespace Mercados.Infrastructure.Services { + /// + /// Servicio que gestiona el envío de notificaciones por correo electrónico. + /// public class EmailNotificationService : INotificationService { private readonly ILogger _logger; private readonly IConfiguration _configuration; + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Logger para registrar información y errores. + /// Configuración de la aplicación para obtener los ajustes SMTP. public EmailNotificationService(ILogger logger, IConfiguration configuration) { _logger = logger; _configuration = configuration; } + /// 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) diff --git a/src/Mercados.Infrastructure/Services/FinnhubHolidayService.cs b/src/Mercados.Infrastructure/Services/FinnhubHolidayService.cs index 7463755..4f40799 100644 --- a/src/Mercados.Infrastructure/Services/FinnhubHolidayService.cs +++ b/src/Mercados.Infrastructure/Services/FinnhubHolidayService.cs @@ -5,26 +5,41 @@ using Microsoft.Extensions.Logging; namespace Mercados.Infrastructure.Services { - public class FinnhubHolidayService : IHolidayService + /// + /// Servicio para consultar si una fecha es feriado de mercado utilizando la base de datos interna. + /// + public class FinnhubHolidayService : IHolidayService { private readonly IMercadoFeriadoRepository _feriadoRepository; private readonly IMemoryCache _cache; private readonly ILogger _logger; - public FinnhubHolidayService( - IMercadoFeriadoRepository feriadoRepository, - IMemoryCache cache, - ILogger logger) - { - _feriadoRepository = feriadoRepository; - _cache = cache; - _logger = logger; - } + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Repositorio para acceder a los feriados de mercado. + /// Caché en memoria para almacenar los feriados. + /// Logger para registrar información y errores. + public FinnhubHolidayService( + IMercadoFeriadoRepository feriadoRepository, + IMemoryCache cache, + ILogger logger) + { + _feriadoRepository = feriadoRepository; + _cache = cache; + _logger = logger; + } - public async Task IsMarketHolidayAsync(string marketCode, DateTime date) - { - var dateOnly = DateOnly.FromDateTime(date); - var cacheKey = $"holidays_{marketCode}_{date.Year}"; + /// + /// Determina si una fecha específica es feriado de mercado para el código de mercado proporcionado. + /// + /// Código del mercado a consultar. + /// Fecha a verificar. + /// True si la fecha es feriado de mercado; de lo contrario, false. + public async Task IsMarketHolidayAsync(string marketCode, DateTime date) + { + var dateOnly = DateOnly.FromDateTime(date); + var cacheKey = $"holidays_{marketCode}_{date.Year}"; if (!_cache.TryGetValue(cacheKey, out HashSet? holidays)) { diff --git a/src/Mercados.Infrastructure/Services/INotificationService.cs b/src/Mercados.Infrastructure/Services/INotificationService.cs index a7062f1..3626438 100644 --- a/src/Mercados.Infrastructure/Services/INotificationService.cs +++ b/src/Mercados.Infrastructure/Services/INotificationService.cs @@ -10,6 +10,7 @@ namespace Mercados.Infrastructure.Services /// /// El título de la alerta. /// El mensaje detallado del error. + /// La fecha y hora UTC del evento (opcional). Task SendFailureAlertAsync(string subject, string message, DateTime? eventTimeUtc = null); } } \ No newline at end of file diff --git a/src/Mercados.Worker/DataFetchingService.cs b/src/Mercados.Worker/DataFetchingService.cs index daee879..2a8bcff 100644 --- a/src/Mercados.Worker/DataFetchingService.cs +++ b/src/Mercados.Worker/DataFetchingService.cs @@ -14,23 +14,48 @@ namespace Mercados.Worker private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly TimeZoneInfo _argentinaTimeZone; - - // Expresiones Cron + + /// + /// Expresión Cron para la tarea de Mercado Agroganadero. + /// private readonly CronExpression _agroSchedule; + /// + /// Expresión Cron para la tarea de la Bolsa de Comercio de Rosario (BCR). + /// private readonly CronExpression _bcrSchedule; + /// + /// Expresión Cron para la tarea de las Bolsas (Finnhub y Yahoo Finance). + /// private readonly CronExpression _bolsasSchedule; + /// + /// Expresión Cron para la tarea de actualización de feriados. + /// private readonly CronExpression _holidaysSchedule; - // Próximas ejecuciones + /// Próxima hora de ejecución programada para la tarea de Mercado Agroganadero. private DateTime? _nextAgroRun; + /// Próxima hora de ejecución programada para la tarea de BCR. private DateTime? _nextBcrRun; + /// Próxima hora de ejecución programada para la tarea de Bolsas. private DateTime? _nextBolsasRun; + /// Próxima hora de ejecución programada para la tarea de Feriados. private DateTime? _nextHolidaysRun; + /// + /// Almacena la última vez que se envió una alerta para una tarea específica, para evitar spam. + /// private readonly Dictionary _lastAlertSent = new(); + /// + /// Período de tiempo durante el cual no se enviarán alertas repetidas para la misma tarea. + /// private readonly TimeSpan _alertSilencePeriod = TimeSpan.FromHours(4); - // Eliminamos IHolidayService del constructor + /// + /// Inicializa una nueva instancia de la clase . + /// + /// Logger para registrar información y eventos. + /// Proveedor de servicios para la inyección de dependencias con scope. + /// Configuración de la aplicación para obtener los schedules de Cron. public DataFetchingService( ILogger logger, IServiceProvider serviceProvider, @@ -60,8 +85,10 @@ namespace Mercados.Worker } /// - /// 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. /// + /// Token de cancelación para detener el servicio de forma segura. protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now); @@ -154,6 +181,9 @@ namespace Mercados.Worker /// /// Ejecuta un fetcher específico por su nombre, gestionando el scope de DI y las notificaciones. /// + /// El nombre del a ejecutar. + /// Token de cancelación para detener la operación si el servicio se está parando. + /// Este método crea un nuevo scope de DI para resolver los servicios necesarios. private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken) { if (stoppingToken.IsCancellationRequested) return; @@ -193,6 +223,8 @@ namespace Mercados.Worker /// /// Ejecuta todos los fetchers en paralelo al iniciar el servicio. /// + /// Token de cancelación para detener la operación si el servicio se está parando. + /// Esta función se usa principalmente para una ejecución de prueba al arrancar. private async Task RunAllFetchersAsync(CancellationToken stoppingToken) { _logger.LogInformation("Ejecutando todos los fetchers al iniciar en paralelo..."); @@ -211,6 +243,8 @@ namespace Mercados.Worker /// /// Determina si se debe enviar una alerta o si está en período de silencio. /// + /// El nombre de la tarea que podría generar la alerta. + /// True si se debe enviar la alerta; de lo contrario, false. private bool ShouldSendAlert(string taskName) { if (!_lastAlertSent.ContainsKey(taskName)) @@ -224,8 +258,13 @@ namespace Mercados.Worker #endregion - // Creamos una única función para comprobar feriados que obtiene el servicio - // desde un scope. + /// + /// Comprueba si una fecha dada es feriado para un mercado específico. + /// + /// El código del mercado (ej. "US", "BA"). + /// La fecha a comprobar. + /// True si es feriado, false si no lo es o si ocurre un error. + /// Este método resuelve el desde un nuevo scope de DI para cada llamada. private async Task IsMarketHolidayAsync(string marketCode, DateTime date) { using var scope = _serviceProvider.CreateScope(); diff --git a/src/Mercados.Worker/Mercados.Worker.csproj b/src/Mercados.Worker/Mercados.Worker.csproj index d0aec2c..bd51e60 100644 --- a/src/Mercados.Worker/Mercados.Worker.csproj +++ b/src/Mercados.Worker/Mercados.Worker.csproj @@ -5,6 +5,7 @@ enable enable dotnet-Mercados.Worker-0e9a6e84-c8fb-400b-ba7b-193d9981a046 + true