Files
baya-monorepo/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Scheduling/Jobs/AuditLogRetentionJob.cs
T
2026-07-13 22:52:57 +03:30

69 lines
3.1 KiB
C#

#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;
/// <summary>
/// Retention/archival policy for the append-only <c>ops.AuditLogs</c> 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: <b>financial &amp; verification</b> entity types (refunds, clawbacks, payouts,
/// payout batches, verification decisions, config changes, partner centers) keep a long window
/// (<c>audit_retention_financial_days</c>, default ~7 years); everyday operational rows keep a shorter one
/// (<c>audit_retention_general_days</c>, default ~2 years). Idempotent: a re-run simply finds nothing new to
/// delete. Runs on the <c>audit_retention_scan_cadence_hours</c> cadence.
/// </summary>
internal sealed class AuditLogRetentionJob(ILogger<AuditLogRetentionJob> 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;
/// <summary>
/// The <c>IAuditable</c> entity types whose audit rows carry money- or verification-legal weight. Their
/// <c>EntityType</c> is the CLR type name written by <c>AuditFieldInterceptor</c>. Everything else uses the
/// shorter general window.
/// </summary>
private static readonly IReadOnlyCollection<string> FinancialEntityTypes =
[
"Refund",
"NurseClawback",
"NursePayout",
"NursePayoutBatch",
"NurseVerification",
"PlatformConfig",
"PartnerCenter"
];
public string Name => "audit_log_retention";
public async ValueTask<TimeSpan> GetIntervalAsync(IServiceProvider services, CancellationToken cancellationToken)
{
var config = services.GetRequiredService<IPlatformConfig>();
var hours = await config.GetConfig<int>("audit_retention_scan_cadence_hours", cancellationToken);
return TimeSpan.FromHours(hours);
}
public async ValueTask RunAsync(IServiceProvider services, CancellationToken cancellationToken)
{
var config = services.GetRequiredService<IPlatformConfig>();
var audit = services.GetRequiredService<IAuditLogger>();
var generalDays = await config.GetConfig<int>("audit_retention_general_days", cancellationToken);
var financialDays = await config.GetConfig<int>("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);
}
}