Retention as a scheduled job, not a policy document

A retention policy that exists only in a document is a statement of intent, and the data is still there in five years.

// one job per category, each with its own rule and its own log line
final class PurgeExpiredData
{
    public function handle(): void
    {
        $n = Order::where('completed_at', '<', now()->subYears(7))
            ->whereNull('legal_hold_at')
            ->limit(5000)
            ->delete();

        Log::info('retention.purged', ['category' => 'orders', 'rows' => $n]);
    }
}

The legal hold check is the exception that has to exist from the start, because the first time a dispute requires preserving one customer’s records is not the time to add it. Logging the count per run is what makes the policy auditable — a job that silently deletes nothing for six months because a column was renamed looks identical to one that works. Batching keeps the delete off the replication lag graph.