ADR 0011 — Bons cadeaux et points de fidélité en moyens de paiement, non en lignes de facture
Date : 17.08.2026 Statut : Proposé — arbitrages Buchard et intégrateur BC en attente
Contexte
Aujourd'hui, un bon cadeau imputé sur une réservation est écrit comme une ligne de facture négative (BookingService.CreateOrUpdateInvoice:2426-2440) :
Account = "2061",
Description = "Bon cadeau N° " + bookingGift.Code,
ForceNoVAT = true,
Price = giftEffectiveValue * -1,Le compte 2061 est un compte de dettes : la contrepartie inscrite au passif le jour où Buchard a vendu le bon. L'utiliser n'est donc pas une réduction de prix, c'est l'extinction d'une dette. Le compte est juste, le type de document est faux — on comptabilise une extinction de dette comme une ligne de vente négative. Les points de fidélité sont dans le même cas (ligne Points fidélité, cf. filtre Balance.liquid:110).
Conséquence : le bon modifie booking.FinalAmount, donc la facture, donc le document BC. Toute la fragilité observée découle de là.
Symptômes accumulés
- Le montant réellement imputé n'est stocké nulle part.
BookingGiftest une pure table de jointure (BookingsId,GiftsId). Le code le re-devine en matchant une chaîne de caractères sur les lignes de facture (BookingService.cs:2761,"Bon cadeau N° " + gift.Code), depuis trois chemins distincts : annulation (:461), recalcul avant édition (:484), retrait du bon dans l'éditeur (:1084). - Cette re-dérivation a déjà produit un bug de sur-restitution (août 2026, cf.
modules/gifts.md§ pièges) : la somme portait sur toutes les factures sans filtreEnabled, le bon était restitué autant de fois qu'il y avait de lignes, et le clamp>= Value → nullmasquait le dépassement — indétectable en base. - Le garde-fou d'usage partiel compare à la mauvaise référence.
BookingService.cs:2573-2620ne se déclenche que si le total de la facture devient négatif : la référence est le montant brut de la réservation, jamais le solde encore dû. Un bon posé sur une résa dont l'acompte est déjà payé est donc consommé en totalité, et le trop-perçu devient un avoir au client au lieu de rester sur le bon.- Cas déclencheur — résa 3214 (Asana 1217482583935874) : brut 1 446, acompte payé 542.50, bon de 1 200 ajouté le 18.02.2026.
1 446 − 1 200 = +246 > 0→ clamp non déclenché → bon vidé, facture 26105147 à 246 avec 542.50 encaissés = 296.50 dus à la cliente au lieu de rester sur le bon. Deux autres cas structurellement identiques en base (8043, 2888).
- Cas déclencheur — résa 3214 (Asana 1217482583935874) : brut 1 446, acompte payé 542.50, bon de 1 200 ajouté le 18.02.2026.
- Ajouter un bon sur une résa facturée déclenche toute la cascade BC : nouvelle facture Horizon + avoir BC + repost + délettrage/relettrage des paiements — avec l'invariant d'ordre décrit dans
modules/billing-payment.md. - Le marqueur
NO_TRANSACTION_REQUIREDexiste uniquement parce qu'un bon couvrant tout ramène le montant dû à 0 sans ouvrir de transaction en ligne (modules/billing-payment.md), avec le trou de sécurité[AllowAnonymous]en GET qui l'accompagne. - Les points de fidélité portent la même famille de bugs (plafonnement à la valeur de la résa, débit à rejouer entre acompte et solde).
Chaque correctif ponctuel ajoute un cas de bord au lieu d'en retirer.
Décision
Adopter un critère unique de classement, et y ranger l'existant :
Ce qui modifie le prix est une ligne de facture. Ce qui éteint le prix est un moyen de paiement.
| Ligne de facture (modifie le prix) | Moyen de paiement (éteint le prix) |
|---|---|
| Rabais, Réduction Early Booking | Bons cadeaux |
| Suppléments globaux | Points de fidélité |
| Forfait TVA zone rose | |
| Assurances |
Concrètement : la facture porte le prix plein du voyage ; le bon règle une partie de ce qui est dû, au même titre qu'un BVR ou un Twint. Un PaymentType dédié est créé, avec 2061 en compte de contrepartie.
Bons cadeaux et points de fidélité doivent être traités dans le même chantier — même maladie, même remède ; les séparer garantit de refaire le travail.
Conséquences
Ce que ça supprime
- ✅ Le garde-fou d'usage partiel (
:2573-2620) en entier. Un paiement ne peut structurellement pas dépasser ce qui est dû, et le contrôle existe déjà :PaymentService.CreateAsyncintercepte le refus BC et renvoie « le montant dépasse le solde restant à payer » avec le reliquat (modules/billing-payment.md). Le bug de la résa 3214 n'a plus de code dédié, il tombe dans un chemin déjà éprouvé en production. - ✅
GetGiftAmountUsedOnBooking:2761et son matching de chaîne, plus les trois chemins de restitution (:461,:484,:1084). - ✅
Gift.RemainingValuecomme champ stocké mutable → dérivé de la somme des paiements imputés. Plus rien à recaler à la main, plus de clamp>= Value → nullqui masque les dépassements. - ✅ La cascade BC sur ajout de bon. Le bon ne touchant plus
FinalAmount, il ne touche plus la facture ni le document BC : une ligne de journal au lieu d'un avoir + repost + délettrage/relettrage. C'est la plus grosse réduction de surface de risque du lot. - ✅ L'essentiel de la raison d'être du marqueur
NO_TRANSACTION_REQUIRED— un bon étant une transaction, la résa se confirme comme n'importe quel encaissement — et avec lui le trou[AllowAnonymous]en GET. - ✅ Le bloc « balance à zéro » (
:2622-2641) perd son principal déclencheur ; la facture garde sa structure TVA naturelle. - ✅ L'usage partagé entre deux réservations devient trivial : deux paiements issus d'une même source, sans raisonnement « part de cette résa ».
Ce que ça coûte
- ❌ Le montant de l'acompte change. Aujourd'hui le bon réduit le total, donc le pourcentage d'acompte se calcule sur le montant réduit. En moyen de paiement, l'acompte se calcule sur le prix plein et le bon s'impute dessus. Le montant demandé à la réservation n'est plus le même — c'est de la trésorerie, pas du code.
- ❌ Les PDF changent de forme : le bon quitte le bloc prix pour le bloc « déjà payé ». Plus honnête (le client voit enfin la valeur faciale et le reliquat, ce que le modèle actuel masque puisque la ligne affiche le montant imputé), mais visible côté client. Montant QR = le résiduel.
- ❌ Cohabitation de deux modèles pendant la transition : les réservations existantes gardent le modèle ligne. À cadrer explicitement dès le départ (date de bascule, ou backfill des résas ouvertes) sous peine de créer le prochain cas de bord.
- ❌ Paramétrage BC : la contrepartie d'un paiement « bon cadeau » est 2061, un compte de dettes, alors que
PaymentServicemappe aujourd'hui chaquePaymentTypeà un compte de trésorerie en dur (1000/1038/1050/1088F/1088G,1111,1010,1083,1118,1086).
Arbitrages en attente
| Question | Qui tranche |
|---|---|
| L'acompte se calcule-t-il sur le prix plein (le bon s'impute dessus) ou reste-t-il sur le montant réduit ? | Buchard |
| Nouvelle présentation du bon sur les PDF Confirmation / Solde | Buchard |
| Les 3 réservations déjà en avoir (3214, 8043, 2888) : recréditer les bons et rectifier, ou laisser l'avoir ? | Buchard |
Compte de contrepartie 2061 pour un paiement, et paramétrage Bal_Gen_Posting_Type associé | Intégrateur BC |
| Date de bascule vs backfill des réservations ouvertes | Buchard + technique |
Méthode
Ne pas réécrire BookingService et InvoiceService d'un bloc. CreateOrUpdateInvoice fait ~900 lignes dont les blocs commentés sont de l'archéologie d'incidents BC réels ; repartir de zéro perd ce savoir sans bruit et le fait réapprendre en production.
Séquencement retenu :
- Sortir les moyens de paiement (bons cadeaux + points de fidélité) de la facture. C'est le changement qui supprime le plus de code et qui rétrécit précisément la surface à réécrire ensuite.
- Une fois bons et points hors de la facture,
CreateOrUpdateInvoicen'est plus qu'un calcul de prix — réécrivable, avectests/Application(BookingServiceTest) comme filet.
Notes pour l'équipe
- Le critère ligne/moyen-de-paiement est le livrable principal de cet ADR : il doit servir d'arbitre à chaque nouvel élément introduit dans le calcul de prix. Si le nouvel élément change ce que coûte le voyage, c'est une ligne ; s'il change qui le paie, c'est un moyen de paiement.
- Tant que cet ADR est en
Proposé, le comportement décrit dansmodules/gifts.mdreste la référence — aucun correctif n'a été appliqué sur le cas 3214. Booking.Giftsest une jointure many-to-many sans charge utile ; toute solution intermédiaire qui voudrait stocker le montant imputé sans passer aux moyens de paiement devrait la transformer en entité explicite (~40 usages de.Giftssur 16 fichiers, concentrés dansBookingService). Cette voie a été écartée au profit de la présente décision.
Références
- Asana 1217482583935874 — « Bon cadeau prélevé en totalité au lieu du solde dû (résa 3214) »
modules/gifts.md— modèle actuel,RemainingValue, restitutionsmodules/billing-payment.md— cascade avoir/repost/délettrage, marqueurNO_TRANSACTION_REQUIRED, comptes de contrepartieBookingService.cs—:2426-2440(ligne bon),:2573-2620(garde-fou),:2761(re-dérivation),:461/:484/:1084(restitutions)InvoiceService.cs:645— enNewInvoiceProcess, BC ne reçoit que leFullPrice: une seule facture BC au montant total, acompte/solde n'étant qu'un découpage d'affichage Horizon

