PRD + FSD - tenant-admin (TENANT) module¶
Module prefix:
TENANT· Plane: Control · Version scope: V1 · Country scope V1:CIonly 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édentLegal-Compliance-audit.md(RE-RESEARCH NEEDED) est superseded.Legal status discipline (memo v2) - toute obligation
INCERTAIN - avocat requisbloque l'implémentation des stories qui en dépendent et impose une note[LEGAL-REVIEW]dans la spec/story. Toute obligationPRÉLIMINAIREdéclenche un avertissement +[LEGAL-REVIEW]. La conformité ne se déduit jamais d'un statutINCERTAIN. 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_codeNOT NULL (defaultCI, peut être changé pré-launch).db_profile∈ {SHARED,BYO}, defaultSHARED.notif_modeplaceholder.SHAREDuniquement (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 brancheBYO. MigrationSHARED -> BYOpour 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, defaultCI),db_profile(defaultSHARED),agency_idnullable (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 retourneSHAREDv0.0.0 à v0.0.3, gagneBYObranch à v0.0.4. - Aucune assumption qui bloqueraitSCHEMA(V3 mid-market) ouDATABASE(V3 large) : pas depublicschema 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 degroup_iden 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->BYOpour 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à :
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) | 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
tenanttable IS the tenant directory. It lives in the platform schema (shared DB) and MUST not be partitioned bytenant_id(chicken-and-egg). - All other entities in this module (
agency,tenant_settings,tenant_compliance_doc) ARE tenant-scoped: every query MUST includetenant_idper CLAUDE.md rule #1, REVIEWER will block merges otherwise. - BYO-DB applicability: a BYO-DB tenant's
agency,tenant_settings,tenant_compliance_docrows live in their DB; thetenantrow 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
TenantContextis bypassed for thetenanttable only and ONLY for users with thePlatformAdminKeycloak role. All such queries write aPLATFORM_ADMIN_QUERYaudit entry with the SQL identifier executed. - Both V1 DB profiles supported without code-path divergence past
TenantAwareDataSource(NFR-08). TheTenantOnboardingServiceis the single place that knows about thebyo_dbflag. - V2 forward-compatibility:
country_codeexists from day one;group_idcolumn added now (NULL); no UNIQUE constraint involvinglegal_nameorslugextends acrosscountry_codeorgroup_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/YYYYin UI;Africa/Abidjan(UTC+0, no DST) for all timestamps in scheduling jobs (90-day grace, 30-day DPA warning). - Phone: E.164 with
+225default; UI accepts0X XX XX XX XXand normalizes. - Numbers: French formatting (decimal comma where applicable; XOF has none).
C.3 Security¶
- Tenant isolation enforced via
tenant_idon every query touchingagency,tenant_settings,tenant_compliance_doc. Cross-tenant queries ontenantitself require platform-admin role + write aPLATFORM_ADMIN_QUERYaudit entry. - BYO-DB credentials envelope-encrypted at rest. Never logged. Retrieval only via the
TenantConfigRepositorywhich 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:
AUTHmodule 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.