Skip to content
Build 6ff80cd

NAV Carton-Quantity Lookup

Proposed design — not built yet

This is an agreed engineering design, not current system behavior — nothing here is wired. Ticket: CI-494 · Services: cm-int-service-exp, cm-product-prc, cm-cirro-sys · Drafted: 2026-07-08.

Precondition (blocking)

NAV wants predict_pcs_inner ("Forecast Qty per Original Carton"). As of 2026-07-08 the Cirro Open API product/list returns predict_pcs_inner: null for every PopSockets SKU tested in prod, even though the Cirro portal UI shows it populated (open API-vs-UI discrepancy — separate write-up).

This design is not buildable end-to-end until Cirro makes product/list return predict_pcs_inner (matching what the UI shows). The Camel/NAV plumbing can be built and tested against the response shape in parallel, but real values depend on Cirro fixing the API.

Note: receive_pcs_inner — actual received qty per carton — is populated for ~95% of SKUs today. Not what NAV asked for, but it's proof that the pcs-per-carton concept works over the API and a possible fallback if predict_pcs_inner can't be fixed.

Goal

Let NAV fetch predict_pcs_inner (units per original carton) for a set of SKUs, to build packing instructions. Cirro is the source of truth; Camel exposes it to NAV.

Design principles: synchronous request/response (no Service Bus — this is a lookup, not an event), bulk (one NAV call → many SKUs), and machine-actionable (boolean exists + null-or-value, no free-text notes).

Architecture

NAV talks to the experience layer it already authenticates against (cm-int-service-exp, the same home as POST /nav/orders/release). The exp layer forwards to cm-product-prc, which calls the Cirro system layer (cm-cirro-sys), processes the raw Cirro payload, and maps the fields back to the NAV response shape. No new infrastructure.

sequenceDiagram
    participant NAV
    participant Exp as cm-int-service-exp<br/>POST /nav/product/carton-quantity
    participant Prc as cm-product-prc<br/>POST /nav/product/list
    participant Sys as cm-cirro-sys<br/>POST /nav/product/list
    participant Cirro as Cirro Open API<br/>(oms.elogistic.com)

    NAV->>Exp: POST JSON — APIM key + basic auth<br/>{ "skus": [...] }
    Exp->>Exp: Validate & dedupe SKU list
    Exp->>Prc: POST /nav/product/list<br/>{ "skus": [...] }
    loop chunks of ≤200 SKUs
        Prc->>Sys: POST /product/list (chunk)
        Sys->>Cirro: POST /v1/auth/get-access-token (cached)
        Sys->>Cirro: POST /v1/product/list<br/>{ product_sku_list: [...] }
        Cirro-->>Sys: page of product records
        Sys-->>Prc: raw Cirro product data
    end
    Prc->>Prc: Aggregate chunks + map fields<br/>(predict_pcs_inner, exists)
    Prc-->>Exp: mapped results
    Exp-->>NAV: 200 — { requested, returned, results[] }
Component Role
cm-int-service-exp New NAV-facing REST endpoint. Validates/dedupes the SKU list, forwards to cm-product-prc, returns the shaped response to NAV.
cm-product-prc Receives the SKU list from exp, chunks into ≤200-SKU batches, calls cm-cirro-sys per chunk, aggregates raw results, and maps Cirro fields to the NAV contract (predict_pcs_inner, exists).
cm-cirro-sys New POST /product/list proxy route — extends the existing CirroProductAPIRouter (which already proxies unit-list / wrapper-code-list with the same product_sku_list shape). Handles token caching and pagination.

Request

POST /nav/product/carton-quantity
Content-Type: application/json
{ "skus": ["807259", "806876", "807396", "707203", "999999"] }

Response

{
  "requested": 5,
  "returned": 5,
  "results": [
    { "sku": "807259", "predict_pcs_inner": 40,   "exists": true  },
    { "sku": "806876", "predict_pcs_inner": 40,   "exists": true  },
    { "sku": "807396", "predict_pcs_inner": 160,  "exists": true  },
    { "sku": "707203", "predict_pcs_inner": null, "exists": true  },
    { "sku": "999999", "predict_pcs_inner": null, "exists": false }
  ]
}
  • predict_pcs_inner — the value straight from Cirro (same field name NAV/the team already reference). null when the SKU has no value set or doesn't exist.
  • exists — whether the SKU is in the Cirro catalog. NAV branches on exists + null-check:
exists predict_pcs_inner Meaning NAV action
true non-null SKU in Cirro, value set use the value
true null SKU in Cirro, value not set yet apply fallback
false null SKU not in Cirro catalog apply fallback / flag bad SKU

The response array preserves the caller's SKU set. A SKU-keyed map variant is trivial if NAV prefers it (open question — see below).

Field mapping (Cirro → Camel)

Camel field Cirro source Rule
predict_pcs_inner predict_pcs_inner pass through verbatim (null if absent)
exists derived true if the SKU appears in data.list[], else false

Field mapping is performed by cm-product-prc after aggregating the raw Cirro chunks — direct 1:1, no coalescing, no field renaming.

Bulk & pagination

  • Cirro product/list caps page_size at 200 and accepts a product_sku_list array.
  • Camel chunks the incoming SKU list into batches of ≤200, issues one Cirro call per batch, and aggregates the results. One NAV request of any size = a handful of internal calls (e.g. 500 SKUs → 3 Cirro calls).
  • Cirro rate limit is 600 req/min — well clear for NAV's expected usage.

Auth (each hop)

Hop Auth
NAV → cm-int-service-exp APIM subscription key + basic auth (existing NAV pattern — same as /nav/orders/release)
cm-int-service-exp → cm-product-prc Internal (APIM/direct, existing)
cm-product-prc → cm-cirro-sys Internal (APIM/direct, existing)
cm-cirro-sys → Cirro Authorization: Bearer <token> from /v1/auth/get-access-token, token cached until expiry (existing AuthService)

Error & edge handling

  • Per-SKU outcomes are carried by exists + the null-or-value of predict_pcs_inner — never dropped, never prose.
  • Duplicate SKUs in request → deduped before forwarding to Cirro; response reflects the unique set.
  • Cirro unavailable / auth failure → request-level 5xx with an error envelope (the only failure mode that isn't per-SKU). NAV retries or degrades.
  • Empty skus array400 Bad Request.

Config & environments

Add cirro-sys.product.list URL to the config server per env — same pattern as the existing cirro-sys.product.unit-list and wrapper-code-list keys.

Environment Cirro base URL
prod https://oms.elogistic.com/v1
nonprod (dev/qa/stage) https://uat-oms-elogweb.eminxing.com/v1

Out of scope

  • NAV-side packing-instruction logic — the bulk of the effort, owned by the NAV team; this design covers only the Camel lookup contract.
  • No Camel-layer caching initially (Cirro is fast + rate limit is generous); revisit if NAV polls heavily.
  • No write-back to Cirro.
  • Not tied to the Cirro 1.5 release — separate deployment, no Cirro-code dependency.

Open questions

  1. NAV fallback when predict_pcs_inner is absent (exists: true + null, or exists: false) — what does NAV do?
  2. Array vs. SKU-keyed map for the response results — confirm with NAV.
  3. Optional single-SKU convenience route (GET /nav/product/{sku}/carton-quantity) alongside the bulk POST?

See also

  • NAV Order Release — the existing NAV-facing endpoint in cm-int-service-exp; same auth pattern.
  • Camel Topology — where cm-int-service-exp, cm-product-prc, and cm-cirro-sys sit in the service graph.