#nullable enable using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Baya.Application.Contracts.Audit; using Baya.Application.Contracts.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Baya.Infrastructure.Persistence.Services.Scheduling.Jobs; /// /// Retention/archival policy for the append-only ops.AuditLogs table (refinement-phase-9 ยง9.4). The audit /// trail is never pruned by the application except here, on a schedule, and even then a two-tier policy protects /// the legally-sensitive rows: financial & verification entity types (refunds, clawbacks, payouts, /// payout batches, verification decisions, config changes, partner centers) keep a long window /// (audit_retention_financial_days, default ~7 years); everyday operational rows keep a shorter one /// (audit_retention_general_days, default ~2 years). Idempotent: a re-run simply finds nothing new to /// delete. Runs on the audit_retention_scan_cadence_hours cadence. /// internal sealed class AuditLogRetentionJob(ILogger logger) : IRecurringJob { // Bound the per-run working set so a large backlog drains across successive runs without loading the whole table. private const int MaxRowsPerRun = 20_000; /// /// The IAuditable entity types whose audit rows carry money- or verification-legal weight. Their /// EntityType is the CLR type name written by AuditFieldInterceptor. Everything else uses the /// shorter general window. /// private static readonly IReadOnlyCollection FinancialEntityTypes = [ "Refund", "NurseClawback", "NursePayout", "NursePayoutBatch", "NurseVerification", "PlatformConfig", "PartnerCenter" ]; public string Name => "audit_log_retention"; public async ValueTask GetIntervalAsync(IServiceProvider services, CancellationToken cancellationToken) { var config = services.GetRequiredService(); var hours = await config.GetConfig("audit_retention_scan_cadence_hours", cancellationToken); return TimeSpan.FromHours(hours); } public async ValueTask RunAsync(IServiceProvider services, CancellationToken cancellationToken) { var config = services.GetRequiredService(); var audit = services.GetRequiredService(); var generalDays = await config.GetConfig("audit_retention_general_days", cancellationToken); var financialDays = await config.GetConfig("audit_retention_financial_days", cancellationToken); var deleted = await audit.PurgeExpiredAsync( generalDays, financialDays, FinancialEntityTypes, MaxRowsPerRun, cancellationToken); if (deleted > 0) logger.LogInformation( "Audit retention purged {Count} audit row(s) (general>{GeneralDays}d, financial>{FinancialDays}d)", deleted, generalDays, financialDays); } }