| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  | using Mercados.Infrastructure.DataFetchers; | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  | using Cronos; | 
					
						
							| 
									
										
										
										
											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-03 11:57:11 -03:00
										 |  |  |         private readonly IConfiguration _configuration; | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         // 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. | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         private readonly Dictionary<string, DateTime> _lastDailyRun = new(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -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 11:57:11 -03:00
										 |  |  |             _configuration = configuration; | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |              | 
					
						
							|  |  |  |             // 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"); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											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-03 11:44:10 -03:00
										 |  |  |             // 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); | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |             // PeriodicTimer es una forma moderna y eficiente de crear un bucle de "tic-tac" | 
					
						
							|  |  |  |             // sin bloquear un hilo con Task.Delay. | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |             // El bucle se ejecuta cada minuto mientras el servicio no reciba una señal de detención. | 
					
						
							|  |  |  |             while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             { | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |                 await RunScheduledTasksAsync(stoppingToken); | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         /// <summary> | 
					
						
							|  |  |  |         /// Revisa la hora actual y ejecuta las tareas que coincidan con su horario programado. | 
					
						
							|  |  |  |         /// </summary> | 
					
						
							|  |  |  |         private async Task RunScheduledTasksAsync(CancellationToken stoppingToken) | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |             var utcNow = DateTime.UtcNow; | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |             // Obtenemos las expresiones Cron desde la configuración | 
					
						
							|  |  |  |             string? agroSchedule = _configuration["Schedules:MercadoAgroganadero"]; | 
					
						
							|  |  |  |             string? bcrSchedule = _configuration["Schedules:BCR"]; | 
					
						
							|  |  |  |             string? bolsasSchedule = _configuration["Schedules:Bolsas"]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // Comprobamos cada una antes de usarla | 
					
						
							|  |  |  |             if (!string.IsNullOrEmpty(agroSchedule)) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 await TryRunDailyTaskAsync("MercadoAgroganadero", agroSchedule, utcNow, stoppingToken); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             else | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 _logger.LogWarning("No se encontró la configuración de horario para 'MercadoAgroganadero' en appsettings.json."); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (!string.IsNullOrEmpty(bcrSchedule)) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 await TryRunDailyTaskAsync("BCR", bcrSchedule, utcNow, stoppingToken); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             else | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             { | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |                 _logger.LogWarning("No se encontró la configuración de horario para 'BCR' en appsettings.json."); | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |             if (!string.IsNullOrEmpty(bolsasSchedule)) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 await TryRunRecurringTaskAsync(new[] { "YahooFinance", "Finnhub" }, bolsasSchedule, utcNow, stoppingToken); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             else | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 _logger.LogWarning("No se encontró la configuración de horario para 'Bolsas' en appsettings.json."); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             // --- ^ FIN DE LA CORRECCIÓN DE NULABILIDAD ^ --- | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         /// <summary> | 
					
						
							|  |  |  |         /// Comprueba y ejecuta una tarea que debe correr solo una vez al día. | 
					
						
							|  |  |  |         /// </summary> | 
					
						
							|  |  |  |         private async Task TryRunDailyTaskAsync(string taskName, string cronExpression, DateTime utcNow, CancellationToken stoppingToken) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             var cron = CronExpression.Parse(cronExpression); | 
					
						
							|  |  |  |             var nextOccurrence = cron.GetNextOccurrence(utcNow.AddMinutes(-1)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (nextOccurrence.HasValue && nextOccurrence.Value <= utcNow) | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             { | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |                 if (HasNotRunToday(taskName)) | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     await RunFetcherByNameAsync(taskName, stoppingToken); | 
					
						
							|  |  |  |                     _lastDailyRun[taskName] = TimeZoneInfo.ConvertTimeFromUtc(utcNow, _argentinaTimeZone).Date; | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |         /// <summary> | 
					
						
							|  |  |  |         /// Comprueba y ejecuta una tarea que puede correr múltiples veces al día. | 
					
						
							|  |  |  |         /// </summary> | 
					
						
							|  |  |  |         private async Task TryRunRecurringTaskAsync(string[] taskNames, string cronExpression, DateTime utcNow, CancellationToken stoppingToken) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             // Añadimos 'IncludeSeconds' para que la comparación sea precisa y no se ejecute dos veces en el mismo minuto. | 
					
						
							|  |  |  |             var cron = CronExpression.Parse(cronExpression, CronFormat.IncludeSeconds); | 
					
						
							|  |  |  |             // Comprobamos si hubo una ocurrencia en el último minuto. | 
					
						
							|  |  |  |             var nextOccurrence = cron.GetNextOccurrence(utcNow.AddMinutes(-1)); | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             if (nextOccurrence.HasValue && nextOccurrence.Value <= utcNow) | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             { | 
					
						
							| 
									
										
										
										
											2025-07-03 11:57:11 -03:00
										 |  |  |                 _logger.LogInformation("Ventana de ejecución recurrente detectada para: {Tasks}", string.Join(", ", taskNames)); | 
					
						
							|  |  |  |                 foreach (var taskName in taskNames) | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     await RunFetcherByNameAsync(taskName, stoppingToken); | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         /// <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) | 
					
						
							| 
									
										
										
										
											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 11:44:10 -03:00
										 |  |  |             // 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. | 
					
						
							| 
									
										
										
										
											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) | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     _logger.LogError("Falló la ejecución del fetcher {sourceName}: {message}", sourceName, message); | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             else | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 _logger.LogWarning("No se encontró un fetcher con el nombre: {sourceName}", sourceName); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         /// <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) | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         { | 
					
						
							|  |  |  |             _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) | 
					
						
							|  |  |  |             { | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |                 if (stoppingToken.IsCancellationRequested) break; | 
					
						
							|  |  |  |                 await RunFetcherByNameAsync(fetcher.SourceName, stoppingToken); | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |         */ | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |          | 
					
						
							|  |  |  |         #region Funciones de Ayuda para la Planificación | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         private bool HasNotRunToday(string taskName) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-07-03 11:44:10 -03:00
										 |  |  |             return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone).Date; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-01 12:19:00 -03:00
										 |  |  |         #endregion | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |