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