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 badgeTenantSwitchOverlay.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¶
- User taps avatar → menu shows "Mes entreprises" option
- Tap → tenant list appears
- Tap a different tenant → full-screen loading overlay: "Chargement de {name}…"
- Token exchange happens
- All cached data cleared → new tenant data prefetched → redirect to Dashboard
- 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 managementdocs/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