feat: adaptación de los proyectos para utilizar .env y comienzo de preparación para despliegue en docker
This commit is contained in:
		| @@ -113,5 +113,39 @@ namespace Mercados.Api.Controllers | ||||
|                 return StatusCode(500, "Ocurrió un error interno en el servidor."); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [HttpGet("agroganadero/history")] | ||||
|         [ProducesResponseType(typeof(IEnumerable<CotizacionGanado>), StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||
|         public async Task<IActionResult> GetAgroganaderoHistory([FromQuery] string categoria, [FromQuery] string especificaciones, [FromQuery] int dias = 30) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var data = await _ganadoRepo.ObtenerHistorialAsync(categoria, especificaciones, dias); | ||||
|                 return Ok(data); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error al obtener historial para la categoría {Categoria}.", categoria); | ||||
|                 return StatusCode(500, "Ocurrió un error interno en el servidor."); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [HttpGet("granos/history/{nombre}")] | ||||
|         [ProducesResponseType(typeof(IEnumerable<CotizacionGrano>), StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||||
|         public async Task<IActionResult> GetGranoHistory(string nombre, [FromQuery] int dias = 30) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var data = await _granoRepo.ObtenerHistorialAsync(nombre, dias); | ||||
|                 return Ok(data); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error al obtener historial para el grano {Grano}.", nombre); | ||||
|                 return StatusCode(500, "Ocurrió un error interno en el servidor."); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -7,6 +7,7 @@ | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="DotNetEnv" Version="3.1.1" /> | ||||
|     <PackageReference Include="FluentMigrator.Runner" Version="7.1.0" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" /> | ||||
|     <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" /> | ||||
|   | ||||
| @@ -5,6 +5,9 @@ using Mercados.Infrastructure.Persistence; | ||||
| using Mercados.Infrastructure.Persistence.Repositories; | ||||
| using System.Reflection; | ||||
|  | ||||
| // Carga las variables de entorno desde el archivo .env en la raíz de la solución. | ||||
| DotNetEnv.Env.Load(); | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| // Nombre para política de CORS | ||||
| @@ -16,7 +19,10 @@ builder.Services.AddCors(options => | ||||
|     options.AddPolicy(name: MyAllowSpecificOrigins, | ||||
|                       policy => | ||||
|                       { | ||||
|                           policy.WithOrigins("http://localhost:5173", "http://192.168.10.78:5173") | ||||
|                           policy.WithOrigins("http://localhost:5173", // Desarrollo Frontend | ||||
|                                 "http://192.168.10.78:5173", // Desarrollo en Red Local | ||||
|                                 "https://www.eldia.com" // <--- DOMINIO DE PRODUCCIÓN | ||||
|                                 ) | ||||
|                                 .AllowAnyHeader() | ||||
|                                 .AllowAnyMethod(); | ||||
|                       }); | ||||
|   | ||||
| @@ -7,13 +7,13 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "DefaultConnection": "Server=TECNICA3;Database=MercadosDb;User Id=mercadosuser;Password=@mercados1351@;Trusted_Connection=False;Encrypt=False;" | ||||
|     "DefaultConnection": "" | ||||
|   }, | ||||
|   "ApiKeys": { | ||||
|     "Finnhub": "cuvhr0hr01qs9e81st2gcuvhr0hr01qs9e81st30", | ||||
|     "Finnhub": "", | ||||
|     "Bcr": { | ||||
|       "Key": "D1782A51-A5FD-EF11-9445-00155D09E201", | ||||
|       "Secret": "da96378186bc5a256fa821fbe79261ec7172dec283214da0aacca41c640f80e3" | ||||
|       "Key": "", | ||||
|       "Secret": "" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -4,6 +4,7 @@ namespace Mercados.Core.Entities | ||||
|   { | ||||
|     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; } | ||||
|   | ||||
| @@ -1,6 +0,0 @@ | ||||
| namespace Mercados.Database; | ||||
|  | ||||
| public class Class1 | ||||
| { | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,19 @@ | ||||
| using FluentMigrator; | ||||
|  | ||||
| namespace Mercados.Database.Migrations | ||||
| { | ||||
|     [Migration(20240702133000)] | ||||
|     public class AddNameToStocks : Migration | ||||
|     { | ||||
|         public override void Up() | ||||
|         { | ||||
|             Alter.Table("CotizacionesBolsa") | ||||
|                 .AddColumn("NombreEmpresa").AsString(255).Nullable(); | ||||
|         } | ||||
|  | ||||
|         public override void Down() | ||||
|         { | ||||
|             Delete.Column("NombreEmpresa").FromTable("CotizacionesBolsa"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| namespace Mercados.Infrastructure; | ||||
|  | ||||
| public class Class1 | ||||
| { | ||||
|  | ||||
| } | ||||
| @@ -122,8 +122,8 @@ namespace Mercados.Infrastructure.DataFetchers | ||||
|         private async Task<string?> GetAuthTokenAsync(HttpClient client) | ||||
|         { | ||||
|             var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/Login"); | ||||
|             request.Headers.Add("api_key", _configuration["ApiKeys:Bcr:Key"]); | ||||
|             request.Headers.Add("secret", _configuration["ApiKeys:Bcr:Secret"]); | ||||
|             request.Headers.Add("api_key", Environment.GetEnvironmentVariable("BCR_API_KEY")); | ||||
|             request.Headers.Add("secret", Environment.GetEnvironmentVariable("BCR_API_SECRET")); | ||||
|  | ||||
|             var response = await client.SendAsync(request); | ||||
|             response.EnsureSuccessStatusCode(); | ||||
|   | ||||
| @@ -11,9 +11,12 @@ namespace Mercados.Infrastructure.DataFetchers | ||||
|     { | ||||
|         public string SourceName => "Finnhub"; | ||||
|         private readonly List<string> _tickers = new() { | ||||
|             "AAPL", "AMD", "AMZN", "BRK-B", "KO", "MSFT", "NVDA", "GLD",  | ||||
|             "XLF", "XLI", "XLE", "XLK", "YPF", "GGAL", "BMA", "TEO", | ||||
|             "PAM", "CEPU", "LOMA", "CRESY", "BBAR", "TGS", "EDN", "MELI", "GLOB" | ||||
|             // Tecnológicas y ETFs | ||||
|             "AAPL", "MSFT", "AMZN", "NVDA", "AMD", "KO", "BRK-B", "GLD", "XLF", "XLI", "XLE", "XLK", | ||||
|             // Empresas 'Latinas' en Wall Street | ||||
|             "MELI", "GLOB", | ||||
|             // ADRs Argentinos | ||||
|             "YPF", "GGAL", "BMA", "LOMA", "PAM", "TEO", "TGS", "EDN", "CRESY", "CEPU", "BBAR" | ||||
|         }; | ||||
|          | ||||
|         private readonly FinnhubClient _client; | ||||
| @@ -28,7 +31,7 @@ namespace Mercados.Infrastructure.DataFetchers | ||||
|             IFuenteDatoRepository fuenteDatoRepository, | ||||
|             ILogger<FinnhubDataFetcher> logger) | ||||
|         { | ||||
|             var apiKey = configuration["ApiKeys:Finnhub"]; | ||||
|             var apiKey = Environment.GetEnvironmentVariable("FINNHUB_API_KEY"); | ||||
|             if (string.IsNullOrEmpty(apiKey)) | ||||
|             { | ||||
|                 throw new InvalidOperationException("La clave de API de Finnhub no está configurada en appsettings.json (ApiKeys:Finnhub)"); | ||||
| @@ -53,9 +56,11 @@ namespace Mercados.Infrastructure.DataFetchers | ||||
|                     if (quote.Current == 0 || quote.PreviousClose == 0) continue; | ||||
|  | ||||
|                     var pctChange = ((quote.Current - quote.PreviousClose) / quote.PreviousClose) * 100; | ||||
|                      | ||||
|                     cotizaciones.Add(new CotizacionBolsa | ||||
|                     { | ||||
|                         Ticker = ticker, | ||||
|                         NombreEmpresa = TickerNameMapping.GetName(ticker), | ||||
|                         Mercado = "EEUU", | ||||
|                         PrecioActual = (decimal)quote.Current, | ||||
|                         Apertura = (decimal)quote.Open, | ||||
|   | ||||
| @@ -0,0 +1,61 @@ | ||||
| namespace Mercados.Infrastructure.DataFetchers | ||||
| { | ||||
|     public static class TickerNameMapping | ||||
|     { | ||||
|         private static readonly Dictionary<string, string> Names = new(StringComparer.OrdinalIgnoreCase) | ||||
|         { | ||||
|           // USA | ||||
|             { "SPY", "S&P 500 ETF" }, // Cambiado de GSPC a SPY para Finnhub | ||||
|             { "AAPL", "Apple Inc." }, | ||||
|             { "MSFT", "Microsoft Corp." }, | ||||
|             { "AMZN", "Amazon.com, Inc." }, | ||||
|             { "NVDA", "NVIDIA Corp." }, | ||||
|             { "AMD", "Advanced Micro Devices" }, | ||||
|             { "KO", "The Coca-Cola Company" }, | ||||
|             { "BRK-B", "Berkshire Hathaway Inc." }, | ||||
|             { "GLD", "SPDR Gold Shares" }, | ||||
|             { "XLF", "Financial Select Sector SPDR" }, | ||||
|             { "XLI", "Industrial Select Sector SPDR" }, | ||||
|             { "XLE", "Energy Select Sector SPDR" }, | ||||
|             { "XLK", "Technology Select Sector SPDR" }, | ||||
|             { "MELI", "MercadoLibre, Inc." }, | ||||
|             { "GLOB", "Globant" }, | ||||
|              | ||||
|             // ADRs Argentinos que cotizan en EEUU | ||||
|             { "YPF", "YPF S.A. (ADR)" }, | ||||
|             { "GGAL", "Grupo Financiero Galicia (ADR)" }, | ||||
|             { "BMA", "Banco Macro (ADR)" }, | ||||
|             { "LOMA", "Loma Negra (ADR)" }, | ||||
|             { "PAM", "Pampa Energía (ADR)" }, | ||||
|             { "TEO", "Telecom Argentina (ADR)" }, | ||||
|             { "TGS", "Transportadora de Gas del Sur (ADR)" }, | ||||
|             { "EDN", "Edenor (ADR)" }, | ||||
|             { "CRESY", "Cresud (ADR)" }, | ||||
|             { "CEPU", "Central Puerto (ADR)" }, | ||||
|             { "BBAR", "BBVA Argentina (ADR)" }, | ||||
|  | ||||
|             // Argentina Local | ||||
|             { "^GSPC", "S&P 500 Index" }, // Lo dejamos por si Yahoo lo devuelve | ||||
|             { "^MERV", "S&P Merval" }, | ||||
|             { "GGAL.BA", "Grupo Financiero Galicia" }, | ||||
|             { "YPFD.BA", "YPF S.A." }, | ||||
|             { "PAMP.BA", "Pampa Energía" }, | ||||
|             { "BMA.BA", "Banco Macro" }, | ||||
|             { "COME.BA", "Sociedad Comercial del Plata" }, | ||||
|             { "TECO2.BA", "Telecom Argentina" }, | ||||
|             { "EDN.BA", "Edenor" }, | ||||
|             { "CRES.BA", "Cresud" }, | ||||
|             { "TXAR.BA", "Ternium Argentina" }, | ||||
|             { "MIRG.BA", "Mirgor" }, | ||||
|             { "CEPU.BA", "Central Puerto" }, | ||||
|             { "LOMA.BA", "Loma Negra" }, | ||||
|             { "VALO.BA", "Banco de Valores" }, | ||||
|             { "MELI.BA", "MercadoLibre (CEDEAR)" }, // Aclaramos que es el CEDEAR | ||||
|         }; | ||||
|  | ||||
|         public static string? GetName(string ticker) | ||||
|         { | ||||
|             return Names.GetValueOrDefault(ticker); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -9,9 +9,10 @@ namespace Mercados.Infrastructure.DataFetchers | ||||
|     { | ||||
|         public string SourceName => "YahooFinance"; | ||||
|         private readonly List<string> _tickers = new() { | ||||
|             "^GSPC", // Índice S&P 500 | ||||
|             "^MERV", "GGAL.BA", "YPFD.BA", "PAMP.BA", "BMA.BA", "COME.BA",  | ||||
|             "TECO2.BA", "EDN.BA", "CRES.BA", "TXAR.BA", "MIRG.BA",  | ||||
|             "CEPU.BA", "LOMA.BA", "VALO.BA" | ||||
|             "CEPU.BA", "LOMA.BA", "VALO.BA", "MELI.BA" | ||||
|         }; | ||||
|          | ||||
|         private readonly ICotizacionBolsaRepository _cotizacionRepository; | ||||
| @@ -33,7 +34,6 @@ namespace Mercados.Infrastructure.DataFetchers | ||||
|             _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); | ||||
|             try | ||||
|             { | ||||
|                 // La librería puede obtener múltiples tickers en una sola llamada. | ||||
|                 var securities = await Yahoo.Symbols(_tickers.ToArray()).Fields(Field.RegularMarketPrice, Field.RegularMarketOpen, Field.RegularMarketPreviousClose, Field.RegularMarketChangePercent).QueryAsync(); | ||||
|                 var cotizaciones = new List<CotizacionBolsa>(); | ||||
|  | ||||
| @@ -41,10 +41,13 @@ namespace Mercados.Infrastructure.DataFetchers | ||||
|                 { | ||||
|                     if (sec.RegularMarketPrice == 0 || sec.RegularMarketPreviousClose == 0) continue; | ||||
|                      | ||||
|                     string mercado = sec.Symbol.EndsWith(".BA") || sec.Symbol == "^MERV" ? "Local" : "EEUU"; | ||||
|  | ||||
|                     cotizaciones.Add(new CotizacionBolsa | ||||
|                     { | ||||
|                         Ticker = sec.Symbol, | ||||
|                         Mercado = "Local", | ||||
|                         NombreEmpresa = TickerNameMapping.GetName(sec.Symbol), | ||||
|                         Mercado = mercado, | ||||
|                         PrecioActual = (decimal)sec.RegularMarketPrice, | ||||
|                         Apertura = (decimal)sec.RegularMarketOpen, | ||||
|                         CierreAnterior = (decimal)sec.RegularMarketPreviousClose, | ||||
|   | ||||
| @@ -18,8 +18,8 @@ namespace Mercados.Infrastructure.Persistence.Repositories | ||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||
|  | ||||
|             const string sql = @" | ||||
|                 INSERT INTO CotizacionesBolsa (Ticker, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro)  | ||||
|                 VALUES (@Ticker, @Mercado, @PrecioActual, @Apertura, @CierreAnterior, @PorcentajeCambio, @FechaRegistro);"; | ||||
|                 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); | ||||
|         } | ||||
| @@ -27,10 +27,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories | ||||
|         public async Task<IEnumerable<CotizacionBolsa>> ObtenerUltimasPorMercadoAsync(string mercado) | ||||
|         { | ||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||
|  | ||||
|             // Esta consulta usa una "Common Table Expression" (CTE) | ||||
|             // y la función ROW_NUMBER() para obtener el registro más reciente para cada Ticker | ||||
|             // dentro del mercado especificado. Es extremadamente eficiente. | ||||
|              | ||||
|             const string sql = @" | ||||
|                 WITH RankedCotizaciones AS ( | ||||
|                     SELECT  | ||||
| @@ -42,7 +39,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories | ||||
|                         Mercado = @Mercado | ||||
|                 ) | ||||
|                 SELECT  | ||||
|                     Id, Ticker, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro | ||||
|                     Id, Ticker, NombreEmpresa, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro | ||||
|                 FROM  | ||||
|                     RankedCotizaciones | ||||
|                 WHERE  | ||||
| @@ -57,7 +54,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories | ||||
|  | ||||
|             const string sql = @" | ||||
|                 SELECT  | ||||
|                     Id, Ticker, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro | ||||
|                     Id, Ticker, NombreEmpresa, Mercado, PrecioActual, Apertura, CierreAnterior, PorcentajeCambio, FechaRegistro | ||||
|                 FROM  | ||||
|                     CotizacionesBolsa | ||||
|                 WHERE | ||||
| @@ -65,7 +62,7 @@ namespace Mercados.Infrastructure.Persistence.Repositories | ||||
|                     AND Mercado = @Mercado | ||||
|                     AND FechaRegistro >= DATEADD(day, -@Dias, GETUTCDATE()) | ||||
|                 ORDER BY | ||||
|                     FechaRegistro ASC;"; // ASC es importante para dibujar la línea del gráfico | ||||
|                     FechaRegistro ASC;"; | ||||
|  | ||||
|             return await connection.QueryAsync<CotizacionBolsa>(sql, new { Ticker = ticker, Mercado = mercado, Dias = dias }); | ||||
|         } | ||||
|   | ||||
| @@ -43,5 +43,28 @@ namespace Mercados.Infrastructure.Persistence.Repositories | ||||
|  | ||||
|             return await connection.QueryAsync<CotizacionGanado>(sql); | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias) | ||||
|         { | ||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||
|  | ||||
|             const string sql = @" | ||||
|                 SELECT  | ||||
|                     * | ||||
|                 FROM  | ||||
|                     CotizacionesGanado | ||||
|                 WHERE | ||||
|                     Categoria = @Categoria | ||||
|                     AND Especificaciones = @Especificaciones | ||||
|                     AND FechaRegistro >= DATEADD(day, -@Dias, GETUTCDATE()) | ||||
|                 ORDER BY | ||||
|                     FechaRegistro ASC;"; | ||||
|  | ||||
|             return await connection.QueryAsync<CotizacionGanado>(sql, new {  | ||||
|                 Categoria = categoria,  | ||||
|                 Especificaciones = especificaciones,  | ||||
|                 Dias = dias  | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -44,5 +44,23 @@ namespace Mercados.Infrastructure.Persistence.Repositories | ||||
|  | ||||
|             return await connection.QueryAsync<CotizacionGrano>(sql); | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias) | ||||
|         { | ||||
|             using IDbConnection connection = _connectionFactory.CreateConnection(); | ||||
|  | ||||
|             const string sql = @" | ||||
|                 SELECT  | ||||
|                     * | ||||
|                 FROM  | ||||
|                     CotizacionesGranos | ||||
|                 WHERE | ||||
|                     Nombre = @Nombre | ||||
|                     AND FechaRegistro >= DATEADD(day, -@Dias, GETUTCDATE()) | ||||
|                 ORDER BY | ||||
|                     FechaRegistro ASC;"; | ||||
|  | ||||
|             return await connection.QueryAsync<CotizacionGrano>(sql, new { Nombre = nombre, Dias = dias }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -6,5 +6,6 @@ namespace Mercados.Infrastructure.Persistence.Repositories | ||||
|     { | ||||
|         Task GuardarMuchosAsync(IEnumerable<CotizacionGanado> cotizaciones); | ||||
|         Task<IEnumerable<CotizacionGanado>> ObtenerUltimaTandaAsync(); | ||||
|         Task<IEnumerable<CotizacionGanado>> ObtenerHistorialAsync(string categoria, string especificaciones, int dias); | ||||
|     } | ||||
| } | ||||
| @@ -6,5 +6,6 @@ namespace Mercados.Infrastructure.Persistence.Repositories | ||||
|     { | ||||
|         Task GuardarMuchosAsync(IEnumerable<CotizacionGrano> cotizaciones); | ||||
|         Task<IEnumerable<CotizacionGrano>> ObtenerUltimasAsync(); | ||||
|         Task<IEnumerable<CotizacionGrano>> ObtenerHistorialAsync(string nombre, int dias); | ||||
|     } | ||||
| } | ||||
| @@ -11,8 +11,9 @@ namespace Mercados.Infrastructure | ||||
|  | ||||
|     public SqlConnectionFactory(IConfiguration configuration) | ||||
|     { | ||||
|       _connectionString = configuration.GetConnectionString("DefaultConnection") | ||||
|           ?? throw new ArgumentNullException(nameof(configuration), "La cadena de conexión 'DefaultConnection' no fue encontrada."); | ||||
|       // Leemos directamente de la variable de entorno | ||||
|       _connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") | ||||
|           ?? throw new ArgumentNullException(nameof(configuration), "La variable de entorno 'DB_CONNECTION_STRING' no fue encontrada."); | ||||
|     } | ||||
|  | ||||
|     public IDbConnection CreateConnection() | ||||
|   | ||||
| @@ -2,70 +2,109 @@ using Mercados.Infrastructure.DataFetchers; | ||||
|  | ||||
| namespace Mercados.Worker | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Servicio de fondo que orquesta la obtención de datos de diversas fuentes | ||||
|     /// de forma programada y periódica. | ||||
|     /// </summary> | ||||
|     public class DataFetchingService : BackgroundService | ||||
|     { | ||||
|         private readonly ILogger<DataFetchingService> _logger; | ||||
|         private readonly IServiceProvider _serviceProvider; | ||||
|         private readonly TimeZoneInfo _argentinaTimeZone; | ||||
|  | ||||
|         // Diccionario para rastrear la última vez que se ejecutó una tarea diaria. | ||||
|         // Diccionario para rastrear la última vez que se ejecutó una tarea diaria | ||||
|         // y evitar que se ejecute múltiples veces si el servicio se reinicia. | ||||
|         private readonly Dictionary<string, DateTime> _lastDailyRun = new(); | ||||
|  | ||||
|         public DataFetchingService(ILogger<DataFetchingService> logger, IServiceProvider serviceProvider) | ||||
|         { | ||||
|             _logger = logger; | ||||
|             _serviceProvider = serviceProvider; | ||||
|              | ||||
|             // Se define explícitamente la zona horaria de Argentina. | ||||
|             // Esto asegura que los cálculos de tiempo sean correctos, sin importar | ||||
|             // la configuración de zona horaria del servidor donde se ejecute el worker. | ||||
|             try | ||||
|             { | ||||
|                 // El ID estándar para Linux y macOS | ||||
|                 _argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("America/Argentina/Buenos_Aires"); | ||||
|             } | ||||
|             catch (TimeZoneNotFoundException) | ||||
|             { | ||||
|                 // El ID equivalente para Windows | ||||
|                 _argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Argentina Standard Time"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Método principal del servicio. Se ejecuta una vez cuando el servicio arranca. | ||||
|         /// </summary> | ||||
|         protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|         { | ||||
|             _logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now); | ||||
|  | ||||
|             // Ejecutamos una vez al inicio para tener datos frescos inmediatamente. | ||||
|             await RunAllFetchersAsync(); | ||||
|             // Se recomienda una ejecución inicial para poblar la base de datos inmediatamente | ||||
|             // al iniciar el servicio, en lugar de esperar al primer horario programado. | ||||
|             //await RunAllFetchersAsync(stoppingToken); | ||||
|  | ||||
|             // Usamos un PeriodicTimer que "despierta" cada minuto para revisar si hay tareas pendientes. | ||||
|             // PeriodicTimer es una forma moderna y eficiente de crear un bucle de "tic-tac" | ||||
|             // sin bloquear un hilo con Task.Delay. | ||||
|             using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1)); | ||||
|  | ||||
|             while (await timer.WaitForNextTickAsync(stoppingToken)) | ||||
|             // El bucle se ejecuta cada minuto mientras el servicio no reciba una señal de detención. | ||||
|             while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) | ||||
|             { | ||||
|                 await RunScheduledTasksAsync(); | ||||
|                 await RunScheduledTasksAsync(stoppingToken); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private async Task RunScheduledTasksAsync() | ||||
|         /// <summary> | ||||
|         /// Revisa la hora actual y ejecuta las tareas que coincidan con su horario programado. | ||||
|         /// </summary> | ||||
|         private async Task RunScheduledTasksAsync(CancellationToken stoppingToken) | ||||
|         { | ||||
|             // --- Lógica de Planificación --- | ||||
|             var now = DateTime.Now; | ||||
|             // Se obtiene la hora actual convertida a la zona horaria de Argentina. | ||||
|             var nowInArgentina = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone); | ||||
|  | ||||
|             // Tarea 1: Mercado Agroganadero (todos los días a las 11:00) | ||||
|             if (now.Hour == 11 && now.Minute == 0 && HasNotRunToday("MercadoAgroganadero")) | ||||
|             // --- Tarea 1: Mercado Agroganadero (L-V a las 11:00 AM) --- | ||||
|             if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 0 && HasNotRunToday("MercadoAgroganadero")) | ||||
|             { | ||||
|                 await RunFetcherByNameAsync("MercadoAgroganadero"); | ||||
|                 _lastDailyRun["MercadoAgroganadero"] = now.Date; | ||||
|                 await RunFetcherByNameAsync("MercadoAgroganadero", stoppingToken); | ||||
|                 _lastDailyRun["MercadoAgroganadero"] = nowInArgentina.Date; | ||||
|             } | ||||
|  | ||||
|             // Tarea 2: Granos BCR (todos los días a las 11:30) | ||||
|             if (now.Hour == 11 && now.Minute == 30 && HasNotRunToday("BCR")) | ||||
|             // --- Tarea 2: Granos BCR (L-V a las 11:30 AM) --- | ||||
|             if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 30 && HasNotRunToday("BCR")) | ||||
|             { | ||||
|                 await RunFetcherByNameAsync("BCR"); | ||||
|                 _lastDailyRun["BCR"] = now.Date; | ||||
|                 await RunFetcherByNameAsync("BCR", stoppingToken); | ||||
|                 _lastDailyRun["BCR"] = nowInArgentina.Date; | ||||
|             } | ||||
|  | ||||
|             // Tarea 3: Mercados de Bolsa (cada 10 minutos si el mercado está abierto) | ||||
|             if (IsMarketOpen(now) && now.Minute % 10 == 0) | ||||
|             // --- Tarea 3 y 4: Mercados de Bolsa (L-V, durante horario de mercado, una vez por hora) --- | ||||
|             // Se ejecutan si el mercado está abierto y si el minuto actual es exactamente 10. | ||||
|             // Esto replica la lógica de "cada hora a las y 10". | ||||
|             if (IsArgentineMarketOpen(nowInArgentina) && nowInArgentina.Minute == 10) | ||||
|             { | ||||
|                 _logger.LogInformation("Mercados abiertos. Ejecutando fetchers de bolsa."); | ||||
|                 await RunFetcherByNameAsync("Finnhub"); | ||||
|                 await RunFetcherByNameAsync("YahooFinance"); | ||||
|                 _logger.LogInformation("Hora de actualización de mercados de bolsa. Ejecutando fetchers..."); | ||||
|                  | ||||
|                 await RunFetcherByNameAsync("YahooFinance", stoppingToken); | ||||
|                 // Si Finnhub está desactivado en Program.cs, este simplemente no se encontrará y se omitirá. | ||||
|                 await RunFetcherByNameAsync("Finnhub", stoppingToken); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Esta función crea un "scope" para ejecutar un fetcher específico. | ||||
|         // Esto es crucial para que la inyección de dependencias funcione correctamente. | ||||
|         private async Task RunFetcherByNameAsync(string sourceName) | ||||
|         /// <summary> | ||||
|         /// Ejecuta un fetcher específico por su nombre. Utiliza un scope de DI para gestionar | ||||
|         /// correctamente el ciclo de vida de los servicios (como las conexiones a la BD). | ||||
|         /// </summary> | ||||
|         private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken) | ||||
|         { | ||||
|             if (stoppingToken.IsCancellationRequested) return; | ||||
|  | ||||
|             _logger.LogInformation("Intentando ejecutar fetcher: {sourceName}", sourceName); | ||||
|              | ||||
|             // Crea un "scope" de servicios. Todos los servicios "scoped" (como los repositorios) | ||||
|             // se crearán de nuevo para esta ejecución y se desecharán al final, evitando problemas. | ||||
|             using var scope = _serviceProvider.CreateScope(); | ||||
|             var fetchers = scope.ServiceProvider.GetRequiredService<IEnumerable<IDataFetcher>>(); | ||||
|             var fetcher = fetchers.FirstOrDefault(f => f.SourceName.Equals(sourceName, StringComparison.OrdinalIgnoreCase)); | ||||
| @@ -84,32 +123,42 @@ namespace Mercados.Worker | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Función de ayuda para ejecutar todos los fetchers (usada al inicio). | ||||
|         private async Task RunAllFetchersAsync() | ||||
|         /// <summary> | ||||
|         /// Ejecuta todos los fetchers al iniciar el servicio. Esto es útil para poblar | ||||
|         /// la base de datos inmediatamente al arrancar el worker. | ||||
|         /// </summary> | ||||
|         /* | ||||
|         private async Task RunAllFetchersAsync(CancellationToken stoppingToken) | ||||
|         { | ||||
|             _logger.LogInformation("Ejecutando todos los fetchers al iniciar..."); | ||||
|             using var scope = _serviceProvider.CreateScope(); | ||||
|             var fetchers = scope.ServiceProvider.GetRequiredService<IEnumerable<IDataFetcher>>(); | ||||
|             foreach (var fetcher in fetchers) | ||||
|             { | ||||
|                 await RunFetcherByNameAsync(fetcher.SourceName); | ||||
|                 if (stoppingToken.IsCancellationRequested) break; | ||||
|                 await RunFetcherByNameAsync(fetcher.SourceName, stoppingToken); | ||||
|             } | ||||
|         } | ||||
|         */ | ||||
|          | ||||
|         #region Funciones de Ayuda para la Planificación | ||||
|  | ||||
|         private bool HasNotRunToday(string taskName) | ||||
|         { | ||||
|             return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < DateTime.Now.Date; | ||||
|             // Comprueba si la tarea ya se ejecutó en la fecha actual (en zona horaria de Argentina). | ||||
|             return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone).Date; | ||||
|         } | ||||
|  | ||||
|         private bool IsMarketOpen(DateTime now) | ||||
|         private bool IsWeekDay(DateTime now) | ||||
|         { | ||||
|             // Lunes a Viernes (1 a 5, Domingo es 0) | ||||
|             if (now.DayOfWeek == DayOfWeek.Saturday || now.DayOfWeek == DayOfWeek.Sunday) | ||||
|                 return false; | ||||
|             return now.DayOfWeek >= DayOfWeek.Monday && now.DayOfWeek <= DayOfWeek.Friday; | ||||
|         } | ||||
|  | ||||
|         private bool IsArgentineMarketOpen(DateTime now) | ||||
|         { | ||||
|             if (!IsWeekDay(now)) return false; | ||||
|              | ||||
|             // Horario de mercado de 11:00 a 17:15 (hora de Argentina) | ||||
|             // Rango de 11:00 a 17:15, para asegurar la captura del cierre a las 17:10. | ||||
|             var marketOpen = new TimeSpan(11, 0, 0); | ||||
|             var marketClose = new TimeSpan(17, 15, 0); | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="DotNetEnv" Version="3.1.1" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,9 @@ using Mercados.Infrastructure.Persistence; | ||||
| using Mercados.Infrastructure.Persistence.Repositories; | ||||
| using Mercados.Worker; | ||||
|  | ||||
| // Carga las variables de entorno desde el archivo .env en la raíz de la solución. | ||||
| DotNetEnv.Env.Load(); | ||||
|  | ||||
| Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); | ||||
| // --- Configuración del Host --- | ||||
| // Esto prepara el host del servicio, permitiendo la inyección de dependencias, | ||||
|   | ||||
| @@ -7,13 +7,13 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "DefaultConnection": "Server=TECNICA3;Database=MercadosDb;User Id=mercadosuser;Password=@mercados1351@;Trusted_Connection=False;Encrypt=False;" | ||||
|     "DefaultConnection": "" | ||||
|   }, | ||||
|   "ApiKeys": { | ||||
|     "Finnhub": "cuvhr0hr01qs9e81st2gcuvhr0hr01qs9e81st30", | ||||
|     "Finnhub": "", | ||||
|     "Bcr": { | ||||
|       "Key": "D1782A51-A5FD-EF11-9445-00155D09E201", | ||||
|       "Secret": "da96378186bc5a256fa821fbe79261ec7172dec283214da0aacca41c640f80e3" | ||||
|       "Key": "", | ||||
|       "Secret": "" | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user