Story CP-009 : Génération de Factures + Moteur de Tarification¶
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
Objectif¶
Construire le moteur de facturation principal : génération de factures avec champs conformes FNE, segments de tarification (S1–S6), calcul des frais de plateforme pour S4–S6, et l'abstraction de paiement agnostique au fournisseur (avec paiement manuel pour le MVP). L'UI de gestion des abonnements et les options de cycle de facturation sont gérées dans CP-025 et CP-026.
Périmètre Backend¶
Entités & Records¶
Entité Invoice (base tenant) :
| Champ | Type | Contraintes |
|---|---|---|
| id | UUID v7 | PK |
| tenant_id | UUID | NOT NULL |
| invoice_number | String | NOT NULL, unique par 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 (zéro décimale) |
| tva_rate | BigDecimal | NOT NULL (18% pour CI) |
| tva_amount | BigDecimal | NOT NULL |
| total_ttc | BigDecimal | NOT NULL |
| qr_code | String | Nullable (vide jusqu'à l'intégration API DGI FNE V2) |
| fne_fiscal_number | String | Nullable (vide jusqu'à V2) |
| payment_status | InvoicePaymentStatus enum | NOT NULL |
| is_proforma | boolean | NOT NULL, défaut false (défini à la génération selon le statut NIF courant ; jamais modifié rétroactivement) |
| billing_period_start | LocalDate | NOT NULL |
| billing_period_end | LocalDate | NOT NULL |
| created_date | Instant | NOT NULL |
InvoicePaymentStatus : DRAFT, SENT, PAID, OVERDUE, CANCELLED
Entité InvoiceLine (base tenant) :
| Champ | Type | Contraintes |
|---|---|---|
| id | UUID v7 | PK |
| invoice_id | UUID | FK vers Invoice |
| module_code | String | NOT NULL |
| module_name | String | NOT NULL |
| quantity | int | NOT NULL (nombre d'employés ou 1) |
| unit_price | BigDecimal | NOT NULL |
| line_total | BigDecimal | NOT NULL |
| pack_discount_applied | boolean | défaut false |
Entité Payment (base tenant) :
| Champ | Type | Contraintes |
|---|---|---|
| id | UUID v7 | PK |
| tenant_id | UUID | NOT NULL |
| invoice_id | UUID | FK vers 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 (pour confirmation manuelle) |
| created_date | Instant | NOT NULL |
| confirmed_date | Instant | Nullable |
PaymentStatus : PENDING, PROCESSING, CONFIRMED, FAILED, CANCELLED
PricingSegment (données de référence, en code ou config) :
| Segment | Nombre d'employés | Modèle |
|---|---|---|
| S1 | 2–12 | Forfait/mois |
| S2 | 13–29 | Forfait/mois |
| S3 | 30–49 | Forfait/mois |
| S4 | 50–99 | Par utilisateur/mois + Frais de plateforme |
| S5 | 100–199 | Par utilisateur/mois + Frais de plateforme |
| S6 | 200–350 | Par utilisateur/mois + Frais de plateforme |
Formule des frais de plateforme (S4–S6) : FP = [1 + (x - 1) × 0,15] × base_fee / 12
où x = nombre de modules actifs
| Segment | Frais de base (annuel) |
|---|---|
| S4 | 360 000 FCFA |
| S5 | 540 000 FCFA |
| S6 | 660 000 FCFA |
Table de compteur de factures (base tenant) :
| Champ | Type |
|---|---|
| tenant_id | UUID, PK |
| fiscal_year | int, PK |
| next_value | int, NOT NULL, défaut 1 |
Couche Repository¶
InvoiceRepository.java— Tenant-aware : findById, findByTenantId(Pageable), saveInvoiceLineRepository.java— Tenant-aware : findByInvoiceIdPaymentRepository.java— Tenant-aware : findByInvoiceId, saveInvoiceCounterRepository.java— Tenant-aware : findByTenantIdAndFiscalYearForUpdate
Couche Service¶
BillingService.java :
- generateInvoice(UUID tenantId) → Invoice — Génération de facture mensuelle :
1. Snapshot du nombre d'employés actifs → déterminer le segment
2. Obtenir les modules actifs → calculer les prix par module pour le segment
3. Détecter les packs → appliquer la meilleure remise
4. Calculer les frais de plateforme (S4–S6 uniquement)
5. Calculer le sous-total HT, TVA (18%), total TTC
6. Générer le numéro de facture via InvoiceNumberGenerator
7. Définir is_proforma si le tenant n'a pas de NIF
8. Tous les montants BigDecimal, zéro décimale (XOF)
PricingEngine.java :
- calculatePriceForSegment(String moduleCode, String segment) → BigDecimal
- detectPack(Set<String> activeModules) → Optional<PackResult> — Retourne le nom du pack + prix remisé si un pack correspond. Si plusieurs packs correspondent, retourne celui avec la remise la plus importante. Définitions de packs codées en dur pour le MVP (5 packs : Contrôle, Finance, Essentiel, Standard, Complet — voir PRD FR-CP-022). Piloté par base de données en V2.
- calculatePlatformFee(String segment, int activeModuleCount) → BigDecimal
InvoiceNumberGenerator.java :
- nextNumber(UUID tenantId, int fiscalYear) → String — FOR UPDATE sur la ligne de compteur → format FAC-{YYYY}-{zeroPadded(5)}. Sans lacune (exigence FNE).
Interface PaymentProvider :
public interface PaymentProvider {
PaymentSession initiatePayment(UUID invoiceId, BigDecimal amount, String currency, String returnUrl);
PaymentConfirmation handleWebhook(String webhookPayload);
PaymentStatus getPaymentStatus(String paymentSessionId);
}
ManualPaymentProvider.java — Implémentation MVP : enregistre le paiement comme confirmé par l'admin. MobileMoneyPaymentProvider.java — Stub pour MVP : lève "Not yet implemented". Partenaire fournisseur à définir.
Endpoints API¶
| Méthode | Chemin | Corps de requête | Réponse | Auth |
|---|---|---|---|---|
| GET | /api/v1/billing/invoices | — | Page |
ROLE_DG |
| GET | /api/v1/billing/invoices/{id}/pdf | — | Flux d'octets PDF | ROLE_DG |
| POST | /api/v1/billing/payments/initiate | {invoiceId, provider} |
PaymentSessionResponse | ROLE_DG |
| POST | /api/v1/billing/payments/webhook | Payload fournisseur | 200 | Public (webhook secret) |
Règles de validation¶
- Tous les montants monétaires : BigDecimal, zéro décimale (XOF)
- Numéro de facture : séquentiel par année fiscale, sans lacune (FOR UPDATE + transaction unique)
- Taux TVA : 18% pour CI
- Frais de plateforme : s'appliquent uniquement à S4–S6
- Détection de pack : automatique, applique la meilleure remise
Considérations multi-tenant¶
- Entités Invoice et Payment dans la base tenant
- Compteur de factures dans la base tenant (numérotation séquentielle par tenant)
- Config de tarification (prix par segment, règles de pack) dans la base plateforme ou la config applicative
Migrations Flyway¶
V007__create_invoice_payment.sql— Base tenant : tables invoices, invoice_lines, paymentsV008__create_counters.sql— Base tenant : table invoice_counters
Périmètre Frontend¶
Aucun pour cette story. Le tableau de bord des abonnements et l'UI de facturation sont dans CP-025.
Critères d'acceptation¶
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
Règles OHADA & Réglementaires¶
- FNE (Arrêté n° 0337, 9 mai 2025) : Toutes les factures B2B doivent être conformes FNE. La numérotation séquentielle sans lacune est obligatoire. Le NIF de l'acheteur est requis pour une pleine conformité. Les champs QR code et numéro fiscal sont réservés pour l'intégration API DGI (V2).
- TVA 18% : Taux TVA CI standard. Appliqué sur toutes les factures.
- XOF zéro décimale : XOF et XAF sont des devises sans décimale. 15 000 FCFA, jamais 15 000,00.
- Immuabilité des factures : Les factures ne peuvent pas être modifiées. Corrections via notes de crédit (nouvelle facture négative).
- AUDCIF Art. 24 : Factures conservées 10 ans minimum.
- BigDecimal UNIQUEMENT : Tous les calculs financiers utilisent BigDecimal. Jamais de 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 tablesdocs/standards/api-guidelines.md— §Financial API patterns, §PDF generation
Exigences de test¶
Tests unitaires¶
- PricingEngine : détermination du segment pour les 6 segments
- PricingEngine : détection de pack pour les 5 packs (Contrôle, Finance, Essentiel, Standard, Complet)
- PricingEngine : sélection de la meilleure remise lorsque plusieurs packs correspondent
- PricingEngine : formule des frais de plateforme pour S4, S5, S6
- InvoiceNumberGenerator : séquentiel, sans lacune
- BillingService : flag proforma quand le NIF est absent
Tests d'intégration¶
- Génération de facture de bout en bout : nombre d'employés → segment → tarification → facture
- Numérotation des factures : les requêtes concurrentes produisent des numéros séquentiels
- Flux de paiement : initier → webhook → confirmer → facture PAID → mise à jour du statut tenant
Ce que le QA validera¶
- Liste de factures avec les statuts corrects
- Téléchargement PDF des factures
- Tous les montants affichés au format XOF (espace comme séparateur de milliers)
Hors périmètre¶
- UI tableau de bord des abonnements — CP-025
- Ajout/suppression de module depuis l'abonnement — CP-025
- Cycle de facturation (annuel/mensuel) — CP-026
- Tests de conformité XOF — CP-026
- Intégration du fournisseur Mobile Money — Stub pour MVP. Fournisseur réel à définir.
- Relance automatique du paiement — V2
- Proratisation en cours de cycle — V2 (le nouveau segment s'applique au prochain cycle de facturation)
- Intégration API DGI FNE (QR code, numéro fiscal) — V2
- Notes de crédit — V2
- UI de confirmation de paiement admin — CP-012
Définition de Done¶
- [ ] Tous les endpoints backend implémentés et retournant les réponses correctes
- [ ] Tous les critères d'acceptation passent manuellement
- [ ] Tests unitaires écrits et passants
- [ ] Tests d'intégration écrits et passants
- [ ] Isolation multi-tenant vérifiée (aucune fuite de données inter-tenant)
- [ ] Pas de types Java bruts — tous les génériques explicites
- [ ] Les réponses API respectent le format d'enveloppe standard
- [ ] Entrées d'audit créées pour chaque mutation de données