diff --git a/src/KArtSell.Host/Jobs/GenerateDailyRecommendationJob.cs b/src/KArtSell.Host/Jobs/GenerateDailyRecommendationJob.cs index c007b0b0..1de6500e 100644 --- a/src/KArtSell.Host/Jobs/GenerateDailyRecommendationJob.cs +++ b/src/KArtSell.Host/Jobs/GenerateDailyRecommendationJob.cs @@ -38,8 +38,16 @@ public sealed class GenerateDailyRecommendationJob 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}", diff --git a/src/KArtSell.Host/Jobs/GenerateMonthlyRecommendationJob.cs b/src/KArtSell.Host/Jobs/GenerateMonthlyRecommendationJob.cs index 07a56b36..dd22e744 100644 --- a/src/KArtSell.Host/Jobs/GenerateMonthlyRecommendationJob.cs +++ b/src/KArtSell.Host/Jobs/GenerateMonthlyRecommendationJob.cs @@ -41,8 +41,16 @@ public sealed class GenerateMonthlyRecommendationJob // Get start of current month var monthStart = new DateTime(currentDate.Year, currentDate.Month, 1); + var idempotencyKey = $"monthly:{monthStart:yyyy-MM-dd}"; + if (await reportGenerator.HasReportBeenSentAsync(idempotencyKey, cancellationToken)) + { + _logger.LogInformation("Monthly report already sent for {Month}. Skipping", monthStart.ToString("yyyy-MM")); + return; + } + var report = await reportGenerator.GenerateMonthlyRecommendationAsync(monthStart, cancellationToken); await reportGenerator.SendRecommendationReportAsync(report, cancellationToken); + await reportGenerator.MarkReportSentAsync(idempotencyKey, cancellationToken); _logger.LogInformation( "Monthly recommendation report sent. Month={Month}, Recommendations={Count}", diff --git a/src/KArtSell.Host/Jobs/GenerateWeeklyRecommendationJob.cs b/src/KArtSell.Host/Jobs/GenerateWeeklyRecommendationJob.cs index f666cdb2..cf4cc95c 100644 --- a/src/KArtSell.Host/Jobs/GenerateWeeklyRecommendationJob.cs +++ b/src/KArtSell.Host/Jobs/GenerateWeeklyRecommendationJob.cs @@ -44,8 +44,16 @@ public sealed class GenerateWeeklyRecommendationJob daysToSubtract += 7; var weekStart = currentDate.AddDays(-daysToSubtract); + var idempotencyKey = $"weekly:{weekStart:yyyy-MM-dd}"; + if (await reportGenerator.HasReportBeenSentAsync(idempotencyKey, cancellationToken)) + { + _logger.LogInformation("Weekly report already sent for week starting {Date}. Skipping", weekStart); + return; + } + var report = await reportGenerator.GenerateWeeklyRecommendationAsync(weekStart, cancellationToken); await reportGenerator.SendRecommendationReportAsync(report, cancellationToken); + await reportGenerator.MarkReportSentAsync(idempotencyKey, cancellationToken); _logger.LogInformation( "Weekly recommendation report sent. WeekStart={Date}, Recommendations={Count}", diff --git a/src/KArtSell.Host/Jobs/RecommendationReportGenerator.cs b/src/KArtSell.Host/Jobs/RecommendationReportGenerator.cs index f5b59e62..4aa53b6b 100644 --- a/src/KArtSell.Host/Jobs/RecommendationReportGenerator.cs +++ b/src/KArtSell.Host/Jobs/RecommendationReportGenerator.cs @@ -101,14 +101,47 @@ public sealed class RecommendationReportGenerator await SendTelegramMessageAsync(message, cancellationToken); } - public Task HasReportBeenSentAsync(string idempotencyKey, CancellationToken cancellationToken) + public async Task HasReportBeenSentAsync(string idempotencyKey, CancellationToken cancellationToken) { - return Task.FromResult(false); + const string sql = @" + SELECT COUNT(1) + FROM recommendation_sent_log + WHERE idempotency_key = $1"; + + await using var connection = await _connectionFactory.OpenAsync(cancellationToken); + await using var command = connection.CreateCommand(); + command.CommandText = sql; + command.Parameters.Add(command.CreateParameter()); + command.Parameters[0].Value = idempotencyKey; + + var result = await command.ExecuteScalarAsync(cancellationToken); + return (long?)result > 0; } - public Task MarkReportSentAsync(string idempotencyKey, CancellationToken cancellationToken) + public async Task MarkReportSentAsync(string idempotencyKey, CancellationToken cancellationToken) { - return Task.CompletedTask; + const string createTableSql = @" + CREATE TABLE IF NOT EXISTS recommendation_sent_log ( + id BIGSERIAL PRIMARY KEY, + idempotency_key TEXT NOT NULL UNIQUE, + sent_at TIMESTAMP NOT NULL DEFAULT NOW() + )"; + + await using var connection = await _connectionFactory.OpenAsync(cancellationToken); + await using var createCmd = connection.CreateCommand(); + createCmd.CommandText = createTableSql; + await createCmd.ExecuteNonQueryAsync(cancellationToken); + + const string insertSql = @" + INSERT INTO recommendation_sent_log (idempotency_key, sent_at) + VALUES ($1, NOW()) + ON CONFLICT (idempotency_key) DO NOTHING"; + + await using var insertCmd = connection.CreateCommand(); + insertCmd.CommandText = insertSql; + insertCmd.Parameters.Add(insertCmd.CreateParameter()); + insertCmd.Parameters[0].Value = idempotencyKey; + await insertCmd.ExecuteNonQueryAsync(cancellationToken); } private async Task> GetSellDecisionsAsync(