Aller au contenu

PRD + FSD - tenant-admin (TENANT) module

Module prefix: TENANT · Plane: Control · Version scope: V1 · Country scope V1: CI only Owner author: ANALYST · Source PRD: docs/prd/prd-v1.md · Source vision: docs/prd/prd-vision-v2-v3.md · Source blueprint: docs/architecture/platform-saas-blueprint.md §4 + §5.1 Source legal audit: docs/legal/audit/memo-conformite-CI-v2-audit.md (audité le 30/04/2026 - disposition LEGAL REVIEW REQUIRED). Mémo source : docs/legal/memo-conformite-CI-v2.md. L'audit précédent Legal-Compliance-audit.md (RE-RESEARCH NEEDED) est superseded.

Legal status discipline (memo v2) - toute obligation INCERTAIN - avocat requis bloque l'implémentation des stories qui en dépendent et impose une note [LEGAL-REVIEW] dans la spec/story. Toute obligation PRÉLIMINAIRE déclenche un avertissement + [LEGAL-REVIEW]. La conformité ne se déduit jamais d'un statut INCERTAIN. Items à surveiller pour TENANT - qualification responsable de traitement (courtier/assureur) vs. sous-traitant (plateforme) = VÉRIFIÉ (Loi 2013-450 art. 1). Désignation CPD + rapport annuel ARTCI au plus tard le 31/01 N+1 = VÉRIFIÉ (Loi 2013-450 art. 42 + Arrêté n°511/MPTIC/CAB du 11/11/2014 + Décision ARTCI 2023-0877 art. 7) → le module DOIT exposer une configuration par tenant pour l'identité du CPD et le calendrier de rapport. Durées de conservation sectorielles assurance CI = PRÉLIMINAIRE (analogie fragile avec décision GRH 2023-0877) → toute durée de rétention configurée dans le country profile CI doit porter [LEGAL-REVIEW] jusqu'à délibération ARTCI sectorielle. Le mémo memo-conformite-CI-v2 ne couvre PAS la facturation plateforme→tenant (FNE/DGI) ; cf. billing-prd.md.


V0 Envelope Scope (ajout 2026-05-21)

Lire docs/prd/prd-v0.md § 4 (ligne TENANT) et § 8 avant toute story V0. Ce PRD reste le contrat V1. Le V0 coupe FROM cette spec ; chaque sous-version V0 est additive et empile vers la cible V1.

TENANT dans le V0 (per prd-v0.md § 4 + § 8) :

Sous-version Périmètre vs V1 PRD Story IDs
[V0.0.0] Création tenant + provisioning admin cabinet via Keycloak (admin plateforme). country_code NOT NULL (default CI, peut être changé pré-launch). db_profile ∈ {SHARED, BYO}, default SHARED. notif_mode placeholder. SHARED uniquement (BYO connection string + decryption deferred). Pas d'agences. Un seul admin cabinet par tenant (invité par admin plateforme). TENANT-001
[V0.0.2] Multi-agence : admin cabinet crée des agences via UI + assigne utilisateurs. Les agences sont des sous-unités au sein du tenant (PAS des tenants séparés). Pas d'import bulk CSV pour agences (création UI seulement). TENANT-002
[V0.0.4] BYO DB provisioning live pour onboarding greenfield (NSIA production go-live). Schema identique à SHARED (Flyway migrations applicables identiquement). Datasource resolver gagne la branche BYO. Migration SHARED -> BYO pour tenant existant hors scope V0 (deferred V2). TENANT-003
[V1] Spec V1 complète telle qu'écrite ci-dessous (delegated admin, multi-admin par tenant, BYO migration path, BILL module). -

Verrouillé V0.0.0 onwards (cf. prd-v0.md § 4 ligne TENANT + § 5.1 + CLAUDE.md DB profile taxonomy) : - Schema columns présents Day 1 (no later migration on tenant-scoped tables) : tenant_id, country_code (NOT NULL, default CI), db_profile (default SHARED), agency_id nullable (utilisé à partir de v0.0.2). - Tenant resolution via datasource resolver (D1/D2) en TOUS POINTS d'accès données, dès v0.0.0. Resolver retourne SHARED v0.0.0 à v0.0.3, gagne BYO branch à v0.0.4. - Aucune assumption qui bloquerait SCHEMA (V3 mid-market) ou DATABASE (V3 large) : pas de public schema hardcodé, pas de contrainte cross-tenant en DB partagée. - Group/holding au-dessus des tenants reste un concept V2/V3 (consolidated reporting, shared admin). Pas de group_id en V0 ; nullable column added by V2 migration.

Hors V0 entièrement : - BILL (facturation plateforme->tenant, FNE pour tenants CI) : reporté entièrement à V1. V0 ship sans UI de billing ni e-invoicing. Voir banner BILL pour les détails. - Delegated admin (plusieurs admins par tenant, délégation de droits) : reporté à V1. - Migration SHARED -> BYO pour tenant existant : reporté à V2. v0.0.4 = greenfield onboarding NSIA uniquement.


Part A - Product Requirements

A.1 Executive Summary

The tenant-admin module is the control-plane backbone of every other module: it is where a brokerage cabinet (the tenant) becomes a real, scoped, billable, auditable entity in the platform. Without a tenant, no tenant_id exists, no audit log can be partitioned, no FNE invoice can be issued, no signed link can be generated, no Keycloak operator user can be created.

In V1, the module solves a narrow but critical problem:

How do we onboard a small Ivorian brokerage cabinet into Papillon Collection Solution in a single platform-admin session, with all the regulatory artifacts (CIMA agrément, NCC for FNE invoicing, signed DPA) captured before the first byte of customer data is processed, and with a clean lifecycle (PENDING → ACTIVE → SUSPENDED → DELETED) that respects 10-year fiscal retention and ARTCI data-minimization?

V1 is deliberately operator-driven (no public self-onboard, no automatic billing-driven suspension). It exists to make the multi-tenant contract real while keeping the surface small enough for two-Papillon-staff to operate.

A.2 User Personas

Persona Side Scope V1
Platform admin (PLATFORM_ADMIN) [ADMIN] Papillon staff. Creates, activates, suspends, reactivates, soft-deletes tenants. Sets plan/tier. Uploads compliance docs (DPA, agrément, NCC proof). Manages BYO-DB. Role lifecycle owned by AUTH (D-3); UI hosted here. V1
Cabinet admin (CABINET_ADMIN) [OPERATOR] The cabinet's own administrator user (one per tenant by default; first one bootstrapped by AUTH on TenantOnboarded per AUTH-ADM-04). Manages branding, agencies, and tenant-level settings within hard platform-set bounds. Agency Admin persona (V0 §7, v0.0.2+) maps to CABINET_ADMIN with agency_scope = [single agency] per AUTH D-12: no separate Keycloak role in V1. V1
Cabinet user (OPERATOR, SUPERVISOR, READ_ONLY) [OPERATOR] Day-to-day cabinet user. Out of TENANT scope: managed in AUTH/Keycloak (5-role enum locked in AUTH D-2); only consumes the tenant + agency referential from this module. V1
End customer [CUSTOMER] No direct interaction with TENANT. Sees tenant branding through customer-portal and via signed-link landings. V1
Sales rep [ADMIN] Hands signed contract to platform admin for provisioning. No separate UI in V1. A V2 sales-driven onboarding UI is explicitly out of V1 scope. V2
Group / holding admin [ADMIN] Cross-tenant role for a group operating several legal entities (multi-country). Out of V1 scope. Tenant table carries country_code from day one to keep V2 non-breaking. V2

A.3 User Stories (with Given/When/Then ACs)

TENANT-US-01 [V1] [ADMIN] - Create a new tenant

As a platform admin I want to create a new tenant by entering the cabinet's legal identity, regulatory references, primary contact, and uploading the signed DPA So that the cabinet can be activated and start using the platform with full CIMA + ARTCI + FNE compliance.

ACs: - AC-01.1 - Mandatory fields enforced: GIVEN I am on the "Nouveau cabinet" form, WHEN I submit without one of {legal name, commercial name, country_code, CIMA agrément number, NCC, postal address, primary contact name, primary contact email, primary contact phone (+225...), signed DPA PDF upload}, THEN the form rejects submission with field-level French error messages and no tenant row is created. - AC-01.2 - Country lock V1: GIVEN the form, WHEN country_code is anything other than CI, THEN the form refuses with « Pays non disponible en V1 (Côte d'Ivoire uniquement) » and logs an attempt for V2 demand tracking. - AC-01.3 - Duplicate NCC blocked: GIVEN an existing tenant with NCC = 1234567 X, WHEN I attempt to create another tenant with the same NCC, THEN creation is rejected with « Un cabinet avec ce NCC existe déjà : . Contactez le superviseur. » - AC-01.4 - Soft-warn on similar legal name: GIVEN existing tenant "Cabinet Akwaba Assurances", WHEN I submit "Cabinet Akwaba Assurance" (Levenshtein ≤ 3), THEN the UI shows a confirmation dialog « Cabinets existants similaires : Cabinet Akwaba Assurances. Continuer ? » and only proceeds on explicit confirmation.

TENANT-US-02 [V1] [ADMIN] - Provision and activate the tenant

As a platform admin I want the system to provision the tenant (DB if BYO-DB, Keycloak client, default agency, country-profile binding) automatically once I submit, with clear failure recovery So that activation is a single-click step, not a 5-tool dance.

ACs: - AC-02.1 - Default-agency auto-creation: WHEN a tenant transitions PENDING → ACTIVE, THEN exactly one agency is created with code = 'PRINCIPAL', name = <tenant.legal_name>, city = <tenant.address.city>, status = ACTIVE. - AC-02.2 - Idempotent provisioning + retry: GIVEN provisioning failed (e.g. BYO-DB connection refused, Keycloak realm timeout), WHEN platform admin clicks "Relancer le provisionnement", THEN only the steps that have not completed re-run; previously-completed steps (DB schema applied, Keycloak client created) are detected and skipped; tenant status stays PENDING with a populated last_provisioning_error until success. - AC-02.3 - TenantOnboarded emitted: WHEN tenant transitions PENDING → ACTIVE, THEN exactly one TenantOnboarded is published carrying {tenantId, countryCode, plan, primaryContact, agencyId(default)}. Re-runs on the same tenant must NOT re-emit.

TENANT-US-03 [V1] [ADMIN] - Choose isolation profile (Shared DB vs BYO-DB)

As a platform admin I want to mark a tenant as BYO-DB at creation and supply their PostgreSQL connection So that an enterprise tenant who requires their own database can be onboarded without code change.

ACs: - AC-03.1 - BYO-DB checkbox unlocks JDBC fields: GIVEN the create form, WHEN I tick "BYO-DB (clause contractuelle)", THEN fields {jdbc_url, db_username, db_password, pool_size} appear; all become required. - AC-03.2 - Connection validated before activation: WHEN the form is submitted with BYO-DB, THEN provisioning runs (a) SELECT 1 on the supplied connection, (b) Flyway migrate to baseline, (c) tenant_id-scoping smoke test (insert + read + delete on a fixture). If any step fails, tenant stays PENDING with a French failure reason. - AC-03.3 - Credentials encrypted at rest: WHEN BYO-DB credentials are stored, THEN db_password is encrypted with the platform KEK (envelope encryption); the plaintext NEVER appears in any log line, audit entry, or error message returned to the UI.

TENANT-US-04 [V1] [ADMIN] - Suspend / reactivate a tenant

As a platform admin I want to suspend a tenant (and later reactivate) with a reason captured for audit So that I can stop service immediately for a billing or regulatory reason and restore it cleanly.

ACs: - AC-04.1 - Suspend = hard freeze: WHEN I confirm suspension with a reason text and a reason code (MANUAL_BILLING, MANUAL_LEGAL, MANUAL_TENANT_REQUEST, MANUAL_OTHER), THEN within 30 seconds: (a) operator Keycloak access is revoked, (b) all signed-link verifications for that tenant return HTTP 503 with French page « Service temporairement indisponible. Contactez votre cabinet. », (c) PAY refuses any new payment initiation, (d) OP campaign sends scheduled in the future are paused. - AC-04.2 - Audit + reason persisted: WHEN suspension occurs, THEN one audit entry is written with event=TENANT_SUSPENDED, actor, reason_code, reason_text (free), timestamp, prev_status, new_status. The reason is NOT shown to the customer-facing 503 page. - AC-04.3 - Reactivation symmetric: WHEN I reactivate a SUSPENDED tenant, THEN access is restored within 30 seconds, TenantReactivated is emitted, audit entry written. Suspension/reactivation can occur N times within the 90-day grace.

TENANT-US-05 [V1] [ADMIN] - Soft-delete after grace period

As a platform admin I want a SUSPENDED tenant to soft-delete after 90 days unless reactivated So that dead tenants stop consuming attention while regulatory retention is preserved.

ACs: - AC-05.1 - 90-day grace clock: GIVEN a tenant has been SUSPENDED for ≥ 90 days continuously, WHEN the daily lifecycle job runs, THEN the tenant transitions to DELETED (soft) and a TenantSoftDeleted is emitted. The clock resets to zero on any reactivation. - AC-05.2 - Personal data + source files purged: WHEN soft-delete completes, THEN: (a) customer table rows for the tenant are deleted; (b) contract table rows are anonymized (PII fields nulled, IDs preserved for audit); (c) source ingestion files in MinIO under tenant/{tenantId}/ingestion/ are deleted; (d) hashes + metadata of those files survive in the audit log. - AC-05.3 - Fiscal + audit retained: WHEN soft-delete completes, THEN: (a) FNE invoices and payment proofs survive in MinIO under tenant/{tenantId}/billing/ for the 10-year CI fiscal retention; (b) the audit log for the tenant survives indefinitely, queryable by platform admin only via a "Cabinets supprimés" view.

TENANT-US-06 [V1] [ADMIN] - Manage compliance documents

As a platform admin I want to upload, replace, and view the regulatory documents of a tenant (DPA, CIMA agrément scan, FNE registration proof) So that the platform's compliance posture is reconstructible at any point in time.

ACs: - AC-06.1 - Versioned, immutable storage: WHEN I upload a new version of any compliance document, THEN the previous version is preserved (never deleted), the new version is stored with uploaded_at, uploader_id, SHA-256 hash; the active version pointer is updated. - AC-06.2 - DPA expiry warning: GIVEN a DPA has expires_at set and is within 30 days of expiry, WHEN the daily compliance job runs, THEN the platform-admin dashboard shows a warning banner with the tenant + days remaining. No automatic suspension in V1. - AC-06.3 - DPA missing blocks activation: GIVEN PENDING tenant with no DPA on file, WHEN platform admin clicks "Activer", THEN activation is blocked with « DPA signé manquant - uploadez le PDF avant activation ».

TENANT-US-07 [V1] [OPERATOR] - Tenant admin manages branding

As a tenant admin I want to edit my cabinet's logo, primary color, and support email So that my customers see my branding in notifications and on the customer portal.

ACs: - AC-07.1 - Allowed fields only: GIVEN I am the tenant admin, WHEN I open "Identité du cabinet", THEN editable fields are exactly {logo, primary_color (hex), support_email}; legal_name, commercial_name, NCC, agrément, country_code are visible but READ-ONLY with a tooltip « Modifiable par le support Papillon uniquement ». - AC-07.2 - Logo constraints: WHEN I upload a logo, THEN file must be PNG/SVG, ≤ 200 KB, dimensions ≥ 64×64 px. Otherwise rejected with French message stating which constraint failed. - AC-07.3 - Audit + propagation: WHEN I save, THEN one audit entry per changed field is written with old/new value, AND a TenantSettingsChanged is emitted with the changed-keys list so downstream caches can invalidate.

TENANT-US-08 [V1] [OPERATOR] - Tenant admin manages agencies

As a tenant admin I want to create, edit, and disable agencies within my tenant So that my domestic branch network is reflected in operator filters and campaign scoping.

ACs: - AC-08.1 - Required fields: WHEN I create an agency, THEN {code, name, city} are required; {address, phone, manager_name} are optional. code must be unique within the tenant (case-insensitive); duplicate is rejected with « Code agence déjà utilisé ». - AC-08.2 - Disable is soft: WHEN I disable an agency, THEN status=INACTIVE, an AgencyDisabled is emitted, no new campaigns/ingestion/operator assignments may target it, but historical contracts/payments stay tied to it. No hard delete in V1. - AC-08.3 - Cannot disable the only active agency: GIVEN the tenant has exactly one ACTIVE agency, WHEN I attempt to disable it, THEN action is refused with « Au moins une agence active est requise par cabinet. »

TENANT-US-09 [V1] [OPERATOR] - Tenant admin manages tenant-level settings

As a tenant admin I want to adjust my renewal window (X days), preferred notification channel, WhatsApp template ID, and (within hard floors) retention durations So that the platform behaves the way my cabinet operates.

ACs: - AC-09.1 - Bounded settings: GIVEN platform-set hard bounds (renewal_window_days ∈ [7, 90], min_auth_level ∈ {1} for V1, audit_retention_years ≥ 10, source_file_retention_days ≥ 90), WHEN I save a value outside the bound, THEN the form refuses with the French explanation of the bound. - AC-09.2 - Settings change emits event: WHEN any tenant setting is saved, THEN one audit entry per changed key is written AND a TenantSettingsChanged carries {tenantId, changedKeys: [...]} so OP/AUTH/NOTIF caches invalidate. - AC-09.3 - V1 lock on auth level: GIVEN V1, WHEN I attempt to set min_auth_level > 1, THEN action is refused with « Niveaux 2 et 3 disponibles en V2 ».

A.4 Non-Functional Requirements

ID Requirement
NFR-01 All TENANT screens are operator-side (admin back-office). Online-first; NOT subject to the customer-side < 200 KB initial-payload rule.
NFR-02 Page load ≤ 2 s on broadband (operator typical context); ≤ 5 s on degraded 4G fallback (admin in branch).
NFR-03 Provisioning pipeline (US-02) must complete end-to-end within 30 s under nominal conditions; failure reason surfaced within 60 s.
NFR-04 Suspend / reactivate must propagate to AUTH/CP/PAY within 30 s of platform-admin confirmation. Eventual consistency via Spring Application Events is acceptable.
NFR-05 All TENANT API endpoints validate input (bean validation) AND tenant-scope authorization before any DB read/write.
NFR-06 Compliance document storage in MinIO uses server-side encryption; bucket lifecycle rules forbid hard-delete on tenant/{tenantId}/legal/ and .../billing/.
NFR-07 French is the sole UI language in V1. All micro-copy reviewed against CIMA-area French (« cabinet », « agence », « assuré », « quittance »).
NFR-08 TENANT must function under both V1 DB profiles (Shared DB / shared schema, BYO-DB) without code path divergence past the TenantAwareDataSource.
NFR-09 Audit-log writes are best-effort-immediate but never on the request critical path: failure to write audit MUST NOT roll back the business action.
NFR-10 All money fields (plan price, retention quotas) stored as XOF integer minor units. Display: 15 000 XOF with space thousands separator, 0 decimals.

A.5 MVP Scope

IN V1: - Platform-admin internal back-office for tenant CRUD (create / view / edit-restricted / suspend / reactivate / soft-delete). - Tenant lifecycle state machine (PENDING / ACTIVE / SUSPENDED / DELETED) with 90-day grace. - BYO-DB profile selectable at creation, validated and Flyway-migrated before activation. - Compliance document store (DPA, CIMA agrément, FNE registration) versioned in MinIO. - Tenant-admin self-service for branding, agencies, and bounded tenant-level settings. - Country profile lookup (CI seeded read-only). - Domain events: TenantOnboarded, TenantSuspended, TenantReactivated, TenantSoftDeleted, AgencyDisabled, TenantSettingsChanged. - Audit entries for tenant + agency lifecycle. - Plan/tier enum field on tenant, set by platform admin, consumed by BILL.

EXPLICITLY OUT of V1 (V2/V3): - Sales-rep onboarding UI (V2). - Public self-service tenant signup (V2). - Group / holding entity above tenants (V2 - group_id column will be added then). - Foreign-subsidiary tenants (V2; non-CI country_code values). - Automatic suspension triggers - billing-overdue, termination-request workflow, regulatory kill-switch (all V2). - Tenant-admin self-serve offboarding request (V2). - Tenant-admin editing of regulatory identity fields (legal name, NCC, agrément) (V2 - needs lawyer-validated workflow). - Country-profile editor UI (V2 when adding SN/BJ/CM/GA). - Realm-per-tenant in Keycloak (V2 if a silo enterprise asks). - DB-per-tenant or schema-per-tenant isolation profiles (V2+). - Custom subdomain per tenant (<slug>.papillon.ci) (V2).

A.6 Open Questions

# Question Owner Blocks?
OQ-01 The interview deliberately did not mark "tenant identity changes" (legal name, NCC, agrément, DPA replacement) as audit-logged events. These directly affect FNE invoices and CIMA positioning. Recommend revisiting before development - the cost to add later is high. Founder + LEGAL_AUDITOR No - but creates audit gap
OQ-02 The interview deliberately did not mark "tenant settings changes" (X days, channel preference, retention overrides) as audit-logged events. Recommend at minimum logging changes to retention overrides (legal-floor sensitive). Founder No
OQ-03 Hard legal floors for retention overrides (audit_retention_years, source_file_retention_days, payment_proof_retention_years) need primary-source confirmation from the (re-researched) compliance memo. LEGAL_AUDITOR (re-research) Affects AC-09.1 floor enforcement
OQ-04 Tenant slug regeneration / collision policy: what happens if two tenants generate the same slug from similar legal names? Proposal: append a numeric suffix on collision. ANALYST + ARCHITECT No
OQ-05 Plan/tier price list - V1 ships with the plan enum field only; pricing values themselves live in BILL (out of scope here). BILL ANALYST session must confirm the plan→price mapping. BILL ANALYST No (consumer-side)
OQ-06 Whether tenant admin should receive an in-app notification (or email) 30 days before DPA expiry, in addition to the platform-admin warning (AC-06.2). Founder No

A.7 Temporarily Validated (compliance assumptions to confirm)

The legal audit Legal-Compliance-audit.md reports RE-RESEARCH NEEDED. The following V1 assumptions in this PRD are made temporarily pending re-researched primary-source confirmation:

TV-ID Assumption Re-research item
TV-01 A signed PDF Data Processing Agreement (controller=cabinet, processor=Papillon) under CI law n°2013-450 is sufficient to lawfully process assuré PII (no separate ARTCI authorization required for the platform publisher acting as sub-processor). Audit gap CRITICAL #3 (ARTCI publisher formality).
TV-02 Required tenant identity fields for FNE invoicing in CI = legal name, NCC, postal address. (No additional DGI-FNE field requirements identified.) Audit gap CRITICAL #5 (payment merchant + fiscal primary sources).
TV-03 Retention floors used in AC-09.1 (audit_retention_years ≥ 10, source_file_retention_days ≥ 90) reflect Papillon's prudent baseline; primary-source confirmation pending. Audit gap HIGH #11 (retention periods).
TV-04 The CIMA agrément number captured at onboarding is sufficient evidence that the tenant qualifies as a CIMA-licensed intermediary; no additional verification (e.g. CIMA registry lookup) required in V1. Audit gap CRITICAL #1 (Reg. 01-24 positioning) - escalation required for the broader positioning question.
TV-05 Soft-delete model (PII purge after 90-day grace, audit + invoices retained 10 years) satisfies law n°2013-450 minimization while preserving CIMA + CI fiscal evidence. Audit gap HIGH #11 + CRITICAL #2 (audit retention).

Part B - Functional Specifications

B.1 Functional Rules

Rule TR-001 - Tenant creation gating

IF country_code != 'CI'                                    THEN reject (V1 lock)
IF NCC duplicate of any existing tenant (incl. DELETED)    THEN reject (hard)
IF legal_name fuzzy-match (Levenshtein ≤ 3) any existing   THEN warn + require confirmation
IF any of {legal_name, commercial_name, country_code,
           agrement_number, ncc, address.city, address.line1,
           primary_contact.{name,email,phone}} missing     THEN reject (mandatory)
IF DPA PDF not uploaded                                    THEN reject (compliance gate)
IF byo_db = TRUE AND any of {jdbc_url, db_username,
                              db_password, pool_size} missing THEN reject (mandatory)
ELSE persist tenant with status = PENDING

Rule TR-002 - Provisioning pipeline (idempotent)

ON tenant.status = PENDING (or retry triggered):
  STEP 1: country profile binding
    IF profile_for(country_code) not found THEN fail (last_error = CP_NOT_SEEDED)
  STEP 2: data store
    IF byo_db
      IF connection test fails               THEN fail (last_error = BYO_DB_UNREACHABLE)
      IF Flyway baseline + migrate fails     THEN fail (last_error = BYO_DB_MIGRATION)
      IF tenant_id smoke test fails          THEN fail (last_error = BYO_DB_SMOKE)
    ELSE skip (shared DB)
  STEP 3: Keycloak
    IF Keycloak client for tenant exists     THEN skip (idempotent)
    ELSE create client + default roles {Admin, Operator, Viewer}
    IF Keycloak unreachable                  THEN fail (last_error = KEYCLOAK_UNREACHABLE)
  STEP 4: default agency
    IF any agency exists for tenant          THEN skip (idempotent)
    ELSE create agency(code='PRINCIPAL', name=tenant.legal_name, city=tenant.address.city)
  STEP 5: initial tenant admin user (delegated to AUTH module via TenantOnboarded)
    Note: AUTH consumes the event, creates the Keycloak user, sends activation email.
  STEP 6: status transition
    SET status = ACTIVE
    SET activated_at = now()
    PUBLISH TenantOnboarded

Rule TR-003 - Suspension propagation

ON suspend(tenant, reason_code, reason_text):
  IF tenant.status NOT IN {ACTIVE}            THEN reject (illegal transition)
  SET tenant.status = SUSPENDED
  SET tenant.suspended_at = now()
  SET tenant.suspension_reason_code = reason_code
  SET tenant.suspension_reason_text = reason_text
  WRITE audit(event=TENANT_SUSPENDED, ...)
  PUBLISH TenantSuspended  // AUTH revokes operator access; CP/PAY freeze; OP pauses scheduled

Rule TR-004 - Reactivation

ON reactivate(tenant):
  IF tenant.status NOT IN {SUSPENDED}         THEN reject
  SET tenant.status = ACTIVE
  SET tenant.suspended_at = NULL
  SET tenant.suspension_reason_code = NULL
  SET tenant.suspension_reason_text = NULL
  WRITE audit(event=TENANT_REACTIVATED, ...)
  PUBLISH TenantReactivated

Rule TR-005 - Soft-delete after 90-day grace

DAILY JOB at 02:00 Africa/Abidjan:
  FOR EACH tenant WHERE status = SUSPENDED AND suspended_at <= now() - 90 days:
    SET tenant.status = DELETED
    SET tenant.deleted_at = now()
    PUBLISH TenantSoftDeleted
    TRIGGER purge job (B.2 state diagram, transition SUSPENDED→DELETED)

Rule TR-006 - Purge on soft-delete

ON TenantSoftDeleted(tenantId):
  // PII purge
  DELETE FROM customer WHERE tenant_id = tenantId
  UPDATE contract SET assured_first_name=NULL, assured_last_name=NULL,
                      assured_phone=NULL, assured_dob=NULL, assured_email=NULL
                  WHERE tenant_id = tenantId
  // Source file purge (hashes + metadata in audit log survive)
  DELETE OBJECTS UNDER minio://papillon/tenant/{tenantId}/ingestion/
  // Retain
  KEEP minio://papillon/tenant/{tenantId}/legal/        // 10y CI fiscal + ARTCI
  KEEP minio://papillon/tenant/{tenantId}/billing/      // 10y CI fiscal
  KEEP audit_log WHERE tenant_id = tenantId             // indefinite
  WRITE audit(event=TENANT_PURGED, ...)

Rule TR-007 - Cannot disable last active agency

ON disable_agency(tenantId, agencyId):
  active_count = COUNT(agency WHERE tenant_id=tenantId AND status=ACTIVE)
  IF active_count <= 1                        THEN reject (« Au moins une agence active est requise »)
  SET agency.status = INACTIVE
  WRITE audit(event=AGENCY_DISABLED, ...)
  PUBLISH AgencyDisabled

Rule TR-008 - Tenant-admin field allowlist

Tenant-admin role can edit:
  ALLOWED: logo, primary_color, support_email,
           agencies.*,
           settings.{renewal_window_days, default_channel, whatsapp_template_id,
                     audit_retention_years_override, source_file_retention_days_override}
  FORBIDDEN: legal_name, commercial_name, country_code, agrement_number, ncc,
             postal_address, plan, byo_db_*, lifecycle status, DPA upload,
             primary_contact_*

Rule TR-009 - Settings bounds

renewal_window_days        ∈ [7, 90]            (default 30; consumed by OP per V1 PRD §15.1)
min_auth_level             ∈ {1}                (V1 lock; V2 unlocks 2)
default_channel            ∈ {WHATSAPP, SMS}    (consumed by NOTIF)
audit_retention_years      ≥ 10                 (LEGAL FLOOR - TV-03)
source_file_retention_days ≥ 90                 (LEGAL FLOOR - TV-03)
quittance_retention_years  ≥ 10                 (LEGAL FLOOR - TV-03)

B.2 State Machines

B.2.1 Tenant lifecycle

stateDiagram-v2
    [*] --> PENDING: createTenant()
    PENDING --> PENDING: retryProvisioning() / on failure
    PENDING --> ACTIVE: provisioningComplete() / emit TenantOnboarded
    ACTIVE --> SUSPENDED: suspend(reason) / emit TenantSuspended
    SUSPENDED --> ACTIVE: reactivate() / emit TenantReactivated (resets 90d grace)
    SUSPENDED --> DELETED: graceExpired (suspended_at + 90d <= now) / emit TenantSoftDeleted
    DELETED --> [*]
    note right of SUSPENDED
        Hard freeze:
        - operator Keycloak access revoked
        - signed-link verification → 503
        - PAY refuses initiation
        - OP scheduled sends paused
    end note
    note right of DELETED
        Soft-delete only.
        PII + source files purged.
        Audit + FNE invoices retained.
    end note

B.2.2 Agency lifecycle

stateDiagram-v2
    [*] --> ACTIVE: createAgency()
    ACTIVE --> ACTIVE: editAgency() / audit_log
    ACTIVE --> INACTIVE: disable() [active_count > 1] / emit AgencyDisabled
    INACTIVE --> ACTIVE: reactivate() / audit_log
    INACTIVE --> INACTIVE: editAgency() / audit_log (limited fields)
    note right of INACTIVE
        Soft only (no hard delete in V1).
        Historical data preserved.
        No new ingestion / campaigns / assignments.
    end note

B.2.3 Compliance document version chain

stateDiagram-v2
    [*] --> v1_active: upload(doc) (first version)
    v1_active --> v1_archived: upload(doc) (new version)
    v1_archived --> v1_archived: NEVER deleted (CIMA + ARTCI retention)
    v1_active --> v2_active: (auto, on new upload)
    v2_active --> vN_active: ...
    note right of v1_archived
        MinIO bucket lifecycle policy
        forbids deletion under
        tenant/{tenantId}/legal/
    end note

B.3 Decision Tables

B.3.1 Action permission matrix

Action Platform admin Tenant admin Operator Viewer
Create tenant
Edit tenant identity (name/NCC/agr.)
Edit tenant branding
Set / change plan
Suspend / reactivate tenant
Soft-delete tenant ✅ (or auto job)
Upload / replace compliance docs
Configure BYO-DB
Create / edit / disable agency ✅ (override)
Edit tenant settings (X days, etc.) ✅ (override)
View tenant + agencies

B.3.2 Tenant lifecycle action validity

Current status activate suspend reactivate softDelete (job) edit identity edit branding edit settings manage agencies
PENDING ✅ (provisioning success)
ACTIVE
SUSPENDED ✅ (after 90d) ✅ (platform-admin only)
DELETED ❌ (read-only)

B.4 Data Requirements

Aligned with V1 PRD §27 ("Cabinet" = Tenant; "Agency" = Agency).

Entity: tenant

Field Type Mandatory Notes
id UUID PK Stable opaque identifier. Used as tenant_id everywhere.
slug TEXT (unique) Derived from legal_name; suffix on collision (OQ-04).
legal_name TEXT Raison sociale.
commercial_name TEXT Nom commercial / DBA.
country_code CHAR(2) NOT NULL DEFAULT 'CI' ISO 3166-1 alpha-2. V1 lock = 'CI'. Column exists for V2 non-breaking.
group_id UUID NULL Reserved for V2. Always NULL in V1. Do NOT add unique constraints involving this.
agrement_number TEXT CIMA intermediary license.
ncc TEXT (unique within active) Numéro de Compte Contribuable. Required for FNE.
address_line1, _line2 TEXT ✅ / ❌ Postal address (FNE-required).
address_city TEXT
address_postal_code TEXT
address_country CHAR(2) Mirrors country_code in V1.
primary_contact_name TEXT
primary_contact_email TEXT Becomes initial tenant admin user.
primary_contact_phone TEXT (E.164, +225...)
support_email TEXT Editable by tenant admin. Shown in customer-portal footer.
logo_object_key TEXT (MinIO key) Editable by tenant admin.
primary_color TEXT (hex, #RRGGBB) Editable by tenant admin.
plan ENUM (STARTER, GROWTH, ENTERPRISE) Set by platform admin.
byo_db BOOLEAN NOT NULL DEFAULT FALSE
byo_db_jdbc_url TEXT when byo_db Encrypted at rest (envelope KEK).
byo_db_username TEXT when byo_db Encrypted at rest.
byo_db_password_ciphertext BYTEA when byo_db NEVER plain. NEVER logged.
byo_db_pool_size INTEGER when byo_db
status ENUM (PENDING, ACTIVE, SUSPENDED, DELETED)
last_provisioning_error TEXT NULL Surfaced on retry button.
activated_at TIMESTAMPTZ NULL
suspended_at TIMESTAMPTZ NULL Drives 90-day grace clock.
suspension_reason_code ENUM NULL MANUAL_BILLING / MANUAL_LEGAL / MANUAL_TENANT_REQUEST / MANUAL_OTHER.
suspension_reason_text TEXT NULL Free text, audit-only, never customer-facing.
deleted_at TIMESTAMPTZ NULL
created_at, updated_at TIMESTAMPTZ

Entity: agency

Field Type Mandatory Notes
id UUID PK
tenant_id UUID FK → tenant Tenant scope.
code TEXT (unique within tenant) Operator-facing short ID.
name TEXT
city TEXT
address_line1 TEXT
phone TEXT (E.164)
manager_name TEXT
status ENUM (ACTIVE, INACTIVE)
created_at, updated_at TIMESTAMPTZ

Entity: tenant_settings

Field Type Default Notes
tenant_id UUID PK FK → tenant -
renewal_window_days INTEGER 30 Consumed by OP per V1 PRD §15.1.
min_auth_level INTEGER 1 V1 locked to 1.
default_channel ENUM (WHATSAPP, SMS) WHATSAPP Consumed by NOTIF.
whatsapp_template_id TEXT NULL NULL Tenant-specific Meta-approved template.
notif_daily_cap INTEGER TBD Per-day NOTIF dispatch cap. Consumed by NOTIF (notif.daily_cap). V1 default set during onboarding.
notif_sms_sender_id TEXT NULL NULL Per-tenant SMS sender ID. Consumed by NOTIF (notif.sms_sender_id). Missing value blocks dispatch (HTTP 400) per NOTIF dispatch contract. Set during onboarding (carrier whitelisting).
notif_wa_business_id TEXT NULL NULL Per-tenant WhatsApp Business Account ID. Consumed by NOTIF (notif.wa_business_id). Missing value blocks dispatch (HTTP 400). Set during onboarding (WA Business profile).
notif_wa_consent_acknowledged_at TIMESTAMPTZ NULL NULL Optional. TBD if V2 introduces it (Round-2 ARTCI alignment).
audit_retention_years_override INTEGER NULL NULL (=10) Hard floor 10 (TR-009).
source_file_retention_days_override INTEGER NULL NULL (=90) Hard floor 90 (TR-009).
quittance_retention_years_override INTEGER NULL NULL (=10) Hard floor 10 (TR-009).
updated_at TIMESTAMPTZ now()

Activation gate: notif_sms_sender_id and notif_wa_business_id MUST be populated before TENANT can flip the tenant status from PENDING to ACTIVE. Enforced via AC-06 activation checklist (see also R-TEN-NOTIF-01 below).

R-TEN-NOTIF-01: Setting a missing notif_sms_sender_id or notif_wa_business_id during onboarding is a BLOQUANT for activation. The NOTIF dispatch contract rejects sends when either is NULL.

Entity: tenant_compliance_doc

Field Type Mandatory Notes
id UUID PK
tenant_id UUID FK
doc_type ENUM (DPA, AGREMENT, FNE_REGISTRATION, OTHER)
version INTEGER 1, 2, 3, … per (tenant, doc_type)
is_active BOOLEAN Exactly one TRUE per (tenant, doc_type).
object_key TEXT (MinIO key) tenant/{tenantId}/legal/{type}/v{n}.pdf
sha256 CHAR(64)
signed_at DATE NULL ❌ (✅ for DPA) Date on the signed document.
expires_at DATE NULL Drives 30-day expiry warning.
uploaded_by UUID (platform admin user)
uploaded_at TIMESTAMPTZ

Entity: country_profile (seed-only V1)

Field Type Mandatory Notes
country_code CHAR(2) PK V1 seed: CI. ISO 3166-1 alpha-2.
display_name_fr TEXT « Côte d'Ivoire »
currency_code CHAR(3) XOF (V1, UEMOA). XAF for CEMAC.
phone_dial_code TEXT +225
payment_providers JSONB List [{code, endpoint_url, webhook_secret_ref, taxpayer_info_required, ...}, ...]. Consumed by PAY. Seed CI: ORANGE_MONEY_CI, WAVE_CI. taxpayer_info_required=true for CM/GA.
notif_sms_providers JSONB List [{code, endpoint_url, ...}, ...]. Consumed by NOTIF. Seed CI: ORANGE_SMS_CI.
notif_wa_providers JSONB List [{code, endpoint_url, ...}, ...]. V1: WhatsApp Cloud API only.
e_invoicing_provider ENUM FNE_CI (V1). SN (FNE_SN_EQUIV) / BJ (FNE_BJ_EQUIV) declared but unseeded V1; consumed by BILL.
e_invoicing_endpoint TEXT NULL Endpoint URL for the country's mandated e-invoicing system. Consumed by BILL.
data_authority TEXT ARTCI (CI). CDP (SN), APDP (BJ) per CLAUDE.md.
cima_variation_ref TEXT NULL Placeholder for national variation of CIMA Reg. 01-24 application.
locale TEXT fr-CI (V1). ISO 639-1 + country.
working_calendar JSONB Holidays + working-week. May be NULL in V1.

B.5 Multi-Tenant Implications

  • TENANT is the only module whose primary entity is NOT itself tenant-scoped at row level: the tenant table IS the tenant directory. It lives in the platform schema (shared DB) and MUST not be partitioned by tenant_id (chicken-and-egg).
  • All other entities in this module (agency, tenant_settings, tenant_compliance_doc) ARE tenant-scoped: every query MUST include tenant_id per CLAUDE.md rule #1, REVIEWER will block merges otherwise.
  • BYO-DB applicability: a BYO-DB tenant's agency, tenant_settings, tenant_compliance_doc rows live in their DB; the tenant row itself stays in the platform DB (the routing decision needs the row to exist before routing can resolve).
  • Cross-tenant queries (platform-admin "list all tenants", "list deleted tenants") run in a privileged context: the TenantContext is bypassed for the tenant table only and ONLY for users with the PlatformAdmin Keycloak role. All such queries write a PLATFORM_ADMIN_QUERY audit entry with the SQL identifier executed.
  • Both V1 DB profiles supported without code-path divergence past TenantAwareDataSource (NFR-08). The TenantOnboardingService is the single place that knows about the byo_db flag.
  • V2 forward-compatibility: country_code exists from day one; group_id column added now (NULL); no UNIQUE constraint involving legal_name or slug extends across country_code or group_id (so a V2 group can hold two same-named subsidiaries in different countries).

B.6 Cross-Module Dependencies

Events PUBLISHED by TENANT

Event Payload Consumers
TenantOnboarded {tenantId, countryCode, plan, primaryContact, defaultAgencyId, byoDb} BILL (seed first invoice cycle), AUTH (create initial Keycloak admin user), NOTIF (send activation email), AUDIT (mirror)
TenantSuspended {tenantId, reasonCode, suspendedAt} AUTH (revoke operator sessions + tokens), CP (signed-link verifier returns 503), PAY (refuse new initiations), OP (pause scheduled campaigns), AUDIT
TenantReactivated {tenantId, reactivatedAt} Same consumers as above (restore)
TenantSoftDeleted {tenantId, deletedAt} ING (purge source files), CP/customer module (purge PII), AUDIT
AgencyDisabled {tenantId, agencyId, disabledAt} OP (filter from campaign UI), ING (refuse new files for that agency), AUTH (block new operator assignments), AUDIT
TenantSettingsChanged {tenantId, changedKeys: [...]} OP (reload renewal_window_days), AUTH (re-evaluate min_auth_level), NOTIF (flush channel cache), AUDIT

Services CONSUMED by TENANT

Service From Purpose
KeycloakAdminClient AUTH Create tenant client + roles during provisioning (US-02 Step 3).
MinioObjectStore shared infra Store compliance docs + tenant logo.
AuditService.log(...) AUDIT Write all TENANT audit entries.
CountryProfileService.lookup(cc) this module's own service Seeded read, also called by PAY/BILL/NOTIF.
Encryptor.envelope(...) shared infra Wrap BYO-DB credentials.

REST API surface (control-plane, all under /api/control/tenants/...)

Verb Path Caller Purpose
POST /api/control/tenants Platform admin Create (PENDING).
POST /api/control/tenants/{id}/provision Platform admin Trigger / retry provisioning.
GET /api/control/tenants?status=...&q=... Platform admin List (cross-tenant, audited).
GET /api/control/tenants/{id} Platform admin Detail.
PATCH /api/control/tenants/{id}/identity Platform admin Edit name / NCC / agrément (audited).
POST /api/control/tenants/{id}/suspend Platform admin Body: {reasonCode, reasonText}.
POST /api/control/tenants/{id}/reactivate Platform admin
POST /api/control/tenants/{id}/compliance-docs Platform admin Multipart upload.
GET /api/control/tenants/{id}/compliance-docs Platform admin List versions.
GET /api/tenant/me Tenant admin Read own tenant + settings.
PATCH /api/tenant/me/branding Tenant admin Logo, color, support_email.
PATCH /api/tenant/me/settings Tenant admin Bounded settings (TR-008, TR-009).
GET /api/tenant/me/agencies Tenant admin / Operator List own agencies.
POST /api/tenant/me/agencies Tenant admin Create.
PATCH /api/tenant/me/agencies/{agencyId} Tenant admin Edit.
POST /api/tenant/me/agencies/{agencyId}/disable Tenant admin Soft-disable (TR-007 enforced).
POST /api/tenant/me/agencies/{agencyId}/reactivate Tenant admin

All endpoints validate input + verify Keycloak role + (for /tenant/me/...) verify TenantContext matches the user's tenant.


Part C - Constraints & Boundaries

C.1 Regulatory Compliance

CIMA Reg. 01-24 (digital distribution / management of insurance contracts). Tenant-admin contributes by: - Capturing the CIMA agrément number at onboarding (TV-04) so the platform can document, at any moment, that every active tenant is a licensed intermediary. This defends the "technical service provider, not insurer/broker" positioning held throughout the product. - Storing the signed contract / agrément scan in MinIO under tenant/{tenantId}/legal/agrement/, immutably versioned. - Surfacing tenant identity (legal name, agrément, support contact) so downstream modules (CP, NOTIF) can include the regulator-required information on customer-facing surfaces.

CI law n°2013-450 + ARTCI. - Mandatory DPA upload at onboarding (US-01, AC-06.3). Stored, hashed, versioned. Platform cannot activate a tenant without it. - Sub-processing chain is documented per tenant: TENANT references Papillon's master sub-processor list (Twilio, Meta WhatsApp Cloud API, mobile-money providers), available to the tenant admin via a static page (« Sous-traitants ») linked from the tenant settings screen. - Data-minimization on soft-delete: TR-006 purges customer PII and source files; only audit + invoices are retained, which is the legal-evidence carve-out. - Data-subject rights (access / rectification / erasure on request) are NOT implemented as a tenant-admin self-service in V1; they are handled via the Papillon support email and surfaced on the tenant compliance page. (V2 candidate.)

Country-scoped fiscal e-invoicing (FNE for CI). - NCC + postal address captured at onboarding so that BILL can issue compliant FNE invoices from the very first billing cycle (per blueprint Risk #8: "no later" for CI tenants). - TENANT is the source of truth; BILL reads the tenant identity and country profile.

C.2 Localization

  • Primary language: French (CIMA-area). All micro-copy, error messages, audit reason codes, and confirmation dialogs in French.
  • Currency: XOF for V1 (CI). 0 decimals, space thousands separator (15 000 XOF).
  • Dates: DD/MM/YYYY in UI; Africa/Abidjan (UTC+0, no DST) for all timestamps in scheduling jobs (90-day grace, 30-day DPA warning).
  • Phone: E.164 with +225 default; UI accepts 0X XX XX XX XX and normalizes.
  • Numbers: French formatting (decimal comma where applicable; XOF has none).

C.3 Security

  • Tenant isolation enforced via tenant_id on every query touching agency, tenant_settings, tenant_compliance_doc. Cross-tenant queries on tenant itself require platform-admin role + write a PLATFORM_ADMIN_QUERY audit entry.
  • BYO-DB credentials envelope-encrypted at rest. Never logged. Retrieval only via the TenantConfigRepository which audits each access.
  • Compliance documents encrypted at rest in MinIO (server-side encryption); object keys are unguessable; access via signed URLs valid for a short TTL (≤ 5 min) bound to the platform-admin session.
  • Logo upload: MIME-sniff + size limit (200 KB) + dimension check; SVG sanitization to strip scripts and external references.
  • TENANT-PURGE job runs in a single transaction wherever possible; PII deletion is irreversible.
  • Initial tenant admin password: AUTH module sets a one-time activation token and emails it. TENANT does not handle passwords.
  • Audit log entries for every action listed in V1 PRD §18 covered by this module:
  • file import → N/A (ING)
  • parameter change → covered when settings audit is added (OQ-02)
  • manual modification of a file → covered when identity-edit audit is added (OQ-01)
  • Tenant + agency lifecycle events covered fully.

C.4 Connectivity

TENANT is operator-side (admin back-office). The customer-side low-bandwidth/resumable rules of V1 PRD §30 do not apply. NFR targets: - Page load ≤ 2 s on broadband; ≤ 5 s on degraded 4G fallback. - Provisioning timeout 30 s nominal; long-running steps (BYO-DB Flyway migration) report progress via the retry mechanism (US-02 AC-02.2) rather than blocking the UI. - Compliance-doc upload tolerant of slow uplinks: chunked upload (5 MB chunks), resume on failure, max 25 MB per file. - The 30-day DPA expiry warning is computed by a daily job, not on page load - no perf cost on every admin session.

C.5 Out of Scope (V1)

Anchored in A.5 "EXPLICITLY OUT of V1". To prevent scope creep, the following are out of V1 even when an interlocutor "really wants them": - Public marketing-site self-onboard form. - Sales-rep onboarding UI. - Group / holding hierarchy above tenants. - Foreign subsidiaries (any non-CI tenant). - Automatic suspension based on billing arrears, compliance triggers, or tenant termination requests. - Tenant-admin self-service offboarding workflow. - Tenant-admin editing of regulatory identity fields. - Editable country profile UI (the read-only seeded model is sufficient until SN/BJ/CM/GA arrive in V2). - Per-tenant Keycloak realm. - Schema-per-tenant or DB-per-tenant isolation. - Custom subdomain per tenant. - Data-subject-rights self-service portal (access / rectification / erasure on request). - Multi-language UI (French only V1).


End of PRD+FSD - tenant-admin (TENANT) - V1 Next stage: ARCHITECT - docs/architecture/tenant-admin-arch.md, refining Part B against the locked stack and platform blueprint §4.