Aller au contenu

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) → Tenant
  • TenantSettingsRepository.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 :
    public record TenantStatusChanged(
        UUID tenantId, TenantStatus oldStatus, TenantStatus newStatus,
        @Nullable String reason, Instant timestamp
    ) {}
    

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–12
  • working_days_per_week : 1–7
  • working_hours_per_day : 1–24
  • matricule_prefix : 1–10 caractères, alphanumérique uniquement
  • matricule_padding : 2–8
  • rccm : max 50 caractères
  • nif : max 50 caractères
  • atmp_risk_class : doit correspondre à un code de classe de risque valide dans la configuration pays
  • legal_form : doit être l'une des valeurs SARL, SA, SAS, SUARL, EI (ou null)

Considérations multi-tenant

  • L'entité Tenant elle-même est dans la base plateforme (non tenant-scoped — c'est le tenant lui-même)
  • TenantSettings est 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 : table tenants
  • V001__create_tenant_settings.sql — Base tenant : table tenant_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 champ
  • MatriculePreview.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 CNPS sous 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 patterns
  • docs/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 responses
  • docs/standards/react-typescript-guidelines.md — §Auto-save pattern, §Form components
  • docs/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 any TypeScript — 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