Acheteur Account Deletion

Summary

An authenticated acheteur can permanently delete their account by submitting their current password and an optional reason. The system verifies the password, then performs a RGPD-compliant soft-delete and anonymization via a shared anonymizeAndSoftDelete() helper — the same code path used by admin-initiated deletion. All personal data is overwritten, related records cascade-deleted, and an audit entry is created. A SHA-256 hash of the pre-anonymization email is sent to Slack for traceability without storing PII. The session cookie is cleared.

State Diagram

stateDiagram-v2
    state "Suppression demandée" as DeleteRequested
    state "Mot de passe invalide" as PasswordInvalid
    state "Anonymisation en cours" as Anonymizing
    state "Données anonymisées" as DataAnonymized
    state "Enregistrements liés supprimés" as RelatedRecordsDeleted
    state "Dossiers prêt anonymisés" as MortgageAppsAnonymized
    state "Audit créé" as AuditCreated
    state "Fichiers supprimés" as FilesDeleted
    state "Slack notifié" as SlackNotified
    state "Cookie effacé" as CookieCleared
    [*] --> DeleteRequested: acheteur requests account deletion
    DeleteRequested --> PasswordInvalid: password does not match
    PasswordInvalid --> [*]: 401 UNAUTHORIZED
    DeleteRequested --> Anonymizing: password verified
    Anonymizing --> DataAnonymized: acheteur row PII wiped, deletedAt set
    DataAnonymized --> RelatedRecordsDeleted: refresh tokens, favorites, mortgage docs, co-borrowers, broker assignments deleted
    RelatedRecordsDeleted --> MortgageAppsAnonymized: profileData/financialData cleared on applications
    MortgageAppsAnonymized --> AuditCreated: AcheteurAccountAction logged
    AuditCreated --> FilesDeleted: physical mortgage document files removed (best-effort)
    FilesDeleted --> SlackNotified: SHA-256 email hash posted to Slack (fire-and-forget)
    SlackNotified --> CookieCleared: acheteurRefreshToken cookie expired
    CookieCleared --> [*]: 200 returned, account deleted

Steps

1. Submit Deletion Request (Actor: acheteur)

Client sends DELETE /acheteur/profile with { password, reason? }.

Rate limit: 10 requests per minute, keyed on acheteurId.

Outcome: Request reaches the handler.

2. Password Verification (Actor: system)

The acheteur row is fetched. If not found → 404 NOT_FOUND. The submitted password is verified against passwordHash. Google-only accounts (passwordHash: null) → 401 UNAUTHORIZED (they cannot self-delete via this route as-is).

A SHA-256 hash of acheteur.email is computed before anonymization wipes it — used for the Slack notification.

Outcome: emailHash computed, acheteur identity confirmed.

3. Anonymize & Soft-Delete (Actor: system)

anonymizeAndSoftDelete(acheteurId, null, reason ?? "Demande de l'utilisateur", logger) is called. This is the RGPD invariant — diverging from this shared helper is a data-leak risk.

Inside a single Prisma transaction:

  • acheteur row: firstName → "", lastName/phone/passwordHash/googleId → null, email → "deleted-<uuid>@removed.local", deletedAt and deletedBy: "self" set.
  • acheteurRefreshToken rows deleted.
  • favorite rows deleted.
  • mortgageDocument rows deleted.
  • coBorrower rows deleted.
  • brokerAssignment rows deleted.
  • mortgageApplication rows: profileData and financialData cleared to {}.
  • AcheteurAccountAction audit entry created: action: "deleted", reason, performedBy: null (self-service).

After the transaction: physical mortgage document files are deleted from disk concurrently via Promise.allSettled (best-effort — failures are logged but do not roll back the transaction).

Outcome: Account fully anonymized, all sensitive data removed.

4. Slack Notification (Actor: system)

notifyAcheteurDeleted(emailHash, reason) is called (fire-and-forget). Errors are logged but do not affect the response. The hash allows internal identification without exposing PII in Slack.

Outcome: Slack notification posted (or silently failed).

5. Clear Session (Actor: system)

The acheteurRefreshToken cookie is overwritten with an empty value and maxAge: 0, expiring it immediately.

Outcome: 200 { data: { message: "Compte supprimé." } }.

Error States

  • Wrong password → 401 UNAUTHORIZED — “Mot de passe incorrect.”
  • Account not found (race condition) → 404 NOT_FOUND
  • Slack notification failure → logged, non-blocking
  • Physical file deletion failure → logged per file, non-blocking (transaction already committed)