Invariants money & date
Money = bigint centimes
Section intitulée « Money = bigint centimes »Tout montant monétaire est représenté en centimes comme bigint — aucune exception.
Pourquoi pas number ?
Section intitulée « Pourquoi pas number ? »JavaScript number est un IEEE 754 double (53 bits de mantisse). Pour les montants en centimes :
9_007_199_254_740_991(max safe integer) = ~90 milliards d’euros- La précision est perdue sur les opérations arithmétiques au-delà de ~9 quadrillions de centimes
- Plus fondamentalement :
0.1 + 0.2 !== 0.3en float — inacceptable pour de la comptabilité
bigint garantit une précision exacte pour toute opération entière.
Règles opératoires sur Money
Section intitulée « Règles opératoires sur Money »// @pms/shared-kernel/Moneyconst a = Money.of(12000n, currency('EUR')); // 120,00 €const b = Money.of(9000n, currency('EUR')); // 90,00 €
a.add(b) // ok → 210,00 €a.subtract(b) // ok → 30,00 €a.multiply(3n) // ok → 360,00 € (entier uniquement)// a.multiply(1.5) → erreur TS — pas de multiplication par flottant
// Devise-safe : EUR + GBP → exception (pas de conversion cross-devise en V1)Money.of(100n, currency('EUR')).add(Money.of(100n, currency('GBP'))); // throwMoneyRatio — basis-points
Section intitulée « MoneyRatio — basis-points »Pour les calculs de taux (TVA, commission) :
import { applyBasisPoints, applyBasisPointsCeil, decomposeFromTtc } from '@pms/shared-kernel';
// TVA 20% = 2000 bpapplyBasisPoints(Money.of(10000n, currency('EUR')), 2000n); // 2000 centimes (arrondi half-up)
// Taxe DGFiP → toujours plafondapplyBasisPointsCeil(base, rate);
// Décomposition TTC → HT + TVA (exact, net + tax === ttc, vérifié fast-check)decomposeFromTtc(Money.of(12000n, currency('EUR')), 2000n);// → { ht: 10000n, tax: 2000n } (12000 = 10000 + 2000 ✓)Côté API et fronts
Section intitulée « Côté API et fronts »- L’API expose les montants comme
amountCents: string(JSON — pas denumberpour éviter la perte de précision des int64) - Les fronts ne castent pas
string → numberpour les montants - Conversion string → bigint :
BigInt(response.amountCents) - Conversion euros → centimes (saisie utilisateur) :
eurosToCents(str)BigInt-exact (pas deparseFloat) - Affichage :
Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' })avec division entière
// Correct (BigInt-exact)function eurosToCents(str: string): bigint { const [euros, cents = '00'] = str.replace(',', '.').split('.'); return BigInt(euros) * 100n + BigInt(cents.padEnd(2, '0').slice(0, 2));}
// Faux (float, perte de précision possible)function eurosToCents_WRONG(str: string): bigint { return BigInt(Math.round(parseFloat(str) * 100));}businessDate serveur-autoritaire
Section intitulée « businessDate serveur-autoritaire »La businessDate est toujours résolue côté serveur (ADR-00D78). Les fronts ne calculent jamais la date métier eux-mêmes.
Résolution serveur
Section intitulée « Résolution serveur »operatingMandateId → MandateEstablishmentReader.getEstablishmentId() → BusinessDateProvider.getCurrentBusinessDate(establishmentId) → date métier de l'établissement (stockée en DB, avancée par Night Audit)Si la résolution échoue : Result.fail('folio', 'FOLIO_BUSINESS_DATE_UNRESOLVABLE', ...).
Règle entre minuit et Night Audit
Section intitulée « Règle entre minuit et Night Audit »Entre minuit et 3h00 (heure du Night Audit), la businessDate reste sur J. Les opérations effectuées dans ce créneau appartiennent comptablement à J, même si l’horloge dit J+1.
Côté fronts
Section intitulée « Côté fronts »Les fronts consomment la businessDate via GET /v1/me → EstablishmentContextService. Valeur null = établissement non initialisé, affiche un message — pas de fallback sur new Date().toISOString().slice(0, 10).
Périmètre fiscal = operating_mandate_id
Section intitulée « Périmètre fiscal = operating_mandate_id »Le grand total NF203, la numérotation des factures et la chaîne HMAC sont par mandat fiscal, pas par établissement.
Pourquoi ?
Section intitulée « Pourquoi ? »Un même établissement peut changer de gestionnaire (SAS exploitante cède à une autre). Dans ce cas :
- Le nouveau gestionnaire ouvre un nouveau
OperatingMandate - Le grand total repart de 1 sur le nouveau mandat
- La chaîne HMAC repart de son genesis
- L’ancienne chaîne reste intacte et vérifiable
Si on utilisait l’establishmentId, un changement de gestionnaire serait invisible dans la numérotation — violation NF203.
Impact sur le code
Section intitulée « Impact sur le code »Sur tout nouveau use-case impliquant un folio, une facture ou une entrée d’audit :
- Identifier le
operating_mandate_idassocié (via la réservation ou le mandat actif de l’établissement) - Utiliser
operating_mandate_idcomme clé de partitionnement NF203 - Il n’y a pas de fitness function qui vérifie cela automatiquement — contrôle manuel obligatoire
Taxe de séjour — invariants
Section intitulée « Taxe de séjour — invariants »La taxe de séjour respecte des invariants distincts des autres charges :
chargeNature = 'collected_tax'(pas'revenue')- Compte 4471 (dette envers la commune, pas du CA)
- Hors base de calcul TVA
- Barème effective-dated (la commune peut changer son barème — la taxe appliquée est celle valide à la date du séjour)
- Inaltérable après posting (pas de void de la taxe de séjour — contra bon sens, mais réglementaire)
- Dans le reporting : restitution (lecture depuis
NightAuditCompleted), jamais recalculée a posteriori
VACAF — ventilation de paiement, jamais remise
Section intitulée « VACAF — ventilation de paiement, jamais remise »L’aide CAF (VACAF) est une ventilation de paiement — une partie du règlement vient de la CAF directement. Ce n’est pas une remise sur le prix.
Conséquence comptable :
- Le folio reste à son montant plein
- Le règlement est fractionné : X € client + Y € CAF
- La recette de l’établissement est identique (X + Y)
Si VACAF était traitée comme une remise, la recette serait minorée — incorrection fiscale.
ACOMPTE vs ARRHES
Section intitulée « ACOMPTE vs ARRHES »Déterminé par MandateFiscalProfile.depositNature (fail-closed, zéro défaut) :
| ACOMPTE (CGI 269-2-c) | ARRHES (art. 1590 Cc) | |
|---|---|---|
| TVA | Exigible à l’encaissement | Hors champ |
| Non-présentation client | Remboursable en principe | Acquis par l’établissement |
| Comptabilisation | CRÉDIT 419 + 4457x | Compte spécifique sans TVA |
| Facture finale | Déduit (anti-double TVA) | Régularisation spécifique |
La nature ACOMPTE/ARRHES est figée par le MandateFiscalProfile de l’établissement — il n’est pas possible de choisir au cas par cas.