Aller au contenu

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 caching
  • docs/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