Build the supplier: reorder.requested -> decide -> deliver

The service's own supplier, simulated: a fixed lead time or a 15% chance of a
stockout is the whole model, on purpose — this simulates that fulfilment can
fail, not why a given supplier would.

One service, two capacities: this workflow both raises reorder.requested (the
periodic run) and answers its own request (this workflow), under the same
Kafka account and Keycloak client, per the 2026-08-31 decision that
réapprovisionnement stays one service.

The delivery write goes through the front door — patch-product, under a new
restocker role that may PATCH a product and nothing else — so the resulting
product.stock_changed event is the only stock-update fact. No second,
n8n-emitted event claims a delivery happened with nothing to certify it.

Shipped with, and fixed, a bug where the delivery-application node spread the
wrong upstream , silently dropping reorder_id/productcode/quantity two
nodes before the failure surfaced. Verified separately: the write+event chain
(a real bug, now fixed) and the state machine (persists correctly on a real
trigger; a manual test run does not persist staticData, which is a testing
limitation, not a production one).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Stéphane Tailland
2026-08-31 16:41:46 +02:00
parent d11ca36764
commit 1ec9e308ba
3 changed files with 570 additions and 1 deletions
+75
View File
@@ -21,6 +21,81 @@ POST /rest/workflows/<id>/activate {"versionId": "<current>"}
`PATCH {"active": true}` returns 200 and leaves the workflow inactive, silently:
activation has its own endpoint.
## supply-procurement
The service's own supplier, simulated. Reacts to `reorder.requested` (emitted
by the periodic run below, once it finds a reference under threshold),
decides — a fixed lead time, or a 15% chance the supplier cannot fulfil —
answers with `reorder.confirmed`/`reorder.rejected`, and on the confirmed
delivery date writes the stock itself.
```
reorder.requested {productcode, quantity, requested_at}
→ reorder.confirmed {deliver_on} — 85% of the time
→ reorder.rejected {reason: "supplier stockout"} — 15%
```
No purchase-order entity, no supplier entity, no negotiation — the whole model
is one probability and one lead time, on purpose: this simulates *that*
fulfilment can fail, not *why* a given supplier would.
### One service, two capacities — not two identities
This is still the supply agent, not a separate "procurement" component. It
both requests (the periodic run) and answers its own request (this workflow),
under the same Kafka account and the same Keycloak client — the 2026-08-31
decision that réapprovisionnement is one service, not a split into a demand
side and a supply side.
### The write goes through the front door
On the delivery date, this workflow reads the product's current stock and
PATCHes it — through Kong, through the same `patch-product` MCP tool a human
product manager would use, under a **new, narrow role**: `restocker` may PATCH
a product and nothing else, not create, not replace, not delete. It sends
`X-Stock-Change-Reason: delivery:<reorder-id>`, which `classic-models-api`
only honours in that exact shape; anything else falls back to
`manual-adjustment`. The resulting `product.stock_changed` event is the *only*
stock-update event — there is deliberately no second, n8n-emitted fact
claiming a delivery happened, because nothing would certify it against the
one that actually changed the database. See `classic-models-api`'s commit
"Accept a delivery reason on a stock PATCH" for the backend side.
### Two triggers, one shared state
`$getWorkflowStaticData('global').pendingDeliveries`, keyed by reorder id —
the same pattern the stock-watch workflow already uses for its watchlist, now
shared across **two** trigger chains in this one workflow: the Kafka-triggered
decision writes an entry when the supplier confirms, and a daily schedule
(06:15, deliberately after the periodic run's 06:00) reads what is due and
applies it. A pending entry is deleted only once the PATCH is confirmed by the
API's own response — a failed write leaves it in place so tomorrow's run
retries it, rather than losing a delivery nobody applied.
### A bug this shipped with, and how it was found
`Apply delivery` sits between `Read current stock` and `Update stock`, and its
first version wrote `{ ...$json, new_quantity }` — but `$json` at that point is
`Read current stock`'s own output, not the due-delivery record two nodes
earlier. `reorder_id`/`productcode`/`quantity` were silently dropped, and
`Update stock` failed two nodes later with `"path_productcode" is a required
property` — a symptom far from its cause. Fixed by naming the source
explicitly: `$('Due deliveries').item.json` for what the delivery *is*, `$json`
for what the catalog *says now*. The same rule `check_availability` already
follows for `Recent movement` — an implicit `$json` is only ever the
*immediately preceding* node, never "whichever upstream node has the field I
want."
### Manual test runs do not persist static data
Verified the hard way: triggering this workflow via `/rest/workflows/{id}/run`
executes for real (the write happens, the event is genuine) but the
`pendingDeliveries` mutation is **not** saved back to the workflow — the entry
reappears on the next check. Only a genuine trigger firing (a real Kafka
message, the real schedule) persists it. A manual run is fine for proving the
logic; it is not fine for proving the state machine, and the two were verified
separately here.
## supply-check-availability
Working. `POST /webhook/check-availability` with the end user's token in