Story TENANT-003: Tenant create (PENDING) — API + minimal platform-admin form¶
Module: tenant-admin Slice: S2 (Vertical Slice Decomposition §11) Side: [ADMIN] + [BACKEND] Version target: [V0.0.0] Priority: 3 Depends on: TENANT-002 Can develop concurrently with: None (TENANT-004 builds on the created tenant) Merge order: After TENANT-002, before TENANT-004 Estimated complexity: M PRD User Stories: TENANT-US-01 Wireframe: docs/wireframes/tenant-admin/TENANT-003/ (minimal create form; design ref ADM-002 étape 1–2)
Objective¶
Let a Papillon platform admin create a tenant (a brokerage cabinet) by entering its legal identity, contact and address. The tenant is persisted with status = PENDING; provisioning/activation is a separate step (TENANT-004). V0 ships the POST /api/control/tenants endpoint plus a minimal single-page create form (not the full V1 5-step wizard). The CI country lock, duplicate-NCC block and fuzzy-name warning are enforced here.
V0 simplification (read carefully): The V1 spec (AC-01.1) lists a signed DPA PDF upload among the mandatory create fields. Compliance-document upload is a V1 slice (out of V0 — see Out of Scope), so the V0 create form does NOT require a DPA. Instead, because real SMS/WhatsApp ship in V0.0.0 (NOTIF-001), the V0 form captures
notif_sms_sender_idandnotif_wa_business_id(stored intenant_settings); these are the V0 activation gate (checked in TENANT-004), replacing the DPA gate.
Backend Scope¶
Entities¶
Uses tenant and tenant_settings from TENANT-002. No new tables.
Migrations¶
None.
Service Layer¶
TenantOnboardingService.create(CreateTenantCommand)— validates (TR-001), generates slug (SlugGenerator), insertstenant(status=PENDING) + atenant_settingsrow with defaults (renewal_window_days=30, default_channel=WHATSAPP) and the suppliednotif_sms_sender_id/notif_wa_business_id. Returns{tenantId}.- TR-001 gating logic:
country_code != 'CI'→ reject (V0 lock).nccduplicate of any non-DELETED tenant → reject (hard) — usesexistsByNccActive.legal_namefuzzy match (Levenshtein ≤ 3) against existing → return a soft-warn payload; persist only whenconfirmed=trueis re-sent.- any mandatory field missing → reject with field-level errors.
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| POST | /api/control/tenants |
{legalName, commercialName, countryCode, agrementNumber, ncc, addressLine1, addressLine2?, addressCity, addressPostalCode?, primaryContactName, primaryContactEmail, primaryContactPhone, plan, notifSmsSenderId, notifWaBusinessId, whatsappTemplateId?, confirmed?} |
201 {data:{tenantId, status:"PENDING", slug}} · 400 field errors · 422 NCC dup · 200-with-warning on similar name |
Keycloak realm role papillon-platform-admin |
- Field-level errors: HTTP 400, body
errors:[{field, code, messageFr}]. - Duplicate NCC: HTTP 422, French message.
- CSRF via the operator console double-submit cookie.
Validation Rules¶
- Mandatory (V0 set):
legal_name,commercial_name,country_code(=CI),agrement_number,ncc,address_line1,address_city,address_country,primary_contact_name,primary_contact_email(valid email),primary_contact_phone(E.164+225…, accept0X XX XX XX XXand normalise),plan∈ {STARTER,GROWTH,ENTERPRISE},notif_sms_sender_id,notif_wa_business_id. country_codemust equalCI(TR-001) — else « Pays non disponible en V1 (Côte d'Ivoire uniquement) ».- DPA upload is NOT required in V0 (deferred to V1 compliance-docs slice).
Multi-Tenant Considerations¶
tenant is the directory table on the platform DB; create goes through PlatformAdminTenantRepository (JdbcClient), under the papillon-platform-admin sentinel context (no tenant_id claim). The tenant_settings row is tenant-scoped and written after the tenant id exists. country_code is captured per tenant (drives FNE/notif provider resolution), never per agency.
Audit & Logging¶
- Emit audit event
TENANT_CREATED(AUDIT §B.1 / arch §7.3) on successful create:actor_id,actor_role,tenant_id,target_type=TENANT,after(redacted — no secrets),ip,occurred_at,correlation_id. Best-effort-immediate (NFR-09): audit-write failure must NOT roll back the create.
Frontend Scope¶
Pages & Routes¶
/admin/tenants/nouveau→TenantCreatePage(minimal). Role-gatedPLATFORM_ADMINvia existingAuthBoundary. (The rich 5-step wizard + tenant list/detail console are V1.)- A bare minimal
/admin/tenantslist is provided by TENANT-004 (status + provision/retry); this story only needs the create form and to redirect back to it on success.
Components¶
- Reuse design-system:
Input,Select,Button,InlineBanner,Modal(for similar-name confirm),Badge. - Single scrollable form grouped into two cards: « Identité légale » and « Contact & Adresse » + a small « Notifications » block (SMS Sender ID, WA Business ID). No StepIndicator in V0 (wizard is V1).
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Submit button → loading spinner, form disabled |
— |
| Empty | Form starts empty | — |
| Error | Field error + inline message; network → Toast error, form preserved; NCC dup → field error; similar name → confirm modal |
NCC dup: « Un cabinet avec ce NCC existe déjà : {legal_name}. Contactez le superviseur. » · Pays: « Pays non disponible en V1 (Côte d'Ivoire uniquement). » · Required: « Ce champ est obligatoire. » · Email: « Adresse email invalide. » · Phone: « Numéro de téléphone invalide. Format attendu : +225 XX XX XX XX XX » · Similar: « Cabinets similaires détectés : {names}. Ces cabinets existent déjà. Continuer quand même ? » |
| Offline / low network | Banner; submit blocked (server validation required immediately) | « Connexion instable — votre saisie est conservée. » |
| Success | PENDING tenant created → toast → redirect to minimal tenant list (TENANT-004) | « Cabinet {legal_name} créé (en attente de provisionnement). » |
Responsive Behavior¶
Desktop-first (1440 → 1024 two-col field grid; 768 single col, sidebar collapsed; 360–767 single col + sticky bottom save, with the platform-admin small-screen banner « Veuillez utiliser un écran plus grand pour l'administration plateforme. »).
Connectivity Behavior (CUSTOMER stories only)¶
N/A — operator/admin-side, online-first (NFR-01). Form state preserved in React local state on connection loss; submit re-enabled on reconnect.
Interactions¶
- Pays
Selectis disabled, pinned to « Côte d'Ivoire (CI) », tooltip « V1 : Côte d'Ivoire uniquement. Sénégal, Bénin, Cameroun et autres pays CIMA disponibles en V2. » - Phone field shows locked
+225prefix; normalises local format. - On 422 similar-name warning, open confirm modal; on confirm, re-POST with
confirmed=true.
Acceptance Criteria¶
(Copied from the PRD, scoped to the V0 mandatory-field set — DPA excluded per the V0 simplification above.)
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...), SMS Sender ID, WA Business Account ID}
Then the form rejects submission with field-level French error messages and no tenant row is created
V0 note: the V1 "signed DPA PDF upload" mandatory item is replaced in V0 by the NOTIF sender-ID fields. DPA becomes mandatory again when the V1 compliance-docs slice lands.
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 non-DELETED 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à : <legal_name>. 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 similaires détectés : Cabinet Akwaba Assurances. Continuer ? » and only proceeds on explicit confirmation
Compliance Rules¶
- TV-04 / CIMA Reg. 01-24 (
PRÉLIMINAIRE,[LEGAL-REVIEW]): capture the CIMA agrément number at onboarding so the platform can document that each tenant is a licensed intermediary (defends the "technical service provider, not insurer/broker" positioning). Helper copy: « Numéro d'agrément délivré par la CRCA pour les intermédiaires. Conservé conformément à la Réglementation CIMA 01-24. » - TV-02 / FNE (
PRÉLIMINAIRE):ncc+ postal address captured for BILL (V1) FNE invoicing. Helper: « Requis pour la facturation électronique FNE. » - DPA / Loi 2013-450 art. 1 gate is deferred to V1 in V0 (no DPA capture here).
Standards & Conventions¶
docs/standards/api-guidelines.md—{data, meta?, errors?}envelope, 400 field errors, 422 business rule, never 404 for isolation.docs/standards/java-spring-guidelines.md— bean validation DTOs, package…controlplane.tenant.docs/standards/react-typescript-guidelines.md— form handling, TanStack Query mutation.docs/standards/design-system.md— Input/Select/Button/InlineBanner/Modal/Badge tokens.docs/standards/design-principles.md— error prevention, forgiving interface.docs/standards/compliance-discipline.md—[LEGAL-REVIEW]markers.docs/standards/critical-rules.md— tenant isolation, audit on mutation.
Testing Requirements¶
Unit Tests¶
- TR-001 branches: country lock, NCC dup, fuzzy-name warn, missing-field rejection.
- Phone normalisation (
0X…→+225…).
Integration Tests¶
- POST with full valid body → 201 PENDING +
TENANT_CREATEDaudit emitted +tenant_settingsrow seeded. - Non-CI country → 422; logged for V2 demand.
- Duplicate active NCC → 422 with French message; duplicate against a DELETED tenant → allowed.
- Similar name without
confirmed→ 200-with-warning; withconfirmed=true→ 201. - 403 (not 404) when caller lacks
papillon-platform-admin.
What QA Will Validate¶
- Form renders at 360/768/1024; all 5 UI states; French copy matches exactly; PENDING tenant appears for provisioning.
Out of Scope¶
- Provisioning/activation, Keycloak Org, default agency,
TenantOnboarded(TENANT-004). - DPA / compliance-document upload + versioning + DPA-expiry warning (V1).
- Full 5-step creation wizard, rich tenant list (tabs/search/DPA banner), tenant detail tabs (V1).
- BYO-DB fields on the form (TENANT-007 / V0.0.4 adds the isolation block).
- Identity edit (PATCH) (V1).
Definition of Done¶
- [ ]
POST /api/control/tenantsimplemented; 201 PENDING; field/dup/country validation per TR-001 - [ ] Minimal create form renders at 360 / 768 / 1024 px
- [ ] All 5 UI states implemented (loading, empty, error, offline/low-network, success)
- [ ] French micro-copy matches design spec exactly
- [ ] AC-01.1 (V0 field set), AC-01.2, AC-01.3, AC-01.4 pass manually
- [ ]
TENANT_CREATEDaudit entry emitted (best-effort, never rolls back create) - [ ] Tenant isolation verified (
tenant_settingsrow written under tenant id) - [ ] 403 (not 404) on cross-role access
- [ ] Unit + integration tests passing
- [ ] No silent failure on network errors (form state preserved)