Aller au contenu

Story TENANT-004: Provisioning pipeline (shared-DB) + Keycloak Organization + TenantOnboarded

Module: tenant-admin Slice: S3a (Vertical Slice Decomposition §11, shared-DB subset) + S3c (Keycloak) Side: [ADMIN] + [BACKEND] Version target: [V0.0.0] Priority: 4 Depends on: TENANT-002, TENANT-003 Can develop concurrently with: TENANT-007 (BYO step), TENANT-008 (lifecycle) — once this merges Merge order: After TENANT-003 Estimated complexity: L PRD User Stories: TENANT-US-02 Wireframe: docs/wireframes/tenant-admin/TENANT-004/ (minimal provisioning UI; design ref ADM-003 onglet Provisionnement + ProvisioningPipeline component)


Objective

Turn a PENDING tenant into an ACTIVE one in a single, idempotent, recoverable step. The pipeline binds the country profile, creates the tenant's Keycloak Organization + client/roles, flips status to ACTIVE, and emits TenantOnboarded (consumed by AUTH to bootstrap the initial CABINET_ADMIN user, and by NOTIF/AUDIT). Step state is persisted so a retry re-runs only steps not yet DONE. V0.0.0 covers the shared-DB path only — the BYO-DB step and the default-agency step are added later (TENANT-007 / V0.0.4 and TENANT-005 / V0.0.2 respectively). A minimal admin UI shows the live 5-step stepper with per-step retry.


Backend Scope

Entities

tenant_provisioning_run (platform DB): id UUID PK · tenant_id UUID FK · step TEXT (CP_BIND|BYO_DB|KEYCLOAK|DEFAULT_AGENCY|EVENT_EMIT) · status TEXT (PENDING|RUNNING|DONE|FAILED) · last_error TEXT NULL · attempt_count INT · started_at TIMESTAMPTZ NULL · completed_at TIMESTAMPTZ NULL.

Migrations

  • db/migration/platform/V3__tenant_provisioning_run.sql — PK(id); IDX(tenant_id, step); UNIQUE(tenant_id, step) (exactly one row per (tenant,step) for idempotency).

Service Layer

  • TenantOnboardingService.provision(tenantId) — idempotent orchestrator (TR-002). Idempotency key = tenant.id. Upserts run rows {CP_BIND, BYO_DB, KEYCLOAK, DEFAULT_AGENCY, EVENT_EMIT}=PENDING, then executes in order, skipping steps already DONE:
  • CP_BIND: CountryProfileService.lookup(country_code); on not-found → FAILED last_error=CP_NOT_SEEDED, tenant stays PENDING.
  • BYO_DB: in V0.0.0 this step is a no-op DONE (skipped — shared DB). The real BYO branch is added in TENANT-007.
  • KEYCLOAK: via KeycloakProvisioningPort (thin interface, mockable). Idempotent: existsByClientId/org-exists check first; else create the tenant's Keycloak Organization (shared papillon realm, prd-v0 §5.8) + client + default roles {Admin, Operator, Viewer}; set org attributes tenant_id, country_code, group_id(null). On unreachable → FAILED last_error=KEYCLOAK_UNREACHABLE.
  • DEFAULT_AGENCY: in V0.0.0 this step is a no-op DONE (the agency table does not exist until V0.0.2). TENANT-005 turns this into a real "create PRINCIPAL agency" step (AC-02.1).
  • EVENT_EMIT: flip tenant.status=ACTIVE, set activated_at=now(), publish TenantOnboarded (AFTER_COMMIT). Re-runs MUST NOT re-emit.
  • V0 activation gate: before flipping to ACTIVE, assert tenant_settings.notif_sms_sender_id AND notif_wa_business_id are non-null (R-TEN-NOTIF-01). If missing → FAILED with a French message; do not activate. (The V1 DPA-present gate is deferred to V1.)
  • KeycloakProvisioningPort interface + a real impl (keycloak-admin-client) and a test mock.
  • TenantOnboarded record in com.altarys.papillon.pcs.controlplane.tenant.events: {tenantId, countryCode, plan, primaryContact, defaultAgencyId(nullable in V0.0.0), byoDb, occurredAt}. Dispatched via ApplicationEventPublisher; persisted in spring-modulith-events-jdbc outbox so a worker re-delivers on failure.

API Endpoints

Method Path Request Response Auth
POST /api/control/tenants/{id}/provision header Idempotency-Key: {tenantId} 200 {data:{status:"ACTIVE"}} · 422 {step, error} on step failure papillon-platform-admin
GET /api/control/tenants/{id} 200 detail incl. status, lastProvisioningError, per-step run states papillon-platform-admin
GET /api/control/tenants?status=&cursor=&limit= 200 minimal list (id, legalName, status) for the V0 admin screen; writes PLATFORM_ADMIN_QUERY audit papillon-platform-admin

The list endpoint is intentionally minimal in V0 (just enough to find a tenant and trigger provision/retry). The rich list (status tabs, search, DPA banner) and full detail tabs are V1.

Validation Rules

  • Provision allowed only when tenant.status = PENDING.
  • Idempotency-Key required on POST /provision; must equal the path tenant id.

Multi-Tenant Considerations

tenant_provisioning_run lives on the platform DB (read by the orchestrator before tenant routing is valid). The list endpoint is the only cross-tenant read in V0 TENANT — it runs through PlatformAdminTenantRepository under the papillon-platform-admin role and writes a PLATFORM_ADMIN_QUERY audit entry. Keycloak uses one shared realm with one Organization per tenant (prd-v0 §5.8); the tenant_id claim is emitted by a protocol mapper.

Audit & Logging

  • TENANT_PROVISIONED on successful PENDING→ACTIVE.
  • PLATFORM_ADMIN_QUERY on the cross-tenant list.
  • Audit best-effort-immediate via outbox (NFR-09). Prometheus-style alert when a tenant stays PENDING > 24 h (Technical Risk R3) — note for ops; instrumentation hooks land with OP-020 observability.

Frontend Scope

Pages & Routes

  • /admin/tenants — minimal TenantListPage (table: raison sociale, statut badge, action).
  • /admin/tenants/:id — minimal detail showing the ProvisioningPipeline component (live 5-step stepper) + a « Relancer » button. (Full tabbed detail page is V1.)

Components

  • New module-specific ProvisioningPipeline component (design §8.8): renders the 5 steps with per-step status icon, duration, and a retry button on the failed step.
  • Reuse Badge (status mapping), Button, InlineBanner, Skeleton.

UI States (ALL REQUIRED)

State Behavior French Copy
Loading Stepper polls; current step shows spinner « ⏳ {n}. {label} — En cours… »
Empty No run yet for a freshly created PENDING tenant « Aucun provisionnement lancé. » + [Lancer le provisionnement]
Error Failed step shows French error code + retry button; ✅ steps not re-run KEYCLOAK: « Service d'authentification indisponible. Réessayez dans quelques minutes. » · CP_NOT_SEEDED: « Profil pays CI introuvable. Contactez l'équipe Papillon. » · retry: « Relancer à partir de l'étape {n} » · skipped: « ⊘ {n}. {label} (ignoré — déjà complété) »
Offline / low network Pipeline keeps last known state; « Annuler » (ghost) does not undo completed (idempotent) steps; no UI timeout before 60 s (NFR-03) « Connexion lente — état non actualisé. »
Success All steps ✅ → toast → tenant ACTIVE « Cabinet {legal_name} activé. »

Responsive Behavior

Desktop-first; stepper stacks vertically on ≤ 768 px. Platform-admin small-screen banner on 360–767.

Connectivity Behavior (CUSTOMER stories only)

N/A — admin-side. Provisioning is resilient to the admin's network dropping between POSTs because the step state is server-persisted and the call is idempotent (see Test 13.2.3 in arch).

Interactions

  • « Relancer » re-issues POST /provision with the same Idempotency-Key; only PENDING/FAILED steps re-run.

Acceptance Criteria

AC-02.2: Idempotent provisioning + retry
Given provisioning failed (e.g. Keycloak realm timeout)
When the platform admin clicks "Relancer le provisionnement"
Then only steps not yet completed re-run; previously-completed steps are detected and skipped
And tenant status stays PENDING with a populated last_provisioning_error until success
AC-02.3: TenantOnboarded emitted exactly once
Given a tenant transitions PENDING → ACTIVE
Then exactly one TenantOnboarded is published carrying {tenantId, countryCode, plan, primaryContact, defaultAgencyId(nullable in V0.0.0), byoDb}
And re-runs on the same tenant do NOT re-emit it

AC-02.1 (default-agency auto-creation) is deferred to TENANT-005 / V0.0.2, because the agency table is introduced there. In V0.0.0 the DEFAULT_AGENCY step is a recorded no-op and defaultAgencyId is null in the event.


Compliance Rules

  • No new INCERTAIN/PRÉLIMINAIRE obligation raised. The CIMA/FNE assumptions captured at create (TENANT-003) carry forward. The V0 activation gate uses NOTIF sender IDs (R-TEN-NOTIF-01), not the V1 DPA gate.

Standards & Conventions

  • docs/standards/api-guidelines.md — idempotency, envelope, cursor pagination, 422 step failure.
  • docs/standards/java-spring-guidelines.md — domain events past-tense no Event suffix in …tenant.events; Spring Modulith outbox.
  • docs/standards/multi-tenant-model.md — Keycloak shared-realm + Organizations (prd-v0 §5.8).
  • docs/standards/multi-datasource-patterns.md — provisioning is the only place that knows the byo_db flag (single chokepoint).
  • docs/standards/design-system.md — ProvisioningPipeline, Badge, Button.
  • docs/standards/critical-rules.md — audit on mutation; tenant isolation.

Testing Requirements

Unit Tests

  • Orchestrator skips DONE steps on retry; FAILED step re-runs and increments attempt_count.
  • Activation gate blocks ACTIVE when notif sender IDs missing.

Integration Tests

  • Idempotent retry (arch §13.2.2): force KEYCLOAK failure, assert CP_BIND stays DONE/attempt=1 while KEYCLOAK retries; on recovery → ACTIVE.
  • Network-drop analogue (arch §13.2.3): processed-but-response-dropped, retry with same key → 5 steps DONE, exactly one TenantOnboarded recorded.
  • TenantOnboarded published once; outbox row persisted.
  • PLATFORM_ADMIN_QUERY audit written on list; 403 (not 404) on cross-role.
  • Keycloak step idempotent (existsByClientId/org-exists short-circuits).

What QA Will Validate

  • Smoke per arch §14.3 (create → provision → status ACTIVE).
  • Stepper renders all 5 states; retry only re-runs failed steps; French copy matches.

Out of Scope

  • BYO-DB provisioning step + Flyway-on-BYO + credential encryption (TENANT-007 / V0.0.4).
  • Default PRINCIPAL agency creation (TENANT-005 / V0.0.2 — turns DEFAULT_AGENCY into a real step + AC-02.1).
  • Rich tenant list (tabs/search/DPA banner) and full tabbed detail page (V1).
  • Suspend/reactivate/soft-delete (TENANT-008 / V0.1.0).
  • Initial CABINET_ADMIN user creation itself (owned by AUTH, consuming TenantOnboarded).

Definition of Done

  • [ ] POST /provision idempotent with Idempotency-Key=tenantId; per-step state in tenant_provisioning_run
  • [ ] Steps: CP_BIND real; BYO_DB + DEFAULT_AGENCY recorded no-op DONE in V0.0.0; KEYCLOAK creates Organization+client+roles (idempotent, mockable port); EVENT_EMIT flips ACTIVE + publishes TenantOnboarded once
  • [ ] V0 activation gate (notif sender IDs present) enforced
  • [ ] Minimal list + provisioning detail UI renders at 360/768/1024; all 5 stepper states
  • [ ] French micro-copy matches design spec exactly
  • [ ] AC-02.2 + AC-02.3 pass manually
  • [ ] TENANT_PROVISIONED + PLATFORM_ADMIN_QUERY audit entries present; 403 (not 404) on cross-role
  • [ ] Idempotency + network-drop tests pass; exactly-once TenantOnboarded
  • [ ] Unit + integration tests passing; no silent failure on Keycloak/DB unavailability