Flags & configuration
The stock & ordering subsystem is deliberately flag-governed: every behaviour change since the #2886 campaign shipped behind an environment variable that is off by default — flag OFF means byte-identical to the previous behaviour. This page is the exhaustive reference: for each flag, its semantics, default, production-profile value, and when (not) to flip it.
The "production profile" is the production tenant (amisdelaferme), verified 2026-07-11. Other deployments start from the defaults (everything OFF) — by design: a fresh deployment behaves like the historical platform until you arm something.
Where the flags live
Flags are set in the deployment .env (apps/app/.env on the tenant box). That file
is not versioned: it survives git reset and deploys. To read the current state:
grep -E "^(STOCK_|SUPPLIER_|AUTO_VALIDATE_|BEXT_|LEDGER_)" apps/app/.env
Two read families coexist — they determine how you apply a flip:
| Family | Flags | To apply |
|---|---|---|
| Read at module load | STOCK_LEDGER_PESSIMISTIC, STOCK_TRUST_MAX_COVER_WEEKS |
Process restart required |
| Read per call | everything else | Picked up on the next call in the affected process |
The Next app bakes env at build time. On the tenant deployment, flipping a flag for
paths served by the application requires a rebuild (next build), not just a
restart. The crons, however, re-read the env on every run (bun auto-loads
apps/app/.env when the cwd is apps/app) — so a flipped flag is often live on the cron
side before it is on the app side. Account for that window during a flip.
A guard catches the "env silently not loaded" failure: if
STOCK_LEDGER_PESSIMISTIC=false and STOCK_TRUST_MAX_COVER_WEEKS=0, the ordering
engine logs a STOCK SAFEGUARD OFF warning (stockTrustConfigWarning(),
packages/services-supplier-order/src/utils/supplier-order-utils-new.ts). That shape
almost always means "the .env wasn't loaded", not a deliberate choice.
Which flags gate what
The pipeline reads as two halves: the write side (the physical movements feeding the ledger) and the read side (the supplier ordering that consumes it). Every annotated arrow is a checkpoint gated by a flag:
flowchart TB
subgraph W["Write side — physical movements"]
MV["Reception · delivery · adjustment<br/>(merchant seal, UI)"] --> RPM["recordPhysicalMovement<br/>(pure math)"]
AV["Auto-validate cron<br/>(safety net)"] -- "AUTO_VALIDATE_GRACE_HOURS<br/>AUTO_VALIDATE_WRITES_STOCK" --> RPM
PSY["PrestaShop product sync"] -- "RESET_STOCK_ON_SYNC<br/>(code constant, OFF)" --> RPM
RPM -- "STOCK_LEDGER_ALLOW_BACKORDER" --> LG[("Product.stockLedger — signed<br/>Product.stock = max(0, ledger)")]
end
subgraph R["Read side — supplier ordering"]
EV["Order events<br/>+ PrestaShop webhooks"] -- "BEXT_SUPPLIER_RECALC" --> Q[["supplier-orders-recalc queue"]]
Q --> ENG["Recalc engine<br/>(per-site write lock)"]
DEM["Client demand to cover"] -- "STOCK_FORWARD_DELIVERY_ONLY" --> ENG
ENG -- "SUPPLIER_NO_PAST_SLOTS" --> SO["DRAFT supplier orders"]
end
LG -- "trustedStock():<br/>STOCK_LEDGER_PESSIMISTIC<br/>STOCK_TRUST_MAX_COVER_WEEKS<br/>STOCK_TRUST_ALLOW_NEGATIVE" --> ENG
Ledger — write side
These flags govern how a physical movement is recorded into the ledger. The
underlying contract (every Product.stock write checks manageStock and emits a
StockAction in the same transaction) cannot be turned off — it is structural. See
Write paths.
| Variable | Default | Prod | Read | Effect |
|---|---|---|---|---|
STOCK_LEDGER_ALLOW_BACKORDER |
false |
true (since 2026-07-09) |
per call | Signed ledger = authority; Product.stock = clamped mirror |
STOCK_LEDGER_BACKORDER_CUTOVER |
— | 2026-07-09T20:00:18Z |
per call | Time bound for the I6 audit counter |
AUTO_VALIDATE_WRITES_STOCK |
true |
true |
each run | false → auto-validate goes "paperwork-only" (no stock writes) |
AUTO_VALIDATE_GRACE_HOURS |
12 |
12 |
each run | Hours before a planned date is presumed to have actually happened |
STOCK_LEDGER_ALLOW_BACKORDER
The central Option B flag (#2886). OFF (default): writes are based on the
Product.stock counter, clamped ≥ 0 — a manual decrement is capped to what actually
exists, and the applied delta is recorded so counter and ledger move by the same
amount. ON (prod): Product.stockLedger becomes the signed authority — a
decrement records the true −quantity even below zero (a negative is a real backorder:
delivered before the covering receipt was booked; the next reception nets it out), and
Product.stock is written as the display mirror max(0, ledger). Phantom stock via
clamping becomes structurally impossible.
- Read site:
allowBackorderEnabled()inpackages/services/src/stock/rolling-stock-math.ts— read per call (tests flip it per case). - Under the flag, the I5 audit invariant reads
stock == max(0, Σ COMPLETED)instead of strict equality — see Safety nets. - Companion:
STOCK_LEDGER_BACKORDER_CUTOVER(ISO timestamp of the cutover) bounds the audit's I6 counter — "fresh clamps since cutover", which must stay at 0.
Do not flip this flag back OFF once the cutover has happened: writes after the
cutover carry the signed semantics, and the Product.stock mirror has been rebased on
it. Reverting would mix the two conventions inside the same history.
AUTO_VALIDATE_WRITES_STOCK
Controls whether the auto-validate cron (the safety net that seals forgotten
receptions and deliveries — see Daily timeline)
actually writes stock. The exact condition in
packages/scripts/src/tasks/auto-validate-stock-actions.ts:
const stockWritesDisabled =
process.env.AUTO_VALIDATE_WRITES_STOCK === "false" ||
process.env.STOCK_LEDGER_PESSIMISTIC === "true";
So: enabled by default (only an explicit "false" disables it), and forcibly disabled
in pessimistic mode. When off, the cron is still useful — it creates the
SupplierOrderDelivery / OrderFulfillment records (the "paperwork") without touching
Product.stock or the ledger.
AUTO_VALIDATE_GRACE_HOURS
The same cron's temporal guard. A deliveryDate / expectedDeliveryDate is a planned
date, not proof a delivery happened: auto-validating same-day deliveries produced the
2026-06-10 incident (30 fulfillments + 20 phantom receptions). The cron only
auto-validates items whose planned date is at least GRACE_HOURS in the past — at the
default 12 h, the cron is safe at any run time or frequency. A merchant who delivers
early simply confirms manually (through the UI, which the cron then skips via its
idempotency guards).
Trust boundary — read side
These flags govern how much stock the ordering engine believes it holds when
computing a supplier order. Everything flows through trustedStock()
(packages/services-supplier-order/src/utils/supplier-order-utils-new.ts), the function
pinned by the 77 stock-ledger-trust-boundary.test.ts tests.
| Variable | Default | Prod | Read | Effect |
|---|---|---|---|---|
STOCK_LEDGER_PESSIMISTIC |
false |
false |
module load | Kill switch: stock ignored everywhere (→ 0) |
STOCK_TRUST_MAX_COVER_WEEKS |
0 (off) |
8 |
module load | "Phantom stock" ceiling in weeks of consumption |
STOCK_TRUST_ALLOW_NEGATIVE |
false |
false |
per call | Pass a negative ledger (backorder) through to ordering |
LEDGER_AS_SOURCE_OF_TRUTH |
false |
true |
per call | BO overview: start-of-period stock computed from the ledger |
STOCK_LEDGER_PESSIMISTIC — the kill switch
true → trustedStock() returns 0 unconditionally: ordering covers the full client
demand while ignoring stock entirely. This is the survival mode for when the ledger has
known drift (sources disagreeing, un-reversed virtual entries, no recent physical count).
Accepted risk: over-ordering for products with genuine stock — mitigated by draft review
before sending.
When to flip to true: observed ledger drift, an ongoing data incident, or serious
doubt during a migration. When to stay at false (current state): --strict audit
green (I1–I5 = 0 violations), all write paths atomic and audited.
- Side effect: it forces
stockWritesDisabledin auto-validate (see above) — in pessimistic mode, nothing writes stock automatically anymore. - Read once at module load: flipping requires restarting the cron process and rebuilding the app.
STOCK_TRUST_MAX_COVER_WEEKS — the tunable knob
The anti-"phantom stock" ceiling. Some Product.stock values are deliberately inflated
in PrestaShop (set very high so the storefront keeps selling at stock ≤ 0, then synced
in): internally consistent (the I5 audit passes) but physically fictional. Without a
ceiling they silently zero out supplier orders → stockouts.
When > 0, trustedStock() caps raw on-hand at N weeks of recent consumption
(Σ COMPLETED DECREMENTs over the last 56 days, turned into a weekly rate by
attachRecentWeeklyConsumption). Zero consumption ⇒ ceiling 0 ⇒ stock fully
distrusted — the conservative direction (over-order rather than stock out). 0 /
unset = disabled, zero extra queries.
The tuning trade-off:
- Tight (2–4 weeks): catches more phantoms, but over-orders slow movers whose stock is nonetheless real.
- Loose (12 weeks): only bounds the absurd (991 units on hand at ~1 sale/week).
- Prod: 8 — to be relaxed (12 or off) once the ledger is proven clean over time.
Two per-product exemptions: data.stockTrusted (the merchant validated the count on the
phantom-stock page of the back office → the real value
passes uncapped) and madeToOrder vendors (just-in-time, no inventory → trusted 0
outright). The UI uses the same bar: phantomCoverBarWeeks() mirrors the flag (or 8 by
default), so what the "Phantom stock" page flags is exactly what ordering would refuse to
believe.
Tune this ceiling with data, not gut feeling: the backtest scripts
backtest-ledger-flip.ts (A/B delta on the orders) and backtest-ledger-coverage.ts
(per-product weeks-of-cover hit-list) replay production read-only.
STOCK_TRUST_ALLOW_NEGATIVE
The read side of Option B. OFF (default and prod): a negative ledger is clamped to 0
before the computation (the historical #2725 rule "negative stock = 0"). ON: a
negative stockLedger passes through trustedStock() — a real backorder must
increase the next order, not be ignored. The passthrough only applies to the ledger
path (useStockLedger vendors); the Product.stock counter path stays clamped.
Flip guidance — strict prerequisites, in order:
STOCK_LEDGER_ALLOW_BACKORDER=truealready in place (otherwise a negative is legacy drift, not a backorder).- ≥ 4 weeks of I6 counter = 0 (no fresh clamps since the cutover) and a green forward-check watchdog.
- Tests for the gated behaviour are already pinned — no code to write, only the flip.
LEDGER_AS_SOURCE_OF_TRUTH
A display/reporting flag, not ordering math: in the back-office stock overview
(getProductStockOverview, packages/services/src/stock/stock-actions.service.ts), the
start-of-period stock is computed from the ledger (Σ of StockActions before
dateFrom) instead of Product.stock − Σ COMPLETED after. More robust as soon as the
ledger is trustworthy — which is the case in prod (true).
Ordering pipeline
These flags govern which demand gets counted and where it materialises. Full context: Supplier orders.
| Variable | Default | Prod | Read | Effect |
|---|---|---|---|---|
BEXT_SUPPLIER_RECALC |
false |
true |
per call | Event-driven recalc via the bext supplier-orders-recalc queue |
SUPPLIER_NO_PAST_SLOTS |
false |
true |
per call | Floor send slots at now + roll overdue demand forward |
SUPPLIER_SLOT_FLOOR_GRACE_MIN |
0 |
0 |
per call | Grace (minutes) added to the slot floor |
SUPPLIER_NO_PAST_SLOTS_SINCE |
— | 2026-07-10T19:45:00Z |
per call | Excludes pre-guard rows from the health probe's gate |
STOCK_FORWARD_DELIVERY_ONLY |
false |
true |
per call | Only count demand with a future delivery |
STOCK_FORWARD_DELIVERY_GRACE_DAYS |
0 |
0 |
per call | Grace (days) below the delivery floor |
BEXT_SUPPLIER_RECALC
OFF: enqueueSupplierOrderRecalc() (packages/common/src/bext-sdk.ts) is a no-op —
ordering lives off the 30-minute cron alone. ON: every producer (customer order
create / update / cancel / delete / status change, supplier-order cancel, reception,
PrestaShop webhooks, manual stock adjustment) pushes a scoped job into the bext
supplier-orders-recalc queue. The producer is best-effort: deduped over a short
window (a burst of changes on the same scope collapses into one job), never throws,
never blocks the caller. The worker serialises against the cron via the per-site
supplier write lock.
SUPPLIER_NO_PAST_SLOTS
OFF (default): the engine assigns overdue demand to its causal slot — even if it is
already in the past (shown as "late"). ON (prod): three rules kick in — send slots
are floored at now + grace (SUPPLIER_SLOT_FLOOR_GRACE_MIN), causality
(send ≥ source order creation), and roll-forward: overdue demand rolls to the
next future slot instead of being materialised in the past. The slot validator
accepts late placement when the optimal send has already passed — without that, creation
failed and the demand was dropped.
- The historical replay (retrospective audit engine) explicitly pins
allowPastSlots: true: it is immune to the guard, so its as-of-a-past-date analyses keep the legacy semantics. SUPPLIER_NO_PAST_SLOTS_SINCEdoes not change engine behaviour: it bounds the health probe's gate so rows created before the guard was armed are not counted against it.
STOCK_FORWARD_DELIVERY_ONLY
OFF (default): empty filter fragment, zero change. ON (prod): demand discovery
only counts client orders whose delivery is still ahead of us —
deliveryDate ≥ floor (today, minus any STOCK_FORWARD_DELIVERY_GRACE_DAYS); orders
without a deliveryDate fall back to invoiceDate against the conservative floor.
Past-due unfulfilled demand is deliberately dropped (the merchant's ask). The paired
sweep sweepStalePastDeliveryItems drains items already materialised before the floor —
the DB filter and the sweep share the same floor function (forwardDeliveryFloor), so
both agree on what "past" means.
Note: the UI's in-place recalc button computes forward-only explicitly (not via the flag), so it behaves the same regardless of the gate's state.
PrestaShop sync
| Setting | Type | Value | Effect |
|---|---|---|---|
RESET_STOCK_ON_SYNC |
code constant (not env) | false |
Who owns stock: the back office (false) or PrestaShop (true) |
RESET_STOCK_ON_SYNC — careful, this is not an environment variable
Unlike everything else on this page, this setting lives in code:
SYSTEM_CONFIG.PRESTASHOP_SYNC.RESET_STOCK_ON_SYNC in
packages/common/src/utils/constants.ts. Changing it = commit + deploy.
false(current value): the back office is the stock authority. The product sync never rewrites an existing product'sProduct.stock, a new product starts at 0 (the merchant enters the real count), and the sync emits no ledger entry — fix #2798: when the sync does not own stock, it re-reads concurrent decrements (cron, manual edits) after the fact and would attribute them to itself as phantom "PrestaShop stock sync" entries, double-counting demand into the ledger.true: PrestaShop is the authority. The sync resetsProduct.stockfrom PrestaShop and emits the matching COMPLETED StockAction (buildPsStockLedgerEntry, #2749 —OPENING_BALANCEfor a new product, aPRESTASHOP_SYNCdelta otherwise), sostock == Σ ledgerholds.
In both positions, rule 17 holds: never a stock write without its paired ledger entry when you own the write; nothing at all when you don't.
Risky combinations
| Shape | Symptom | Verdict |
|---|---|---|
PESSIMISTIC=false + MAX_COVER_WEEKS=0 |
STOCK SAFEGUARD OFF warning at engine boot |
Almost always an unloaded .env — check the cwd/loading first |
STOCK_TRUST_ALLOW_NEGATIVE=true without STOCK_LEDGER_ALLOW_BACKORDER=true |
Legacy-drift negatives inflate the orders | Forbidden — the passthrough presumes Option B semantics |
Re-flipping ALLOW_BACKORDER to false after the cutover |
History mixing two conventions | Forbidden — see the warning above |
Flag flipped in .env but app not rebuilt |
Cron and app disagree on the gate | Flip window: flip, rebuild, restart, verify both |
Flip checklist
Before flipping any flag on this page in production:
# 1. Ledger audit — 0 violations required
bun run cli --task stock-ledger-audit \
--tenantId <tenant> --siteId <site> --strict
# 2. Replay diff (read-only) — the change doesn't move orders unexpectedly
bun packages/scripts/src/dev/replay-supplier-orders.ts \
--tenant-id <tenant> --site-id <site>
# 3. The subsystem's pinned tests
npm run test:unit
After the flip: watch the daily supplier-stock-health probe (it must stay HEALTHY) and
the audit invariants — see Safety nets.
See also
- Overview — the stock & ordering model in one page
- The stock ledger — signed authority, clamped mirror, invariants
- Write paths — every write site and its contract
- Supplier orders — the pipeline these flags gate
- Daily timeline — the crons that read these flags
- PrestaShop sync — the sync's ledger integration
- Safety nets — audit, replay, pinned tests, probe