ADR-014: Shipment Return Flow
Status: Proposed Date: 2026-03-23
Context
When a warehouse fulfils an order, it pushes shipment data back to Portitor. This is the return direction of the integration: warehouse → platform → webshop. It is as important as the order pipeline (ADR-013) and carries equal complexity.
The shipment payload from the warehouse contains:
- The warehouse's own transaction and document references (
transactionNo,documentNo) - The warehouse location (
locationCode) - Line-level quantities actually shipped — which may be less than what was ordered
- Status and posting type
This payload must be correlated back to the original Portitor order, mapped to the webshop model, and used to update the order in the webshop: line item quantities, tracking numbers per line, and order status.
Partial fulfilment is a first-class case, not an edge case. A single order may receive multiple shipment events as one or more warehouse locations dispatch different line items across different times.
Decision
The shipment return flow follows the same configurable pipeline and event-handler model as the order pipeline (ADR-012). The warehouse adapter pushes a shipment event; fixed and configurable steps process it; fixed event handlers notify the source adapter in parallel.
Inbound API
The warehouse adapter plugin posts shipment data to:
POST /{company}/shipmentThe platform:
- Writes the payload to S3 at the canonical key path (ADR-001 pointer pattern)
- Fires
ShipmentReceived - Returns
202immediately — processing is asynchronous
Fixed Steps
Three steps are always present and cannot be removed:
- Ingest — writes the shipment payload to S3, fires
ShipmentReceived, enqueues the pointer. Always first. - Correlate — resolves the warehouse's
documentNoto the Portitor order UUID and tenant using the warehouse-facing reference recorded inOrderSentToWarehouse(ADR-013). This reference was generated by the mapper at dispatch time specifically to make this lookup unambiguous — a single warehouse may serve multiple tenants, and the reference encodes enough to identify both the order and the correct tenant. Lookup is a direct S3 key path match (ADR-002), not a search. Always second. If the reference cannot be resolved, firesShipmentCorrelationFailedand halts — no subsequent step can operate without knowing which order and tenant the shipment belongs to. - Apply — calls the source adapter to update the order in the webshop: line item quantities, tracking numbers, and order status. Fires
ShipmentApplied. Always last.
Fixed Event Handlers
ShipmentReceivedReporter— reacts toShipmentReceived; records the raw receipt timestamp and warehouse reference in the dashboard order timeline. Runs in parallel with pipeline processing.ShipmentAppliedNotifier— reacts toShipmentApplied; triggers downstream notifications (e.g. Business Central sync, merchant alerts). Runs in parallel after the pipeline completes.
Configurable Steps
All steps between Correlate and Apply are optional and operator-configured:
| Step | Description |
|---|---|
map | Maps the warehouse shipment model to the webshop model — produces status, per-line item updates, tracking numbers, and reduced items |
add-order-note | Adds a note to the order in the webshop confirming the shipment was received and correlated |
customs-declaration | Posts a customs declaration for the shipment (cross-border only — requires route context from the original order) |
Partial Fulfilment
A shipment may fulfil only a subset of the ordered line items. The map step produces:
updates— per line item: quantity actually shipped and tracking numberreduced— items where shipped quantity is less than ordered quantity, with bothwantedandactuallyvalues recorded
Partial fulfilment is a valid state, not a failure. Portitor's responsibility ends at updating the webshop order with the actual quantities and tracking via the source adapter. What the webshop does with that information — adjusting the charged amount, marking the order fulfilled, backordering remaining items — is the webshop's own concern and outside Portitor's scope.
The reduced[] field on ShipmentApplied carries the data downstream plugins need to act on partial fulfilment. For example, a Business Central integration plugin can subscribe to ShipmentApplied and update the BC order with the reduced quantities — this is a configurable event handler, not a platform concern.
Customs Declaration
When a customs-declaration step is configured, it runs after map and uses the mapped line items (HS codes, quantities, weights, receiver details) to post a declaration to the configured customs authority. This step is only meaningful for cross-border shipments.
Route context (origin country, destination country) is resolved from the original order's pipeline context stored at OrderSentToWarehouse time. If route context is absent, the step behaves identically to the ai-validate step in ADR-012: it skips the declaration and records an info finding.
Correlation Failure
ShipmentCorrelationFailed means the warehouse posted a documentNo that does not match any known Portitor order. The shipment payload remains in S3. The failure appears in the dashboard with the warehouse reference for manual operator resolution. The operator can:
- Correct the reference and reprocess via
POST /{company}/shipment/{id}/reprocess - Dismiss the shipment if it is genuinely unknown
Domain Events
| Event | Fired when |
|---|---|
ShipmentReceived | Shipment payload accepted and written to S3 |
ShipmentCorrelationFailed | Warehouse documentNo could not be resolved to a Portitor order |
ShipmentApplied | Shipment processed and applied to the order in the webshop |
Event Shapes
ShipmentApplied carries the full outcome of the shipment:
{
"shipmentId": "...",
"orderId": "...",
"warehouseRef": "...",
"locationCode": "...",
"status": "...",
"updates": [
{
"lineItemId": "...",
"quantityShipped": 2,
"trackingNumber": "..."
}
],
"reduced": [
{
"name": "Blue Widget L",
"wanted": 3,
"actually": 2
}
]
}Projections
| Projection | Contents |
|---|---|
| Unhandled orders | Removes an order when ShipmentApplied is received and all line items are fulfilled |
| Partially fulfilled orders | Orders with at least one ShipmentApplied but remaining open line items |
| Failed correlations | Shipments in ShipmentCorrelationFailed state, with warehouse reference, visible in dashboard for operator resolution |
Both projections are rebuildable from the event log at any time (ADR-010).
Default Pipeline
ingest → correlate → map → applyAPI Surface
| Endpoint | Method | Role | Description |
|---|---|---|---|
/{company}/shipment | POST | warehouse-adapter | Ingest a shipment from the warehouse |
/{company}/shipment/{id}/reprocess | POST | platform-operator | Reprocess a shipment after manual correlation fix |
Consequences
Positive:
- Partial fulfilment is modelled explicitly —
reduced[]onShipmentAppliedgives downstream plugins (BC integrations, webshop handlers) the data they need without Portitor owning the financial or fulfilment logic - Correlation failure is recoverable — the payload is in S3, the operator resolves the reference and reprocesses without re-submission from the warehouse
- Customs declaration is a configurable step — domestic shipments carry no declaration overhead
- The same pipeline and event-handler model as ADR-012 — no new primitives
Negative:
- Correlation depends on the warehouse returning the same
documentNothat was in the original order. If the warehouse internally splits or renumbers an order, correlation fails and requires operator intervention - Multiple partial shipments for one order require the partially fulfilled projection to accumulate state across events — a projection that must handle repeated
ShipmentAppliedevents for the same order correctly
Alternatives Considered
- Synchronous processing (block until applied): Rejected — the warehouse adapter should not wait for the webshop update to complete; same reasoning as order ingest in ADR-013
- Merge with ADR-013: Rejected — the return flow has a distinct trigger, distinct correlation step, distinct partial fulfilment semantics, and distinct failure modes; combining them obscures both directions