Vue d'ensemble

Cette section documente le système stock & réapprovisionnement fournisseur de la plateforme : un ledger de stock signé qui dit la vérité physique, un moteur de réappro événementiel qui régénère les commandes fournisseur à chaque changement de demande, et un pipeline email en trois temps (stage → review → send) garantissant que ce que l'opérateur relit est exactement ce qui part. Cette page est la carte : les acteurs, le flux de bout en bout, le modèle de données, et où trouver le détail de chaque sous-système.

Les acteurs

Acteur Rôle
Back office marchand (/manage) Crée/modifie les commandes, scelle les livraisons et réceptions, relit la file email fournisseur, corrige le stock
Boutique PrestaShop Source des commandes client — arrive en temps réel (webhooks) et par sync de rattrapage (toutes les 3 h)
Webhooks PrestaShop /api/webhooks/prestashop/* — poussent commandes et produits vers la plateforme dès l'événement
Les 7 crons de production Sync commandes/produits (3 h), réappro (30 min), stage email (06:00), sonde santé (06:30), réconciliation (07:15), auto-validation (12:01 + toutes les 2 h) — voir Cycle quotidien
File bext supplier-orders-recalc Sérialise les recalculs de commandes fournisseur déclenchés par les événements, sous le write-lock fournisseur
Fournisseurs (×159) Reçoivent les emails de commande, livrent la marchandise ; chaque vendor porte son calendrier de créneaux d'envoi
PostgreSQL partagé Product.stockLedger / Product.stock, StockAction, Order, SupplierOrder — la même base pour l'app, les crons et les scripts

La carte du système

Le flux nominal, de la commande boutique jusqu'à l'écriture ledger à la réception :

flowchart TB
  PS["Commande client<br/>(boutique PrestaShop)"]
  PS -->|webhook temps réel| WH["/api/webhooks/prestashop"]
  PS -->|"sync de rattrapage (3 h)"| SYNC["Cron order-sync"]
  WH --> ORD[("Order local")]
  SYNC --> ORD
  BO["Back office marchand"] -->|création / modif / annulation| ORD
  ORD -->|producteurs d'événements| Q{{"File bext<br/>supplier-orders-recalc"}}
  Q -->|sous write-lock| RECALC["recalculateVendorOrdersUnified"]
  RECALC --> SO["Brouillons de commandes<br/>fournisseur (DRAFT)"]
  SO -->|"stage 06:00 (render-only)"| MAIL["File email — review opérateur"]
  MAIL -->|envoi validé| VEND["Fournisseurs (×159)"]
  VEND -->|livraison| RCPT["Réception confirmée"]
  RCPT -->|"StockAction COMPLETED (+)"| LEDGER[("Product.stockLedger")]
  BO -->|"scellé livraison client (−)"| LEDGER

À lire de haut en bas :

  1. Entrée de la demande — une commande client arrive deux fois : en temps réel par webhook, et par le cron de sync toutes les 3 h qui rattrape ce que le webhook aurait manqué (les deux chemins convergent sur le même Order local, idempotent).
  2. Réappro événementiel — chaque création/modification/annulation/changement de statut de commande client (plus : annulation d'un bon fournisseur, réception, webhook produit) pousse un message dans la file bext supplier-orders-recalc. Le worker recalcule les brouillons du vendor concerné sous le write-lock fournisseur, ce qui le sérialise avec le cron de réappro. Détail : Commandes fournisseur.
  3. Email en trois temps — le cron de 06:00 rend l'email de chaque brouillon (render-only, rien ne part), l'opérateur relit et ajuste dans la file de review, puis l'envoi re-rend depuis les items courants et bloque si les quantités ont dérivé depuis le stage. Review == sent.
  4. Le stock ne bouge qu'au mouvement physique — la livraison client décrémente, la réception fournisseur incrémente. La sync de commandes ne touche jamais le stock (ce chemin a été supprimé le 2026-06-14). Détail : Chemins d'écriture.
Note

Le cron auto-supplier-orders (toutes les 30 min) subsiste comme filet derrière la file événementielle : si un événement se perd, le cron rattrape au prochain passage. De même, l'auto-validation de 12:01 scelle les livraisons/réceptions que le marchand a oubliées — le scellé au moment de l'événement physique (UI) est le chemin primaire.

Le modèle de stock à deux colonnes

Depuis le cutover backorder (#2886 « Option B », STOCK_LEDGER_ALLOW_BACKORDER=true en production depuis le 2026-07-09), le stock vit dans deux colonnes aux rôles distincts :

flowchart LR
  MV["recordPhysicalMovement<br/>(chaque mouvement physique)"] --> SA["StockAction COMPLETED<br/>± quantité"]
  SA -->|même transaction| L[("Product.stockLedger<br/>autorité SIGNÉE<br/>négatif = précommande")]
  L -->|"stock = max(0, stockLedger)"| S[("Product.stock<br/>miroir d'affichage bridé")]
  L -->|"trustedStock (garde-fous)"| R["Moteur de réappro"]
  S --> UI["Affichage boutique / back office"]
  • Product.stockLedger est l'autorité signée : ouverture + Σ réceptions − Σ livraisons. Il peut passer négatif — c'est un backorder réel (livré au client avant que la réception couvrante soit enregistrée), et la réception suivante le nette. Comme il n'est jamais bridé, le phantom stock (un compteur gonflé par un clamp à 0) est structurellement impossible.
  • Product.stock est le miroir d'affichage : l'invariant est stock == max(0, stockLedger). C'est ce que voient la boutique et les listes du back office ; il ne porte aucune décision de réappro.
  • Chaque écriture physique passe par StockLedgerService.recordPhysicalMovement (math pure dans packages/services/src/stock/rolling-stock-math.ts) : vérifie manageStock, émet la StockAction et met à jour les deux colonnes dans la même transaction. Aucun site d'écriture ne contourne ce contrat — voir Chemins d'écriture.
  • La lecture côté réappro passe par trustedStock() (packages/services-supplier-order/src/utils/supplier-order-utils-new.ts), qui applique les garde-fous : manageStock=false → 0, kill-switch STOCK_LEDGER_PESSIMISTIC → 0 (OFF en production), négatif bridé à 0 (tant que le gate STOCK_TRUST_ALLOW_NEGATIVE n'est pas armé), et un plafond anti-phantom à STOCK_TRUST_MAX_COVER_WEEKS semaines de consommation récente (8 en production). Détail : Ledger et Configuration.

Les StockAction forment le journal : statuts PENDING (projection — p. ex. « entrée virtuelle » attendue d'un bon fournisseur envoyé) / COMPLETED (appliquée au ledger) / CANCELLED ; types INCREMENT / DECREMENT / ADJUSTMENT ; sources MANUAL, ORDER_UPDATE, SUPPLIER_DELIVERY, INVENTORY_ADJUSTMENT, etc. Seules les COMPLETED comptent dans le ledger.

Attention

La règle cardinale pour tout nouveau code : jamais d'écriture de Product.stock sans (a) vérifier manageStock et (b) émettre la StockAction correspondante dans la même transaction. Une écriture brute désynchronise le miroir de l'autorité et casse l'invariant I5 de l'audit. C'est la règle 17 du dépôt (.claude/rules/17-stock-ledger.md).

Le rythme quotidien en bref

Heure / cadence Cron Ce qu'il fait
toutes les 3 h (0 */3) Sync commandes PrestaShop Importe/rafraîchit les commandes (40 derniers jours) — ne décrémente pas le stock
toutes les 3 h (0 */3) Sync produits PrestaShop Produits + réconciliation vendors (--link_vendors) ; chaque écriture stock émet sa StockAction
toutes les 30 min Auto-supplier-orders Filet de réappro derrière la file événementielle
06:00 Stage email fournisseur Rend les previews d'email (render-only) pour la review opérateur
06:30 Sonde supplier-stock-health Lecture seule — métriques de santé vers la tour de contrôle
07:15 Reconcile-supplier-orders --apply --removeOrphans — recalcule l'état attendu, corrige la dérive ; seul cron qui remplit les bons pour les produits manageStock=false
12:01 Auto-validate-stock-actions Filet de scellé : réceptions et livraisons planifiées passées de ≥ 12 h (AUTO_VALIDATE_GRACE_HOURS) et pas encore confirmées

La chronologie complète, avec les interactions entre crons et les pièges d'alerting, est sur Cycle quotidien.

Les sous-systèmes

Sous-système Rôle en une ligne Page
Ledger de stock stockLedger signé = autorité ; stock = miroir bridé ; frontière de confiance trustedStock /stock-ordering/ledger
Chemins d'écriture Tous les sites qui touchent le stock (scellé UI, réception, inventaire, annulation, cron) et leur contrat commun /stock-ordering/write-paths
Commandes fournisseur Réappro événementiel, file supplier-orders-recalc, créneaux d'envoi, pipeline email stage → review → send /stock-ordering/supplier-orders
Cycle quotidien Les 7 crons de production, leurs horaires réels et leur enchaînement /stock-ordering/daily-timeline
Semaine type Vagues fournisseurs, tournées client, réceptions supposées et fenêtres de comptage sur une semaine /stock-ordering/weekly-cycle
Sync PrestaShop Sync commandes/produits, webhooks temps réel, intégration ledger (#2749), réconciliation vendors /stock-ordering/prestashop-sync
Back office Les surfaces /manage : grille de stocks, journal des mouvements, comptages physiques, santé /stock-ordering/back-office
Configuration Les flags d'environnement (STOCK_LEDGER_ALLOW_BACKORDER, SUPPLIER_NO_PAST_SLOTS, …) et leur état de production /stock-ordering/configuration
Filets de sécurité Audit d'invariants I1–I5, replay-diff, tests épinglés, sonde quotidienne /stock-ordering/safety-nets

Trois invariants à retenir

  1. Le ledger dit la vérité physique. Le stock ne bouge qu'à la livraison ou à la réception, jamais à la prise de commande. Un stockLedger négatif est une information (backorder), pas une erreur.
  2. On ne commande jamais dans le passé. Le gate SUPPLIER_NO_PAST_SLOTS impose un plancher à maintenant, la causalité (send ≥ création de la commande source), et fait rouler la demande en retard vers le prochain créneau futur.
  3. Ce que l'opérateur relit est ce qui part. L'envoi d'un email fournisseur re-rend depuis les items courants et refuse (CONFLICT) si l'empreinte des items a changé depuis le stage. Une commande annulée ne s'envoie jamais.

Pour vérifier la santé du système à tout moment, l'audit d'invariants se lance depuis le dépôt applicatif :

cd apps/app && bun run cli --task stock-ledger-audit \
  --tenantId <tenant> --siteId <site> --strict   # sort non-zéro à la moindre violation

Où aller ensuite

Voir aussi