Facturation
Facturation
Les modules billing (app/mypromo/server/modules/billing/) et einvoicing (app/mypromo/server/modules/einvoicing/) couvrent respectivement le cycle financier des réservations et la transmission électronique des factures vers les opérateurs de dématérialisation partenaires (ODP).
Modèles économiques
Le module billing supporte deux modèles, pilotés par le champ billingModel d'une réservation :
| Valeur | Description |
|---|---|
legacy_mandate | Modèle mandat d'encaissement (historique) — paiement en deux temps : arrhes à la réservation, solde en fin de prestation |
principal_seller | Modèle vendeur principal (cible) — paiement 100 % à la réservation, facture unique plateforme → industriel |
La fonction resolveBillingModel dans billing/domain/billing-model-rules.ts retourne legacy_mandate par défaut si la valeur est absente ou inconnue. Les nouvelles réservations utilisent principal_seller.
Reprise de données — les réservations créées avant la Phase 1 du chantier facturation (2026-07) restent stockées avec
pricingCommissionAmount = 0etbillingModel = legacy_mandatepar défaut. Un script de reprise (hors périmètre Phase 1, cf. risque R5 du plandocs/ai/tasks/2026-07-05-facturation-...) devra être exécuté avant la mise en production pour aligner l'historique surprincipal_seller. Ne pas lancer la collecte enprincipal_sellersur ces réservations sans reprise préalable — la commission serait à 0 %.
Modèle legacy — mandat d'encaissement
Dans ce modèle :
- La plateforme agit comme mandataire d'encaissement pour le compte du magasin.
- Le magasin est le vendeur officiel de la prestation ; l'industriel ne reçoit jamais de facture de la plateforme.
- Le paiement se déroule en deux étapes : arrhes (dépôt) à la réservation, solde en fin de prestation.
Structure économique (exemple avec loyer HT magasin = 10 000 €, commission 20 %) :
| Flux | Montant HT |
|---|---|
| Arrhes magasin (ex. 30 % du loyer) | 3 000 € |
| Commission plateforme (intégrée aux arrhes) | 2 000 € |
| Total arrhes perçues | 5 000 € |
| Solde fin de prestation | 7 000 € |
| Total facturé industriel | 12 000 € |
| Reversement net magasin (après commission) | 10 000 € |
Les montants sont exprimés en centimes (amountCents) dans toutes les interfaces internes. La conversion entier → centimes applique Math.round(value * 100).
Types de facture legacy (BillingInvoiceKind) :
| Kind | Émetteur → Destinataire | Usage |
|---|---|---|
store_deposit | Magasin → Industriel | Facture d'arrhes émise par mandat |
store_balance | Magasin → Industriel | Facture de solde en fin de prestation |
platform_commission | Plateforme → Magasin | Commission plateforme prélevée sur flux magasin |
Ces types restent lisibles pour l'historique ; les nouvelles réservations n'en génèrent plus.
Modèle vendeur principal
Dans ce modèle :
- La plateforme est vendeur officiel : elle facture l'industriel en son nom propre.
- Paiement 100 % à la réservation (statut
reservation_total_due→reservation_total_paid). - La plateforme émet une auto-facture fournisseur (self-billing) pour la part magasin.
Structure économique :
industrialTotalAmount = storeBaseAmount + platformCommissionAmount
Les taux sont exprimés en points de base (bps, 1 bps = 0,01 %). À titre d'exemple, une commission de par exemple 2 000 bps (20 %) correspond à 20 % du montant magasin. Le taux réel est appliqué à la création de la réservation via readPlatformCommissionRateBps() (billing/domain/commission-config.ts), qui lit la variable d'environnement globale PLATFORM_COMMISSION_RATE_BPS — cf. section « Variables d'environnement » ci-dessous. La validation de cohérence est assurée par computeReservationFinancialBreakdown dans billing/domain/financial-rules.ts.
À la création (campaigns/application/reservations/reservation-service.ts) et à l'acceptation d'une contre-proposition (counter-proposal-service.ts), les réservations sont désormais persistées avec billingModel = principal_seller et une commission calculée sur le loyer magasin HT (buildReservationPricingFromCartItem / buildReservationPricingFromBaseEuros). L'invariant grossAmount = commissionAmount + netDistributorAmount est vérifié par assertReservationInvariants avant écriture.
Types de facture cible (BillingInvoiceKind) :
| Kind | Émetteur → Destinataire | Usage |
|---|---|---|
platform_sale | Plateforme → Industriel | Facture de vente principale |
platform_credit_note | Plateforme → Industriel | Avoir sur facture de vente |
supplier_self_billed | Magasin → Plateforme | Auto-facture fournisseur (self-billing) |
supplier_credit_note | Magasin → Plateforme | Avoir sur auto-facture fournisseur |
Cycle de vie financier
Statuts financiers (BillingFinancialStatus)
Définis dans billing/domain/financial-status-rules.ts :
draft
├── deposit_due ──→ deposit_paid ──→ balance_due ──→ balance_paid (legacy)
├── reservation_total_due ──→ reservation_total_paid (principal_seller)
│ ├── partially_refunded ──→ fully_refunded
│ ├── supplier_settlement_pending ──→ supplier_settlement_paid
│ └── written_off
└── cancelled
Note — chemin nominal uniquement. L'arbre ci-dessus représente le chemin heureux. En réalité,
cancelledetwritten_offsont accessibles depuis la plupart des statuts : voir la mapBILLING_FINANCIAL_TRANSITIONSdansfinancial-status-rules.tspour les transitions exactes autorisées.Par ailleurs, le passage de
draftàreservation_total_duen'est pas une transition formelle au sens deBILLING_FINANCIAL_TRANSITIONS: il s'effectue via unupsertdirect dansreservation-total-collection.ts, qui écrit le statutreservation_total_duesans passer par le mécanisme de transition.
Valeurs exhaustives : draft · deposit_due · deposit_paid · balance_due · balance_paid · reservation_total_due · reservation_total_paid · partially_refunded · fully_refunded · supplier_settlement_pending · supplier_settlement_paid · cancelled · written_off.
Statuts de paiement (BillingPaymentStatus)
Définis dans billing/domain/payment-order-rules.ts :
pending ──→ submitted ──→ processing ──→ paid
└──→ failed
└──→ cancelled
Types d'ordre de paiement (BillingPaymentKind)
deposit · balance · reservation_total · refund · supplier_payout.
Méthodes de paiement (BillingPaymentMethod)
sepa_dd · bank_transfer.
Contrainte : sepa_dd requiert un mandateRef valide (mandat SEPA actif pour l'organisation).
Statuts de facture (BillingInvoiceStatus)
issued · partially_paid · paid · cancelled · credited.
Flux de collecte — modèle vendeur principal
L'endpoint POST /api/billing/reservations/{reservationId}/collect déclenche la collecte complète :
- Vérification que la réservation est en statut
approvedoucompleted. - Vérification que le modèle est
principal_seller. - Résolution du mandat SEPA actif (si
paymentMethod = sepa_dd). - Création de l'ordre de paiement (
kind = reservation_total,status = pending → paid). - Transition financière
reservation_total_due → reservation_total_paid. - Émission de la facture
platform_sale. - Écriture des entrées de grand livre (trois lignes : débit industriel, crédit revenus plateforme, crédit fournisseur payable).
Le header Idempotency-Key prévient les doubles collectes : si une clé est connue, la réponse retourne le résultat existant sans rejouer le flux.
Auto-collecte à l'approbation (Phase 7 — D2 révisé)
Depuis la Phase 7, la collecte est automatiquement déclenchée à chaque passage de la réservation à approved — politique « on facture lorsque l'industriel réserve ». Deux points de transition sont câblés (les seuls existants) :
reviewReservationAsDistributor(campaigns/application/reservations/distributor-review-service.ts) — validation distributeur avecstatus = 'approved'.respondToCounterProposalAsIndustrial(.../counter-proposal-service.ts) — acceptation par l'industriel d'une contre-proposition (decision = 'accept').
Le trigger unique triggerAutoCollectOnReservationApprovalNonBlocking (billing/application/reservation-approval-collect-trigger.ts) :
- Fait tourner
collectReservationTotalForReservationenvoid-promesse, JAMAIS bloquant : une erreur de collecte n'annule PAS l'approbation. L'état financier restereservation_total_dueet la relance passe par l'endpointPOST .../collectexistant. - Choix de méthode :
sepa_ddsi un mandat SEPA actif existe pour l'industriel, sinonbank_transfer(aucune tentative de collecte SEPA sans mandat). - Idempotency-Key déterministe :
auto-collect-<reservationId>— un second passage àapproved(rare) ne double-collecte pas. - Skip en dehors de
principal_seller— les réservations legacy conservent leur flux manuel.
Télémétrie :
| Scope | Émission |
|---|---|
billing.reservation.collect.auto_triggered | Trigger appelé (avec paymentMethod choisi) |
billing.reservation.collect.auto_succeeded | Collecte automatique OK |
billing.reservation.collect.auto_failed | Collecte automatique KO (code + errorMessage) |
Annulations et avoirs
POST /api/billing/invoices/{invoiceId}/credit-note— émet un avoir sur une facture existante (permissionbilling.manage.all).POST /api/billing/invoices/{invoiceId}/refund— déclenche un remboursement partiel ou total (permissionbilling.manage.all). AccepteamountCents,reason,paymentMethod,Idempotency-Key.
Seules les factures de kind platform_sale sont remboursables via ce flux. Un avoir parallèle sur la dette fournisseur doit être géré séparément.
API — exemples représentatifs
Authentification — la plateforme utilise des sessions par cookie (Better Auth). La session est établie à la connexion et transmise automatiquement via le cookie
better-auth.session_token. Dans les exemples curl, utilisez-b 'better-auth.session_token=<jeton-de-session>'.
Résumé financier d'une réservation
Accessible aux membres de l'organisation (rôles manager, member, billing).
GET /api/billing/reservations/{reservationId}/summary
Cookie: better-auth.session_token=<jeton-de-session>
Collecter le total de réservation (modèle vendeur principal)
Requiert KYB approuvé (billing.collect), rôles manager ou billing.
POST /api/billing/reservations/{reservationId}/collect
Content-Type: application/json
Idempotency-Key: res-collect-<reservationId>
Cookie: better-auth.session_token=<jeton-de-session>
{
"paymentMethod": "sepa_dd"
}
Réponse en cas de succès :
{
"reservationId": "...",
"paymentOrderId": "po_tot_...",
"invoiceNumber": "FAC-RES-...",
"financialStatus": "reservation_total_paid",
"paymentStatus": "paid",
"idempotent": false
}
Lister les factures de l'organisation connectée
GET /api/billing/invoices?limit=50&kind=platform_sale&kind=platform_credit_note
Cookie: better-auth.session_token=<jeton-de-session>
Query parameters :
| Param | Type | Description |
|---|---|---|
limit | number | Nombre max d'items retournés (défaut 50, max 200). |
kind | string[] | Filtre par BillingInvoiceKind (répétable). Les valeurs inconnues sont ignorées ; laisser vide pour toutes. L'espace industriel envoie platform_sale + platform_credit_note, le distributeur supplier_self_billed + supplier_credit_note. |
Réponse — pour chaque item : invoiceNumber, kind, status, amountExclTaxCents, vatAmountCents, vatRatePercent, amountInclTaxCents (recalculés serveur via computeVatBreakdownCents + readPlatformVatRatePercent() — la TVA n'étant pas persistée en base, on la calcule à la volée pour rester cohérent avec le PDF Factur-X et le payload Agicap), issuedAt, dueAt, hasPdf (booléen : indique si le PDF Factur-X est disponible ; jamais la clé S3 elle-même).
Émettre un avoir (admin)
POST /api/billing/invoices/{invoiceId}/credit-note
Content-Type: application/json
Cookie: better-auth.session_token=<jeton-de-session>
{
"reason": "Annulation exceptionnelle — accord commercial"
}
Déclencher un remboursement (admin)
POST /api/billing/invoices/{invoiceId}/refund
Content-Type: application/json
Idempotency-Key: refund-<invoiceId>-<timestamp>
Cookie: better-auth.session_token=<jeton-de-session>
{
"amountCents": 50000,
"reason": "Remboursement partiel convenu",
"paymentMethod": "bank_transfer"
}
Collecte legacy (dépôt / solde)
Les endpoints suivants restent opérationnels pour les réservations historiques en legacy_mandate :
POST /api/billing/reservations/{reservationId}/deposit/collect
POST /api/billing/reservations/{reservationId}/balance/collect
Ces routes sont vouées à être dépréciées une fois l'historique épuisé. Elles ne doivent pas être utilisées pour les nouvelles réservations.
Module einvoicing — facturation électronique
Rôle et architecture
Le module einvoicing orchestre la transmission des factures légalement émises par la plateforme vers l'opérateur de dématérialisation partenaire (Agicap). Il est découplé du module billing : il consomme les événements d'émission de factures via un orchestrateur interne et maintient un état de transmission indépendant.
Chemin : app/mypromo/server/modules/einvoicing/.
Intégration Agicap (Phase 3)
Le provider Agicap est branché sur la vraie API E-Invoicing v1 :
- Authentification OAuth2
client_credentials— endpointAGICAP_TOKEN_URL(défauthttps://myaccount.agicap.com/connect/token), scopeagicap:public-api. Le token est mis en cache mémoire parcreateAgicapTokenProvider(infrastructure/agicap-oauth-client.ts), rafraîchi ~60 s avant expiration. Le secret n'est jamais journalisé (contrat testé). - Header
X-FlowMode—SandboxouProduction, valeur pilotée parAGICAP_FLOW_MODE, envoyée sur chaque requête. - Soumission Factur-X —
POST {AGICAP_BASE_URL}/public/einvoicing/v1/flowsenmultipart/form-dataà deux parts :file: le PDF Factur-X (Blobapplication/pdf).flowInfo: string JSON simple{"entityId": <AGICAP_ENTITY_ID>, "flowSyntax": "Factur-X"}. Piège documenté par myIT : un Blob avecfilenamesurflowInfoest traité par le binding ASP.NET comme un upload et déclenche unHTTP 400 flowInfo field is required. Le client (infrastructure/agicap-http-client.ts) le force en string.
- Statut —
POST {AGICAP_BASE_URL}/public/einvoicing/v1/flows/search, body{ where: { flowMode, flowId }, limit, offset }. Il n'y a plus de webhook fiable : la progressionPending → Ok/Error/Archivedest captée uniquement par polling. - Retries — 2 essais supplémentaires sur
429/503(backoff simple, 500 ms × attempt) intégrés au client HTTP. - Mapping Agicap ↔ machine à états myPromo :
Ack Agicap Statut provider ( EinvoiceProviderStatus)Statut transmission ( EinvoiceTransmissionStatus) viasyncEinvoiceTransmissionStatusPendingprocessingsent(aucune transition destructive tant qu'Agicap n'a pas tranché)Okacceptedsent → acknowledged(terminal)Archivedacceptedsent → acknowledged(terminal)Errorrejectedsent → rejected(terminal)(inconnu) processingreste sent(on repasse au prochain polling) - Prérequis PDF — la soumission requiert un PDF Factur-X pointé par
EinvoiceTransmission.documentPdfKey. Sans PDF, le dispatch fail enEINVOICE_MISSING_PDF. Le champ est alimenté automatiquement par le hook post-facture billing (Phase 4 — FIN-100, cf. section « Flux billing → einvoicing » ci-dessous). EnE2E_TEST_MODE=1, le client HTTP est stubé (aucun appel réseau) et le dispatch tolère undocumentPdfKeyabsent. - Webhook déprécié —
POST /api/webhooks/agicaprépond désormais410 Goneavec{ ok: false, error: { code: 'WEBHOOK_DEPRECATED', ... } }et journalise l'appelant (compat historique). - Polling — activé si
EINVOICING_POLLING_ENABLED=true. Le plugin Nitroserver/plugins/25-einvoicing-polling.tsdémarre une bouclesetInterval(défaut 15 min) qui charge jusqu'à 50 transmissions ensent/dispatchinget appellesyncEinvoiceTransmissionStatussur chacune. Une erreur isolée ne casse pas le lot ; arrêt propre au hookclosede Nitro.
Types de flux (EinvoiceFlowType)
La politique de routage est définie dans einvoicing/domain/einvoice-routing-policy.ts :
| Flow | Condition |
|---|---|
fr_domestic_b2b | Émetteur FR, destinataire FR — transmission structurée obligatoire |
eu_intra_b2b | Émetteur FR, destinataire UE — e-reporting |
export_b2b | Émetteur FR, destinataire hors UE — e-reporting |
Seul le flux fr_domestic_b2b déclenche une transmission structurée ; les autres génèrent un e-reporting. Seul l'émetteur FR est supporté à ce stade.
Types de documents (EinvoiceDocumentKind)
invoice · credit_note.
Cycle de vie d'une transmission (EinvoiceTransmissionStatus)
Défini dans einvoicing/domain/einvoice-status-rules.ts :
pending_dispatch ──→ dispatching ──→ sent ──→ acknowledged (terminal)
└──→ rejected (terminal)
└──→ failed ──→ dispatching (retry)
└──→ cancelled (terminal)
Statuts terminaux : acknowledged · rejected · cancelled.
Seul le statut failed autorise un retry (transition vers dispatching).
Depuis la Phase 3 (Agicap réel) :
- Le dispatch Agicap ne renvoie qu'un
flowIdimmédiat — la transmission passe ensent(providerStatus=processing, correspondant à l'ack AgicapPending). - Le polling
/flows/searchfait ensuite avancer la transmission versacknowledged(Ok/Archived) ourejected(Error). - Un statut Agicap inconnu ne provoque aucune transition destructive : la transmission reste
sentet sera reprise au prochain polling.
Flux billing → einvoicing (Phase 4 — FIN-100 résolu)
Depuis la Phase 4, chaque facture billing émise déclenche automatiquement la création d'une transmission Agicap (draft pending_dispatch) — le GAP historique « billing n'appelle jamais createEinvoiceTransmissionDraft » (FIN-100) est refermé.
Hook post-facture non-bloquant
Un hook unique runPostInvoiceHooksNonBlocking(invoiceNumber) (billing/application/post-invoice-hook.ts) est déclenché en void-promesse après émission d'une facture par :
reservation-total-collection.ts— factureplatform_sale(FAC-RES-*).credit-notes.ts— avoir (kind mappé viamapSourceKindToCreditNoteKind:platform_sale → platform_credit_note,supplier_self_billed → supplier_credit_note).supplier-settlement.ts— autofacturesupplier_self_billed(FAC-SUP-*).
Le hook enchaîne :
- Génération PDF Factur-X + stockage S3 + persistance
BillingInvoice.pdfStorageKey(Phase 2 —generateAndStoreInvoicePdfNonBlocking). - Résolution du kind einvoicing via
mapBillingKindToEinvoiceKind(platform_sale/supplier_self_billed → invoice,*_credit_note → credit_note, mapper pur domaine, cf.billing/domain/billing-einvoicing-mapper.ts). - Résolution du pays acheteur (par défaut FR —
PLATFORM_COUNTRY_CODE, dérivé deOrganizationBillingProfile.billingAddressCountrypuisOrganization.legalAddressCountry). - Calcul TTC via
computeVatBreakdownCents(HT stocké + tauxPLATFORM_VAT_RATE_PERCENT). - Appel
createEinvoiceTransmissionDraft(...)(viaserver/modules/einvoicing/index.tsuniquement — respect strict de la règle de frontière modules) avecdocumentPdfKey = pdfStorageKey.
Ordonnancement PDF → transmission
La transmission a besoin du documentPdfKey, or le PDF est généré en asynchrone non-bloquant. La création de la transmission est donc enchaînée APRÈS le stockage du PDF, dans le même chemin asynchrone que le hook PDF. Conséquences :
- PDF ok → transmission créée (
pending_dispatch,documentPdfKeyrenseigné). Télémétrie :billing.einvoicing.transmission_draft_created. - PDF échoué → transmission skippée (journalisé sur
billing.einvoicing.transmission_draft_skipped, sans tentative de création qui échouerait au dispatch). Le trou est détecté par la réconciliation admin (missing_transmission) — c'est le filet de sécurité voulu, rattrapage via retry manuel possible. - einvoicing throw → journalisé sur
billing.einvoicing.transmission_draft_failed, le flux principal (collecte / avoir / autofacture) n'est jamais annulé.
Gating self-billed → Phase 5
Le hook est structurellement prêt pour la Phase 5 (transmission Agicap des autofactures supplier_self_billed / supplier_credit_note). En Phase 4, la branche self-billed est explicitement skippée (log billing.einvoicing.transmission_draft_skipped — reason: 'self-billed transmission gated to Phase 5') le temps que le flag EINVOICING_SELF_BILLING_ENABLED + le mandat d'autofacturation Yousign soient livrés (D4 + D3-c du plan).
Idempotence
L'idempotence est portée par createEinvoiceTransmissionDraft : la clé par défaut ${invoiceId}:${kind}:${flowType} (avec invoiceId = invoiceNumber côté billing) garantit qu'une deuxième invocation du hook pour la même facture renvoie la transmission existante avec idempotent: true, sans doublon en base.
Réconciliation — filtre kinds
getEinvoiceReconciliationSnapshot (einvoicing/application/reconciliation.ts) restreint désormais son WHERE Prisma à kind IN ('platform_sale', 'platform_credit_note') : les autofactures (transmission Phase 5) et les kinds legacy (store_*, platform_commission) sont exclus pour éviter les fausses discrepancies missing_transmission. La Phase 5 étendra ce filtre à supplier_self_billed / supplier_credit_note quand le flag EINVOICING_SELF_BILLING_ENABLED sera activé.
Autofacturation différée (Phase 7 — D2 révisé)
L'autofacturation est le mécanisme par lequel la plateforme émet, au nom et pour le compte du distributeur, la facture correspondant à la part magasin (supplier_self_billed, CII type_code 389).
Depuis la Phase 7 (D2 révisé), l'autofacture n'est plus émise à la collecte : elle est différée et déclenchée par un scheduler applicatif qui sélectionne les réservations dont endsAt + 3 jours ouvrés français est révolu, sans incident ouvert et avec un mandat d'autofacturation signé. Le déclenchement post-collecte de la Phase 5 a été retiré — le rattrapage admin ?force=true est conservé.
Vue d'ensemble
Réservation `principal_seller` payée
│
└── ... temps passe ...
│
▼
endsAt + 3 jours ouvrés FR révolu ? (SELF_BILLING_SCHEDULER_INTERVAL_MS ~ 1 h)
│
▼
runSelfBillingSchedulerPass({limit}) (server/plugins/26-self-billing-scheduler.ts)
│
├── SELECT réservations `principal_seller` en `reservation_total_paid`
│ sans autofacture `supplier_self_billed` et sans incident `open`
│
└── Pour chaque : tryIssueSelfBillingForReservation()
├── hasOpenReservationIncident ? → skip `open_incident`
├── hasSignedSelfBillingMandate ? → skip `no_mandate` sinon
└── issueSupplierSelfBilledInvoiceForReservation()
│
└── runPostInvoiceHooksNonBlocking → PDF Factur-X 389
│
└── EINVOICING_SELF_BILLING_ENABLED=true ?
→ transmission Agicap 389
sinon → skip télémétrié
Le scheduler (billing/application/self-billing-scheduler.ts) journalise systématiquement les compteurs eligible / issued / skippedNoMandate / skippedOpenIncident / skippedNotDue / failed en fin de passe. Une erreur unitaire est capturée et comptabilisée en failed — jamais propagée.
Le helper tryIssueSelfBillingForReservation (billing/application/self-billing-auto-trigger.ts) porte la logique de gating et est réutilisable en test ou en rattrapage ciblé.
Mandat d'autofacturation Yousign (D3-c)
Le mandat matérialise l'accord préalable exigé par l'article 289 du CGI. Il est signé par le signataire légal du distributeur (OrganizationBillingProfile.signatory*) via Yousign, sur un PDF généré par la plateforme.
Cycle de vie (billing/domain/self-billing-mandate-rules.ts) :
draft ──▶ pending_signature ──▶ signed ──▶ revoked
└──▶ declined
Les statuts declined et revoked sont terminaux. La contrainte « un seul mandat actif par organisation » est garantie par un index partiel PostgreSQL (billing_self_billing_mandates_active_unique, statut IN (pending_signature, signed)) doublé d'une garde applicative renvoyant 409 avant tout appel Prisma.
Contenu du document (billing/application/self-billing-mandate-document.ts) :
- Identification des deux parties (mandataire plateforme via
PLATFORM_LEGAL_*; fournisseur distributeur viaOrganization+OrganizationBillingProfile). - Objet du mandat (accord d'autofacturation, article 289 du CGI).
- Acceptation tacite / expresse — délai de contestation de 30 jours par facture.
- Durée et révocation — sans terme, révocable à tout moment par notification.
- Signature électronique Yousign — le signataire est le signataire légal du distributeur.
Endpoints :
POST /api/billing/organizations/{organizationId}/self-billing-mandate # Admin : initie la demande (409 si mandat actif existant)
GET /api/billing/organizations/{organizationId}/self-billing-mandate # Admin OU membre de l'organisation : statut courant
DELETE /api/billing/organizations/{organizationId}/self-billing-mandate # Admin : révoque le mandat signé (409 si aucun)
La vue GET masque esignRequestRef et documentStorageKey pour les non-admins (uniquement les statuts et dates leur sont exposés). Le PDF de mandat est stocké dans le contexte S3 privé billing-self-billing-mandate (/billing/self-billing-mandates, 5 Mo max, application/pdf seul).
Webhook Yousign — le webhook existant POST /api/webhooks/yousign a été étendu pour fan-outer les événements vers le module billing après le traitement auth. Séquence :
- Vérification HMAC (auth
verifyYousignWebhookSignature). - Persistance de l'événement + match d'
AuthSignatureRequestparprocessYousignWebhook. - Extraction de l'identité (
parseYousignWebhookIdentity— publiée dans le barrelauth). - Fan-out billing :
applyYousignEventToSelfBillingMandate({providerSignatureRequestId, eventName, declineReason})— recherche paresignRequestRef, applique la transition. NE throw JAMAIS. La déduplication est portée par les transitions (signed → signedNO-OP ; transition invalide silencée).
Rattrapage admin manuel
L'endpoint existant POST /api/billing/reservations/{id}/supplier-self-billing reste disponible mais applique désormais deux gates :
- Mandat — sans mandat signé → 409
BILLING_MANDATE_MISSING. - Incident (Phase 7) — si un incident
openbloque la réservation → 409CONFLICT.
L'admin peut forcer le passage via ?force=true. Chaque contournement est journalisé en WARN :
billing.supplier.self_billing.forced_without_mandatebilling.supplier.self_billing.forced_with_open_incident
Incidents de réservation (Phase 7)
Un incident matérialise un problème signalé par l'industriel de la réservation (affichage manquant, contenu non conforme…). Il bloque l'émission de l'autofacture distributeur tant qu'il n'est pas libéré par un admin. Modèle Prisma : BillingReservationIncident (billing_reservation_incidents, migration 20260706120000_add_billing_reservation_incidents).
Cycle de vie (billing/domain/reservation-incident-rules.ts) :
open ──▶ released (terminal)
Un seul incident open par réservation à la fois — garanti par un index partiel PostgreSQL (billing_reservation_incidents_open_unique, WHERE status = 'open') doublé d'une garde applicative renvoyant 409 avant tout appel Prisma.
Fenêtre de déclaration :
- Ouverture :
startsAtde la réservation (début de la prestation). - Fermeture :
endsAt + 3 jours ouvrés français, alignée sur l'échéance du scheduler d'autofacture. Calcul FR sans dépendance externe — algorithme de Butcher + fériés fixes/mobiles centralisés dansbilling/domain/business-days.ts.
Endpoints :
POST /api/billing/reservations/{reservationId}/incident # Industriel de la réservation
GET /api/billing/reservations/{reservationId}/incident # Industriel/distributeur/admin
POST /api/admin/billing/incidents/{incidentId}/release # Admin (billing.manage.all)
GET /api/admin/billing/incidents?status=open # Admin (liste paginée)
- POST incident — RBAC : rôle
industrial+ membre actif de l'org industrielle de la réservation. Motif obligatoire (reason). 422 hors fenêtre, 409 si un incidentopenexiste déjà. - GET incident — retourne
{ incident, eligibility }.eligibility.canDeclare(booléen) évite au client de recalculer les jours ouvrés. Raisonsok / not_industrial / not_started / window_closed / incident_open. - POST release — RBAC :
billing.manage.all. Note de résolution (resolutionNote) obligatoire. Ne rejoue pas l'autofacture — le prochain passage du scheduler la détectera automatiquement. - GET admin list — statut par défaut
open,?status=releasedpour l'archive. Paginationlimit / offset.
Télémétrie :
| Scope | Émission |
|---|---|
billing.reservation.incident.declared | Nouvel incident (par l'industriel) |
billing.reservation.incident.released | Libération par l'admin |
billing.supplier.self_billing.skipped_open_incident | Scheduler ou manuel — bloqué par un incident ouvert |
Feature flag transmission Agicap (D4)
Le PDF Factur-X 389 est TOUJOURS généré à l'émission de l'autofacture — cela permet la mise à disposition du document au distributeur et au comptable sans dépendre d'Agicap. La transmission Agicap est pilotée par EINVOICING_SELF_BILLING_ENABLED (défaut false) :
- Flag
false→post-invoice-hookskippe la création de transmission pour les kindssupplier_self_billed/supplier_credit_note, journalisée surbilling.einvoicing.transmission_draft_skipped. - Flag
trueet mandat signé côté distributeur émetteur → création de la transmissionpending_dispatchavecdocumentPdfKey, télémétriebilling.supplier.self_billing.transmission_createden plus debilling.einvoicing.transmission_draft_created. - Flag
truesans mandat signé → transmission skippée + télémétrie (même scope que ci-dessus).
Le filtre de réconciliation getEinvoiceReconciliationSnapshot étend son WHERE Prisma aux kinds supplier_self_billed / supplier_credit_note UNIQUEMENT quand EINVOICING_SELF_BILLING_ENABLED=true (évite les fausses discrepancies missing_transmission quand le flag est off).
Suivi Phase 4 — refunds → PDF/transmission de l'avoir
Le flux issueRefundForInvoice (billing/application/refunds.ts) branche désormais runPostInvoiceHooksNonBlocking sur l'avoir platform_credit_note créé — mêmes garanties non-bloquantes que les autres émissions.
Télémétrie Phase 5
| Scope | Émission |
|---|---|
billing.self_billing_mandate.requested | Nouvelle demande Yousign créée pour une organisation |
billing.self_billing_mandate.signed | Événement Yousign done → transition mandate signed |
billing.self_billing_mandate.declined | Événement Yousign declined / expired / canceled / error → transition mandate declined |
billing.self_billing_mandate.revoked | Révocation admin |
billing.self_billing_mandate.request_failed | Orchestrator Yousign a levé une exception |
billing.self_billing_mandate.webhook_ignored | Événement Yousign reçu sur mandat non pending_signature |
billing.self_billing_mandate.webhook_apply_failed | Erreur pendant l'application du webhook (repository) |
billing.supplier.self_billing.issued | Autofacture émise (auto ou manuelle) |
billing.supplier.self_billing.skipped_no_mandate | Auto-trigger skippé — pas de mandat signé |
billing.supplier.self_billing.forced_without_mandate | Admin a forcé la manuel-émission sans mandat (?force=true) |
billing.supplier.self_billing.trigger_failed | Erreur du trigger auto (non-bloquant) |
billing.supplier.self_billing.transmission_created | Transmission Agicap 389 créée (flag on + mandat signé) |
Administration (/admin/einvoicing)
Les routes admin sont accessibles uniquement avec la permission admin :
GET /api/admin/einvoicing/transmissions
GET /api/admin/einvoicing/reconciliation
POST /api/admin/einvoicing/transmissions/{transmissionId}/retry
POST /api/admin/einvoicing/transmissions/{transmissionId}/resync
Les seuils d'alerte de réconciliation (calculés côté serveur) :
| Indicateur | Warning | Critical |
|---|---|---|
discrepancy_rate | ≥ 5 % | ≥ 15 % |
provider_failed_backlog | ≥ 5 | ≥ 20 |
missing_transmission_backlog | ≥ 10 | ≥ 30 |
Runbook — incidents e-invoicing
Diagnostic initial (10 premières minutes)
- Ouvrir
/admin/einvoicinget noter : discrepancies totales, cartes d'alerte actives, top desfailedaveclastErrorMessage. - Vérifier la santé du provider : erreurs de dispatch sortant (timeout/auth), volume traité par la boucle de polling (scope de log
einvoicing-polling), backlog ensent. - Classer le type d'incident : indisponibilité provider, panne OAuth (token 401 persistant), boucle de polling arrêtée, transmission manquante.
Scénario A — Provider indisponible
Symptômes : croissance des transmissions failed, messages réseau/auth dans les logs de dispatch.
Actions :
- Geler les retries massifs jusqu'au rétablissement du provider.
- Tracer la fenêtre d'incident (heure de début, nombre de transmissions impactées).
- Une fois le provider stable, relancer des retries ciblés depuis
/admin/einvoicing. - Relancer la réconciliation et confirmer la décroissance du backlog.
Escalade : critique si provider_failed_backlog est en zone critique pendant plus de 15 minutes.
Scénario B — Polling arrêté ou 401 OAuth persistant
Symptômes : les transmissions restent en sent bien après leur soumission ; aucune ligne de log einvoicing-polling scope récente ; ou apparition répétée de EINVOICE_PROVIDER_FAILED avec un message « Agicap credentials are invalid or missing required permissions ».
Actions :
- Vérifier que
EINVOICING_POLLING_ENABLED=trueest bien positionné en production et que le plugin25-einvoicing-pollinga démarré (chercherpolling loop startedau boot). - Vérifier
AGICAP_CLIENT_ID,AGICAP_CLIENT_SECRET,AGICAP_TOKEN_URL,AGICAP_ENTITY_ID. En cas de rotation de secret, redémarrer l'instance (le cache token est en mémoire). - Pour les transmissions bloquées entre-temps, utiliser l'action admin
Resync statusqui force unPOST /flows/searchimmédiat. - Confirmer la transition vers
acknowledged/rejectedsur les transmissions relancées.
Escalade : critique si le polling reste inactif > 30 min ou si le token OAuth échoue globalement après un déploiement ou une rotation.
Scénario C — Transmission manquante
Symptômes : discrepancy missing_transmission en réconciliation.
Actions :
- Vérifier que la facture existe dans
billinget est éligible au flux e-invoicing. - Confirmer que l'événement d'orchestration a été émis sans déduplication incorrecte.
- Re-déclencher la création de transmission via le chemin applicatif (jamais via patch direct en base).
- Valider que la transmission apparaît dans la liste admin et relancer la réconciliation.
Escalade : critique quand missing_transmission_backlog atteint le seuil critique.
Scénario D — Transmission rejetée par le provider
Symptômes : statut rejected avec raison de validation provider.
Actions :
- Collecter la raison de rejet et classifier (payload vs règle métier).
- Corriger les données sources dans le domaine
billingsi nécessaire. - Ré-émettre le document corrigé via le flux normal.
- Conserver la trace de la rationale de correction dans l'audit trail.
Contrôles manuels disponibles
| Action | Statuts source autorisés |
|---|---|
| Retry | failed · pending_dispatch |
| Resync status | Tous sauf statuts terminaux |
Après chaque action batch : rafraîchir les transmissions, rafraîchir la réconciliation, capturer les compteurs résultants.
Télémétrie et audit
Événements de télémétrie cibles :
billing.reservation.collect.requested·paid·failedbilling.supplier.self_billing.issued·adjustedbilling.supplier.payout.requested·paid·failedbilling.refund.requested·paid·failed
L'audit trail est obligatoire pour : remboursements, ajustements fournisseur, corrections manuelles de statuts.
Variables d'environnement
Les variables ci-dessous sont validées au boot par server/plugins/00-env-validation.ts (via server/utils/env.ts). Les secrets ne sont jamais journalisés : seul le statut « présent » ou « absent » apparaît dans les warnings et erreurs.
Facturation — commission plateforme
| Variable | Rôle | Défaut | Comportement |
|---|---|---|---|
PLATFORM_COMMISSION_RATE_BPS | Taux global de commission plateforme en points de base (1 bps = 0,01 %) appliqué en principal_seller. | 2000 (20 %) | Absente → warning + fallback 2000 bps. Hors bornes [0, 10000] ou non-entier → erreur bloquante au boot. |
E-invoicing — provider Agicap (émission Factur-X)
Ces variables sont exigées en production (sauf si E2E_TEST_MODE=1, où le stub prend le relais) et rétrogradées en warnings hors production pour permettre le développement local sans compte Agicap.
| Variable | Rôle | Défaut / exemple |
|---|---|---|
AGICAP_CLIENT_ID | Client ID OAuth2 (client_credentials). | secret — pas de défaut |
AGICAP_CLIENT_SECRET | Client secret OAuth2. Ne jamais logger. | secret — pas de défaut |
AGICAP_TOKEN_URL | Endpoint OAuth2 (connect/token). | https://myaccount.agicap.com/connect/token |
AGICAP_BASE_URL | Base URL de l'API Agicap (soumission de flux, recherche de statuts). | https://api.agicap.com |
AGICAP_ENTITY_ID | Identifiant de l'entité juridique côté Agicap. | secret — pas de défaut |
AGICAP_FLOW_MODE | Sandbox ou Production. Détermine le header X-FlowMode. | Sandbox |
AGICAP_SELLER_AP | Access point vendeur (adresse électronique vendeur configurée sur le compte Agicap). | secret — pas de défaut |
AGICAP_SELLER_AP_SCHEME | Schéma de l'access point (BT-34). Ex. 0225 pour l'Île de France. | 0225 |
AGICAP_WEBHOOK_SECRETretiré (Phase 3). L'API Agicap réelle n'expose pas de webhook fiable — la variable et le code de vérification HMAC associé ont été supprimés. Toute présence résiduelle dans les secrets de déploiement peut être nettoyée.
E-invoicing — polling et feature flags
| Variable | Rôle | Défaut applicatif |
|---|---|---|
EINVOICING_POLLING_ENABLED | Active la boucle de polling /flows/search (Agicap n'expose pas de webhook fiable). | false |
EINVOICING_POLLING_INTERVAL_MS | Intervalle de polling en millisecondes. Doit être un entier positif. | 900000 (15 min) |
EINVOICING_SELF_BILLING_ENABLED | Feature flag : transmission Agicap des autofactures fournisseur (type_code 389). Le PDF est toujours généré ; ce flag pilote uniquement la transmission. | false |
Scheduler d'autofacturation différée (Phase 7)
| Variable | Rôle | Défaut applicatif |
|---|---|---|
SELF_BILLING_SCHEDULER_ENABLED | Active la boucle scheduler qui émet les autofactures distributeur endsAt + 3 jours ouvrés FR après la fin de la prestation. Voir 26-self-billing-scheduler.ts. | false |
SELF_BILLING_SCHEDULER_INTERVAL_MS | Intervalle du scheduler en millisecondes. Doit être un entier positif. Utile en dev pour un premier passage rapide. | 3600000 (1 h) |
Génération PDF — identité légale plateforme (Phase 2)
Ces variables portent l'identité juridique de Pulse my Promo (SAS). Elles alimentent :
- l'émetteur des PDF
platform_sale/platform_credit_note(CII type_code380) ; - l'acheteur des autofactures
supplier_self_billed/supplier_credit_note(CII type_code389, émetteur = distributeur).
Sont validées au boot par server/utils/env.ts. Requises en production sauf E2E_TEST_MODE=1.
| Variable | Rôle | Défaut applicatif |
|---|---|---|
PLATFORM_LEGAL_COMPANY_NAME | Raison sociale de Pulse my Promo (BT-27 / issuer.company.legalName). | Placeholder lisible (dev) |
PLATFORM_LEGAL_SIRET | SIRET plateforme (14 chiffres). Sert de BT-30 vendeur et de BT-49 acheteur pour les autofactures. | 00000000000000 (dev) |
PLATFORM_LEGAL_VAT_NUMBER | N° TVA intracommunautaire (BT-31 / BT-48). Optionnel mais recommandé (catégorie TVA standard). | undefined |
PLATFORM_LEGAL_STREET | Rue / voie (BT-35). | — |
PLATFORM_LEGAL_POSTAL_CODE | Code postal (BT-38). | — |
PLATFORM_LEGAL_CITY | Ville (BT-37). | — |
PLATFORM_LEGAL_COUNTRY | Code pays ISO 3166-1 alpha-2 (BT-40). | FR |
PLATFORM_LEGAL_EMAIL | Email de facturation. | no-reply@pulsemypromo.local |
PLATFORM_LEGAL_IBAN | IBAN affiché sur les factures plateforme (bloc paiement). | undefined |
PLATFORM_VAT_RATE_PERCENT | Taux de TVA global appliqué à la génération (%). Cf. hypothèse HT ci-dessous. | 20 (Phase 2) |
Génération PDF et Factur-X
À partir de la Phase 2, chaque facture billing supportée génère un PDF Factur-X (CII XML embarqué) dès sa création. Le PDF est stocké sur S3 privé et référencé par BillingInvoice.pdfStorageKey.
Hypothèse métier — TVA calculée à la génération
Les montants stockés sur les réservations et les factures (amountExclTaxCents) sont HT. La TVA n'est pas persistée en base : elle est calculée au moment de la génération du document (PDF + transmission) via readPlatformVatRatePercent() (variable PLATFORM_VAT_RATE_PERCENT, défaut 20 %). taxAmountCents reste à 0 sur BillingInvoice. Le hook post-facture (Phase 4) utilise la même formule pour peupler EinvoiceTransmission.amountInclTaxCents — cohérence garantie entre le PDF Factur-X et le payload transmis à Agicap. La persistance de la ventilation TVA en base est reportée (v-next) — le sujet n'est plus bloquant tant que le taux global reste stable.
Pipeline
- Chargement facture + organisation acheteuse (industriel) et/ou émettrice (distributeur pour autofacture) +
OrganizationBillingProfile. - Résolution du bundle (émetteur / acheteur / ligne / totaux) selon le
kind:Kind Émetteur Acheteur CII type_code platform_salePlateforme (env PLATFORM_LEGAL_*)Organisation industrielle 380platform_credit_notePlateforme Organisation industrielle 380supplier_self_billed(autofacture)Distributeur ( Organization+OrganizationBillingProfile)Plateforme 389supplier_credit_noteDistributeur Plateforme 389 - Rendu du PDF humain via
@aidalinfo/invoice-kit— templateclassic+ branding orange myPromo (#F97316/#C2410C) + tagline « Publicité en magasin — B2B ». Le template dédiépulse-mypromofait l'objet de l'issue powerpackages Phase 2 (voir plus bas). En cas d'échec du template, fallback minimal viapdf-lib(Helvetica standard) — la conformité fiscale reste portée par le CII XML embarqué. - Construction du CII XML via
buildCiiXml(server/modules/einvoicing/infrastructure/cii-xml.ts) — profil Factur-X Basic EN 16931, mention obligatoire « Autofacturation » (subject codeABL) quandtype_code = 389. - Embarquement du XML dans le PDF via
embedCiiXmlInPdf(server/modules/einvoicing/infrastructure/facturx-embed.ts, pdf-lib) — AFRelationshipAlternative, XMPfx:ConformanceLevel=BASIC.
PDF/A-3 non strict — l'embed n'audite ni polices ni colorspace : risque accepté v1 (ingestion PDP Agicap sandbox suffisante). Consolidation prévue via
@aidalinfo/facturx-builder(issue powerpackages ci-dessous).
Stockage S3 privé
- Contexte de stockage :
billing-invoice-pdf(racine/billing/invoices, bucket privé, max 5 Mo,application/pdfseul). - Clé S3 canonique :
billing/invoices/{invoiceNumber}.pdf— écrit de manière idempotente (une même facture régénérée écrase l'objet précédent). - Persistée sur
BillingInvoice.pdfStorageKey(nullable).
Endpoint download
GET /api/billing/invoices/{invoiceId}/download
Cookie: better-auth.session_token=<jeton-de-session>
- Contrôle d'accès (strict) : permission
billing.manage.all(admin) OU organisation active du demandeur = émetteur/client de la facture. Toute autre organisation → 403. - 404 code
BILLING_PDF_NOT_READYsi la génération PDF n'a pas encore produit de fichier (pdfStorageKeyNULL). - Le PDF est streamé directement depuis S3 (
Content-Disposition: attachment). L'URL S3 signée intermédiaire n'est jamais journalisée.
Génération non-bloquante
Depuis la Phase 4, la génération PDF est encapsulée dans un hook post-facture unique runPostInvoiceHooksNonBlocking(invoiceNumber) (billing/application/post-invoice-hook.ts), déclenché en void-promesse depuis :
billing/application/reservation-total-collection.ts— après création de la factureplatform_sale.billing/application/supplier-settlement.ts— après création de l'autofacturesupplier_self_billed.billing/application/credit-notes.ts— après création d'un avoir (platform_credit_noteousupplier_credit_note— le kind est correctement mappé depuis la facture source).
Le hook enchaîne PDF Factur-X puis transmission Agicap (Phase 4 — cf. section « Flux billing → einvoicing » plus haut). Il ne re-lève jamais : un échec est journalisé sur le scope billing.invoice.pdf_generation_failed (PDF), billing.einvoicing.transmission_draft_failed (einvoicing throw) ou billing.einvoicing.transmission_draft_skipped (skip volontaire — PDF absent, self-billed Phase 5, kind non éligible), et le flux principal (collecte / autofacturation / avoir) ne peut jamais échouer à cause du PDF ou de la transmission.
Le hook PDF seul (generateAndStoreInvoicePdfNonBlocking) reste exporté et utilisable pour des scénarios ciblés (rattrapage, endpoints admin) — il constitue le premier maillon interne de runPostInvoiceHooksNonBlocking.
Fallback template
Si le template classic d'@aidalinfo/invoice-kit refuse le rendu (données manquantes, erreur d'asset), un PDF minimal pdf-lib est produit. Le CII Factur-X reste embarqué — la facture est légalement conforme.
Issues powerpackages liées
@aidalinfo/invoice-kit— templatepulse-mypromo(branding + section conditionnelle autofacturation).@aidalinfo/facturx-builder— pipeline CII EN 16931 + embed PDF consolidé (remplacera les copies localescii-xml.tsetfacturx-embed.tsdans myIT et myPromo).
Interfaces utilisateur (Phase 6)
Depuis la Phase 6, chaque workspace expose une entrée « Facturation » dédiée. Les pages sont en orchestration — la logique est extraite dans deux composables partagés (useBillingInvoices, useSelfBillingMandate) et des composants sous app/mypromo/app/components/billing/*.
Espace industriel — « Mes factures »
- Route :
/industrial/billing— layoutindustrial. - Kinds affichés :
platform_sale+platform_credit_note. - Filtres : période (bornes locales, appliquées côté client sur la réponse serveur).
- Chaque ligne montre
invoiceNumber, kind + statut (badges sémantiques Nuxt UI), ventilation HT / TVA / TTC (source serveur), et un bouton « Télécharger PDF » conditionné àhasPdf. Le téléchargement passe parGET /api/billing/invoices/{invoiceId}/download— jamais de construction d'URL S3 côté client.
Espace industriel — Transparence commission dans le panier
- L'endpoint
GET /api/campaigns/cartretourne désormais la ventilation par item :netDistributorAmountCents(base fournisseur = loyer HT magasin, ancienreservationTotalCents),platformCommissionAmountCents,industrialTotalPayableCents. Le total agrégé du panier exposetotalNetDistributorCents,totalPlatformCommissionCents,totalIndustrialPayableCents,commissionRateBps. - Le champ
reservationTotalCentsconserve son ancienne sémantique (base fournisseur) pour ne pas casserbuildReservationPricingFromCartItem— la commission est un enrichissement additif. - Le composant
BillingCartPricingBreakdown(app/mypromo/app/components/billing/CartPricingBreakdown.vue) affiche la ventilation dans le popover panier (CartPreview.vue) et dans la page de confirmation (industrial/reservations/new.vue). L'industriel voit dès le panier le total qui sera facturé — cohérence garantie avec la factureplatform_sale.
Espace distributeur — « Mes autofactures »
- Route :
/distributor/billing— layoutdistributor. - Kinds affichés :
supplier_self_billed+supplier_credit_note. - Bandeau mandat en tête (
BillingSelfBillingMandateBanner) — l'organisation active est récupérée viaGET /api/auth/onboarding/companypuis la vue publique du mandat viaGET /api/billing/organizations/{organizationId}/self-billing-mandate. États :- mandat absent (404) —
warning→ « Autofacturation non activée, contactez Pulse ». pending_signature—warning→ attente signature du signataire légal.signed—success+ date de signature + lien vers le document.declined/revoked—error→ contact commercial.
- mandat absent (404) —
- Mention légale permanente sous le bandeau : « Autofacturation — document émis au nom et pour le compte du fournisseur ».
- Téléchargement PDF conditionné à
hasPdf, même endpoint que côté industriel (l'RBAC accepte l'organisation émettrice OU cliente).
Espace admin — Fiche organisation
AdminSelfBillingMandateCard(app/mypromo/app/components/admin/organizations/AdminSelfBillingMandateCard.vue) apparaît uniquement pour les organisationsactorType = 'distributor'.- Affiche l'état courant (badge sémantique + dates + motifs) et propose :
- Initier la demande de signature (
POST /api/billing/organizations/{id}/self-billing-mandate) — actif quand aucun mandat actif n'existe. - Révoquer le mandat (
DELETE .../self-billing-mandate+ motif obligatoire dans une modale de confirmation) — actif quand le mandat estsigned. - Voir le document — lien vers
GET .../self-billing-mandate/document(voir ci-dessous).
- Initier la demande de signature (
Nouveaux endpoints (Phase 6)
GET /api/billing/invoices — filtre kind[] + hasPdf + ventilation TVA
Voir « Lister les factures de l'organisation connectée » plus haut.
GET /api/billing/organizations/{organizationId}/self-billing-mandate/document
Streame le PDF du mandat d'autofacturation courant. RBAC :
- permission
billing.manage.all(admin) → OK ; - membre actif de l'organisation (
manager/member/billing) → OK ; - autre → 403.
Réponses d'erreur :
- 404 — aucun mandat n'existe pour l'organisation.
- 404
BILLING_PDF_NOT_READY— le mandat existe mais le PDF n'a pas encore été stocké.
Le PDF est streamé directement depuis S3 (Content-Disposition: attachment) via getSelfBillingMandatePdfStream ; l'URL S3 signée intermédiaire n'apparaît jamais dans les logs.
i18n
Toutes les clés sont namespacées sous billing.* (fr + en, app/mypromo/i18n/locales/*.json) :
billing.industrial.page.*,billing.distributor.page.*,billing.distributor.legalNotice.*billing.list.*(headers, filters, totals, empty, actions)billing.invoiceKind.*(labels français / anglais par kind)billing.invoiceStatus.*billing.download.*,billing.cart.breakdown.*billing.mandate.status.*,billing.mandate.banner.*,billing.mandate.admin.*layout.nav.billing+layout.nav.selfBilling(entrées de navigation).