using Hangfire; using KArtSell.BuildingBlocks.Time; using Microsoft.Extensions.Logging; namespace KArtSell.Host.Jobs; /// /// Daily recommendation report generation job. /// Runs at 09:00 KST (market open), summarizes sell decisions from previous trading day. /// Idempotent: keyed by trading date to prevent duplicate reports. /// public sealed class GenerateDailyRecommendationJob { private readonly IServiceProvider _serviceProvider; private readonly IClock _clock; private readonly ILogger _logger; public GenerateDailyRecommendationJob( IServiceProvider serviceProvider, IClock clock, ILogger logger) { _serviceProvider = serviceProvider; _clock = clock; _logger = logger; } [Queue("q-recommendation")] public async Task ExecuteAsync(CancellationToken cancellationToken) { _logger.LogInformation("DailyRecommendation job started"); try { using var scope = _serviceProvider.CreateAsyncScope(); var reportGenerator = scope.ServiceProvider.GetRequiredService(); var now = _clock.UtcNow; var reportDate = now.DateTime.Date; var idempotencyKey = $"daily:{reportDate:yyyy-MM-dd}"; if (await reportGenerator.HasReportBeenSentAsync(idempotencyKey, cancellationToken)) { _logger.LogInformation("Daily report already sent for {Date}. Skipping", reportDate); return; } var report = await reportGenerator.GenerateDailyRecommendationAsync(reportDate, cancellationToken); await reportGenerator.SendRecommendationReportAsync(report, cancellationToken); await reportGenerator.MarkReportSentAsync(idempotencyKey, cancellationToken); _logger.LogInformation( "Daily recommendation report sent. Date={Date}, Recommendations={Count}", reportDate, report.Recommendations.Count); } catch (Exception ex) { _logger.LogError(ex, "Daily recommendation job failed"); throw; } } }