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.

Note

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é
Attention

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() dans packages/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.
Attention

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

truetrustedStock() 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 stockWritesDisabled dans 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.

Astuce

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 :

  1. STOCK_LEDGER_ALLOW_BACKORDER=true en place (sinon un négatif est une dérive legacy, pas une précommande).
  2. ≥ 4 semaines de compteur I6 = 0 (aucun écrêtage neuf depuis le cutover) et watchdog forward-check vert.
  3. 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: true explicitement : il est immunisé contre la garde, ses analyses « à date passée » gardent la sémantique legacy.
  • SUPPLIER_NO_PAST_SLOTS_SINCE ne 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 nousdeliveryDate ≥ 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 jamais Product.stock d'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éinitialise Product.stock depuis PrestaShop et émet la StockAction COMPLETED correspondante (buildPsStockLedgerEntry, #2749 — OPENING_BALANCE pour un produit neuf, delta PRESTASHOP_SYNC sinon), pour que stock == Σ ledger tienne.

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