Acheteur Analytics and Account Management

Summary

The admin acheteur module provides KPI dashboards, a searchable paginated acheteur list, and individual account management actions (disable, enable, soft-delete with anonymization, and resend verification email). Phone numbers are truncated in list views for privacy. Soft-delete anonymizes PII via lib/acheteur-lifecycle.ts. All moderation actions are recorded in AcheteurAccountAction and disabledAt/deletedAt timestamps for audit purposes.

State Diagram

stateDiagram-v2
    state "Compte actif" as Active
    state "Compte désactivé" as Disabled
    state "Supprimé (anonymisé)" as SoftDeleted
    [*] --> Active: acheteur registered
    Active --> Disabled: admin disables account
    Disabled --> Active: admin enables account
    Active --> SoftDeleted: admin soft-deletes account
    Disabled --> SoftDeleted: admin soft-deletes account
    SoftDeleted --> [*]: irreversible

Steps

1. KPI Stats (Actor: admin)

GET /admin/acheteurs/stats?period=7d|30d|90d

Parallel queries return:

  • total: active acheteurs (non-deleted)
  • unverified: email not yet verified
  • newInPeriod: registered in the time window
  • avgPerDay: new registrations per day over the period
  • deletionsInPeriod: soft-deletes in the period
  • withFavorites: distinct acheteurs with at least one favorite (raw SQL)
  • withMortgage: distinct acheteurs with at least one mortgage application (raw SQL)
  • authGoogle: OAuth registrations; authEmail = total - authGoogle
  • favoritesPct, mortgagePct: engagement percentages

Outcome: KPI object for dashboard

2. List Acheteurs (Actor: admin)

GET /admin/acheteurs with query parameters:

  • page, limit: offset pagination
  • search: case-insensitive match on firstName, lastName, email; exact match on phone
  • emailVerified: boolean filter
  • authMethod: "email" or "google"
  • hasFavorites, hasMortgage: boolean filters using Prisma relation existence
  • sortBy, sortOrder: any valid column
  • includeDeleted: include soft-deleted accounts (default false)

Phone numbers are truncated in the response: "0612345678""06••••••78".

Each row includes favoritesCount, mortgageStatus (most recent application status), and account state flags (disabledAt, deletedAt, lastLoginAt).

3. Acheteur Detail (Actor: admin)

GET /admin/acheteurs/:id — full profile including:

  • All favorites (programme or lot, with name and city)
  • All mortgage applications (id, status, step, timestamps)
  • Full account action history (action, reason, performer, timestamp)

googleId is stripped from the response; authMethod is derived from it. Phone is returned untruncated in detail view.

4. Disable Account (Actor: admin)

POST /admin/acheteurs/:id/disable with { reason: string }:

  • Guards: account must exist, not already disabled, not deleted.
  • Prisma $transaction:
    1. acheteur.disabledAt = now()
    2. acheteurRefreshToken.deleteMany — revokes all active sessions
    3. acheteurAccountAction.create with action: "disabled"

Outcome: Account disabled, all sessions revoked

5. Enable Account (Actor: admin)

POST /admin/acheteurs/:id/enable with { reason: string }:

  • Guard: account must be disabled.
  • Prisma $transaction:
    1. acheteur.disabledAt = null
    2. acheteurAccountAction.create with action: "enabled"

Outcome: Account re-enabled

6. Soft Delete Account (Actor: admin)

POST /admin/acheteurs/:id/delete with { reason: string }:

  • Guard: account must not already be deleted.
  • Calls anonymizeAndSoftDelete(id, adminId, reason, log) from lib/acheteur-lifecycle.ts.

The lifecycle function anonymizes all PII fields (replaces name, email, phone with redacted values), sets deletedAt, records the deletedBy admin ID, creates a "deleted" account action, and revokes all sessions. This is irreversible.

Outcome: Account anonymized and soft-deleted (GDPR-compliant)

7. Resend Verification Email (Actor: admin)

POST /admin/acheteurs/:id/resend-verification:

  • Guards: account must exist, email must not already be verified.
  • Preserves existing emailVerifyDeadline if set (or extends by 48h if null).
  • Calls buildVerifyUrl(acheteurId, deadline) and sendEmail({ template: "acheteur-verify", ... }).

Outcome: Verification email resent, or 500 EMAIL_FAILED if sending fails

Error States

  • Acheteur not found → 404 NOT_FOUND
  • Disable already-disabled account → 400 ALREADY_DISABLED
  • Disable already-deleted account → 400 ALREADY_DELETED
  • Enable non-disabled account → 400 NOT_DISABLED
  • Delete already-deleted account → 400 ALREADY_DELETED
  • Resend verification to already-verified account → 400 ALREADY_VERIFIED
  • Email send failure → 500 EMAIL_FAILED