Fulfillment depth
The pick/pack/ship loop grew a supply-and-routing spine,
split across two modules by ownership: purchase orders, receiving, and
physical stock locations land in inventory, and
fulfillment nodes, routing modes, dropship vendor costs, and COGS capture
land in fulfillment. Neither module owns the other’s
schema — each builds a repo instance on its own connection via the owning
module’s published createXRepo factory.
Inventory: purchase orders, receiving, locations
The core inventory port (items/lots/FEFO reservations) gained supply-side
depth: vendors, purchase orders (with lines), receiving against a
PO, and per-node physical locations. Receiving fails closed on a
non-receivable PO (po_not_receivable) or an over-receipt
(receive_exceeds_expected).
Operator routes — x-operator-token, under /v1/tenants
| Method | Path | Purpose |
|---|---|---|
POST | /v1/tenants/:tenant/locations | Create a physical stock location; optional node_ref binds it to a fulfillment node. |
GET | /v1/tenants/:tenant/locations | List locations. |
POST | /v1/tenants/:tenant/vendors | Create a supply vendor. |
GET | /v1/tenants/:tenant/vendors | List vendors. |
POST | /v1/tenants/:tenant/purchase-orders | Create a PO + its lines. |
GET | /v1/tenants/:tenant/purchase-orders | List POs. |
GET | /v1/tenants/:tenant/purchase-orders/:id | Fetch a PO + lines. |
POST | /v1/tenants/:tenant/purchase-orders/:id/receive | Receive stock against a PO (creates lots; fails closed on over-receipt). |
Merchant routes — fdk_, scope manage:own_inventory, under /v1/merchant/inventory
| Method | Path | Purpose |
|---|---|---|
GET | /v1/merchant/inventory/vendors | List the caller’s vendors. |
POST | /v1/merchant/inventory/vendors | Create a vendor. |
GET | /v1/merchant/inventory/purchase-orders | List POs. |
GET | /v1/merchant/inventory/purchase-orders/:id | Fetch one PO + lines. |
POST | /v1/merchant/inventory/purchase-orders | Create a PO. |
POST | /v1/merchant/inventory/purchase-orders/:id/receive | Receive against a PO. |
Fulfillment: nodes, modes, dropship costs, COGS
A fulfillment node is a own_warehouse / threepl / vendor endpoint.
Allocation picks a mode:
dedicated— draw from own stock, no node pinned.allocated— pin the queue to a specificown_warehouse/threeplnode and draw FEFO from that node’s bound inventory location. An unbound node fails closed (node_location_unmapped) rather than silently drawing from elsewhere.dropship— route each line to a vendor node without reserving or decrementing own stock, recording a per-line vendor dispatch (the “purchase/dispatch to vendor”) and a dropship shipment row that stands in for the carrier label.
Mode/node selection happens on the first (queued) pass only; a re-entrant
allocate keeps the row’s chosen mode. Mixing own-stock and dropship on one
route is refused (wrong_mode) — dropship has its own route.
COGS capture: every shipped line publishes one
fulfillment.cogs.captured.v1 at ship time (when the reservations commit /
the vendor is dispatched). Dropship COGS resolves each line’s unit cost from
the (node, sku) vendor-cost mapping; a line with no cost recorded is
captured at zero rather than blocking the ship.
Operator routes — x-operator-token, under /v1/tenants
| Method | Path | Purpose |
|---|---|---|
GET | /v1/tenants/:tenant/fulfillment | List the fulfillment queue. |
GET | /v1/tenants/:tenant/fulfillment/:id | Fetch one queue row. |
POST | /v1/tenants/:tenant/fulfillment/:id/allocate | Allocate own stock; a nodeId pins mode allocated, omitting it is dedicated. |
POST | /v1/tenants/:tenant/fulfillment/:id/dropship | Route to a vendor node (mode dropship), no own-stock reservation. |
POST | /v1/tenants/:tenant/nodes | Create a fulfillment node (own_warehouse/threepl/vendor). |
GET | /v1/tenants/:tenant/nodes | List nodes. |
POST | /v1/tenants/:tenant/vendor-costs | Map a per-(node, sku) vendor unit cost (dropship COGS input). |
GET | /v1/tenants/:tenant/vendor-costs | List vendor costs. |
POST | /v1/tenants/:tenant/fulfillment/:id/label | Buy a shipping label (LabelPort, mock|easypost). |
POST | /v1/tenants/:tenant/fulfillment/:id/ship | Ship — commit reservations / dispatch vendor, capture COGS, transition the commerce order. |
POST | /v1/tenants/:tenant/fulfillment/:id/cancel | Cancel the queue row. |
POST | /v1/tenants/:tenant/fulfillment/:id/deliver | Mark delivered (transitions the commerce order to delivered). |
Merchant routes — fdk_, scope manage:own_fulfillment, under /v1/merchant/fulfillment
| Method | Path | Purpose |
|---|---|---|
GET | /v1/merchant/fulfillment | List the queue. |
GET | /v1/merchant/fulfillment/nodes | List nodes. |
POST | /v1/merchant/fulfillment/nodes | Create a node. |
GET | /v1/merchant/fulfillment/vendor-costs | List vendor costs. |
POST | /v1/merchant/fulfillment/vendor-costs | Map a vendor cost. |
GET | /v1/merchant/fulfillment/:id | Fetch one queue row. |
POST | /v1/merchant/fulfillment/:id/allocate | Allocate. |
POST | /v1/merchant/fulfillment/:id/dropship | Dropship-route. |
POST | /v1/merchant/fulfillment/:id/label | Buy a label. |
POST | /v1/merchant/fulfillment/:id/ship | Ship. |
POST | /v1/merchant/fulfillment/:id/cancel | Cancel. |
POST | /v1/merchant/fulfillment/:id/deliver | Mark delivered. |
Events
| Module | Direction | Event |
|---|---|---|
fulfillment | Publishes | fulfillment.cogs.captured.v1 — one per shipped line, at ship time. |
fulfillment | Consumes | commerce.order.placed.v1 — enqueues a queue row + its lines (real dispatcher consumer). |
inventory | Publishes | inventory.unit.shipped.v1. |
Manifest gates
None. Fulfillment routing, POs, receiving, nodes, and COGS are operator/ merchant back-office surfaces with no storefront manifest toggle.
Seams (honest notes)
- The SKU-namespace seam (documented, not stubbed): allocation matches a
fulfillment queue line’s
skuagainstinventory.canonical_items.sku_rootfor the same tenant. This port has no shared catalog/SKU-mapping table, so a line whose SKU has no matching inventory item fails allocation with a surfaced422 inventory_item_not_found, never a silent skip. A future catalog-mapping module is the real fix. - One queue row and one shipment per order — the parent’s package-is-first-class model, reships, COA ship-gates, and per-store comms confirmation are out of scope here.
- Dropship COGS for a line with no vendor cost recorded is captured at zero, not blocked — a documented simplification.