Preuve · rapport

preuves-argent-2026-09-09.md

Rapport de vérification de l'audit, texte brut · sur GitHub (dépôt privé, accès requis)

Lignes citées par l'audit : 35 42 47 52

163 lignes · ← retour à l'audit · catalogue des preuves

1# Preuves — famille « L'argent » du document publié
2## Vérification phrase par phrase, avec nom de fonction opposable
3
4Date : 09/09/2026 · Auteur : `audit-argent` · Régime : lecture seule, aucun accès serveur, aucun code applicatif exécuté.
5
6Document vérifié : `/private/tmp/claude-501/-Users-gregorydelaloeuf-Documents-scalewayxflexylead/ff38fb78-ccda-4574-ab16-1a49423c4d29/scratchpad/audit-v3.html`
7
8Dépôts lus : `/Users/gregorydelaloeuf/Documents/scalewayxflexylead/code/flexylead-crm-0908/` (`730a5e79`), `/Users/gregorydelaloeuf/Documents/scalewayxflexylead/code/flexylead-crm/` (`888cf02`), `/Users/gregorydelaloeuf/Documents/scalewayxflexylead/code/apteva-platform/`.
9
10### Convention de lecture des chemins
11
12- `functions/` = **sources de travail** (README : « working backend sources », « include local changes that are not necessarily deployed »).
13- `backend/` = **copies de la source déployée** (README : « deployed-source snapshots; these are not the editing source of truth »).
14
15**Statut source/production des fichiers portant les quatre constats** (commande : `diff -q functions/X.mjs backend/X.mjs`) :
16
17| Fichier | Source vs déployé |
18|---|---|
19| `flexylead-manage-offer-catalog.mjs` | **identique** |
20| `flexylead-manage-production-economics.mjs` | **identique** |
21| `flexylead-create-sales.mjs` | **identique** |
22| `flexylead-update-sale.mjs` | **identique** |
23| `flexylead-admin-data-mutation.mjs` | 19 lignes d'écart, aucune sur le périmètre argent |
24| `flexylead-manage-facturation.mjs` | écart = mise en paquet (`bundle: true`), schéma inséré en ligne ; logique identique |
25| `flexylead-app-pilotage-query.mjs` vs `backend/flexylead-app-pilotage-remuneration-query.mjs` | 55 lignes d'écart, **aucune** dans le bloc de rémunération (lignes 2229-2540 identiques) |
26
27**Conséquence : les quatre constats de la famille « L'argent » décrivent ce qui tourne, pas seulement la source de travail.** C'est le seul endroit du document où cette garantie est acquise.
28
29---
30
31## Tableau de vérification
32
33| # | Phrase exacte du document | Verdict | Fonction(s) à nommer dans le texte | Preuve `fichier:ligne` | Texte de remplacement si nécessaire |
34|---|---|---|---|---|---|
35| **1.1** | « Aucun montant dû à un centre n'est jamais figé » (titre) | **VRAI** | `evaluateMonthlyFormula`, `evaluateSaleRemuneration`, `buildPilotageProduction` | `code/flexylead-crm-0908/functions/flexylead-app-pilotage-query.mjs:2264` (`evaluateMonthlyFormula`), `:2421` (`evaluateSaleRemuneration`), `:598` (`buildPilotageProduction`). Aucune écriture : voir commande A ci-dessous. | Titre conservé. Ajouter le nom : « Les trois fonctions qui produisent ce montant — `buildPilotageProduction`, `evaluateMonthlyFormula`, `evaluateSaleRemuneration` — n'écrivent rien. » |
36| **1.2** | « Tout est recalculé à chaque ouverture d'écran » | **VRAI** | `loadPilotageProduction`, `coalescePilotageLoad` | `functions/flexylead-app-pilotage-query.mjs:440` ; `:170-172` commentaire de l'auteur : *« This is not a cache: the promise is removed as soon as the calculation settles, so the next request always reads fresh Tables data »* ; `:4500` en-tête `Cache-Control: private, no-store` | — |
37| **1.3** | « avec les barèmes du jour » | **FAUX** (imprécis, et l'imprécision masque un défaut) | `monthlyFormulaRuleFor`, `activeRow`, `effectiveOn` | `functions/flexylead-app-pilotage-query.mjs:2229-2236` : `.filter(activeRow)` **puis** `.filter((rule) => effectiveOn(rule, date))` — `activeRow` (`:2758`) juge l'état **d'aujourd'hui**, `effectiveOn` (`:2746`) juge la **date de la vente** | **« avec les seules règles encore actives aujourd'hui, dans leur version datée du jour de la vente. Une formule désactivée disparaît donc aussi de toutes les périodes passées : c'est `monthlyFormulaRuleFor` qui écarte l'inactif avant de lire les dates. »** C'est le même défaut que le constat 2, côté centre — il n'est pas dit. |
38| **1.4** | « Il n'existe aucune facture de centre. » | **VRAI** | `createInvoice`, `transitionInvoice`, `FACTURATION_TABLES` | `functions/flexylead-facturation-schema.mjs:94-116` : la table `factures` porte `provider`, **aucune colonne de centre** ; `functions/flexylead-manage-facturation.mjs:671` (`createInvoice`) est indexé sur `provider`. Commandes B et C ci-dessous. | — |
39| **1.5** | « Le logiciel sait pourtant le faire : totaux enregistrés, états qui ne reviennent pas en arrière, copie de la règle appliquée dans chaque ligne. » | **À BORNER** — les trois briques existent, mais « états qui ne reviennent pas en arrière » est faux au sens strict | `createInvoice`, `transitionInvoice`, `INVOICE_TRANSITIONS` | Totaux : `flexylead-facturation-schema.mjs:101-103` (`subtotal_cents`, `vat_cents`, `total_cents`), écrits par `createInvoice` `flexylead-manage-facturation.mjs:746-748`. Règle gelée : colonne `frozen_rule` `flexylead-facturation-schema.mjs:134`, remplie `flexylead-manage-facturation.mjs:733`. États : `INVOICE_TRANSITIONS` `flexylead-manage-facturation.mjs:12-19` — **mais `review → draft` est un retour en arrière** ; l'irréversibilité ne commence qu'à `approved`. | **« totaux enregistrés par `createInvoice`, transitions d'état contrôlées par `transitionInvoice` — irréversibles une fois la facture approuvée — et copie de la règle appliquée gelée dans chaque ligne. »** |
40| **1.6** | « Ce dispositif est entièrement tourné vers les fournisseurs. » | **VRAI** | `flexylead-manage-facturation`, `detectProvider` (détection de format), `FacturationRoute` | `flexylead-manage-facturation.mjs:267-283` : quatre fournisseurs en dur (`selectra`, `free_mobile`, `freebox`, `mint_energy`) ; `src/routes/FacturationRoute.tsx:26` : `const PROVIDERS = ['', 'selectra', 'freebox', 'free_mobile', 'mint_energy']`. Le mot « centre » n'apparaît que 2 fois dans l'écran, jamais comme axe de facturation. | — |
41| **1.7** | *(correctif)* « Appliquer aux centres le modèle qui existe déjà pour les fournisseurs. Le code de référence est écrit et éprouvé : il n'a jamais été appliqué au sujet qui fâche. » | **VRAI** | `flexylead-manage-facturation` | Tests : `functions/flexylead-manage-facturation.test.mjs` existe. Fonction déclarée au manifeste `scripts/application-api-manifest.mjs:66`. | Nommer la fonction dans le texte : « Le moteur qui sait figer un montant existe — `flexylead-manage-facturation` — mais il ne connaît que les fournisseurs. » |
42| **2.1** | « Supprimer une règle de prime l'efface de tout le passé » (titre) | **À BORNER** — l'effacement n'est pas immédiat, il est **différé au prochain enregistrement de la vente** | `buildPricingPlan`, `remunerationFor`, `resolveRemunerationSnapshot`, `referenceRemunerationResource` | La valeur déjà écrite sur la vente (`ventes.prime_montant`) **n'est pas touchée** par la suppression : `flexylead-manage-offer-catalog.mjs:134-143` ne modifie que `offer_remuneration_rules`. Elle continue d'être affichée telle quelle (`src/main.tsx:11002` `salePrimeLabel`). Ce qui est perdu, c'est la **capacité à la retrouver**. | **« Supprimer une règle de prime la rend introuvable pour tout le passé »** — puis, dans le corps : « Le montant déjà écrit sur la vente survit ; c'est la règle qui l'explique et qui permettrait de la recalculer qui disparaît. Au premier réenregistrement de la vente, le montant part avec elle. » |
43| **2.2** | « parce que la recherche écarte les règles inactives avant de regarder les dates » | **VRAI, littéralement** | `remunerationFor` (côté modification), `referenceRemunerationResource` + `currentRemunerationRules` (côté catalogue lu par l'application commerciale) | `functions/flexylead-update-sale.mjs:246-248` : `.filter((row) => isActive(row.active))` **puis, ligne suivante**, `.filter((row) => effectiveOn(row, saleDate))`. Et `functions/flexylead-app-commercial-resource-query.mjs:486-488` : le filtre `{ col: 'active', op: 'eq', value: true }` est posé **dans la requête**, les dates ne sont examinées qu'après, par `currentRemunerationRules` (`:579`). | Ajouter les noms : « `remunerationFor`, dans la modification de vente, et `referenceRemunerationResource`, dans le catalogue lu par l'application, écartent l'inactif avant de lire les dates. » |
44| **2.3** | « Rouvrir une vente ancienne après un tel retrait efface sa prime, ou bloque la modification. » | **À BORNER** — vrai seulement si l'offre ou la nature de vente change dans la modification | `remunerationFor`, `existingRemunerationSnapshot` | `functions/flexylead-update-sale.mjs:41-42` définit `remunerationUnchanged` (même offre **et** même `vente_type`) ; `:62-64` : si `remunerationUnchanged`, c'est `existingRemunerationSnapshot` (`:278`) qui rend la valeur stockée — **la prime est préservée**. Sinon `remunerationFor` est appelée : retour `null` → `:82` écrit `prime_montant: null` ; ou levée d'erreur `:268` / `:270` (« Nature de vente non autorisée pour cette offre. ») → modification bloquée. | **« Rouvrir une vente ancienne et y changer l'offre ou la nature de la vente efface sa prime — ou bloque la modification. `existingRemunerationSnapshot` protège le cas où rien de tout cela ne bouge ; `remunerationFor` gouverne tous les autres. »** |
45| **2.4** | *(correctif)* « Regarder les dates avant l'état actif. Deux lignes de logique, et il faut vérifier ce qui a déjà été effacé. » | **À BORNER** — ce n'est pas deux lignes, c'est **quatre endroits** | `remunerationFor`, `resolveRemunerationSnapshot`, `referenceRemunerationResource`, et — même défaut, côté centre — `monthlyFormulaRuleFor` | `flexylead-update-sale.mjs:246`, `flexylead-create-sales.mjs:736`, `flexylead-app-commercial-resource-query.mjs:487`, `flexylead-app-pilotage-query.mjs:2231` (dans `monthlyFormulaRuleFor`) | **« Faire porter l'état actif sur la date de la vente et non sur l'instant de la lecture. Quatre fonctions le font à l'envers — `remunerationFor`, `resolveRemunerationSnapshot`, `referenceRemunerationResource` et, côté centre, `monthlyFormulaRuleFor`. Il faut ensuite vérifier ce qui a déjà été perdu. »** |
46| **2.5** | *(nouveau, absent du document — à ajouter)* Créer une vente après un tel retrait ne bloque rien : la vente naît **sans prime**, sans message. | **VRAI** | `resolveRemunerationSnapshot` | `functions/flexylead-create-sales.mjs:733-762` : `hasRequiredVariants` (`:741`) est faux quand toutes les variantes ont été désactivées ; `:749` `if (!selected \|\| payoutMode === "none") return null` → `:699-701` écrit `prime_montant: null`. Aucune erreur levée. | **« Et une vente créée après ce retrait naît sans prime, sans avertissement : `resolveRemunerationSnapshot` rend une valeur vide au lieu de refuser. »** |
47| **3.1** | « Côté centre, impossible d'entrer une date passée » | **VRAI, et plus strict encore** | `validatedEffectiveFrom` | `functions/flexylead-manage-production-economics.mjs:562-569` : refus si `candidate <= today` — **le jour même est refusé aussi**, avec le motif écrit dans le test : *« rejects the current day because date-only rules would affect earlier sales »* (`flexylead-manage-production-economics.test.mjs:362`) | Nommer : « `validatedEffectiveFrom` refuse toute date antérieure au lendemain. » |
48| **3.2** | « changer le barème referme l'ancien la veille au lieu de l'écraser » | **VRAI** | `versionRule` | `functions/flexylead-manage-production-economics.mjs:515-541` : `shiftDate(effectiveFrom, -1)` puis `ends_at` sur l'ancienne ligne, insertion d'une nouvelle ligne `local_id` suffixée de la date. Test dédié : `flexylead-manage-production-economics.test.mjs:324` *« saving a new configuration closes the prior version without rewriting it »* | Nommer : `versionRule`. |
49| **3.3** | « Côté prime du commercial : date facultative, rétroactivité libre, écriture par-dessus l'ancienne valeur. » | **VRAI** | `normalizeRule`, `nullableDate`, `buildPricingPlan`, `applyPricingPlan` | Date facultative : `functions/flexylead-manage-offer-catalog.mjs:245-250` `nullableDate` rend `null` sur chaîne vide. Rétroactivité libre : `:179-181`, aucune comparaison à aujourd'hui, seul contrôle `startsAt > endsAt` (`:181`). Écrasement : `:126` `updates.push({ id: Number(current.id), ... })` → `:209` `rows_update` sur la **même ligne**. Test qui l'atteste : `flexylead-manage-offer-catalog.test.mjs:169-173` (montant remplacé sur place). | Nommer : « `normalizeRule` accepte une date vide et n'interdit aucune date passée ; `applyPricingPlan` réécrit la ligne existante au lieu d'en ouvrir une nouvelle. » |
50| **3.4** | « Et l'écran ne rattrape rien. » | **VRAI** | `OfferEditorModal`, `updateRule` | `src/dashboard/pages/Offres.tsx:699` : `<input type="date" ... />` **sans attribut `min`**, valeur pouvant repasser à `null` (`:699` et `:702`) ; composant `OfferEditorModal` (`:414`), correcteur `updateRule` (`:480`). | Nommer : `OfferEditorModal`. |
51| **3.5** | *(correctif)* « Le modèle est à trente lignes de là, dans le même dossier. » | **FAUX** — le compte de lignes est inexact | `validatedEffectiveFrom`, `versionRule` | Les deux fonctions sont dans `flexylead-manage-production-economics.mjs` (lignes 515 et 562, soit 47 lignes d'écart) ; la fonction à corriger, `normalizeRule`, est dans **un autre fichier** du même dossier, `flexylead-manage-offer-catalog.mjs:173`. | **« Le modèle est dans le fichier voisin, `flexylead-manage-production-economics` : `validatedEffectiveFrom` pour la date, `versionRule` pour la nouvelle version. Rien à inventer, tout à recopier. »** |
52| **4.1** | « Pour chaque contrat d'énergie, le système établit quelle règle s'est appliquée et pourquoi. » | **VRAI** | `evaluateSaleRemuneration` | `functions/flexylead-app-pilotage-query.mjs:2421-2440` : `breakdown.push({ ...rule, score, applied })` — libellé de la règle, note retenue, verdict, pour chaque contrat. | Nommer : `evaluateSaleRemuneration`. |
53| **4.2** | « Il ne l'écrit nulle part et ne l'affiche nulle part, sauf pour le montant mensuel. » | **VRAI** | `saleRemunerationForEntry` et `saleRuleMetricsForPeriod` (jettent) ; `evaluateMonthlyFormula` (survit) | Jeté : `functions/flexylead-app-pilotage-query.mjs:2460-2469` — `saleRemunerationForEntry` appelle `evaluateSaleRemuneration` puis ne rend que `{ total, remuneratedCount, excludedCount }` ; idem `saleRuleMetricsForPeriod` `:2442-2458`. Non écrit : aucune écriture dans le fichier (commande A). Exception mensuelle : le `breakdown` de `evaluateMonthlyFormula` (`:2264`) est porté jusqu'à `remunerationPeriods` (`:1116`) et rendu à l'écran par `PilotageProductionRoute` (`src/routes/PilotageProductionRoute.tsx:1105`). | Nommer les deux côtés : « `evaluateSaleRemuneration` produit l'explication ; `saleRemunerationForEntry` la jette en ne gardant que le total. Seul le `breakdown` de `evaluateMonthlyFormula` survit jusqu'à l'écran. » |
54| **4.3** | « Un centre qui conteste un montant n'a donc rien à opposer, et FLEXYLEAD non plus. » | **VRAI** | — | Corollaire de 1.1, 1.4 et 4.2 : rien d'enregistré, rien d'affiché par contrat. | — |
55| **4.4** | *(correctif)* « Écrire ce que le code produit déjà. Le calcul existe ; il lui manque une destination. » | **VRAI** | `evaluateSaleRemuneration`, `saleRemunerationForEntry` | idem 4.2 | Nommer les deux fonctions dans le texte. |
56
57---
58
59## Carte, étage 4
60
61| # | Phrase exacte | Verdict | Fonction(s) à nommer | Preuve | Remplacement |
62|---|---|---|---|---|---|
63| **M1** | « La prime du commercial — Calculée au moment de la vente, à partir de l'offre, du type de vente, du centre et de la date. Écrite sur la vente. » | **VRAI** | `resolveRemunerationSnapshot` | `functions/flexylead-create-sales.mjs:733-762` : filtres sur `offre_id` (`:735`), `active` + `effectiveOn(saleDate)` (`:736`), portée `global`/`centre` (`:737-738`), variante `vente_type` (`:740`). Écriture sur la vente : `:699-701` (`prime_montant`, `prime_devise`, `prime_rule_id`). Fichier **identique** entre source et copie déployée. | Nommer : `resolveRemunerationSnapshot`. |
64| **M2** | « Solide — La note de qualité n'entre pas dans ce calcul. Une mauvaise note ne retire rien au commercial. » | **VRAI** | `resolveRemunerationSnapshot`, `remunerationFor` | Commande D ci-dessous : **zéro occurrence** du mot `score` dans `functions/flexylead-create-sales.mjs` et `functions/flexylead-update-sale.mjs`. Les totaux de prime affichés sont de simples sommes de `prime_montant` (`functions/flexylead-app-commercial-resource-query.mjs:749`, `functions/flexylead-app-dashboard-query.mjs:1168`). | — |
65| **M3** | « Ce qui est versé au centre — Recalculé à chaque ouverture de l'écran, à partir des ventes, des notes de qualité et des barèmes en vigueur. » | **À BORNER** sur « barèmes en vigueur » (même défaut qu'en 1.3) ; **VRAI** sur le reste, avec une précision à ajouter sur les notes | `buildPilotageProduction`, `latestReportScoreIndex`, `evaluateMonthlyFormula` | Notes : `functions/flexylead-app-pilotage-query.mjs:2379-2397` — `latestReportScoreIndex` retient la note **la plus récemment modifiée** (`modified >= current.modified`), donc une renotation postérieure change le montant d'une période close. Recalcul : voir 1.2. | **« Recalculé à chaque ouverture de l'écran, à partir des ventes, de la dernière note de qualité en date — `latestReportScoreIndex` retient toujours la plus récente, même postérieure à la période — et des règles encore actives aujourd'hui. »** |
66| **M4** | « Défaut — Aucun montant n'est jamais enregistré. Il n'existe aucune facture de centre dans le système. » | **VRAI** | `buildPilotageProduction`, `flexylead-manage-facturation` | Commandes A, B, C ci-dessous. | — |
67
68---
69
70## « Ce qui tient »
71
72| # | Phrase exacte | Verdict | Fonction(s) à nommer | Preuve | Remplacement |
73|---|---|---|---|---|---|
74| **T1** | « La note de qualité n'entre pas dans la prime du commercial. Une mauvaise note ne lui retire rien. » | **VRAI** | `resolveRemunerationSnapshot` | idem M2 (commande D). **Contre-lecture menée** : la note *retire* en revanche au **centre** — `evaluateSaleRemuneration` peut exclure un contrat (`effect: "exclude"`, `functions/flexylead-app-pilotage-query.mjs:2432-2434`). Les deux moitiés du calcul ne traitent pas la note de la même façon ; le document a raison de créditer le côté commercial, il gagnerait à dire l'asymétrie. | Ajouter une phrase : « `resolveRemunerationSnapshot` ne lit jamais de note. Côté centre, `evaluateSaleRemuneration` en lit une et peut exclure un contrat : l'asymétrie est voulue, elle n'est écrite nulle part. » |
75| **T2** | « Les barèmes sont versionnés » | **FAUX tel qu'écrit** — un seul des deux barèmes l'est, et le document le dit lui-même trois paragraphes plus loin (constat 3) | `versionRule` (versionne) · `applyPricingPlan` (ne versionne pas) | Versionné : `functions/flexylead-manage-production-economics.mjs:515` + test `:324`. Non versionné : `functions/flexylead-manage-offer-catalog.mjs:126` et `:209` — `rows_update` sur la ligne existante ; test `flexylead-manage-offer-catalog.test.mjs:169-173`. | **« Le barème du centre est versionné : `versionRule` ouvre une nouvelle ligne et referme l'ancienne. »** — et supprimer la généralisation. En l'état, la phrase contredit le constat 3 du même document. |
76| **T3** | « et une suppression est toujours une désactivation, jamais un effacement » *(même phrase que T2)* | **FAUX** — hors de mon périmètre nominal, mais **une vente porte un montant**, donc je le signale | `flexylead-admin-data-mutation`, `deleteVente` | `functions/flexylead-admin-data-mutation.mjs:7` : la politique de la table `ventes` est déclarée `policy([...], true)` — le second argument est `allowDelete`. `:35` autorise l'opération, `:55` appelle `rows_delete`. Chemin d'écran : `src/dashboard/api/client.ts:4423` `deleteVente` → `src/dashboard/pages/Operations.tsx:2043`. La vente supprimée emporte sa prime et son instantané de CA. | **« Une suppression est une désactivation partout, sauf pour une vente : `flexylead-admin-data-mutation` est la seule table de la chaîne d'argent où l'effacement définitif est autorisé, et l'écran `deleteVente` l'expose. »** |
77| **T4** | « Trois doctrines saines, vérifiées par leurs propres tests. » | **À BORNER** | `flexylead-manage-production-economics.test.mjs` | Le test existe et est probant pour le barème centre (`:324`, `:348`, `:362`). Il n'existe **aucun** test équivalent pour le barème de prime : `flexylead-manage-offer-catalog.test.mjs` ne teste ni date d'effet ni versionnement. | **« Une doctrine saine, vérifiée par son propre test — côté centre seulement. »** |
78
79---
80
81## « Ce que nous avons retiré », point 3
82
83| # | Phrase exacte | Verdict | Fonction(s) à nommer | Preuve | Remplacement |
84|---|---|---|---|---|---|
85| **R1** | « L'opération enregistre l'état avant et l'état après » | **VRAI**, pour le chemin du commercial | `createAuditEvent` | `functions/flexylead-update-sale.mjs:490-499` : insertion dans `commercial_sale_edit_events` de `before_state` et `after_state`, plus `previous_version` et `actor_auth_user_id`. Le contrôle de concurrence `:507-517` ajoute une garantie d'unicité. | Nommer : `createAuditEvent`. |
86| **R2** | « le seul endroit de toute la chaîne d'argent où la valeur précédente est conservée » | **VRAI** | `createAuditEvent` (conserve) ; `createAudit` de `flexylead-admin-data-mutation` (ne conserve pas) | `functions/flexylead-admin-data-mutation.mjs:175-191` : la table `admin_mutation_events` ne comporte que `payload_json` (l'état **demandé**) et `result_json` — **aucune colonne d'état antérieur** ; insertion `:213-218`. `flexylead-manage-offer-catalog.mjs:126` garde bien un `before`, mais **en mémoire seulement**, pour la reprise (`rollback` `:226`, journal alimenté ligne `:210`) — rien n'est persisté. | Renforcer, en nommant le contre-exemple : « …le seul endroit : `createAuditEvent` conserve la valeur précédente, `createAudit` du côté administrateur n'enregistre que la valeur demandée, et le `before` de `applyPricingPlan` ne vit qu'en mémoire, le temps d'une reprise sur erreur. » |
87| **R3** | « celui qui modifie n'est pas un administrateur, c'est le commercial propriétaire » | **FAUX tel qu'écrit** — il y a **deux** chemins, et c'est celui de l'administrateur qui laisse le moins de trace | `assertSaleOwnership` (chemin commercial) ; `flexylead-admin-data-mutation` + `updateVente` (chemin administrateur) | Chemin commercial : `functions/flexylead-update-sale.mjs:602-604` `assertSaleOwnership` — *« Vous ne pouvez modifier que vos propres ventes. »*, et `resolveActorCommercial` (`:616`) refuse toute identité empruntée. Chemin administrateur : `functions/flexylead-admin-data-mutation.mjs:7` autorise `prime_montant` et `prime_devise` sur la table `ventes` ; `assertAdmin` (`:226`) exige un rôle d'administrateur ; écran `src/dashboard/pages/Operations.tsx:5172-5174` (`prime_montant`, `prime_devise`, `prime_rule_id`) → `src/dashboard/api/client.ts:4414` `updateVente` → `adminDataMutation` (`:122`). | **« Deux chemins existent, et ils ne se valent pas. `flexylead-update-sale` n'accepte que le commercial propriétaire — `assertSaleOwnership` — sans limite de temps et même après validation ; il conserve l'état antérieur. `flexylead-admin-data-mutation` accepte un administrateur, autorise la prime, et n'enregistre que la valeur demandée. La trace complète est du côté du commercial ; le pouvoir sans trace complète est du côté de l'administrateur. »** |
88| **R4** | « sans limite de temps, même après validation » | **VRAI** | `EDITABLE_STATUSES` | `functions/flexylead-update-sale.mjs:4` : `EDITABLE_STATUSES` contient `"validee"`. Aucune borne temporelle : commande E ci-dessous. | Nommer : `EDITABLE_STATUSES`. |
89| **R5** | *(nouveau, à ajouter au même point)* Le champ qui documente la règle appliquée, `prime_rule_id`, n'est **pas** dans la liste des champs qu'un administrateur peut écrire, alors que le montant l'est. L'écran l'envoie quand même. | **VRAI** | `POLICIES` / `validatedUpdatePayload` | `functions/flexylead-admin-data-mutation.mjs:7` : la liste autorisée pour `ventes` contient `prime_montant` et `prime_devise`, **pas** `prime_rule_id`. `validatedUpdatePayload` (`:88-129`) le range dans les champs protégés et ne le laisse passer que s'il est **identique** au stocké (`equivalentSubmittedValue` `:150`). L'écran `Operations.tsx:5174` l'envoie systématiquement. | **« Un administrateur peut changer le montant de la prime, pas l'identifiant de la règle qui l'explique : `validatedUpdatePayload` accepte le premier et protège le second. Après une correction, le montant et sa justification ne se correspondent plus. »** |
90
91---
92
93## Constats de mon périmètre absents du document
94
95Ils ne sont pas des corrections de phrases publiées : ce sont des trous. Je les pose avec leur preuve, l'agent principal arbitre.
96
971. **La prime du commercial sert de solde de secours au centre.** Quand aucune règle mensuelle ne se résout pour un contrat d'énergie, `buildPilotageProduction` ajoute à la rémunération du centre le montant `prime_montant` de la vente — c'est-à-dire la prime du commercial. `functions/flexylead-app-pilotage-query.mjs:993-997` et `:1005-1007` (`else profit.centreRemuneration += fixedCentreRemuneration`), où `fixedCentreRemuneration` retombe sur `nonNegative(entry.sale.prime_montant)` faute de règle. Deux grandeurs de nature différente sont additionnées dans le même total. `FAIT`.
982. **Le rattrapage rétroactif existe pour le CA fournisseur, pas pour la prime.** `flexylead-reconcile-sale-revenue` renseigne les instantanés de CA manquants — et **seulement les manquants**, avec le commentaire de l'auteur `functions/flexylead-reconcile-sale-revenue.mjs:11` : *« Existing financial snapshots are immutable »*. Aucune fonction équivalente pour `prime_montant` : commande F. Une vente créée pendant un trou de configuration reste sans prime pour toujours. `FAIT`.
993. **Deux définitions de période coexistent dans la chaîne d'argent.** La rémunération du centre s'appuie sur `remunerationPeriodForDate` et la table `stats_periods` (`functions/flexylead-app-pilotage-query.mjs:2508`), qui ne sont pas des mois civils ; la facturation fournisseur impose le mois civil — `finalizePeriod` `functions/flexylead-manage-facturation.mjs:566` : `if (!/^\d{4}-\d{2}$/.test(period))` et `:574-575` bornes `${period}-01T00:00:00Z` → premier du mois suivant. Un rapprochement entre les deux ne peut pas tomber juste par construction. `FAIT`.
1004. **La borne de période est un jour, pas un instant.** `effectiveOn` compare des chaînes tronquées à dix caractères (`functions/flexylead-update-sale.mjs:350`, `functions/flexylead-create-sales.mjs:` `effectiveDate`). Une vente horodatée à 02:00 un jour de bascule est du côté du **jour civil du serveur**, sans fuseau explicite ; c'est précisément le motif invoqué par le test de `flexylead-manage-production-economics` pour refuser le jour même. Le comportement est cohérent, il n'est pas documenté. `FAIT` sur le mécanisme, `SUGGÉRÉ` sur la conséquence exacte tant que le fuseau du serveur n'est pas connu.
101
102---
103
104## Commandes de recherche établissant les affirmations d'absence
105
106Toutes exécutées depuis `/Users/gregorydelaloeuf/Documents/scalewayxflexylead/code/`, en lecture seule.
107
108**A — aucune fonction qui évalue la rémunération d'un centre n'écrit quoi que ce soit**
109```
110for f in $(grep -rl "evaluateMonthlyFormula" flexylead-crm-0908/functions flexylead-crm-0908/backend | grep -v test); do
111  echo "$(grep -c 'rows_insert\|rows_update\|rows_delete' "$f") écritures — $f"; done
112```
113Résultat : `0` pour les 8 fichiers concernés (2 sources de travail, 6 copies déployées).
114
115**B — aucune table de la base ne stocke un montant dû à un centre**
116```
117grep -rhn "cents\|montant\|amount" flexylead-crm-0908/functions/*.mjs | grep -E "column\(|name: '" | sort -u
118```
119Résultat : 8 colonnes, **toutes** dans `flexylead-facturation-schema.mjs` (facturation fournisseur). Aucune colonne de montant de centre.
120
121**C — la facturation ne connaît pas la notion de centre**
122```
123grep -rn "facture" flexylead-crm-0908/functions flexylead-crm-0908/backend \
124  flexylead-crm/functions flexylead-crm/backend apteva-platform | grep -i "centre"
125```
126Résultat : aucune ligne.
127
128**D — la note de qualité n'apparaît pas dans le calcul de la prime**
129```
130grep -n "score\|rapport\|quality\|qualite\|ai_" flexylead-crm-0908/functions/flexylead-create-sales.mjs
131grep -n "score\|rapport\|quality" flexylead-crm-0908/functions/flexylead-update-sale.mjs
132```
133Résultat : aucune ligne dans les deux fichiers.
134
135**E — aucune borne temporelle à la modification d'une vente**
136```
137grep -n "jours\|days\|window\|delai\|deadline\|expire" flexylead-crm-0908/functions/flexylead-update-sale.mjs
138```
139Résultat : seules occurrences liées à `CLAIM_TTL_MS` (verrou de numéro de contrat, 10 min), aucune sur l'ancienneté de la vente.
140
141**F — aucun rattrapage rétroactif de la prime**
142```
143grep -n "prime_montant\|prime_rule_id" flexylead-crm-0908/functions/flexylead-reconcile-sale-revenue.mjs
144```
145Résultat : aucune ligne.
146
147---
148
149## Ce que je n'ai pas pu établir
150
151- **Les volumes.** Aucune ligne de la base de production n'a été lue. Aucun des écarts ci-dessus n'est chiffré, et je ne propose aucun chiffrage : il n'existe pas de source de volume dans le corpus qui ne soit pas elle-même un écran contesté. Le chiffrage exige la base.
152- **La version réellement en service.** Les sept fichiers du périmètre argent sont identiques entre source de travail et copie déployée, ce qui écarte le doute **pour ces fichiers**. Il subsiste pour l'écran (`src/`), dont aucune copie déployée n'est fournie : les constats 3.4 (absence de garde-fou à l'écran) et R3/R5 (formulaire d'administration) reposent donc sur la **source de travail seule** et doivent être signalés comme tels.
153- **`flexylead-crm` (`888cf02`).** Sa copie déployée ne contient ni `flexylead-manage-offer-catalog` ni `flexylead-update-sale` ; je n'ai donc pas pu vérifier l'état antérieur des constats 2 et 3 sur cet instantané.
154
155## Contre-lecture
156
157J'ai cherché le cas où chaque constat serait faux.
158
159- **Constat 1.** Hypothèse écartée : la fixation pourrait se faire ailleurs, par un travail périodique. Vérifié : aucune tâche planifiée n'appelle une fonction de rémunération de centre, et aucune des huit fonctions qui évaluent la formule n'écrit (commande A). Hypothèse non écartée : `factures` **pourrait** techniquement accueillir un centre dans sa colonne `provider`, qui est un texte libre. Rien dans le code ni dans l'écran ne le prévoit, mais ce n'est pas structurellement interdit.
160- **Constat 2.** Hypothèse examinée et **retenue en partie** : écarter les règles inactives est peut-être la définition métier voulue — « une règle retirée n'a plus cours ». Elle tient pour l'avenir ; elle ne tient pas pour la relecture du passé, et le code lui-même prouve que l'auteur connaît la bonne doctrine, puisqu'il l'applique dans `versionRule` à trois fichiers de là. C'est cette incohérence interne, et non une opinion d'auditeur, qui fonde le constat.
161- **Constat 3.** Hypothèse examinée : l'absence de garde-fou sur la prime pourrait être délibérée, une prime étant une décision commerciale rétroactivement négociable, là où un barème de centre est contractuel. C'est plausible et je ne peux pas l'écarter. Ce que je maintiens : la rétroactivité libre est ici **silencieuse** — `applyPricingPlan` écrase sans conserver, donc même une rétroactivité voulue devient invérifiable.
162- **Constat 4.** Hypothèse écartée : le détail pourrait être reconstitué a posteriori. Non — il dépend de la note retenue **au moment du calcul**, et `latestReportScoreIndex` ne garde que la dernière. Une renotation postérieure rend la reconstitution impossible.
163