Aller au contenu

Story TENANT-008: Lifecycle hardening — suspend / reactivate + 90-day soft-delete (events only)

Module: tenant-admin Slice: S5 (suspend/reactivate) + S6 event-emission part (Vertical Slice Decomposition §11) Side: [ADMIN] + [BACKEND] Version target: [V0.1.0] Priority: 8 Depends on: TENANT-004 Can develop concurrently with: TENANT-005, TENANT-007 Merge order: After TENANT-004 Estimated complexity: M PRD User Stories: TENANT-US-04, TENANT-US-05 (AC-05.1 only) Wireframe: docs/wireframes/tenant-admin/TENANT-008/ (design ref ADM-004 suspend modal + ADM-005 reactivate modal)


Objective

Production-hardening of the tenant lifecycle (the V0.1.0 milestone). A platform admin can suspend a tenant (with an audited reason) and later reactivate it; the lifecycle daily job soft-deletes a tenant that has been SUSPENDED for ≥ 90 days. This story owns the emission of TenantSuspended, TenantReactivated, and TenantSoftDeleted; the consumption side (AUTH revoking sessions, CP returning 503, PAY/OP freezing/pausing) lives in those modules (per arch §0.1, covered by OP-005 + AUTH-006). The actual PII purge on soft-delete is deferred to V1 (arch §0.3) — V0.1.0 emits TenantSoftDeleted but does not purge.


Backend Scope

Entities

Uses tenant (status, suspended_at, suspension_reason_code/text, deleted_at) from TENANT-002. Optional tenant_lifecycle_audit reversal record (arch §9.2): prior state, new state, actor, reason — to allow safe manual reversal through the service (never raw SQL).

Migrations

  • (Optional) db/migration/platform/V4__tenant_lifecycle_audit.sql if the reversal record is implemented.

Service Layer

  • TenantLifecycleService.suspend(tenantId, reasonCode, reasonText) (TR-003): allowed only from ACTIVE; set status=SUSPENDED, suspended_at=now(), reason fields; write TENANT_SUSPENDED audit; publish TenantSuspended AFTER_COMMIT (outbox-persisted). Illegal transition → reject.
  • reactivate(tenantId) (TR-004): allowed only from SUSPENDED; set status=ACTIVE, clear suspended_at + reason; write TENANT_REACTIVATED audit; publish TenantReactivated. The 90-day grace clock resets on reactivation. Suspend/reactivate may occur N times within the grace.
  • TenantLifecycleJob (@Scheduled, 02:00 Africa/Abidjan, TR-005): SELECT id FROM tenant WHERE status='SUSPENDED' AND suspended_at <= now() - 90d (indexed scan on (status, suspended_at)); for each, set status=DELETED, deleted_at=now(), publish TenantSoftDeleted (persisted in spring-modulith-events-jdbc). Each transition in its own transaction.
  • Events (…controlplane.tenant.events): TenantSuspended{tenantId, reasonCode, suspendedAt}, TenantReactivated{tenantId, reactivatedAt}, TenantSoftDeleted{tenantId, deletedAt}.
  • Lifecycle transitions are gated by this service (no raw SQL UPDATE tenant SET status) — CI test forbids it (Technical Risk R7).
  • Cache invalidation: publish triggers invalidation of TenantConfigCache entries for the tenant.

API Endpoints

Method Path Request Response Auth
POST /api/control/tenants/{id}/suspend {reasonCode, reasonText?} 200 · 409 illegal transition papillon-platform-admin
POST /api/control/tenants/{id}/reactivate 200 · 409 illegal transition papillon-platform-admin

reasonCode ∈ {MANUAL_BILLING, MANUAL_LEGAL, MANUAL_TENANT_REQUEST, MANUAL_OTHER}.

Validation Rules

  • suspend only from ACTIVE; reactivate only from SUSPENDED (B.3.2 lifecycle validity).
  • reasonCode required and from the enum; reasonText optional, audit-only, NEVER customer-facing.

Multi-Tenant Considerations

Lifecycle operates on the platform tenant row (the directory). Propagation to other modules is via events carrying tenantId (async, AFTER_COMMIT, outbox). Eventual consistency target ≤ 30 s (NFR-04). The daily job is bounded by N(SUSPENDED ≥ 90d).

Audit & Logging

  • TENANT_SUSPENDED (actor, reason_code, reason_text, prev_status, new_status), TENANT_REACTIVATED, TENANT_SOFT_DELETED. Best-effort via outbox (NFR-09); a regulator-relevant event must not be lost (Technical Risk R9 — outbox queue, alert on publication failure).

Frontend Scope

Pages & Routes

Adds suspend/reactivate actions to the minimal tenant detail (/admin/tenants/:id) via two modals (design ADM-004 / ADM-005).

Components

Reuse Modal, radio group, Button (danger / success variants), InlineBanner, Badge.

UI States (ALL REQUIRED)

State Behavior French Copy
Loading Confirm button → loading; modal stays open during API call
Empty N/A
Error InlineBanner in modal; button re-enabled « Impossible de suspendre. Réessayez. »
Offline / low network Confirm blocked « Connexion requise pour cette action. »
Success Modal closes; toast; detail badge updates « Cabinet suspendu. » / « Cabinet réactivé. »

Responsive Behavior

Modal 560 px desktop, 100 % viewport minus 32 px on mobile, centered.

Connectivity Behavior (CUSTOMER stories only)

N/A — admin-side.

Interactions

  • Suspend modal (design D4): danger warning banner « ⛔ Cette action révoque immédiatement l'accès de tous les opérateurs du cabinet et bloque les nouveaux paiements. »; reason radios « Facturation impayée » / « Raison légale ou réglementaire » / « Demande du cabinet » / « Autre »; optional detail textarea; confirm « ⛔ Confirmer la suspension ». No "type the name to confirm".
  • Reactivate modal: info « ✅ La réactivation rétablira l'accès des opérateurs et relancera les campagnes planifiées. »; shows « Suspendu depuis : {date} ({n} jours) · Motif : {reason} »; confirm « ✅ Réactiver le cabinet ».
  • SUSPENDED tenant banner (all detail tabs): « ⚠ Ce cabinet est suspendu depuis le {date}. Sans réactivation, il sera supprimé le {date+90j}. Les données d'audit et factures seront conservées 10 ans. Les données personnelles des assurés seront supprimées. »

Acceptance Criteria

AC-04.1: Suspend = hard freeze (emission side)
When I confirm suspension with a reason text and a reason code (MANUAL_BILLING / MANUAL_LEGAL / MANUAL_TENANT_REQUEST / MANUAL_OTHER)
Then tenant.status becomes SUSPENDED and TenantSuspended is published within the AFTER_COMMIT phase
And consumers act within 30 s (NFR-04): AUTH revokes operator access, signed-link verification returns 503, PAY refuses new initiations, OP pauses scheduled sends

Consumer behaviour (AUTH/CP/PAY/OP) is implemented in those modules (OP-005 + AUTH-006). TENANT verifies it emits the event with the correct payload; cross-module propagation is asserted by integration tests where those listeners exist.

AC-04.2: Audit + reason persisted
When suspension occurs
Then one audit entry is written with event=TENANT_SUSPENDED, actor, reason_code, reason_text, timestamp, prev_status, new_status
And the reason is NOT shown on the customer-facing 503 page
AC-04.3: Reactivation symmetric
When I reactivate a SUSPENDED tenant
Then status returns to ACTIVE within 30 s, TenantReactivated is emitted, an audit entry is written
And the 90-day grace clock resets; suspend/reactivate can occur N times within the grace
AC-05.1: 90-day grace clock + soft-delete event
Given a tenant has been SUSPENDED for >= 90 days continuously
When the daily lifecycle job runs (02:00 Africa/Abidjan)
Then the tenant transitions to DELETED (soft), deleted_at is set, and TenantSoftDeleted is emitted (outbox-persisted)
And the clock resets to zero on any prior reactivation

AC-05.2 (PII purge) and AC-05.3 (fiscal/audit retention) are deferred to V1 (arch §0.3): V0.1.0 emits the event but performs no purge. See Out of Scope.


Compliance Rules

  • Loi 2013-450 / data-minimization: the soft-delete model (PII purge after 90-day grace; audit + invoices retained 10 years) is assumption TV-05 (PRÉLIMINAIRE, [LEGAL-REVIEW]). In V0.1.0 only the lifecycle transition + event emission ship; the purge that realises minimization is V1. Do not claim minimization compliance from the event alone.
  • Suspension reason text is audit-only and MUST NOT surface on any customer-facing page (CIMA/ARTCI hygiene).

Standards & Conventions

  • docs/standards/java-spring-guidelines.md@Scheduled, @TransactionalEventListener(AFTER_COMMIT), events past-tense in …tenant.events, Spring Modulith outbox.
  • docs/standards/api-guidelines.md — 409 on illegal transition; envelope.
  • docs/standards/multi-tenant-model.md — lifecycle propagation via events.
  • docs/standards/design-system.md — Modal, Button danger/success, Badge.
  • docs/standards/compliance-discipline.md — TV-05 [LEGAL-REVIEW].
  • docs/standards/critical-rules.md — audit on every mutation.

Testing Requirements

Unit Tests

  • Illegal transitions rejected (suspend from non-ACTIVE; reactivate from non-SUSPENDED).
  • Grace-clock reset on reactivation.

Integration Tests

  • Suspend → TenantSuspended published once (AFTER_COMMIT) + TENANT_SUSPENDED audit; reactivate symmetric.
  • Daily job soft-deletes only tenants SUSPENDED ≥ 90d; emits TenantSoftDeleted; each in its own transaction.
  • CI guard: no raw UPDATE tenant SET status outside TenantLifecycleService (Technical Risk R7).
  • Outbox persistence of lifecycle events (re-delivery on failure).

What QA Will Validate

  • Suspend/reactivate modals render all states; French copy matches ADM-004/005; SUSPENDED banner shows correct purge date (+90j); soft-delete job flips status without purging PII in V0.

Out of Scope

  • PII purge + source-file deletion on soft-delete (AC-05.2) — V1 (TR-006 purge worker).
  • Fiscal/audit retention enforcement + "Cabinets supprimés" view (AC-05.3) — V1.
  • Automatic suspension triggers (billing arrears, regulatory kill-switch) — V2.
  • Consumer-side reactions (AUTH session kill, CP 503, PAY/OP freeze) — owned by AUTH/CP/PAY/OP (OP-005 + AUTH-006).

Definition of Done

  • [ ] suspend/reactivate endpoints enforce legal transitions (409 otherwise); reason enum validated
  • [ ] TenantSuspended / TenantReactivated / TenantSoftDeleted emitted AFTER_COMMIT, outbox-persisted, once each
  • [ ] daily lifecycle job (02:00 Africa/Abidjan) soft-deletes SUSPENDED ≥ 90d; grace resets on reactivation
  • [ ] NO PII purge in V0 (explicitly deferred); event emitted only
  • [ ] suspend/reactivate modals + SUSPENDED banner render at 360/768/1024; all 5 UI states
  • [ ] French micro-copy matches ADM-004 / ADM-005 exactly
  • [ ] AC-04.1, AC-04.2, AC-04.3, AC-05.1 pass; reason never customer-facing
  • [ ] TENANT_SUSPENDED/REACTIVATED/SOFT_DELETED audit entries present
  • [ ] CI guard against raw status UPDATE; unit + integration tests passing