Story CP-007 : Module Gating Backend (Interceptor + Règle BUDMGT)¶
Module : control-plane Slice : Slice 7a from architecture (split from original CP-007) Brand context : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority : 7 Depends on : CP-001, CP-002 Estimated complexity : M
Objectif¶
Implémenter le mécanisme backend de contrôle d'accès aux revenus qui garantit que les modules ne fonctionnent que pour les tenants payants. Ce slice crée l'entité TenantModule, le suivi du statut des modules, le ModuleGatingInterceptor qui applique l'accès aux modules, le ModuleGatingService et la règle d'auto-inclusion BUDMGT. Aucun frontend dans ce slice — l'intégration à la navigation et les cartes d'upsell sont dans CP-021.
Périmètre Backend¶
Entités & Records¶
Entité TenantModule (base plateforme) :
| Champ | Type | Contraintes |
|---|---|---|
| id | UUID v7 | PK |
| tenant_id | UUID | NOT NULL, FK vers Tenant |
| module_code | String | NOT NULL |
| status | ModuleStatus enum | NOT NULL |
| activated_date | Instant | Nullable |
| deactivated_date | Instant | Nullable |
Enum ModuleStatus : ACTIVE, INACTIVE, TRIAL
Codes de modules vendables : HRCORE, EMPMGT, ABSMGT, QRCONTR, TIMACT, EXPMGT, COMMIT, PAYROL, HSEMGT, PERFOB, GPEC, LTRAIN, ATSMGT, ADVDOC, CRMMGT
Non vendable (auto-inclus) : BUDMGT
Toujours actif (non soumis au gating) : CP (Control Plane), ORGDATA (Organisation & Données Maîtresses)
DTOs :
public record TenantModuleResponse(
String moduleCode, String moduleName, ModuleStatus status,
@Nullable Instant activatedDate, @Nullable Instant deactivatedDate,
boolean isSellable, @Nullable String description, @Nullable String price
) {}
public record ModuleCatalogResponse(
List<TenantModuleResponse> subscribedModules,
List<ModuleCatalogEntry> availableModules
) {}
public record ModuleCatalogEntry(
String moduleCode, String moduleName, String description,
String iconName, boolean available, @Nullable String comingSoonDate,
@Nullable String priceForSegment
) {}
Couche Repository¶
TenantModuleRepository.java— Platform-scoped :findByTenantId(UUID) → List<TenantModule>findByTenantIdAndModuleCode(UUID, String) → Optional<TenantModule>save(TenantModule) → TenantModule
Couche Service¶
ModuleGatingService.java :
- isModuleActive(UUID tenantId, String moduleCode) → boolean — Vérifie le statut TenantModule. Mis en cache dans Redis (TTL : 5 min) avec fallback Caffeine (TTL : 1 min).
- getModulesForTenant(UUID tenantId) → List<TenantModuleResponse> — Liste tous les modules avec leur statut
- activateModule(UUID tenantId, String moduleCode) → void — Définit status=ACTIVE. Publie l'événement TenantModuleActivated. Déclenche la vérification d'auto-inclusion BUDMGT.
- deactivateModule(UUID tenantId, String moduleCode) → void — Définit status=INACTIVE. Publie l'événement TenantModuleDeactivated. Déclenche la vérification d'auto-inclusion BUDMGT. Les données restent lisibles.
- checkBudgetAutoInclusion(UUID tenantId) → void — SI EXPMGT OU COMMIT est ACTIVE → BUDMGT=ACTIVE. SI aucun des deux → BUDMGT=INACTIVE. Enregistré dans l'audit avec la raison "Auto-inclusion rule".
ModuleGatingInterceptor.java (Spring HandlerInterceptor) :
- Inspecte le chemin de la requête pour déterminer le code module (mapping préfixe de chemin : /api/v1/absences/** → ABSMGT, etc.)
- Appelle ModuleGatingService.isModuleActive() pour le tenant courant
- SI module inactif → HTTP 403 :
{
"error": "MODULE_NOT_SUBSCRIBED",
"module": "PAYROL",
"message": "Module non souscrit. Contactez votre administrateur pour activer ce module."
}
Endpoints API¶
| Méthode | Chemin | Corps de requête | Réponse | Auth |
|---|---|---|---|---|
| GET | /api/v1/tenant/modules | — | List |
Tout utilisateur authentifié |
Règles de validation¶
- Le code module doit être un code de module vendable valide
- Impossible de désactiver CP ou ORGDATA (toujours actifs)
- BUDMGT ne peut pas être activé/désactivé manuellement (auto-inclusion uniquement)
Considérations multi-tenant¶
- TenantModule est dans la base plateforme mais scopé par tenant_id
- Cache de vérification des modules : clé Redis =
module:{tenantId}:{moduleCode}, fallback Caffeine - Invalidation du cache lors d'un changement de statut de module (piloté par événements)
Périmètre Frontend¶
Aucun frontend dans ce slice. L'intégration à la navigation et les cartes d'upsell sont dans CP-021.
Pages & Routes¶
N/A — Slice uniquement backend.
États UI (TOUS REQUIS)¶
N/A — Slice uniquement backend.
Comportement responsive¶
N/A
Interactions¶
N/A
Critères d'acceptation¶
AC-052: Module gating blocks inactive module API
Given my tenant has ABSMGT=ACTIVE and PAYROL=INACTIVE
When I call POST /api/v1/payroll/payslips
Then I receive HTTP 403 with {"error": "MODULE_NOT_SUBSCRIBED", "module": "PAYROL"}
AC-053: Active module allows API access
Given my tenant has ABSMGT=ACTIVE
When I call GET /api/v1/absences/leave-requests
Then the request proceeds normally (not blocked by gating)
AC-054: BUDMGT auto-inclusion on EXPMGT activation
Given my tenant has EXPMGT=INACTIVE and BUDMGT=INACTIVE
When EXPMGT is activated
Then BUDMGT is automatically set to ACTIVE
And an audit entry records reason "Auto-inclusion rule"
AC-055: BUDMGT auto-exclusion
Given my tenant has EXPMGT=ACTIVE, COMMIT=INACTIVE, BUDMGT=ACTIVE
When EXPMGT is deactivated
Then BUDMGT is automatically set to INACTIVE
Règles OHADA & Réglementaires¶
- Aucune exigence réglementaire OHADA directe pour le module gating.
- Règle métier : les données des modules désactivés restent lisibles — le tenant ne peut pas perdre l'accès aux données historiques qu'il a déjà créées. Ceci est important pour la conformité AUDCIF Art. 24 (les données financières dans un module Paie désactivé doivent rester accessibles pendant 10 ans).
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §Spring Interceptors, §Redis caching, §Caffeine fallbackdocs/standards/api-guidelines.md— §403 error format for module gating
Exigences de test¶
Tests unitaires¶
- ModuleGatingService : isModuleActive pour ACTIVE, INACTIVE, TRIAL
- ModuleGatingService : logique d'auto-inclusion BUDMGT
- ModuleGatingInterceptor : mapping chemin-vers-module
Tests d'intégration¶
- Flux complet : requête vers un module inactif → 403
- Flux complet : requête vers un module actif → succès
- Auto-inclusion BUDMGT lors de l'activation de EXPMGT
- Cache : changement de statut du module → cache invalidé → la prochaine requête utilise le nouveau statut
- Isolation tenant : la config module du tenant A n'affecte pas le tenant B
Ce que le QA validera¶
- Le module gating bloque réellement l'accès aux modules inactifs (tests au niveau API)
Hors périmètre¶
- Refus d'écriture pour tenant suspendu — CP-020
- API catalogue de modules — CP-020
- Intégration à la navigation + cartes d'upsell (frontend) — CP-021
- UI d'activation/désactivation de modules — dans CP-009 (gestion de l'abonnement/facturation)
- Suspension par module — le modèle de données le supporte (application V2)
- Avertissements de dépendance de modules lors de la désactivation — CP-009
- Remplacement de module par l'admin — CP-012
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 — N/A (uniquement backend)
- [ ] Les 5 états UI implémentés — N/A (uniquement backend)
- [ ] Les micro-textes français correspondent exactement à la spec design — N/A (uniquement backend)
- [ ] 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) — N/A
- [ ] Pas de
anyTypeScript — N/A (uniquement backend) - [ ] 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