Aller au contenu

Story CP-007: Module Gating Backend (Interceptor + BUDMGT Rule)

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


Objective

Implement the core backend revenue-gating mechanism that ensures modules work only for paying tenants. This slice creates the TenantModule entity, the module status tracking, the ModuleGatingInterceptor that enforces module access, the ModuleGatingService, and the BUDMGT auto-inclusion rule. No frontend in this slice — navigation integration and upsell cards are in CP-021.


Backend Scope

Entities & Records

TenantModule entity (platform DB):

Field Type Constraints
id UUID v7 PK
tenant_id UUID NOT NULL, FK to Tenant
module_code String NOT NULL
status ModuleStatus enum NOT NULL
activated_date Instant Nullable
deactivated_date Instant Nullable

ModuleStatus enum: ACTIVE, INACTIVE, TRIAL

Sellable module codes: HRCORE, EMPMGT, ABSMGT, QRCONTR, TIMACT, EXPMGT, COMMIT, PAYROL, HSEMGT, PERFOB, GPEC, LTRAIN, ATSMGT, ADVDOC, CRMMGT

Non-sellable (auto-included): BUDMGT

Always active (not gated): CP (Control Plane), ORGDATA (Organization & Master Data)

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
) {}

Repository Layer

  • TenantModuleRepository.java — Platform-scoped:
  • findByTenantId(UUID) → List<TenantModule>
  • findByTenantIdAndModuleCode(UUID, String) → Optional<TenantModule>
  • save(TenantModule) → TenantModule

Service Layer

ModuleGatingService.java: - isModuleActive(UUID tenantId, String moduleCode) → boolean — Checks TenantModule status. Cached in Redis (TTL: 5 min) with Caffeine fallback (TTL: 1 min). - getModulesForTenant(UUID tenantId) → List<TenantModuleResponse> — Lists all modules with status - activateModule(UUID tenantId, String moduleCode) → void — Sets status=ACTIVE. Publishes TenantModuleActivated event. Triggers BUDMGT auto-inclusion check. - deactivateModule(UUID tenantId, String moduleCode) → void — Sets status=INACTIVE. Publishes TenantModuleDeactivated event. Triggers BUDMGT auto-inclusion check. Data remains readable. - checkBudgetAutoInclusion(UUID tenantId) → void — IF EXPMGT OR COMMIT is ACTIVE → BUDMGT=ACTIVE. IF neither → BUDMGT=INACTIVE. Recorded in audit with reason "Auto-inclusion rule".

ModuleGatingInterceptor.java (Spring HandlerInterceptor): - Inspects request path to determine module code (path prefix mapping: /api/v1/absences/** → ABSMGT, etc.) - Calls ModuleGatingService.isModuleActive() for the current tenant - IF module inactive → HTTP 403:

{
  "error": "MODULE_NOT_SUBSCRIBED",
  "module": "PAYROL",
  "message": "Module non souscrit. Contactez votre administrateur pour activer ce module."
}
- IF tenant is SUSPENDED → deny ALL write operations (POST, PUT, PATCH, DELETE) for gated modules. Read (GET) remains allowed. - Interceptor runs AFTER authentication (tenant must be resolved first)

API Endpoints

Method Path Request Body Response Auth
GET /api/v1/tenant/modules List Any authenticated

Validation Rules

  • Module code must be a valid sellable module code
  • Cannot deactivate CP or ORGDATA (always active)
  • BUDMGT cannot be manually activated/deactivated (auto-inclusion only)

Multi-Tenant Considerations

  • TenantModule is in platform DB but scoped by tenant_id
  • Module check cache: Redis key = module:{tenantId}:{moduleCode}, Caffeine fallback
  • Cache invalidation on module status change (event-driven)

Frontend Scope

No frontend in this slice. Navigation integration and upsell cards are in CP-021.

Pages & Routes

N/A — Backend-only slice.

UI States (ALL REQUIRED)

N/A — Backend-only slice.

Responsive Behavior

N/A

Interactions

N/A


Acceptance Criteria

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

OHADA & Regulatory Rules

  • No direct OHADA regulatory requirements for module gating.
  • Business rule: deactivated modules' data remains readable — tenant cannot lose access to historical data they've already created. This is important for AUDCIF Art. 24 compliance (financial data in deactivated Payroll module must remain accessible for 10 years).

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Spring Interceptors, §Redis caching, §Caffeine fallback
  • docs/standards/api-guidelines.md — §403 error format for module gating

Testing Requirements

Unit Tests

  • ModuleGatingService: isModuleActive for ACTIVE, INACTIVE, TRIAL
  • ModuleGatingService: BUDMGT auto-inclusion logic
  • ModuleGatingInterceptor: path-to-module mapping

Integration Tests

  • Full flow: request to inactive module → 403
  • Full flow: request to active module → success
  • BUDMGT auto-inclusion on EXPMGT activation
  • Cache: module status change → cache invalidated → next request uses new status
  • Tenant isolation: module config for tenant A doesn't affect tenant B

What QA Will Validate

  • Module gating actually blocks access to inactive modules (API-level testing)

Out of Scope

  • Suspended tenant write denial — CP-020
  • Module catalog API — CP-020
  • Navigation integration + upsell cards (frontend) — CP-021
  • Module activation/deactivation UI — that's in CP-009 (Billing/Subscription management)
  • Per-module suspension — data model supports it (V2 enforcement)
  • Module dependency warnings on deactivation — CP-009
  • Admin module override — CP-012

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] All frontend pages render correctly at 360px, 768px, 1024px — N/A (backend-only)
  • [ ] All 5 UI states implemented — N/A (backend-only)
  • [ ] French micro-copy matches design spec exactly — N/A (backend-only)
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Multi-tenant isolation verified (no cross-tenant data leaks)
  • [ ] Offline write+sync works (Papillon) — N/A
  • [ ] No TypeScript any — N/A (backend-only)
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries created for every data mutation