Aller au contenu

Story CP-009: Invoice Generation + Pricing Engine

Module: control-plane Slice: Slice 9 from architecture Brand context: Papillon HR Suite Priority: 9 Depends on: CP-001, CP-002, CP-003, CP-004, CP-005 Estimated complexity: L


Objective

Build the core billing engine: invoice generation with FNE-ready fields, pricing segments (S1–S6), platform fee calculation for S4–S6, and the provider-agnostic payment abstraction (with manual payment for MVP). Subscription management UI and billing cycle options are handled in CP-025 and CP-026.


Backend Scope

Entities & Records

Invoice entity (tenant DB):

Field Type Constraints
id UUID v7 PK
tenant_id UUID NOT NULL
invoice_number String NOT NULL, unique per tenant, format FAC-{YYYY}-{sequence}
invoice_date LocalDate NOT NULL
seller_name String NOT NULL ("ALTARYS ENTERPRISE")
seller_rccm String NOT NULL
seller_nif String NOT NULL
seller_address String NOT NULL
buyer_name String NOT NULL
buyer_rccm String Nullable
buyer_nif String Nullable
buyer_address String Nullable
subtotal_ht BigDecimal NOT NULL (zero decimal)
tva_rate BigDecimal NOT NULL (18% for CI)
tva_amount BigDecimal NOT NULL
total_ttc BigDecimal NOT NULL
qr_code String Nullable (empty until FNE DGI integration V2)
fne_fiscal_number String Nullable (empty until V2)
payment_status InvoicePaymentStatus enum NOT NULL
is_proforma boolean NOT NULL, default false (set at generation time based on current NIF status; never changed retroactively)
billing_period_start LocalDate NOT NULL
billing_period_end LocalDate NOT NULL
created_date Instant NOT NULL

InvoicePaymentStatus: DRAFT, SENT, PAID, OVERDUE, CANCELLED

InvoiceLine entity (tenant DB):

Field Type Constraints
id UUID v7 PK
invoice_id UUID FK to Invoice
module_code String NOT NULL
module_name String NOT NULL
quantity int NOT NULL (employee count or 1)
unit_price BigDecimal NOT NULL
line_total BigDecimal NOT NULL
pack_discount_applied boolean default false

Payment entity (tenant DB):

Field Type Constraints
id UUID v7 PK
tenant_id UUID NOT NULL
invoice_id UUID FK to Invoice
provider String NOT NULL (MOBILE_MONEY, BANK_TRANSFER, MANUAL)
amount BigDecimal NOT NULL
currency String(3) NOT NULL, "XOF"
status PaymentStatus enum NOT NULL
provider_reference String Nullable
confirmed_by_user_id UUID Nullable (for manual confirmation)
created_date Instant NOT NULL
confirmed_date Instant Nullable

PaymentStatus: PENDING, PROCESSING, CONFIRMED, FAILED, CANCELLED

PricingSegment (reference data, in code or config):

Segment Employee Count Model
S1 2–12 Flat fee/month
S2 13–29 Flat fee/month
S3 30–49 Flat fee/month
S4 50–99 Per-user/month + Platform Fee
S5 100–199 Per-user/month + Platform Fee
S6 200–350 Per-user/month + Platform Fee

Platform Fee formula (S4–S6): FP = [1 + (x - 1) × 0.15] × base_fee / 12 where x = number of active modules

Segment Base Fee (annual)
S4 360,000 FCFA
S5 540,000 FCFA
S6 660,000 FCFA

Invoice counter table (tenant DB):

Field Type
tenant_id UUID, PK
fiscal_year int, PK
next_value int, NOT NULL, default 1

Repository Layer

  • InvoiceRepository.java — Tenant-aware: findById, findByTenantId(Pageable), save
  • InvoiceLineRepository.java — Tenant-aware: findByInvoiceId
  • PaymentRepository.java — Tenant-aware: findByInvoiceId, save
  • InvoiceCounterRepository.java — Tenant-aware: findByTenantIdAndFiscalYearForUpdate

Service Layer

BillingService.java: - generateInvoice(UUID tenantId) → Invoice — Monthly invoice generation: 1. Snapshot active employee count → determine segment 2. Get active modules → calculate per-module prices for segment 3. Detect packs → apply best discount 4. Calculate platform fee (S4–S6 only) 5. Calculate subtotal HT, TVA (18%), total TTC 6. Generate invoice number via InvoiceNumberGenerator 7. Set is_proforma if tenant has no NIF 8. All amounts BigDecimal, zero decimal places (XOF)

PricingEngine.java: - calculatePriceForSegment(String moduleCode, String segment) → BigDecimal - detectPack(Set<String> activeModules) → Optional<PackResult> — Returns pack name + discounted price if a pack matches. If multiple packs match, returns the one with greatest discount. Pack definitions hardcoded for MVP (5 packs: Contrôle, Finance, Essentiel, Standard, Complet — see PRD FR-CP-022). DB-driven in V2. - calculatePlatformFee(String segment, int activeModuleCount) → BigDecimal

InvoiceNumberGenerator.java: - nextNumber(UUID tenantId, int fiscalYear) → StringFOR UPDATE on counter row → format FAC-{YYYY}-{zeroPadded(5)}. Gap-free (FNE requirement).

PaymentProvider interface:

public interface PaymentProvider {
    PaymentSession initiatePayment(UUID invoiceId, BigDecimal amount, String currency, String returnUrl);
    PaymentConfirmation handleWebhook(String webhookPayload);
    PaymentStatus getPaymentStatus(String paymentSessionId);
}

ManualPaymentProvider.java — MVP implementation: records payment as confirmed by admin. MobileMoneyPaymentProvider.java — Stub for MVP: throws "Not yet implemented". Provider partner TBD.

API Endpoints

Method Path Request Body Response Auth
GET /api/v1/billing/invoices Page ROLE_DG
GET /api/v1/billing/invoices/{id}/pdf PDF byte stream ROLE_DG
POST /api/v1/billing/payments/initiate {invoiceId, provider} PaymentSessionResponse ROLE_DG
POST /api/v1/billing/payments/webhook Provider payload 200 Public (webhook secret)

Validation Rules

  • All monetary amounts: BigDecimal, zero decimal places (XOF)
  • Invoice number: sequential per fiscal year, gap-free (FOR UPDATE + single transaction)
  • TVA rate: 18% for CI
  • Platform fee: only applies to S4–S6
  • Pack detection: automatic, applies best discount

Multi-Tenant Considerations

  • Invoice and Payment entities in tenant DB
  • Invoice counter in tenant DB (per-tenant sequential numbering)
  • Pricing config (segment prices, pack rules) in platform DB or application config

Flyway Migrations

  • V007__create_invoice_payment.sql — Tenant DB: invoices, invoice_lines, payments tables
  • V008__create_counters.sql — Tenant DB: invoice_counters table

Frontend Scope

None for this story. Subscription dashboard and billing UI are in CP-025.


Acceptance Criteria

AC-074: Invoice generation with correct segment
Given a tenant with 25 active employees (segment S2) and modules ABSMGT + QRCONTR
When the monthly billing job runs
Then an invoice is generated with correct per-module prices for S2
And the Pack Contrôle discount is applied (if applicable)

AC-075: Platform fee calculation
Given a tenant in segment S4 (55 employees) with 3 active modules
When the invoice is generated
Then the platform fee is calculated as: [1 + (3-1) × 0.15] × 360,000/12 = 30,000 × 1.30 = 39,000 FCFA

AC-076: FNE-compliant invoice numbering
Given tenant A has invoice FAC-2026-00003
When the next invoice is generated
Then it is numbered FAC-2026-00004 (no gaps)

AC-077: Proforma for incomplete tenant
Given a tenant with status ACTIVE_INCOMPLETE (no NIF)
When an invoice is generated
Then it is flagged is_proforma=true and buyer_nif is empty

OHADA & Regulatory Rules

  • FNE (Arrêté n° 0337, 9 mai 2025): All B2B invoices must be FNE-compliant. Gap-free sequential numbering is mandatory. Buyer NIF required for full compliance. QR code + fiscal number fields reserved for DGI API integration (V2).
  • TVA 18%: Standard CI TVA rate. Applied on all invoices.
  • XOF zero decimal: XOF and XAF are zero-decimal currencies. 15 000 FCFA, never 15 000,00.
  • Invoice immutability: Invoices cannot be modified. Corrections via credit notes (new negative invoice).
  • AUDCIF Art. 24: Invoices retained 10 years minimum.
  • BigDecimal ONLY: All financial calculations use BigDecimal. Never double/float.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §BigDecimal for financials, §Counter tables (FOR UPDATE)
  • docs/standards/database-guidelines.md — §Immutable records (invoices), §Counter tables
  • docs/standards/api-guidelines.md — §Financial API patterns, §PDF generation

Testing Requirements

Unit Tests

  • PricingEngine: segment determination for all 6 segments
  • PricingEngine: pack detection for all 5 packs (Contrôle, Finance, Essentiel, Standard, Complet)
  • PricingEngine: best discount selection when multiple packs match
  • PricingEngine: platform fee formula for S4, S5, S6
  • InvoiceNumberGenerator: sequential, gap-free
  • BillingService: proforma flag when NIF missing

Integration Tests

  • Invoice generation end-to-end: employee count → segment → pricing → invoice
  • Invoice numbering: concurrent requests produce sequential numbers
  • Payment flow: initiate → webhook → confirm → invoice PAID → tenant status update

What QA Will Validate

  • Invoice list with correct statuses
  • PDF download for invoices
  • All amounts display with XOF formatting (space as thousands separator)

Out of Scope

  • Subscription dashboard UI — CP-025
  • Module add/remove from subscription — CP-025
  • Billing cycle (annual/monthly) — CP-026
  • XOF compliance tests — CP-026
  • Mobile Money provider integration — Stub for MVP. Real provider TBD.
  • Automatic payment retry — V2
  • Mid-cycle proration — V2 (new segment applies on next billing cycle)
  • DGI FNE API integration (QR code, fiscal number) — V2
  • Credit notes — V2
  • Admin payment confirmation UI — CP-012

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] 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 Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries created for every data mutation