import { ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; const TOMBSTONED = "TOMBSTONED"; export interface AnonymizeNotificationsForUserInput { userId: string; } /** * Scrubs a user's Notification and audit rows on erasure: tombstones recipientUserId/occurredBy and clears payload variables and rendered content. Audit-permission authorized; idempotent and non-deleting. */ export async function run( db: Transaction, input: AnonymizeNotificationsForUserInput, ctx: CommandContext, ) { void ctx; const { userId } = input; const now = new Date(); // Anonymize Notifications: tombstone recipientUserId and scrub everything // that can embed personal data — the input payloadVars AND the rendered // subject/body/htmlBody (rendered content interpolates the same variables, // so clearing payloadVars alone would leave PII in place). Re-running on an // already-tombstoned user is a no-op because the WHERE clause matches 0 rows. const updatedNotifications = await db .updateTable("Notification") .set({ recipientUserId: TOMBSTONED, payloadVars: null, subject: TOMBSTONED, body: TOMBSTONED, htmlBody: null, updatedAt: now, }) .where("recipientUserId", "=", userId) .returningAll() .execute(); // Anonymize NotificationDeliveryAudit: replace occurredBy. Audit shell // (id, notificationId, eventType, occurredAt, errorClass) is // preserved by virtue of being absent from SET. const updatedAuditRows = await db .updateTable("NotificationDeliveryAudit") .set({ occurredBy: TOMBSTONED, updatedAt: now }) .where("occurredBy", "=", userId) .returningAll() .execute(); const notificationRows = (updatedNotifications ?? []) as readonly unknown[]; const auditRows = (updatedAuditRows ?? []) as readonly unknown[]; return ok({ notificationsAnonymized: notificationRows.length, auditRowsAnonymized: auditRows.length, }); }