Aller au contenu

Story CP-011: Tenant Switch Backend + Switcher UI

Module: control-plane Slice: Slice 11 from architecture Brand context: Papillon HR Suite Priority: 11 Depends on: CP-003 Estimated complexity: M


Objective

Enable the cabinet comptable (external accountant) workflow: a single user who belongs to multiple tenants can switch between them without re-authenticating. This slice adds the tenant switcher UI component, the token exchange mechanism, and the core switching flow. Dashboard thumbnails and performance optimization are in CP-028.


Backend Scope

Entities & Records

No new entities — uses UserTenantMembership from CP-003.

DTOs (extend from CP-003):

public record SwitchTenantRequest(UUID targetTenantId) {}

public record SwitchTenantResponse(
    String accessToken, String refreshToken,
    UUID tenantId, String tenantName
) {}

Service Layer

TenantSwitchService.java: - switchTenant(UUID userId, UUID targetTenantId) → SwitchTenantResponse: 1. Validate user has active membership in target tenant 2. Request new JWT from Keycloak with target tenant_id claim 3. Invalidate old JWT (if Keycloak supports token revocation) 4. Return new access + refresh tokens 5. Switch completes in < 2 seconds

API Endpoints

Owned by this story (CP-011):

Method Path Request Body Response Auth
GET /api/v1/auth/my-tenants MyTenantsResponse Any authenticated
POST /api/v1/auth/switch-tenant SwitchTenantRequest SwitchTenantResponse Any authenticated

Multi-Tenant Considerations

  • Token exchange replaces one tenant context with another
  • All in-memory caches (permissions, module status) must be invalidated on switch
  • IndexedDB data for old tenant must be cleared on switch
  • New tenant's data prefetched after switch

Frontend Scope

Pages & Routes

  • No dedicated page. Tenant switcher is accessible from:
  • User avatar menu in top bar → "Mes entreprises"

Components

  • TenantSwitcher.tsx — Full-screen bottom sheet (mobile) / dropdown (desktop)
  • TenantSwitcherCard.tsx — Tenant card with logo, name, role, current badge
  • TenantSwitchOverlay.tsx — Full-screen loading during switch: "Chargement de {name}…"

UI States (ALL REQUIRED)

State Behavior French Copy
Loading Spinner while fetching tenant list
Single tenant Switcher not shown (only shown for multi-tenant users)
Multi-tenant Tenant list with current tenant highlighted "Mes entreprises"
Switching Full-screen overlay with pulsing indicator "Chargement de {tenantName}…"
Switch error Toast "Impossible de changer d'entreprise. Veuillez réessayer."
Success Redirect to new tenant's dashboard

Responsive Behavior

  • 360px (mobile): Full-screen bottom sheet showing tenant list. Each card: logo placeholder, company name, user's role, checkmark on current tenant.
  • 1024px+ (desktop): Dropdown from user avatar menu. Same content, compact layout.

Interactions

  1. User taps avatar → menu shows "Mes entreprises" option
  2. Tap → tenant list appears
  3. Tap a different tenant → full-screen loading overlay: "Chargement de {name}…"
  4. Token exchange happens
  5. All cached data cleared → new tenant data prefetched → redirect to Dashboard
  6. Previous tenant context fully cleared from memory

French micro-copy:

Key French Text
tenant_switcher.title "Mes entreprises"
tenant_switcher.current "Entreprise active"
tenant_switcher.switching "Chargement de {name}…"
tenant_switcher.hint "Tapez sur une entreprise pour changer de contexte."

Acceptance Criteria

AC-089: Tenant switch with token exchange
Given I belong to tenant A (current) and tenant B
When I select tenant B from the switcher
Then a new JWT is issued for tenant B
And I land on tenant B's dashboard
And all tenant A data is cleared from cache/IndexedDB

AC-090: Switcher only for multi-tenant users
Given I belong to only 1 tenant
When I open the user menu
Then "Mes entreprises" option does not appear

AC-091: Switcher shows current tenant
Given I belong to 3 tenants and am currently in "Acme SARL"
When I open the tenant switcher
Then "Acme SARL" has a "✓ Entreprise active" badge

OHADA & Regulatory Rules

  • Cabinet comptable is a common business structure in OHADA zones — external accountants managing multiple client companies. The multi-tenant switching feature directly serves this persona.
  • Token exchange must not leak data between tenants — all caches cleared on switch.

Standards & Conventions

  • docs/standards/react-typescript-guidelines.md — §Bottom sheet, §Overlay/loading states, §Cache management
  • docs/standards/offline-sync-guidelines.md — §IndexedDB clearing on context switch

Testing Requirements

Unit Tests

  • TenantSwitchService: valid switch, invalid membership → rejected
  • TenantSwitcher: only renders for multi-tenant users
  • TenantSwitcherCard: current tenant badge rendering

Integration Tests

  • Token exchange: new JWT has correct tenant_id
  • Switch denied: user not a member of target tenant → 403
  • Cache/IndexedDB cleared after switch

What QA Will Validate

  • Switcher appears only for multi-tenant users
  • Current tenant highlighted
  • Switch overlay shows and dismisses correctly
  • All data reflects new tenant after switch (employee list, settings, etc.)

Out of Scope

  • Dashboard tenant thumbnails — CP-028
  • Role display per tenant in switcher — CP-028
  • Switch performance optimization (< 2s) — CP-028
  • Home tenant ID configuration (change which tenant is "home") — V2
  • Tenant list search (for users with many tenants) — V2
  • Cross-tenant reporting (aggregate view across tenants) — V2
  • Offline tenant switching — Not possible (requires token exchange)

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] All frontend components render correctly at 360px, 768px, 1024px
  • [ ] All 6 UI states implemented (loading, single tenant, multi-tenant, switching, error, success)
  • [ ] French micro-copy matches design spec exactly
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Multi-tenant isolation verified (no cross-tenant data leaks)
  • [ ] No TypeScript any — all types explicit
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries created for tenant switch events