/** * The permanent step of account deletion (2026-09-30). One tick finds pending * requests whose 7 days are over and calls `purge_deleted_account` for each. * The database function is all-or-nothing, so a failed purge leaves the * account exactly as it was; the attempt is counted and the next tick retries * until MAX_ATTEMPTS. Logs carry counts and codes only, never ids or emails. */ export const ACCOUNT_DELETION_MAX_ATTEMPTS = 5; export const ACCOUNT_DELETION_BATCH = 20; export const ACCOUNT_DELETION_TICK_MS = 30 * 60 * 1000; export const ACCOUNT_DELETION_FIRST_TICK_MS = 60 * 1000; export type AccountDeletionWorkerDeps = Readonly<{ now: () => Date; listDue: (nowIso: string, limit: number, maxAttempts: number) => Promise; purge: (requestId: string) => Promise<{ ok: true; status: string } | { ok: false; code: string }>; markFailed: (requestId: string, code: string) => Promise; log: (line: string) => void; }>; export type AccountDeletionTickResult = Readonly<{ due: number; completed: number; skipped: number; failed: number }>; export function purgeErrorCode(message: string | undefined): string { if (message?.includes("account_deletion_purge_incomplete")) return "purge_incomplete"; if (message?.includes("account_deletion_request_not_found")) return "request_not_found"; return "purge_failed"; } export async function runAccountDeletionTick(deps: AccountDeletionWorkerDeps): Promise { const due = await deps.listDue(deps.now().toISOString(), ACCOUNT_DELETION_BATCH, ACCOUNT_DELETION_MAX_ATTEMPTS); let completed = 0; let skipped = 0; let failed = 0; for (const requestId of due) { const result = await deps.purge(requestId); if (result.ok) { if (result.status === "completed") completed += 1; else skipped += 1; continue; } failed += 1; await deps.markFailed(requestId, result.code); } if (due.length > 0) { deps.log(`[account-deletion-worker] due=${due.length} completed=${completed} skipped=${skipped} failed=${failed}`); } return { due: due.length, completed, skipped, failed }; }