Aller au contenu

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), save
  • InvoiceLineRepository.java — Tenant-aware : findByInvoiceId
  • PaymentRepository.java — Tenant-aware : findByInvoiceId, save
  • InvoiceCounterRepository.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) → StringFOR 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, payments
  • V008__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 tables
  • docs/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