| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  | using Cronos; | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  | using Mercados.Infrastructure.DataFetchers; | 
					
						
							|  |  |  | using Mercados.Infrastructure.Services; | 
					
						
							|  |  |  | using Microsoft.Extensions.Configuration; | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  | namespace Mercados.Worker | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |     /// <summary> | 
					
						
							|  |  |  |     /// Servicio de fondo que orquesta la obtención de datos de diversas fuentes | 
					
						
							|  |  |  |     /// de forma programada y periódica. | 
					
						
							|  |  |  |     /// </summary> | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |     public class DataFetchingService : BackgroundService | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         private readonly ILogger<DataFetchingService> _logger; | 
					
						
							|  |  |  |         private readonly IServiceProvider _serviceProvider; | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         private readonly TimeZoneInfo _argentinaTimeZone; | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |          | 
					
						
							|  |  |  |         // Expresiones Cron | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |         private readonly CronExpression _agroSchedule; | 
					
						
							|  |  |  |         private readonly CronExpression _bcrSchedule; | 
					
						
							|  |  |  |         private readonly CronExpression _bolsasSchedule; | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |         private readonly CronExpression _holidaysSchedule; | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |         // Próximas ejecuciones | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |         private DateTime? _nextAgroRun; | 
					
						
							|  |  |  |         private DateTime? _nextBcrRun; | 
					
						
							|  |  |  |         private DateTime? _nextBolsasRun; | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |         private DateTime? _nextHolidaysRun; | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |         private readonly Dictionary<string, DateTime> _lastAlertSent = new(); | 
					
						
							|  |  |  |         private readonly TimeSpan _alertSilencePeriod = TimeSpan.FromHours(4); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |         // Eliminamos IHolidayService del constructor | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |         public DataFetchingService( | 
					
						
							|  |  |  |             ILogger<DataFetchingService> logger, | 
					
						
							|  |  |  |             IServiceProvider serviceProvider, | 
					
						
							|  |  |  |             IConfiguration configuration) | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         { | 
					
						
							|  |  |  |             _logger = logger; | 
					
						
							|  |  |  |             _serviceProvider = serviceProvider; | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |             // Se define explícitamente la zona horaria de Argentina. | 
					
						
							|  |  |  |             try | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 _argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("America/Argentina/Buenos_Aires"); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             catch (TimeZoneNotFoundException) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 _argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Argentina Standard Time"); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |             // Parseamos las expresiones Cron UNA SOLA VEZ, en el constructor. | 
					
						
							|  |  |  |             // Si una expresión es inválida o nula, el servicio fallará al iniciar,  | 
					
						
							|  |  |  |             // lo cual es un comportamiento deseable para alertar de una mala configuración. | 
					
						
							|  |  |  |             // El '!' le dice al compilador que confiamos que estos valores no serán nulos. | 
					
						
							|  |  |  |             _agroSchedule = CronExpression.Parse(configuration["Schedules:MercadoAgroganadero"]!); | 
					
						
							|  |  |  |             _bcrSchedule = CronExpression.Parse(configuration["Schedules:BCR"]!); | 
					
						
							|  |  |  |             _bolsasSchedule = CronExpression.Parse(configuration["Schedules:Bolsas"]!); | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |             _holidaysSchedule = CronExpression.Parse(configuration["Schedules:Holidays"]!); | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         /// <summary> | 
					
						
							|  |  |  |         /// Método principal del servicio. Se ejecuta una vez cuando el servicio arranca. | 
					
						
							|  |  |  |         /// </summary> | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         protected override async Task ExecuteAsync(CancellationToken stoppingToken) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             _logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |             // La ejecución inicial sigue comentada | 
					
						
							|  |  |  |             // await RunAllFetchersAsync(stoppingToken); | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |             // Calculamos las primeras ejecuciones programadas al arrancar. | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |             var utcNow = DateTime.UtcNow; | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |             _nextAgroRun = _agroSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone); | 
					
						
							|  |  |  |             _nextBcrRun = _bcrSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone); | 
					
						
							|  |  |  |             _nextBolsasRun = _bolsasSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone); | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |             _nextHolidaysRun = _holidaysSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone); | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |             // Usamos un PeriodicTimer que "despierta" cada 30 segundos. | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |             using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30)); | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |             while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |             { | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |                 utcNow = DateTime.UtcNow; | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |                 var nowInArgentina = TimeZoneInfo.ConvertTimeFromUtc(utcNow, _argentinaTimeZone); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // Tarea de actualización de Feriados (semanal) | 
					
						
							|  |  |  |                 if (_nextHolidaysRun.HasValue && utcNow >= _nextHolidaysRun.Value) | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     _logger.LogInformation("Ejecutando tarea semanal de actualización de feriados."); | 
					
						
							|  |  |  |                     await RunFetcherByNameAsync("Holidays", stoppingToken); | 
					
						
							|  |  |  |                     _nextHolidaysRun = _holidaysSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone); | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |                 // Tarea de Mercado Agroganadero (diaria) | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |                 if (_nextAgroRun.HasValue && utcNow >= _nextAgroRun.Value) | 
					
						
							|  |  |  |                 { | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |                     // Comprueba si NO es feriado en Argentina para ejecutar | 
					
						
							|  |  |  |                     if (!await IsMarketHolidayAsync("BA", nowInArgentina)) | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                         await RunFetcherByNameAsync("MercadoAgroganadero", stoppingToken); | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     else { _logger.LogInformation("Ejecución de MercadoAgroganadero omitida por ser feriado."); } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     // Recalcula la próxima ejecución sin importar si corrió o fue feriado | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |                     _nextAgroRun = _agroSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone); | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |                 // Tarea de Granos BCR (diaria) | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |                 if (_nextBcrRun.HasValue && utcNow >= _nextBcrRun.Value) | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |                 { | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |                     if (!await IsMarketHolidayAsync("BA", nowInArgentina)) | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                         await RunFetcherByNameAsync("BCR", stoppingToken); | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     else { _logger.LogInformation("Ejecución de BCR omitida por ser feriado."); } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |                     _nextBcrRun = _bcrSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone); | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |                 // Tarea de Bolsas (recurrente) | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |                 if (_nextBolsasRun.HasValue && utcNow >= _nextBolsasRun.Value) | 
					
						
							|  |  |  |                 { | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  |                     _logger.LogInformation("Ventana de ejecución para Bolsas detectada."); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     var bolsaTasks = new List<Task>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     // Comprueba el mercado local (Argentina) | 
					
						
							|  |  |  |                     if (!await IsMarketHolidayAsync("BA", nowInArgentina)) | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                         bolsaTasks.Add(RunFetcherByNameAsync("YahooFinance", stoppingToken)); | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     else { _logger.LogInformation("Ejecución de YahooFinance (Mercado Local) omitida por ser feriado."); } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     // Comprueba el mercado de EEUU | 
					
						
							|  |  |  |                     if (!await IsMarketHolidayAsync("US", nowInArgentina)) | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                         bolsaTasks.Add(RunFetcherByNameAsync("Finnhub", stoppingToken)); | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     else { _logger.LogInformation("Ejecución de Finnhub (Mercado EEUU) omitida por ser feriado."); } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     // Si hay alguna tarea para ejecutar, las lanza en paralelo | 
					
						
							|  |  |  |                     if (bolsaTasks.Any()) | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                         _logger.LogInformation("Iniciando {Count} fetcher(s) de bolsa en paralelo...", bolsaTasks.Count); | 
					
						
							|  |  |  |                         await Task.WhenAll(bolsaTasks); | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |                     _nextBolsasRun = _bolsasSchedule.GetNextOccurrence(utcNow, _argentinaTimeZone); | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         /// <summary> | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |         /// Ejecuta un fetcher específico por su nombre, gestionando el scope de DI y las notificaciones. | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         /// </summary> | 
					
						
							|  |  |  |         private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken) | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |             if (stoppingToken.IsCancellationRequested) return; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             _logger.LogInformation("Intentando ejecutar fetcher: {sourceName}", sourceName); | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             using var scope = _serviceProvider.CreateScope(); | 
					
						
							|  |  |  |             var fetchers = scope.ServiceProvider.GetRequiredService<IEnumerable<IDataFetcher>>(); | 
					
						
							|  |  |  |             var fetcher = fetchers.FirstOrDefault(f => f.SourceName.Equals(sourceName, StringComparison.OrdinalIgnoreCase)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (fetcher != null) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 var (success, message) = await fetcher.FetchDataAsync(); | 
					
						
							|  |  |  |                 if (!success) | 
					
						
							|  |  |  |                 { | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |                     var errorMessage = $"Falló la ejecución del fetcher {sourceName}: {message}"; | 
					
						
							|  |  |  |                     _logger.LogError(errorMessage); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     if (ShouldSendAlert(sourceName)) | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                         var notifier = scope.ServiceProvider.GetRequiredService<INotificationService>(); | 
					
						
							|  |  |  |                         await notifier.SendFailureAlertAsync($"Fallo Crítico en el Fetcher: {sourceName}", errorMessage, DateTime.UtcNow); | 
					
						
							|  |  |  |                         _lastAlertSent[sourceName] = DateTime.UtcNow; | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     else | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                         _logger.LogWarning("Fallo repetido para {sourceName}. Alerta silenciada temporalmente.", sourceName); | 
					
						
							|  |  |  |                     } | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             else | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 _logger.LogWarning("No se encontró un fetcher con el nombre: {sourceName}", sourceName); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         /// <summary> | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |         /// Ejecuta todos los fetchers en paralelo al iniciar el servicio. | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         /// </summary> | 
					
						
							|  |  |  |         private async Task RunAllFetchersAsync(CancellationToken stoppingToken) | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-07-03 12:16:04 -03:00
										 |  |  |             _logger.LogInformation("Ejecutando todos los fetchers al iniciar en paralelo..."); | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             using var scope = _serviceProvider.CreateScope(); | 
					
						
							|  |  |  |             var fetchers = scope.ServiceProvider.GetRequiredService<IEnumerable<IDataFetcher>>(); | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |             var tasks = fetchers.Select(fetcher => RunFetcherByNameAsync(fetcher.SourceName, stoppingToken)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 12:16:04 -03:00
										 |  |  |             await Task.WhenAll(tasks); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             _logger.LogInformation("Ejecución inicial de todos los fetchers completada."); | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         #region Funciones de Ayuda para la Planificación | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |         /// <summary> | 
					
						
							|  |  |  |         /// Determina si se debe enviar una alerta o si está en período de silencio. | 
					
						
							|  |  |  |         /// </summary> | 
					
						
							|  |  |  |         private bool ShouldSendAlert(string taskName) | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-07-03 15:55:48 -03:00
										 |  |  |             if (!_lastAlertSent.ContainsKey(taskName)) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 return true; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             var lastAlertTime = _lastAlertSent[taskName]; | 
					
						
							|  |  |  |             return DateTime.UtcNow.Subtract(lastAlertTime) > _alertSilencePeriod; | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         #endregion | 
					
						
							| 
									
										
										
										
											2025-07-15 11:20:28 -03:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // Creamos una única función para comprobar feriados que obtiene el servicio | 
					
						
							|  |  |  |         // desde un scope. | 
					
						
							|  |  |  |         private async Task<bool> IsMarketHolidayAsync(string marketCode, DateTime date) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             using var scope = _serviceProvider.CreateScope(); | 
					
						
							|  |  |  |             var holidayService = scope.ServiceProvider.GetRequiredService<IHolidayService>(); | 
					
						
							|  |  |  |             return await holidayService.IsMarketHolidayAsync(marketCode, date); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |     } | 
					
						
							|  |  |  | } |