# #00 — Socle : source unique de vérité pour les tarifs

> Statut : ✅ **fait** — 14/08/2026. Reste trois arbitrages tarifaires, listés en fin de ticket.

**Formulaire :** transverse · **Charge :** L · **Priorité :** conditionne #02, #05, #06, #08, #13

## Le diagnostic de départ était faux sur un point décisif

Ce ticket annonçait que « chaque tarif existe à deux endroits : les saisies Formidable pour
l'affichage, l'objet `prices` du JS pour le calcul ». Le relevé des trois formulaires concernés
montre autre chose : **aucune saisie Formidable ne porte de prix**. Ni dans les libellés, ni dans
les explications, ni ailleurs. Les formulaires ne contiennent que des clés d'options
(`12m2_nu`, `silver`, `packs_mobiliers_small_3`), et c'est le JavaScript qui injecte les montants
à l'affichage autant qu'au calcul.

La conséquence est plus large que le ticket ne le supposait :

- Le scénario redouté — « le client modifie un prix dans Formidable, le JS diverge » — ne pouvait
  pas se produire, faute de prix dans Formidable. Le client **ne pouvait pas modifier un tarif du
  tout**, sauf à éditer le JavaScript.
- La duplication réelle n'était pas celle des montants, mais celle du **vocabulaire de clés** entre
  les options Formidable et la table du JS, plus les noms de classes CSS répétés dans trois
  fichiers. C'est ce lien-là qui n'était vérifié nulle part.
- Le décompte était également inexact : 78 entrées `stands`, pas 85, dont **52 inutilisées** par le
  parcours 2027 (toutes les variantes `premium`).

Au moment de la refonte, aucune clé de formulaire n'était orpheline : la table était un
sur-ensemble. Le socle n'a donc corrigé aucun montant — il a rendu détectable ce qui ne l'était pas.

## Ce qui a été fait

### Une seule source de prix, millésimée

Les montants vivent désormais dans `plugins/reuseeconomyexpo/inc/reex_tarifs.php`, en PHP, versionné
dans Git. Ils sont transmis au navigateur en JSON par le pied de page (`window.REEX_TARIFS`) : il n'y
a plus qu'une copie à l'exécution.

La table est **par édition**. `reex_tarifs_2027()` part de `reex_tarifs_2026()` et n'exprime que les
écarts, si bien que le corps de la fonction est la liste exhaustive des changements de prix de
l'édition — relisible en revue de commit. Cela ferme au passage un défaut qui n'avait pas été
identifié : la table unique s'appliquait indistinctement aux formulaires 2024, 2026 et 2027, de
sorte que la nouvelle grille de #02 aurait **repricé rétroactivement** tout contrat archivé recalculé
par la suite.

Les éditions dont la table est identique ne sont transmises qu'une fois (renvoi `identique_a`) :
la charge utile fait 7,2 Ko aujourd'hui et ne se dédoublera que le jour où une édition divergera.

### Un tarif introuvable n'est plus zéro

`squelettes/js/reex_tarifs.js` expose un tarificateur dont la règle tient en une ligne : toute clé
absente est enregistrée comme anomalie et renvoie `null`, jamais `0`. Les vingt-quatre accès
`prices.X[cle] || 0` du fichier d'origine ont disparu.

Trois primitives, aux rôles distincts :

| | Rôle |
| --- | --- |
| `prix(...chemin)` | Réclame un montant. Toute impasse est signalée. |
| `existe(...chemin)` | Le chemin mène-t-il à un prix ? Silencieux — sert à trier les réponses d'un formulaire, dont beaucoup sont du texte libre. |
| `connait(...chemin)` | Le chemin existe-t-il, sans exiger un montant ? Silencieux. |

La distinction `connait` / `prix` est ce qui empêche un stand dépourvu du tarif applicable à
l'exposant d'être écarté du récapitulatif sans un mot : on reconnaît le stand, puis on **réclame**
son prix pour le mode tarifaire en cours.

### Le signalement

Quand une anomalie est relevée, un bandeau `.alert-error` est inséré en tête de formulaire, les
détails partent en console, et le total est barré (`.montant-incomplet`). L'envoi n'est **pas**
bloqué : une fausse alerte empêcherait toute inscription, ce qui serait pire que le mal. Mais le
montant ne peut plus être signé pour argent comptant. Passer au blocage tiendrait en une ligne dans
`afficherAnomalies()` si le besoin s'en faisait sentir.

Le contrôle de cohérence n'est pas une passe séparée : les fonctions d'affichage interrogent déjà la
table pour chaque option présentée, il suffisait de rapporter ce qu'elles avaient trouvé.

### Récapitulatif : le total enregistré est confronté au recalcul

`displayRecapPrices()` écrasait silencieusement le total recalculé par le total stocké au moment de
l'envoi du dossier. Un tarif modifié entre-temps donnait donc un contrat dont le total n'était plus
la somme de ses lignes, sans que rien ne le signale. Les deux sont désormais comparés, et l'écart
est signalé. Le montant stocké reste celui qui s'affiche : c'est celui qui a été présenté à
l'exposant, il ne doit pas bouger dans son dos.

### Trois bugs de facturation corrigés au passage

1. **Co-exposants : total corrompu.** `prices.coexposants[nombre]` renvoie un objet indexé par mode
   tarifaire, pas un montant. L'ancien code l'ajoutait tel quel au total, qui se concaténait en
   chaîne (`350[object Object]`). Le formulaire de co-exposition affichait donc un total absurde dès
   qu'un nombre de co-exposants était choisi.
2. **Forfait d'inscription jamais recalculé.** Le gestionnaire de la case appelait
   `updateforfaitInscriptionPrice()` — sans majuscule, fonction inexistante. Cocher la case levait
   une `ReferenceError` et le forfait n'était pas reporté dans le total.
3. **Une seconde logique de facturation, fausse et morte.** `displayFormTotalAmount()`, 170 lignes,
   dupliquait le calcul du récapitulatif en aiguillant par classe plutôt que par valeur. Elle n'était
   appelée nulle part et référençait une variable absente de sa portée : elle aurait levé une
   `ReferenceError`. Supprimée — la laisser aurait garanti qu'un jour quelqu'un corrige la mauvaise.

Les onze blocs quasi identiques du récapitulatif (quantité × prix unitaire) sont réunis en un
tableau `lignesQuantite`. Ils avaient déjà divergé : le mange-debout est stocké sous
`mangedebout_` mais tarifé sous `mange_debout`, et le poste `tv` alimente un champ caché
`packs_mobiliers_tv_amount` **qui n'existe pas** dans `demande_inscription_2027`. Le téléviseur est
donc compté dans le total mais sans ligne enregistrée — à traiter avec #06.

## Vérification

```sh
node --test tests/tarifs/     # 11 tests
```

- **Non-régression des montants.** La table PHP est comparée à un instantané de l'objet `prices`
  d'avant la refonte (`tests/tarifs/tarifs-avant-refonte.json`), extrait mécaniquement du fichier
  d'origine — pas retapé. Aucun montant n'a changé ; les seuls ajouts sont les options « aucun » à
  zéro, qui valaient déjà zéro par l'effet du `|| 0` qu'on supprimait.
- **Correspondance des lignes de quantité.** Les douze triplets (préfixe stocké, chemin de prix,
  champ caché) du nouveau tableau ont été confrontés un à un aux blocs d'origine : correspondance
  exacte.
- **Chaîne complète en local.** Page servie, `window.REEX_TARIFS` présent, `reex_tarifs.js` chargé
  avant `contract_form.js`, plus aucune référence à l'ancien objet `prices` dans le JS compacté.

Les parcours de formulaire eux-mêmes n'ont pas été exercés dans un navigateur : c'est la vérification
qui manque, et elle demande un compte exposant.

## Arbitrages tarifaires à rendre

Le socle a mis au jour trois trous dans la grille. Ce sont des décisions de prix, pas des bugs —
aucun montant n'a été inventé. **Tant qu'ils ne sont pas comblés, un bandeau d'erreur s'affiche**
dans les situations décrites.

| Manque | Qui le rencontre | Comportement avant |
| --- | --- | --- |
| `coexposants.{1,2,3}.fidelite` | Un exposant fidélité sur le formulaire de co-exposition | Prix affiché : `undefined` |
| `coexposants.{1,2,3}.partenaire` | Un partenaire, idem | Prix affiché : `undefined` |
| `forfait_inscription.partenaire` | Un co-exposant en mode partenaire | Total à `NaN` |

Et une quatrième, qui est vraisemblablement une coquille mais coûte 900 € :
`coexposants.2.non_adherent` vaut **100**, entre 500 (1 co-exposant) et 1500 (3). La progression
adhérent est linéaire (330, 660, 990, 1320). Tout indique **1000**. Non corrigé : changer un tarif
sans instruction n'est pas de mon ressort.

## Critères d'acceptation

- ✅ Modifier une option en back-office ne peut plus produire un total erroné en silence.
- ✅ Une clé absente de la table déclenche une erreur visible, jamais un `0`.
- ✅ Le récapitulatif et le total enregistré sont confrontés, et tout écart est signalé.
- ⏳ Parcours complet en navigateur avec un compte exposant.
