NAV Order Release¶
What happens after NAV finishes allocating inventory for an order: NAV pushes XML to Camel, Camel fans the batch into one Service Bus message per order, each order is forwarded to OMS so the order can move out of PENDING_NAV_RELEASE.
This page covers the path inside Camel — from the inbound POST /nav/orders/release to the outbound PATCH on OMS. It does not cover the rest of the order lifecycle (OMS → SFCC, Cirro fulfillment, shipping confirmation) — see B2C Order Flow.
When this fires¶
- B2C orders that NAV has finished allocating (e.g. printable orders waiting for assembly inventory). NAV emits one XML payload per ready batch; one batch can contain N orders.
- The legacy B2B 940 path does not use this endpoint — that flow has its own NAV path (see EDI Pipeline).
High-level sequence¶
sequenceDiagram
participant NAV
participant Exp as cm-int-service-exp<br/>POST /nav/orders/release
participant Blob as Azure Blob Storage<br/>nav-order-release container
participant SB as Azure Service Bus<br/>queue: nav-order-release
participant Prc as cm-order-prc<br/>SB consumer
participant Osor as cm-osor-sys<br/>PUT /order/nav-release
participant OMS
NAV->>Exp: POST XML — Basic Auth — 1 batch, N orders
Exp->>Exp: Split <Order> elements
Exp->>Blob: Upload original full XML<br/>(audit copy)
loop one message per order
Exp->>SB: ServiceBusMessage<br/>body = single-order XML chunk<br/>props: NAVBufferId, blobUrl, X-B3-*
end
Exp-->>NAV: 200 — NAV order release queued for N orders
loop each message
SB->>Prc: deliver single-order XML
Prc->>Prc: Parse Order → NavOrderDto<br/>(docNo, navBufferId, assemblies)
Prc->>Osor: PUT /order/nav-release (JSON)<br/>via APIM (Subscription-Key)
Osor->>Osor: @Valid (docNo, navBufferId,<br/>orderStatus, printableAttribute)
Osor->>OMS: PATCH oms.navReleaseUpdate/docNo — X-USER-TOKEN
OMS-->>Osor: 200
Osor-->>Prc: 200
end
Components¶
| Component | Role | Key entry point |
|---|---|---|
| cm-int-service-exp | Exposes the NAV-facing HTTPS endpoint, splits the batch, persists the raw XML, fans out to Service Bus. | NAVController.java → NAVServiceImpl.processNavOrderRelease() |
| cm-order-prc | Subscribes to the nav-order-release SB queue. Parses one order at a time, builds the JSON payload for OSOR. |
ServiceBusReceiverConfiguration → NavOrderReleaseServiceImpl.processNavOrderRelease() → direct:navOrderRelease route |
| cm-osor-sys | Internal-only HTTP layer. Validates the JSON payload, calls the OMS NAV-release endpoint. | OrderController#navRelease → OrderServiceImpl#navRelease → direct:navReleaseUpdate |
| Azure Blob Storage | Stores the original full NAV XML for audit/replay. Blob name: <NAVBufferId>-<epochMs>.xml. |
container nav-order-release (per-env storage account) |
| Azure Service Bus | Queue nav-order-release. maxDeliveryCount=1 — one consumer failure dead-letters immediately. |
queue per env (e.g. int-prod-nav-order-release) |
| OMS | Updates the order state from PENDING_NAV_RELEASE → RELEASED_FROM_NAV and stores the assembly mappings. |
${oms.navReleaseUpdate}/{docNo} (PATCH) |
Per-order fan-out (exp layer)¶
The exp layer is the part that changed in CI-411. Pre-CI-411 the entire XML batch was published as a single SB message and the consumer had to iterate; if order 1 failed, orders 2..N were lost. Now one SB message = one order.
flowchart TD
A[POST /nav/orders/release<br/>raw XML body] --> B[Spring Security<br/>Basic Auth check]
B -->|401 if bad| Z1[401 Unauthorized]
B --> C[NavMetaExtractor.split]
C -->|no Order elements| Z2[200 — No orders to process]
C --> D[Upload raw XML to Blob<br/>blobUrl = container/blobName]
D --> E{For each NavOrderSplit}
E --> F[Build ServiceBusMessage<br/>body = single-order XML]
F --> G[Set application properties:<br/>NAVBufferId, blobUrl,<br/>X-B3-TraceId, X-B3-SpanId, X-B3-Sampled]
G --> H[navOrderReleaseSender.sendMessage]
H --> E
E -->|all queued| I[200 — NAV order release queued for N orders]
Why blob first, then fan-out:
- The blob is the auditable record of exactly what NAV sent — preserved even if the SB messages later move into the DLQ.
- The
blobUrlis attached to every per-order SB message, so the consumer can correlate any single order back to the full batch if needed.
Message contract — Service Bus¶
| Field | Value | Notes |
|---|---|---|
| Body | Single <Order>...</Order> XML chunk |
Content-Type: application/xml |
Application property NAVBufferId |
The order's <NAVBufferId> text |
Used as the correlation ID through the rest of the chain. |
Application property blobUrl |
Full Azure blob URL of the original batch | For audit / debugging. The consumer does not re-download the blob. |
Application property X-B3-TraceId / X-B3-SpanId / X-B3-Sampled |
Current B3 trace context, set from Tracer.currentSpan() |
The consumer copies these onto the HTTP call to OSOR so the trace stays connected across the SB hop in Datadog. |
Consumer (cm-order-prc)¶
flowchart TD
A[SB message received<br/>processNavOrderRelease] --> B{body empty?}
B -->|yes| Z1[warn + skip]
B --> C[parseSingleOrder<br/>XXE-hardened DocumentBuilder]
C -->|no Order found| Z2[IllegalArgumentException → DLQ]
C --> D[mapOrder:<br/>docNo, navBufferId,<br/>iterate Line → AsmToOrder → Assembly]
D --> E[NavOrderDto<br/>orderStatus='nav_released']
E --> F[direct:navOrderRelease<br/>set PUT + JSON + Subscription-Key]
F --> G[osor-sys.nav.order-release URL<br/>via APIM]
G -->|2xx| H[ack message]
G -->|non-2xx| Z3[exception → DLQ<br/>maxDeliveryCount=1]
Assembly extraction:
NAV inlines the assemblies under each <Line>, scoped by <AsmToOrder>. The consumer walks Order → Line → AsmToOrder → Assembly rather than a global getElementsByTagName("Assembly") so it cannot pick up an <Assembly> from a sibling order or an unrelated subtree.
OSOR-layer contract¶
PUT /order/nav-release on cm-osor-sys. JSON body:
{
"docNo": "OW583018",
"navBufferId": "PSA2434392",
"orderStatus": "nav_released",
"assemblyOrders": [
{
"orderLineNumber": "10000",
"quantity": 1,
"lotNumber": null,
"requestedCompletionDate": "2026-05-20",
"printableAttribute": "1"
}
]
}
Validation (@Valid on the controller — fails with HTTP 400 before any work):
| Field | Constraint |
|---|---|
docNo |
@NotEmpty |
navBufferId |
@NotEmpty |
orderStatus |
@NotEmpty |
assemblyOrders[] |
@Valid (each element validated) |
assemblyOrders[].orderLineNumber |
@NotEmpty |
assemblyOrders[].printableAttribute |
@NotEmpty |
assemblyOrders[].quantity |
@NotNull |
OMS dispatch: PATCH ${oms.navReleaseUpdate}/{docNo} with X-USER-TOKEN (Base64-decoded from config at boot). The body is the JSON above, forwarded as-is. Any non-2xx from OMS surfaces as HTTP 500 to the caller (cm-order-prc).
Failure modes¶
| Where | Symptom | Disposition |
|---|---|---|
| Basic auth fails at exp | NAV gets 401 | NAV retries per its own policy. Nothing reaches the queue. |
Inbound XML has no <Order> elements |
exp returns 200 "No orders to process" and logs a warning |
No SB messages produced. |
| Blob upload fails | exp returns 500 (NavReleaseProcessingException) | No SB messages produced — NAV must resend the batch. |
| Single-order parse fails in consumer | Exception → message DLQs (maxDeliveryCount=1) |
Other orders in the batch are unaffected. |
| OSOR validation fails (400) | Consumer surfaces an exception → DLQ | Bad payload; needs human review of the DLQ message. |
| OMS PATCH fails | OSOR returns 500 → consumer surfaces an exception → DLQ | Same. |
The DLQ is the recovery surface for any per-order failure. Because each SB message is one order, replaying a single DLQ'd order does not re-deliver its siblings.
Configuration keys (Config Server)¶
All keys live in the per-service application file in PG (integration_int_service_sys_<env>.properties rows).
cm-int-service-exp
| Key | Purpose |
|---|---|
nav.security.username / nav.security.password |
Basic auth that NAV uses. Stored literally — NoOpPasswordEncoder does a literal compare. |
azure.servicebus.connection-nav-order-release |
SB namespace conn string. |
azure.servicebus.queue-nav-order-release |
Queue name (e.g. int-prod-nav-order-release). |
azure.storage.blob.connection-string |
Storage account conn string. |
azure.storage.blob.container-name |
Container that receives the audit copies. |
cm-order-prc
| Key | Purpose |
|---|---|
azure.servicebus.nav-order-release.connection |
Consumer connection. |
azure.servicebus.nav-order-release.queue |
Queue name (must match the exp side). |
osor-sys.nav.order-release |
OSOR URL (APIM-fronted in nonprod/prod). |
cm-osor-sys
| Key | Purpose |
|---|---|
oms.navReleaseUpdate |
Base URL of the OMS NAV-release PATCH endpoint. The order's docNo is appended as /{docNo} at request time. |
Tracing¶
The B3 trace context is carried across both async hops:
- Exp → SB:
Tracer.currentSpan()is copied intoX-B3-TraceId,X-B3-SpanId,X-B3-SampledSB application properties. - SB → Prc: those properties become Camel exchange headers and are re-attached to the outbound HTTP call to OSOR. A new span (
navOrderRelease) is started on the consumer side so the wait time on the queue does not roll into the producer's span.
In Datadog, a multi-order batch shows up as one span on the exp side + N spans on the consumer side — all sharing the same TraceId (the NAV-side trace id, if NAV emits one, or the one Tracer creates at the controller).
See also¶
- CI-411 release notes (Confluence) — the per-order fan-out shipped here; rollback plan; production cutover checklist.
- B2C Order Flow — the surrounding lifecycle (this page is one box on that diagram).
- Camel Topology — where these three services sit relative to the rest.