fix: Restore idempotency for recommendation report jobs

**Problem:** Previous commit stubbed HasReportBeenSentAsync/MarkReportSentAsync due to Dapper AOT error, but didn't restore idempotency check/mark calls. This broke CLAUDE.md guarantee: "Each job must be replayable without side effects."

**Solution:** Implement idempotency using proven ADO pattern from GetSellDecisionsAsync:
- HasReportBeenSentAsync: SELECT COUNT from recommendation_sent_log
- MarkReportSentAsync: CREATE TABLE IF NOT EXISTS + INSERT with ON CONFLICT

**Changes:**
- RecommendationReportGenerator: Restored real idempotency logic (ADO pattern, no Dapper)
- GenerateDailyRecommendationJob: Restore idempotency check/mark calls
- GenerateWeeklyRecommendationJob: Restore idempotency check/mark calls
- GenerateMonthlyRecommendationJob: Restore idempotency check/mark calls

**Guarantees Restored:**
- Partial failure safe (Telegram succeeds, job throws → no duplicate on retry)
- Manual trigger safe (dashboard re-run → skips if already sent)
- [DisableConcurrentExecution] per CLAUDE.md blocking rule

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 15:11:11 +09:00
parent 4519fa8231
commit 9a2d939bb6
4 changed files with 61 additions and 4 deletions
@@ -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}",