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.