Flags & configuration
Le sous-système stock & réappro est volontairement gouverné par des flags : chaque changement de comportement depuis la campagne #2886 a été livré derrière une variable d'environnement, désactivée par défaut — flag OFF = comportement octet-identique à l'existant. Cette page est la référence exhaustive : pour chaque flag, la sémantique, le défaut, la valeur du profil production, et quand (ne pas) le basculer.
Le « profil production » est celui du tenant de production (amisdelaferme), vérifié le 2026-07-11. Les autres déploiements partent des défauts (tout OFF) — c'est voulu : un déploiement neuf se comporte comme la plateforme historique tant qu'on n'arme rien.
Où vivent les flags
Les flags se posent dans le .env du déploiement (apps/app/.env sur la box du
tenant). Ce fichier n'est pas versionné : il survit aux git reset et aux
déploiements. Pour lire l'état courant :
grep -E "^(STOCK_|SUPPLIER_|AUTO_VALIDATE_|BEXT_|LEDGER_)" apps/app/.env
Deux familles de lecture coexistent — elles conditionnent comment on applique une bascule :
| Famille | Flags | Pour appliquer |
|---|---|---|
| Lue au chargement du module | STOCK_LEDGER_PESSIMISTIC, STOCK_TRUST_MAX_COVER_WEEKS |
Redémarrage du process obligatoire |
| Lue à l'appel | tous les autres | Prise en compte au prochain appel du process concerné |
L'app Next « bake » l'env au build. Sur le déploiement tenant, basculer un flag pour
les chemins servis par l'application exige un rebuild (next build), pas un simple
restart. Les crons, eux, relisent l'env à chaque exécution (bun charge
automatiquement apps/app/.env quand le cwd est apps/app) — un flag flippé est donc
souvent actif côté cron avant de l'être côté app. Tenez-en compte pendant la fenêtre de
bascule.
Un garde-fou détecte l'oubli de chargement : si STOCK_LEDGER_PESSIMISTIC=false et
STOCK_TRUST_MAX_COVER_WEEKS=0, le moteur de réappro loggue un avertissement
STOCK SAFEGUARD OFF (stockTrustConfigWarning(),
packages/services-supplier-order/src/utils/supplier-order-utils-new.ts). Cette forme
signifie presque toujours « le .env n'a pas été chargé », pas un choix délibéré.
Quels flags gatent quoi
Le pipeline se lit en deux moitiés : le côté écriture (les mouvements physiques qui alimentent le ledger) et le côté lecture (le réappro fournisseur qui consomme ce ledger). Chaque flèche annotée est un point de passage gaté par un flag :
flowchart TB
subgraph W["Côté écriture — mouvements physiques"]
MV["Réception · livraison · ajustement<br/>(scellé marchand, UI)"] --> RPM["recordPhysicalMovement<br/>(math pure)"]
AV["Cron auto-validate<br/>(filet de sécurité)"] -- "AUTO_VALIDATE_GRACE_HOURS<br/>AUTO_VALIDATE_WRITES_STOCK" --> RPM
PSY["Sync produits PrestaShop"] -- "RESET_STOCK_ON_SYNC<br/>(constante code, OFF)" --> RPM
RPM -- "STOCK_LEDGER_ALLOW_BACKORDER" --> LG[("Product.stockLedger — signé<br/>Product.stock = max(0, ledger)")]
end
subgraph R["Côté lecture — réappro fournisseur"]
EV["Événements commandes<br/>+ webhooks PrestaShop"] -- "BEXT_SUPPLIER_RECALC" --> Q[["File supplier-orders-recalc"]]
Q --> ENG["Moteur de recalcul<br/>(write-lock par site)"]
DEM["Demande client à couvrir"] -- "STOCK_FORWARD_DELIVERY_ONLY" --> ENG
ENG -- "SUPPLIER_NO_PAST_SLOTS" --> SO["Bons fournisseurs DRAFT"]
end
LG -- "trustedStock() :<br/>STOCK_LEDGER_PESSIMISTIC<br/>STOCK_TRUST_MAX_COVER_WEEKS<br/>STOCK_TRUST_ALLOW_NEGATIVE" --> ENG
Ledger — côté écriture
Ces flags gouvernent comment un mouvement physique s'enregistre dans le ledger. Le
contrat de fond (chaque écriture de Product.stock vérifie manageStock et émet une
StockAction dans la même transaction) ne se désactive pas — il est structurel. Voir
Chemins d'écriture.
| Variable | Défaut | Prod | Lecture | Effet |
|---|---|---|---|---|
STOCK_LEDGER_ALLOW_BACKORDER |
false |
true (depuis le 2026-07-09) |
à l'appel | Ledger signé = autorité ; Product.stock = miroir bridé |
STOCK_LEDGER_BACKORDER_CUTOVER |
— | 2026-07-09T20:00:18Z |
à l'appel | Borne temporelle du compteur d'audit I6 |
AUTO_VALIDATE_WRITES_STOCK |
true |
true |
à chaque run | false → auto-validate en « paperasse seule » (aucune écriture stock) |
AUTO_VALIDATE_GRACE_HOURS |
12 |
12 |
à chaque run | Heures avant qu'une date planifiée soit réputée réellement advenue |
STOCK_LEDGER_ALLOW_BACKORDER
Le flag central de l'Option B (#2886). OFF (défaut) : la base des écritures est le
compteur Product.stock, bridé à ≥ 0 — un décrément manuel est écrêté à ce qui existe
réellement, et le delta appliqué est enregistré pour que compteur et ledger bougent du
même montant. ON (prod) : Product.stockLedger devient l'autorité signée — un
décrément enregistre le vrai −quantité même sous zéro (un négatif = une précommande
réelle, livrée avant que la réception couvrante soit saisie ; la réception suivante la
nette), et Product.stock est écrit comme miroir d'affichage max(0, ledger). Le
phantom stock par écrêtage devient structurellement impossible.
- Lecture :
allowBackorderEnabled()danspackages/services/src/stock/rolling-stock-math.ts— lu par appel (les tests le flippent par cas). - Sous le flag, l'invariant d'audit I5 se lit
stock == max(0, Σ COMPLETED)au lieu de l'égalité stricte — voir Filets de sécurité. - Compagnon :
STOCK_LEDGER_BACKORDER_CUTOVER(horodatage ISO du cutover) borne le compteur I6 de l'audit — « écrêtages neufs depuis le cutover », qui doit rester à 0.
Ne re-basculez pas ce flag à OFF une fois le cutover fait : les écritures postérieures
au cutover ont la sémantique signée, et le miroir Product.stock a été rebasé dessus.
Un retour arrière re-mélangerait les deux conventions dans le même historique.
AUTO_VALIDATE_WRITES_STOCK
Contrôle si le cron auto-validate (le filet de sécurité qui scelle réceptions et
livraisons oubliées — voir Chronologie quotidienne)
écrit réellement le stock. La condition exacte dans
packages/scripts/src/tasks/auto-validate-stock-actions.ts :
const stockWritesDisabled =
process.env.AUTO_VALIDATE_WRITES_STOCK === "false" ||
process.env.STOCK_LEDGER_PESSIMISTIC === "true";
Donc : activé par défaut (seul un "false" explicite le coupe), et désactivé d'office
en mode pessimiste. Coupé, le cron reste utile — il crée les SupplierOrderDelivery /
OrderFulfillment (la « paperasse ») sans toucher ni Product.stock ni le ledger.
AUTO_VALIDATE_GRACE_HOURS
Le garde-fou temporel du même cron. Une deliveryDate / expectedDeliveryDate est une
date prévue, pas la preuve qu'une livraison a eu lieu : auto-valider une livraison du
jour même a produit l'incident du 2026-06-10 (30 fulfillments + 20 réceptions fantômes).
Le cron n'auto-valide que ce dont la date planifiée est au moins GRACE_HOURS dans le
passé — à 12 h par défaut, le cron est sûr quelle que soit son heure ou sa fréquence
d'exécution. Un marchand qui livre en avance confirme simplement à la main (l'UI, que le
cron saute ensuite via ses gardes d'idempotence).
Frontière de confiance — côté lecture
Ces flags gouvernent combien de stock le moteur de réappro croit détenir au moment de
calculer un bon fournisseur. Tout passe par trustedStock()
(packages/services-supplier-order/src/utils/supplier-order-utils-new.ts), la fonction
épinglée par les 77 tests stock-ledger-trust-boundary.test.ts.
| Variable | Défaut | Prod | Lecture | Effet |
|---|---|---|---|---|
STOCK_LEDGER_PESSIMISTIC |
false |
false |
chargement module | Kill switch : stock ignoré partout (→ 0) |
STOCK_TRUST_MAX_COVER_WEEKS |
0 (off) |
8 |
chargement module | Plafond « stock fantôme » en semaines de consommation |
STOCK_TRUST_ALLOW_NEGATIVE |
false |
false |
à l'appel | Laisse passer un ledger négatif (précommande) au réappro |
LEDGER_AS_SOURCE_OF_TRUTH |
false |
true |
à l'appel | Vue d'ensemble BO : stock de début de période depuis le ledger |
STOCK_LEDGER_PESSIMISTIC — le kill switch
true → trustedStock() renvoie 0 inconditionnellement : le réappro couvre la
demande client complète en ignorant totalement le stock. C'est le mode de survie quand le
ledger a une dérive connue (sources en désaccord, entrées virtuelles non réversées, pas
d'inventaire récent). Risque assumé : sur-commande pour les produits qui ont du vrai
stock — mitigé par la revue des brouillons avant envoi.
Quand basculer à true : dérive constatée du ledger, incident de données en cours,
ou doute sérieux pendant une migration. Quand rester à false (état actuel) : audit
--strict vert (I1–I5 = 0 violation), tous les chemins d'écriture atomiques et audités.
- Effet de bord : force
stockWritesDisableddans auto-validate (voir ci-dessus) — en pessimiste, plus personne n'écrit le stock automatiquement. - Lu une fois au chargement du module : la bascule exige un redémarrage du process cron et un rebuild de l'app.
STOCK_TRUST_MAX_COVER_WEEKS — le bouton de réglage
Le plafond anti-« stock fantôme ». Certaines valeurs Product.stock sont délibérément
gonflées dans PrestaShop (mises très haut pour que la boutique vende à stock ≤ 0, puis
synchronisées) : cohérentes en interne (l'audit I5 passe) mais physiquement fictives.
Sans plafond, elles annulent silencieusement des bons fournisseurs → ruptures.
Quand > 0, trustedStock() plafonne le stock cru à N semaines de consommation
récente (Σ des DECREMENT COMPLETED sur les 56 derniers jours, ramenée à un taux
hebdomadaire par attachRecentWeeklyConsumption). Une consommation nulle ⇒ plafond 0 ⇒
stock totalement ignoré — la direction conservatrice (sur-commander plutôt que
rompre). 0 / non défini = désactivé, zéro requête supplémentaire.
Le compromis du réglage :
- Serré (2–4 semaines) : attrape plus de fantômes, mais sur-commande les produits à rotation lente dont le stock est pourtant réel.
- Lâche (12 semaines) : ne borne que l'absurde (991 unités en stock pour ~1 vente/semaine).
- Prod : 8 — à relâcher (12 ou off) une fois le ledger prouvé propre sur la durée.
Deux exemptions par produit : data.stockTrusted (le marchand a validé le comptage sur
la page Stock fantôme du back-office → la valeur réelle
passe sans plafond) et les fournisseurs madeToOrder (flux tendu, aucun inventaire →
confiance 0 d'office). L'UI utilise le même seuil : phantomCoverBarWeeks() reflète le
flag (ou 8 par défaut), donc ce que la page « Stock fantôme » signale est exactement ce
que le réappro refuserait de croire.
Réglez ce plafond avec des données, pas au doigt mouillé : les scripts de backtest
backtest-ledger-flip.ts (delta A/B sur les bons) et backtest-ledger-coverage.ts
(hit-list des produits par semaines de couverture) rejouent la production en lecture
seule.
STOCK_TRUST_ALLOW_NEGATIVE
La face lecture de l'Option B. OFF (défaut et prod) : un ledger négatif est écrêté à
0 avant le calcul (règle historique #2725 « stocks négatifs = 0 »). ON : un
stockLedger négatif traverse trustedStock() — une précommande réelle doit
augmenter le prochain bon, pas être ignorée. Le passthrough ne concerne que le chemin
ledger (fournisseurs useStockLedger) ; le chemin compteur Product.stock reste bridé.
Consignes de bascule — prérequis stricts, dans l'ordre :
STOCK_LEDGER_ALLOW_BACKORDER=trueen place (sinon un négatif est une dérive legacy, pas une précommande).- ≥ 4 semaines de compteur I6 = 0 (aucun écrêtage neuf depuis le cutover) et watchdog forward-check vert.
- Les tests du comportement gaté sont déjà épinglés — pas de code à écrire, seulement le flip.
LEDGER_AS_SOURCE_OF_TRUTH
Flag d'affichage/reporting, pas de calcul de commande : dans la vue d'ensemble stock
du back-office (getProductStockOverview,
packages/services/src/stock/stock-actions.service.ts), le stock de début de période
est calculé depuis le ledger (Σ des StockActions antérieures à dateFrom) au lieu de
Product.stock − Σ COMPLETED postérieures. Plus robuste dès que le ledger est fiable —
c'est le cas en prod (true).
Pipeline de réappro
Ces flags gouvernent quelle demande on compte et où on la matérialise. Contexte complet : Commandes fournisseurs.
| Variable | Défaut | Prod | Lecture | Effet |
|---|---|---|---|---|
BEXT_SUPPLIER_RECALC |
false |
true |
à l'appel | Recalcul événementiel via la file bext supplier-orders-recalc |
SUPPLIER_NO_PAST_SLOTS |
false |
true |
à l'appel | Plancher les créneaux d'envoi à now + roll-forward de la demande en retard |
SUPPLIER_SLOT_FLOOR_GRACE_MIN |
0 |
0 |
à l'appel | Grâce (minutes) ajoutée au plancher de créneau |
SUPPLIER_NO_PAST_SLOTS_SINCE |
— | 2026-07-10T19:45:00Z |
à l'appel | Exclut les lignes pré-garde du gate de la sonde santé |
STOCK_FORWARD_DELIVERY_ONLY |
false |
true |
à l'appel | Ne compter que la demande à livraison future |
STOCK_FORWARD_DELIVERY_GRACE_DAYS |
0 |
0 |
à l'appel | Grâce (jours) sous le plancher de livraison |
BEXT_SUPPLIER_RECALC
OFF : enqueueSupplierOrderRecalc() (packages/common/src/bext-sdk.ts) est un
no-op — le réappro ne vit que du cron des 30 minutes. ON : chaque producteur
(création / modification / annulation / suppression / changement de statut de commande
client, annulation de bon fournisseur, réception, webhooks PrestaShop, ajustement manuel
de stock) pousse un job scopé dans la file bext supplier-orders-recalc. Le producteur
est best-effort : dédupliqué sur une courte fenêtre (une rafale de changements sur le
même périmètre s'effondre en un job), ne lève jamais, ne bloque jamais l'appelant. Le
worker sérialise contre le cron via le write-lock fournisseur par site.
SUPPLIER_NO_PAST_SLOTS
OFF (défaut) : le moteur assigne la demande en retard à son créneau causal — même
s'il est déjà passé (affiché « en retard »). ON (prod) : trois règles s'ajoutent —
plancher des créneaux d'envoi à now + grâce (SUPPLIER_SLOT_FLOOR_GRACE_MIN),
causalité (send ≥ création de la commande source), et roll-forward : la demande en
retard roule vers le prochain créneau futur au lieu d'être matérialisée dans le
passé. Le validateur de créneau accepte le placement tardif quand l'envoi optimal est
déjà passé — sans quoi la création échouait et la demande était perdue.
- Le replay historique (moteur d'audit rétrospectif) épingle
allowPastSlots: trueexplicitement : il est immunisé contre la garde, ses analyses « à date passée » gardent la sémantique legacy. SUPPLIER_NO_PAST_SLOTS_SINCEne change pas le comportement du moteur : il borne le gate de la sonde santé pour ne pas compter les lignes créées avant l'armement de la garde.
STOCK_FORWARD_DELIVERY_ONLY
OFF (défaut) : fragment de filtre vide, zéro changement. ON (prod) : la
découverte de demande ne compte que les commandes client dont la livraison est encore
devant nous — deliveryDate ≥ plancher (aujourd'hui, moins
STOCK_FORWARD_DELIVERY_GRACE_DAYS éventuels) ; les commandes sans deliveryDate
retombent sur invoiceDate contre le plancher conservateur. La demande en retard non
livrée est volontairement abandonnée (la demande du marchand). Le balayage jumeau
sweepStalePastDeliveryItems draine les lignes déjà matérialisées avant le plancher — DB
filter et sweep partagent la même fonction de plancher (forwardDeliveryFloor), donc les
deux sont d'accord sur ce que « passé » veut dire.
À noter : le bouton de recalcul en place de l'UI calcule le forward-only explicitement (pas via le flag), pour se comporter pareil quel que soit l'état du gate.
Synchronisation PrestaShop
| Réglage | Type | Valeur | Effet |
|---|---|---|---|
RESET_STOCK_ON_SYNC |
constante code (pas env) | false |
Qui possède le stock : le back-office (false) ou PrestaShop (true) |
RESET_STOCK_ON_SYNC — attention, ce n'est pas une variable d'environnement
Contrairement à tout le reste de cette page, ce réglage vit dans le code :
SYSTEM_CONFIG.PRESTASHOP_SYNC.RESET_STOCK_ON_SYNC dans
packages/common/src/utils/constants.ts. Le changer = commit + déploiement.
false(valeur actuelle) : le back-office est l'autorité stock. La sync produits ne réécrit jamaisProduct.stockd'un produit existant, un produit nouveau démarre à 0 (le marchand saisit le réel), et la sync n'émet aucune entrée ledger — correctif #2798 : quand la sync ne possède pas le stock, elle relit après coup des décréments concurrents (cron, éditions manuelles) et se les attribuerait en entrées « Sync stock PrestaShop » fantômes, double-comptant la demande dans le ledger.true: PrestaShop est l'autorité. La sync réinitialiseProduct.stockdepuis PrestaShop et émet la StockAction COMPLETED correspondante (buildPsStockLedgerEntry, #2749 —OPENING_BALANCEpour un produit neuf, deltaPRESTASHOP_SYNCsinon), pour questock == Σ ledgertienne.
Dans les deux positions, la règle 17 tient : jamais d'écriture de stock sans entrée ledger appariée quand on possède l'écriture ; rien du tout quand on ne la possède pas.
Combinaisons à risque
| Forme | Symptôme | Verdict |
|---|---|---|
PESSIMISTIC=false + MAX_COVER_WEEKS=0 |
Warning STOCK SAFEGUARD OFF au boot du moteur |
Presque toujours un .env non chargé — vérifier avant tout le cwd/chargement |
STOCK_TRUST_ALLOW_NEGATIVE=true sans STOCK_LEDGER_ALLOW_BACKORDER=true |
Des négatifs de dérive legacy gonflent les bons | Interdit — le passthrough présuppose la sémantique Option B |
Re-flip ALLOW_BACKORDER à false après cutover |
Historique mixte deux conventions | Interdit — voir l'avertissement plus haut |
Flag flippé dans .env mais app pas rebuildée |
Cron et app en désaccord sur le gate | Fenêtre de bascule : flipper, rebuilder, redémarrer, vérifier les deux |
Check-list de bascule
Avant de flipper un flag de cette page en production :
# 1. Audit du ledger — 0 violation exigée
bun run cli --task stock-ledger-audit \
--tenantId <tenant> --siteId <site> --strict
# 2. Diff de rejeu (lecture seule) — le changement ne déplace pas les bons de façon inattendue
bun packages/scripts/src/dev/replay-supplier-orders.ts \
--tenant-id <tenant> --site-id <site>
# 3. Tests épinglés du sous-système
npm run test:unit
Après la bascule : surveiller la sonde quotidienne supplier-stock-health (elle doit
rester SAINE) et les invariants d'audit — voir
Filets de sécurité.
Voir aussi
- Vue d'ensemble — le modèle stock & réappro en une page
- Le ledger de stock — autorité signée, miroir bridé, invariants
- Chemins d'écriture — tous les sites d'écriture et leur contrat
- Commandes fournisseurs — le pipeline que ces flags gatent
- Chronologie quotidienne — les crons qui lisent ces flags
- Sync PrestaShop — l'intégration ledger de la sync
- Filets de sécurité — audit, rejeu, tests épinglés, sonde