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 :
- 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
Orderlocal, idempotent). - 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. - 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.
- 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.
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.stockLedgerest 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.stockest le miroir d'affichage : l'invariant eststock == 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 danspackages/services/src/stock/rolling-stock-math.ts) : vérifiemanageStock, émet laStockActionet 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-switchSTOCK_LEDGER_PESSIMISTIC → 0(OFF en production), négatif bridé à 0 (tant que le gateSTOCK_TRUST_ALLOW_NEGATIVEn'est pas armé), et un plafond anti-phantom àSTOCK_TRUST_MAX_COVER_WEEKSsemaines 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.
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
- Le ledger dit la vérité physique. Le stock ne bouge qu'à la livraison ou à la
réception, jamais à la prise de commande. Un
stockLedgernégatif est une information (backorder), pas une erreur. - On ne commande jamais dans le passé. Le gate
SUPPLIER_NO_PAST_SLOTSimpose 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. - 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
- Vous découvrez le système → lisez Ledger puis Commandes fournisseur : ce sont les deux moitiés du modèle.
- Vous écrivez du code qui touche le stock → Chemins d'écriture d'abord, puis les Filets de sécurité pour savoir quels tests épingler.
- Vous opérez la production → Cycle quotidien pour les horaires, Configuration pour les gates, et Back office pour les surfaces de contrôle.
- Vous déboguez une divergence PrestaShop → Sync PrestaShop.
Voir aussi
- Ledger de stock — le modèle signé en détail
- Commandes fournisseur — réappro événementiel et pipeline email
- Cycle quotidien — les 7 crons heure par heure
- Filets de sécurité — invariants, audit, tests