Aller au contenu

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_id and notif_wa_business_id (stored in tenant_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), inserts tenant (status=PENDING) + a tenant_settings row with defaults (renewal_window_days=30, default_channel=WHATSAPP) and the supplied notif_sms_sender_id / notif_wa_business_id. Returns {tenantId}.
  • TR-001 gating logic:
  • country_code != 'CI' → reject (V0 lock).
  • ncc duplicate of any non-DELETED tenant → reject (hard) — uses existsByNccActive.
  • legal_name fuzzy match (Levenshtein ≤ 3) against existing → return a soft-warn payload; persist only when confirmed=true is 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…, accept 0X XX XX XX XX and normalise), plan ∈ {STARTER,GROWTH,ENTERPRISE}, notif_sms_sender_id, notif_wa_business_id.
  • country_code must equal CI (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/nouveauTenantCreatePage (minimal). Role-gated PLATFORM_ADMIN via existing AuthBoundary. (The rich 5-step wizard + tenant list/detail console are V1.)
  • A bare minimal /admin/tenants list 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 Select is 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 +225 prefix; 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_CREATED audit emitted + tenant_settings row 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; with confirmed=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/tenants implemented; 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_CREATED audit entry emitted (best-effort, never rolls back create)
  • [ ] Tenant isolation verified (tenant_settings row written under tenant id)
  • [ ] 403 (not 404) on cross-role access
  • [ ] Unit + integration tests passing
  • [ ] No silent failure on network errors (form state preserved)