Story CP-002 : CRUD Tenant + Machine à états du cycle de vie¶
Module : control-plane Slice : Slice 2 from architecture Brand context : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority : 2 Depends on : CP-001 Estimated complexity : L
Objectif¶
Créer l'entité Tenant centrale avec sa machine à états à 10 états, l'entité TenantSettings pour la configuration par tenant, et l'API REST pour la consultation des informations tenant et la gestion des paramètres. Le Tenant est l'entité métier fondatrice de la plateforme — chaque module, chaque utilisateur, chaque employé existe dans le contexte d'un tenant. La machine à états gouverne l'intégralité du cycle de vie commercial, de l'inscription à la suppression.
Périmètre Backend¶
Entités & Records¶
Entité Tenant (base plateforme) :
| Champ | Type | Contraintes |
|---|---|---|
| id | UUID v7 | PK, généré côté serveur |
| name | String | NOT NULL, min 2, max 200 |
| country_code | String(2) | NOT NULL, ISO 3166-1 alpha-2 ("CI" pour le MVP) |
| status | TenantStatus enum | NOT NULL |
| db_profile | String | NOT NULL ("SHARED" ou "SCHEMA" ou "DB_PER_TENANT" ou "BYO_DB") |
| segment | String | Nullable (S1-S6, défini à la facturation) |
| onboarding_channel | String | NOT NULL ("SELF" ou "CRM" ou "ADMIN") |
| salesperson_id | UUID | Nullable (FK, renseigné pour le canal B) |
| db_connection_url | String | Nullable (Profil C uniquement) |
| trial_used | boolean | NOT NULL, défaut false |
| trial_start_date | LocalDate | Nullable |
| trial_end_date | LocalDate | Nullable |
| activated_date | Instant | Nullable |
| suspended_date | Instant | Nullable |
| suspended_reason | String | Nullable |
| deleted_date | Instant | Nullable |
| created_date | Instant | NOT NULL |
| updated_date | Instant | NOT NULL |
Enum TenantStatus :
public enum TenantStatus {
PENDING_VERIFICATION, // Canal A : email pas encore vérifié
PENDING, // Canal B : créé par CRM, mot de passe non défini
TRIAL, // Essai gratuit actif (14 jours)
PAYMENT_REQUIRED, // Doit payer sous 14 jours
ACTIVE_PAYMENT_PENDING,// Virement bancaire choisi, grâce de 10 jours
ACTIVE_INCOMPLETE, // Payé mais RCCM/NIF manquant
ACTIVE, // Pleinement actif et conforme
SUSPENDED, // Lecture seule (expiré ou manuel)
DELETED, // Suppression logique
PROVISIONING_FAILED // Interne uniquement, non visible par le tenant
}
Entité TenantSettings (base tenant) :
| Champ | Type | Contraintes |
|---|---|---|
| id | UUID v7 | PK |
| tenant_id | UUID | NOT NULL, unique |
| fiscal_year_start_month | int | NOT NULL, défaut 1 (janvier) |
| working_days_per_week | int | NOT NULL, défaut 5 |
| working_hours_per_day | int | NOT NULL, défaut 8 |
| currency | String(3) | NOT NULL, défaut "XOF" |
| logo_url | String | Nullable |
| matricule_prefix | String | NOT NULL, max 10 (défini au trigrame de l'entreprise lors du provisionnement, ex. "ALT") |
| matricule_padding | int | NOT NULL, défaut 3 |
| default_approver_user_id | UUID | Nullable (FK vers User, défini après le slice utilisateur) |
| atmp_risk_class | String | Nullable (lien vers le code de classe de risque) |
| legal_form | String | Nullable (SARL, SA, SAS, SUARL, EI) |
| rccm | String | Nullable, max 50 |
| nif | String | Nullable, max 50 |
| address | String | Nullable, max 500 |
| sector | String | Nullable |
| created_date | Instant | NOT NULL |
| updated_date | Instant | NOT NULL |
DTOs :
public record TenantResponse(
UUID id, String name, String countryCode, TenantStatus status,
String segment, String onboardingChannel, boolean trialUsed,
LocalDate trialEndDate, Instant activatedDate, Instant createdDate
) {}
public record TenantSettingsResponse(
int fiscalYearStartMonth, int workingDaysPerWeek, int workingHoursPerDay,
String currency, String logoUrl, String matriculePrefix, int matriculePadding,
UUID defaultApproverUserId, String atmpRiskClass, String legalForm,
String rccm, String nif, String address, String sector
) {}
public record UpdateTenantSettingsRequest(
@Nullable Integer fiscalYearStartMonth,
@Nullable Integer workingDaysPerWeek,
@Nullable Integer workingHoursPerDay,
@Nullable String logoUrl,
@Nullable String matriculePrefix,
@Nullable Integer matriculePadding,
@Nullable UUID defaultApproverUserId,
@Nullable String atmpRiskClass,
@Nullable String legalForm,
@Nullable String rccm,
@Nullable String nif,
@Nullable String address,
@Nullable String sector
) {}
Couche Repository¶
TenantRepository.java— Platform-scoped (NON tenant-aware). Méthodes :findById(UUID id) → Optional<Tenant>findByAdminEmail(String email) → Optional<Tenant>findByStatus(TenantStatus status) → List<Tenant>findAll(Pageable) → Page<Tenant>save(Tenant) → TenantTenantSettingsRepository.java— Tenant-aware. Méthodes :findByTenantId(UUID tenantId) → Optional<TenantSettings>save(TenantSettings) → TenantSettings
Couche Service¶
TenantService.java :
- getTenantInfo(UUID tenantId) → TenantResponse — Retourne les détails du tenant courant
- getSettings(UUID tenantId) → TenantSettingsResponse — Retourne la configuration du tenant
- updateSettings(UUID tenantId, UpdateTenantSettingsRequest) → TenantSettingsResponse — Mise à jour partielle des paramètres tenant. Chaque champ se sauvegarde indépendamment (pattern auto-save de la spec design).
TenantStateMachine.java :
- Encapsule TOUTES les transitions d'état valides. Rejette les transitions invalides avec IllegalStateTransitionException.
- transition(Tenant, TenantStatus targetStatus, @Nullable String reason) → Tenant
- Transitions valides (depuis PRD FR-CP-010 à FR-CP-017) :
| De | Vers | Condition |
|---|---|---|
| PENDING_VERIFICATION | TRIAL | Email vérifié + essai sélectionné |
| PENDING_VERIFICATION | PAYMENT_REQUIRED | Email vérifié + pas d'essai |
| PENDING | PAYMENT_REQUIRED | Mot de passe défini, pas de paiement |
| PENDING | ACTIVE_PAYMENT_PENDING | Mot de passe défini + virement bancaire |
| PENDING | ACTIVE | Mot de passe défini + paiement API |
| TRIAL | ACTIVE | Paiement reçu |
| TRIAL | ACTIVE_INCOMPLETE | Paiement reçu, RCCM/NIF manquant |
| TRIAL | ACTIVE_PAYMENT_PENDING | Virement bancaire choisi |
| TRIAL | SUSPENDED | 14 jours expirés |
| PAYMENT_REQUIRED | TRIAL | Essai unique demandé (trial_used doit être false) |
| PAYMENT_REQUIRED | ACTIVE | Paiement reçu |
| PAYMENT_REQUIRED | ACTIVE_INCOMPLETE | Paiement reçu, RCCM/NIF manquant |
| PAYMENT_REQUIRED | ACTIVE_PAYMENT_PENDING | Virement bancaire choisi |
| PAYMENT_REQUIRED | SUSPENDED | 14 jours expirés |
| ACTIVE_PAYMENT_PENDING | ACTIVE | Paiement confirmé |
| ACTIVE_PAYMENT_PENDING | ACTIVE_INCOMPLETE | Confirmé, RCCM/NIF manquant |
| ACTIVE_PAYMENT_PENDING | SUSPENDED | 10 jours expirés |
| ACTIVE_INCOMPLETE | ACTIVE | RCCM + NIF fournis |
| ACTIVE_INCOMPLETE | SUSPENDED | 3 mois écoulés sans RCCM/NIF (job planifié) |
| ACTIVE | SUSPENDED | Abonnement expiré + grâce OU suspension manuelle |
| SUSPENDED | ACTIVE | Paiement / réactivation |
| SUSPENDED | DELETED | 30 jours sans action (suppression logique) |
- Publie l'Application Event
TenantStatusChangedà chaque transition :
Endpoints API¶
| Méthode | Chemin | Corps de requête | Réponse | Auth |
|---|---|---|---|---|
| GET | /api/v1/tenant | — | TenantResponse | Tout utilisateur authentifié |
| GET | /api/v1/tenant/settings | — | TenantSettingsResponse | ROLE_DG, ROLE_RH_MANAGER |
| PATCH | /api/v1/tenant/settings | UpdateTenantSettingsRequest | TenantSettingsResponse | ROLE_DG, ROLE_RH_MANAGER |
| GET | /api/v1/tenant/default-approver | — | { userId, fullName } |
Tout utilisateur authentifié |
Règles de validation¶
fiscal_year_start_month: 1–12working_days_per_week: 1–7working_hours_per_day: 1–24matricule_prefix: 1–10 caractères, alphanumérique uniquementmatricule_padding: 2–8rccm: max 50 caractèresnif: max 50 caractèresatmp_risk_class: doit correspondre à un code de classe de risque valide dans la configuration payslegal_form: doit être l'une des valeurs SARL, SA, SAS, SUARL, EI (ou null)
Considérations multi-tenant¶
- L'entité
Tenantelle-même est dans la base plateforme (non tenant-scoped — c'est le tenant lui-même) TenantSettingsest dans la base tenant (tenant-scoped)- Les endpoints API pour les paramètres utilisent
TenantContext.current().tenantId()— un tenant ne peut lire/modifier que ses propres paramètres - Les endpoints admin (pour l'admin plateforme qui gère n'importe quel tenant) sont dans CP-012 (Console Admin)
Migrations Flyway¶
V001__create_tenant.sql— Base plateforme : tabletenantsV001__create_tenant_settings.sql— Base tenant : tabletenant_settings
Périmètre Frontend¶
Pages & Routes¶
Page Paramètres entreprise :
- Route : /admin/settings/company (dans le shell tenant authentifié)
- Layout : Layout de page admin standard
Composants¶
CompanySettingsPage.tsx— Formulaire complet de paramètres avec auto-save par champMatriculePreview.tsx— Aperçu en temps réel : "{prefix}-{AAAA}-{MM}-{exemple}" (ex. "ALT-2026-03-001")RiskClassSelector.tsx— Menu déroulant pour la classe de risque AT/MP
États UI (TOUS REQUIS)¶
| État | Comportement | Texte en Français |
|---|---|---|
| Loading | Skeleton : 12 espaces réservés de champs dans le layout formulaire | — |
| Empty | N/A — les paramètres existent toujours (initialisés au provisionnement) | — |
| Error | Toast rouge en haut | "Une erreur est survenue. Veuillez réessayer." |
| Offline | Lecture seule : tous les champs désactivés, bandeau orange en haut | "Hors ligne — les modifications seront possibles une fois la connexion rétablie." |
| Success (par champ) | Bref checkmark vert à côté du label du champ (fondu entrant 150ms, maintien 2s, fondu sortant 300ms) | "Enregistré ✓" |
| Success (sauvegarde hors ligne) | Point orange à côté du champ | "Modification enregistrée hors ligne" |
Comportement responsive¶
- 360px (mobile) : Formulaire une colonne. Tous les champs empilés verticalement. Sections séparées par des séparateurs : "Informations de l'entreprise", "Configuration", "Format Matricule Employé", "Approbateur par défaut".
- 768px (tablette) : Identique au mobile mais avec des champs plus larges.
- 1024px+ (bureau) : Formulaire deux colonnes :
- Colonne gauche : Identité de l'entreprise (nom, forme juridique, RCCM, NIF, adresse, secteur)
- Colonne droite : Configuration (exercice fiscal, jours ouvrables, heures, classe AT/MP, format matricule, approbateur par défaut)
Interactions¶
- Auto-save : Chaque champ se sauvegarde au blur (pas de bouton Enregistrer). Un bref checkmark vert apparaît à côté du label du champ en cas de succès.
- Upload du logo : Appui sur l'espace réservé → sélecteur de fichier → upload vers MinIO → affichage de la miniature. Max 2 Mo, PNG/JPG uniquement.
- Aperçu du matricule : L'aperçu se met à jour en temps réel lors du changement du préfixe/padding. Format :
{prefix}-{AAAA}-{MM}-{zeroRembourré}. - Info classe de risque : Texte d'aide
ℹ Utilisé pour le calcul des cotisations CNPSsous le menu déroulant. - Avertissement NIF : Si le NIF est vide et que le tenant est ACTIVE_INCOMPLETE, afficher
⚠ Requis pour la facturation normalisée (FNE)sous le champ.
Critères d'acceptation¶
AC-008: Get current tenant info
Given I am authenticated as a user of tenant "Acme SARL"
When I call GET /api/v1/tenant
Then I receive the tenant's name, status, country, segment, and key dates
AC-009: Get tenant settings
Given I am authenticated with role DG
When I call GET /api/v1/tenant/settings
Then I receive all configuration fields including fiscal year, working days, matricule format
AC-010: Update tenant settings (auto-save)
Given I am authenticated with role RH_MANAGER
When I call PATCH /api/v1/tenant/settings with {"workingDaysPerWeek": 6}
Then the working days setting is updated to 6
And an audit trail entry is created
AC-011: Settings auto-save on field blur
Given I am on the Company Settings page
When I change the "Nombre de jours ouvrables" field to 6 and tab out
Then a PATCH request is sent automatically
And a green checkmark "Enregistré ✓" appears next to the field label
AC-012: Matricule preview updates live
Given I am on the Company Settings page
When I change the matricule prefix to "ACC" and padding to 5
Then the preview shows "Aperçu : ACC-2026-03-00001"
AC-013: Valid state transitions accepted
Given a tenant in TRIAL status
When the billing engine confirms payment and RCCM+NIF are present
Then the state machine transitions the tenant to ACTIVE
And a TenantStatusChanged event is published
AC-014: Invalid state transitions rejected
Given a tenant in ACTIVE status
When a transition to TRIAL is attempted
Then the state machine throws IllegalStateTransitionException
And no state change occurs
AC-015: ACTIVE_INCOMPLETE NIF warning
Given a tenant with status ACTIVE_INCOMPLETE (NIF is empty)
When I view Company Settings
Then the NIF field shows "⚠ Requis pour la facturation normalisée (FNE)" below it
AC-016: Settings read-only offline
Given the device is offline
When I view Company Settings
Then all fields are displayed but disabled
And an orange banner shows "Hors ligne — les modifications seront possibles une fois la connexion rétablie."
AC-114: Scheduled job downgrades ACTIVE_INCOMPLETE after 3 months
Given a tenant has been in ACTIVE_INCOMPLETE status for 3 or more months without providing RCCM and NIF
When the daily scheduled job runs
Then the tenant's status is transitioned to SUSPENDED (read-only access)
And a TenantStatusChanged event is published
And the tenant receives a notification explaining that read-only mode has been activated and how to restore full access by providing RCCM/NIF
Règles OHADA & Réglementaires¶
- RCCM : Registre du Commerce et du Crédit Mobilier — obligatoire pour les entreprises formelles dans la zone OHADA. Facultatif à l'inscription pour réduire les frictions, mais requis pour la facturation conforme FNE.
- NIF : Numéro d'Identification Fiscale — requis pour le FNE. Sans lui, les factures sont uniquement des proformas (FR-CP-023).
- Formes juridiques (SARL, SA, SAS, SUARL, EI) : l'Acte Uniforme OHADA sur le Droit Commercial Général les définit. L'administrateur tenant sélectionne la forme juridique de son entreprise.
- Classe de risque AT/MP : Décret n° 2024-901 (CI) — la classe de risque de l'employeur détermine le taux de cotisation AT/MP (2 %–5 %). Doit être configurable par tenant.
- Lecture seule ACTIVE_INCOMPLETE après 3 mois : Après 3 mois sans fournir le RCCM/NIF, les modules passent en lecture seule. C'est une règle métier (FR-CP-013), pas une obligation réglementaire, mais elle permet à ALTARYS d'émettre des factures conformes.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §Entities, §Records & sealed types, §Application Events, §Service layer patternsdocs/standards/database-guidelines.md— §Flyway migrations, §UUID v7 primary keys, §Audit columns (created_date, updated_date)docs/standards/api-guidelines.md— §PATCH semantics, §Standard envelope format, §Error responsesdocs/standards/react-typescript-guidelines.md— §Auto-save pattern, §Form componentsdocs/standards/offline-sync-guidelines.md— §Read-only offline mode
Exigences de test¶
Tests unitaires¶
- TenantStateMachine : tester les 21 transitions valides
- TenantStateMachine : tester TOUTES les transitions invalides (chaque paire absente de la liste valide)
- TenantStateMachine : vérifier que l'événement TenantStatusChanged est publié à chaque transition valide
- Validation TenantSettings : valeurs limites pour fiscal_year_start_month (1, 12, 0, 13)
- Génération de l'aperçu matricule
Tests d'intégration¶
- GET /api/v1/tenant — retourne les informations correctes du tenant
- GET /api/v1/tenant/settings — retourne les paramètres, interdit pour le rôle EMPLOYEE
- PATCH /api/v1/tenant/settings — met à jour un champ individuel, crée une entrée d'audit
- PATCH /api/v1/tenant/settings avec données invalides — retourne 400 avec les erreurs de validation
- Isolation tenant : le tenant A ne peut pas lire les paramètres du tenant B
Ce que le QA validera¶
- La page Paramètres entreprise se charge avec les données correctes
- L'auto-save fonctionne sur chaque champ (modification → blur → checkmark)
- L'aperçu du matricule se met à jour en temps réel
- L'avertissement NIF s'affiche pour les tenants ACTIVE_INCOMPLETE
- Les paramètres sont en lecture seule hors ligne
- Layout responsive à 360px, 768px, 1024px
Hors périmètre¶
- Création/inscription du tenant — c'est CP-008 (Auto-inscription)
- Interface de sélection de l'approbateur par défaut (nécessite l'entité User de CP-003)
- Upload du logo vers MinIO — infrastructure. Afficher un espace réservé. Implémenter l'upload quand MinIO est configuré.
- Gestion des tenants en console admin — c'est CP-012
- Processus de suppression/archivage du tenant — procédure opérationnelle V2
- Routage des profils DB B et D — MVP (les 4 profils sont en scope)
Définition de Done¶
- [ ] Tous les endpoints backend implémentés et retournant les réponses correctes
- [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px, 1024px
- [ ] Les 5 états UI implémentés (loading, empty, error, offline, success)
- [ ] Les micro-textes français correspondent exactement à la spec design
- [ ] Tous les critères d'acceptation passent manuellement
- [ ] Tests unitaires écrits et passants
- [ ] Tests d'intégration écrits et passants
- [ ] Isolation multi-tenant vérifiée (aucune fuite de données inter-tenant)
- [ ] Écriture hors ligne + sync fonctionnelle (Papillon) — Lecture seule hors ligne pour les paramètres
- [ ] Pas de
anyTypeScript — tous les types explicites - [ ] Pas de types Java bruts — tous les génériques explicites
- [ ] Les réponses API respectent le format d'enveloppe standard
- [ ] Entrées d'audit créées pour chaque mutation de données