Story CP-026: Billing Cycle + XOF Compliance¶
Module: control-plane Slice: Slice 9c from architecture (split from CP-009) Brand context: Papillon HR Suite Priority: 9.2 Depends on: CP-009 Estimated complexity: M
Objective¶
Implement billing cycle management (monthly/annual with the "14 months for 12" annual deal) and enforce XOF zero-decimal compliance across all billing APIs and UI. Add the billing cycle toggle to the subscription page.
Backend Scope¶
Entities & Records¶
BillingCycle enum: MONTHLY, ANNUAL
Annual billing rule: tenant pays 12 months upfront and receives 14 months of service (2 months free). The invoice covers a 14-month billing period; billing_period_end = billing_period_start + 14 months.
Payment entity (tenant DB): Already defined in CP-009. Used here for manual payment recording on annual invoices.
Service Layer¶
BillingService.java (additions):
- chooseBillingCycle(UUID tenantId, BillingCycle cycle) → void — Switch between MONTHLY and ANNUAL. Annual: charges 12 months upfront, unlocks 14-month billing period. Change takes effect on next billing cycle.
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| POST | /api/v1/billing/cycle | {cycle: "MONTHLY"\|"ANNUAL"} |
204 | ROLE_DG |
Validation Rules¶
- All monetary amounts: BigDecimal, zero decimal places (XOF)
- Annual invoice amount = 12 × monthly price (not 14)
- Annual billing period = 14 months from start date
- Cycle change takes effect at next billing period (no mid-cycle changes)
Multi-Tenant Considerations¶
- Billing cycle stored per tenant in tenant DB
- Annual invoices follow same FNE-compliant numbering as monthly
Frontend Scope¶
Components¶
Billing cycle toggle added to the subscription dashboard page (from CP-025).
UI States¶
| State | Behavior | French Copy |
|---|---|---|
| Monthly selected | Monthly cycle active | "Facturation mensuelle" |
| Annual selected | Annual cycle active with savings badge | "Facturation annuelle" |
| Confirm switch | Confirmation dialog | "Passer à la facturation annuelle ? Vous serez facturé pour 12 mois et bénéficierez de 14 mois de service." |
| Success | Toast + page update | "Facturation annuelle — prochaine échéance : {date}" |
Interactions¶
- Billing cycle toggle on subscription page: "Facturation mensuelle" ↔ "Facturation annuelle"
- Selecting annual shows confirmation dialog with savings info
- After confirmation, page updates to show annual billing info + savings label
French micro-copy:
| Key | French Text |
|---|---|
billing.cycle.annual |
"Facturation annuelle" |
billing.cycle.monthly |
"Facturation mensuelle" |
billing.cycle.savings |
"Économie : 2 mois offerts" |
billing.cycle.switch_confirm |
"Passer à la facturation annuelle ? Vous serez facturé pour 12 mois et bénéficierez de 14 mois de service." |
Acceptance Criteria¶
AC-081: All amounts in XOF (zero decimals)
Given any billing API response
When monetary values are returned
Then all amounts have zero decimal places (e.g., 15000, not 15000.00)
AC-082: Annual billing — 14 months for price of 12
Given a tenant with billing cycle ANNUAL
When the annual invoice is generated
Then the invoice covers a 14-month billing period (billing_period_end = billing_period_start + 14 months)
And the invoiced amount equals 12 × the monthly price for the tenant's active modules and segment
And the invoice line descriptions indicate the annual rate
AC-083: Switch billing cycle to annual
Given I am DG and navigate to /admin/subscription
When I select "Facturation annuelle" and confirm
Then a POST to /api/v1/billing/cycle with {"cycle": "ANNUAL"} is sent
And the page shows "Facturation annuelle — prochaine échéance : {date}"
And the savings label shows "Économie : 2 mois offerts"
OHADA & Regulatory Rules¶
- XOF zero decimal: XOF and XAF are zero-decimal currencies. 15 000 FCFA, never 15 000,00.
- BigDecimal ONLY: All financial calculations use BigDecimal. Never double/float.
- FNE compliance: Annual invoices follow the same FNE-compliant sequential numbering as monthly invoices.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §BigDecimal for financialsdocs/standards/react-typescript-guidelines.md— §Toggle components, §Confirmation dialogsdocs/standards/api-guidelines.md— §Financial API patterns
Testing Requirements¶
Unit Tests¶
- BillingCycle enum serialization/deserialization
- Annual invoice amount = 12 × monthly (not 14)
- Annual billing period = start + 14 months
- All monetary BigDecimal values have scale 0 (zero decimals)
Integration Tests¶
- Switch billing cycle: MONTHLY → ANNUAL → verify next invoice is annual
- XOF compliance: all API responses return zero-decimal amounts
- Annual invoice generation end-to-end
What QA Will Validate¶
- Billing cycle toggle displays correctly
- Confirmation dialog for annual switch
- Savings label "Économie : 2 mois offerts" appears after switch
- All amounts display with XOF formatting (space as thousands separator, zero decimals)
Out of Scope¶
- Invoice generation core logic — CP-009
- Subscription dashboard layout — CP-025
- Module add/remove — CP-025
- Mid-cycle proration — V2
- Automatic billing cycle renewal — V2
Definition of Done¶
- [ ] All backend endpoints implemented and returning correct responses
- [ ] Billing cycle toggle renders correctly at 360px, 768px, 1024px
- [ ] French micro-copy matches design spec exactly
- [ ] All acceptance criteria pass manually
- [ ] Unit tests written and passing
- [ ] Integration tests written and passing
- [ ] Multi-tenant isolation verified (no cross-tenant data leaks)
- [ ] No TypeScript
any— all types explicit - [ ] No Java raw types — all generics explicit
- [ ] API responses follow standard envelope format
- [ ] Audit trail entries created for billing cycle changes