feat: adaptación de los proyectos para utilizar .env y comienzo de preparación para despliegue en docker

This commit is contained in:
2025-07-03 11:44:10 -03:00
parent ab9e77fa81
commit 93b2887bd5
49 changed files with 1610 additions and 356 deletions

View File

@@ -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.");
}
}
}
}

View File

@@ -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" />

View File

@@ -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();
});

View File

@@ -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": ""
}
}
}

View File

@@ -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; }

View File

@@ -1,6 +0,0 @@
namespace Mercados.Database;
public class Class1
{
}

View File

@@ -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");
}
}
}

View File

@@ -1,6 +0,0 @@
namespace Mercados.Infrastructure;
public class Class1
{
}

View File

@@ -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();

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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,

View File

@@ -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 });
}

View File

@@ -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
});
}
}
}

View File

@@ -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 });
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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()

View File

@@ -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);

View File

@@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
</ItemGroup>

View File

@@ -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,

View File

@@ -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": ""
}
}
}