Story CP-020: Module Gating: Suspended Tenant + Module Catalog API¶
Module: control-plane Slice: Slice 7b from architecture (split from CP-007) Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 7 (after CP-007) Depends on: CP-007 Estimated complexity: S
Objective¶
Extend the module gating backend with two capabilities: (1) suspended tenant write denial — when a tenant is SUSPENDED, all write operations (POST, PUT, PATCH, DELETE) on gated modules are blocked while reads (GET) remain allowed; and (2) the module catalog API that returns subscribed modules and available modules with pricing for the tenant's segment.
Backend Scope¶
Entities & Records¶
Reuses entities from CP-007:
- TenantModule entity (already created in CP-007)
- ModuleStatus enum: ACTIVE, INACTIVE, TRIAL (already created in CP-007)
DTOs (from CP-007, used here):
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¶
Reuses TenantModuleRepository from CP-007. No new repositories.
Service Layer¶
ModuleGatingInterceptor.java (extend from CP-007): - Add SUSPENDED tenant check: IF tenant status is SUSPENDED → deny ALL write operations (POST, PUT, PATCH, DELETE) for gated modules. Read (GET) remains allowed. - HTTP 403 response for suspended writes:
{
"error": "TENANT_SUSPENDED",
"message": "Votre compte est suspendu. Les modifications ne sont pas autorisées."
}
ModuleGatingService.java (extend from CP-007):
- getModuleCatalog(UUID tenantId) → ModuleCatalogResponse — NEW method
- Returns subscribedModules (tenant's active/trial modules) + availableModules (all sellable modules not subscribed, with prices for tenant's employee count segment)
- Module metadata (name, description, icon, price) is hardcoded in a ModuleCatalog constant class (not DB-driven for MVP)
Sellable module codes (reference from CP-007): 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)
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/tenant/modules/catalog | — | ModuleCatalogResponse | Any authenticated |
Validation Rules¶
- Catalog returns only valid sellable modules
- Prices returned correspond to tenant's employee count segment
- BUDMGT never appears in availableModules (auto-included only)
Multi-Tenant Considerations¶
- Module catalog is tenant-scoped (each tenant sees their subscriptions + available modules)
- Suspended tenant check uses tenant status from TenantContext (resolved by CP-001 infrastructure)
- Catalog response cached per tenant in Caffeine (TTL: 5 min, invalidated on module status change)
Frontend Scope¶
No frontend in this slice. Frontend integration (navigation + upsell cards) is 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-056: Suspended tenant blocks writes
Given my tenant is SUSPENDED
When I call POST /api/v1/employees (write operation)
Then I receive HTTP 403 with {"error": "TENANT_SUSPENDED"}
When I call GET /api/v1/employees (read operation)
Then the request succeeds (reads allowed during suspension)
AC-060: Module catalog returns correct data
Given my tenant has 2 active modules and 13 available
When I call GET /api/v1/tenant/modules/catalog
Then I receive subscribedModules (2) and availableModules (13 with prices for my segment)
OHADA & Regulatory Rules¶
- Business rule: suspended tenants retain read access to all their data. This is important for AUDCIF Art. 24 compliance — financial data must remain accessible for 10 years even during suspension.
- No direct OHADA regulatory requirements for module catalog.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §Spring Interceptors, §Caffeine cachingdocs/standards/api-guidelines.md— §403 error format for tenant suspension, §Module catalog response format
Testing Requirements¶
Unit Tests¶
- ModuleGatingInterceptor: SUSPENDED tenant → write denied (POST, PUT, PATCH, DELETE)
- ModuleGatingInterceptor: SUSPENDED tenant → read allowed (GET)
- ModuleGatingService: catalog returns correct subscribed vs available split
- ModuleGatingService: BUDMGT excluded from available modules
Integration Tests¶
- Suspended tenant: POST → 403, GET → success
- Catalog endpoint: correct module counts and prices for tenant segment
- Catalog: tenant A sees different subscriptions than tenant B (tenant isolation)
What QA Will Validate¶
- Suspended tenant write denial verified via API calls
- Module catalog returns expected structure and data
Out of Scope¶
- Navigation integration + upsell cards (frontend) — CP-021
- Module activation/deactivation UI — CP-009
- Per-module suspension — V2
- 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 — 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