Skip to content
Build 8b0b16e

Cirro B2B — Enriched Ship Confirmation, 856/945 & B2B/B2C Flag

Proposed design — not built yet

This is an agreed engineering design for the second half of Phase 1.5 of the Cirro B2B migration, not current system behavior — nothing here is wired. Ticket: CI-434 · Epic: CI-432 · Subtasks: CI-440–445, CI-447 (parser tolerance) · Services: cm-order-prc, cm-edi-prc, cm-int-service-sys, cm-ext-service-exp / cm-fulfill-exp (confirmation parsers) · Dated: 2026-06-02 (updated 2026-06-07). The first half — building the per-vendor label_template send — is CI-433, tracked separately.

Two halves of one epic

Phase 1.5 of the Cirro B2B migration is two sibling designs under epic CI-432: the send side (CI-433 — label_template, what we push Cirro per vendor) and the receive side — this page, consuming Cirro's enriched ship confirmation and completing the 856/945 back out.

What this covers

Cirro is enriching the shipping confirmation they send us after an order ships. The enriched payload carries new fields — including, importantly, an explicit B2B vs B2C indicator. Three things follow from that:

  1. Consume and persist the new fields so downstream documents can use them.
  2. Complete the 856 (ASN) and 945 (Ship Advice) — both are wired up already (CI-252) but are incomplete without the new data.
  3. Use Cirro's B2B/B2C flag to classify orders, and remove the database check we use today.

The flow today (baseline)

When Cirro sends an order confirmation, cm-order-prc processes it. To decide whether the order is B2B or B2C it asks our own database — it calls an is-b2b endpoint in cm-int-service-sys that checks whether the order exists in the edi_docs table. Exists → B2B; absent → B2C.

For B2B orders, cm-order-prc then publishes a "shipped" event onto a Service Bus topic (edi-shipped-b2b). cm-edi-prc picks that up, pulls the related EDI documents, and generates the 856 and 945 from them.

The flow after this change

The B2B/B2C decision stops being a database lookup and becomes a simple read of the flag Cirro now sends on the confirmation. The decision point doesn't move — it already happens while we're processing Cirro's confirmation, so the new field lands precisely where the old check was. Everything downstream (the shipped event, the 856/945) stays put, just better-fed.

flowchart TD
    A([Cirro order confirmation<br/>now carries B2B/B2C flag<br/>+ new fields]) --> B[cm-order-prc<br/>confirmation processing]
    DB[(edi_docs lookup<br/>· REMOVED ·)] -. was the source .-> C
    B --> C{B2B or B2C?}
    C -->|now: read the flag<br/>off the payload| D[Classify B2B / B2C]
    D --> F[B2B → publish to<br/>edi-shipped-b2b topic]
    F --> G([cm-edi-prc<br/>shipped-event processing])
    G --> H[Persist the new fields]
    H --> I[Complete 856 ASN]
    H --> J[Complete 945 Ship Advice]

The enriched payload

Fields confirmed by Cirro — final structure still being double-confirmed (CI-440)

The fields below are confirmed (source: Cirro's B2B API Adjustment doc, transcribed in the camel repo at reference/cirro/b2b-api-adjustment.md). Cirro is still double-confirming the final structure internally (CI-440), so treat the names as confirmed but not yet frozen before building against them.

Cirro sends the shipping confirmation back as a StockChangeRecord callback. Here's the message body before → after the enrichment — nothing is removed, the additions are purely additive, and the // ← NEW markers show exactly where they sit.

Before — what we consume today:

{
  "order_code": "EL1038-250529-0037",
  "reference_no": "112-8525766-8344247",
  "order_status": 0,
  "tracking_number": "1234567890",
  "sm_code": "",
  "add_time": "2000-01-02T13:48:19-05:00",
  "sc_id": "21",
  "warehouse_id": "1",
  "outStock_time": "2000-01-02T13:48:19-05:00",
  "so_weight": "1166.667",
  "order_weight": "1166.667",
  "so_shipping_fee": "2571.870",
  "property_label": "",
  "item": [
    { "product_barcode": "G1038-H3166419678", "product_sku": "GR580010", "qty": 1 }
  ],
  "fee_details": { "totalFee": "5618.340", "SHIPPING": "2571.870", "currency_code": "USD" },
  "order_box_info": [
    {
      "box_no": "1",
      "ob_qty": 1,
      "ob_length": "50.00", "ob_width": "40.00", "ob_height": "30.00", "ob_weight": "30.00",
      "tracking_number": "MO201017498MO",
      "product_barcode": "EL122-F5846293533"
    }
  ]
}

After — enriched, new fields marked:

{
  "order_type": "70",                              // ← NEW  B2B/B2C flag: 70=B2B, 0=B2C
  "dispatch_info": [                               // ← NEW  carrier / routing
    { "pro_number": "Pro Number", "carrier": "carrier name",
      "carrier_scac": "", "bol": "", "load_id": "", "arn": "" }
  ],
  "order_code": "EL1038-250529-0037",
  "reference_no": "112-8525766-8344247",
  "order_status": 0,
  "tracking_number": "1234567890",
  "sm_code": "",
  "add_time": "2000-01-02T13:48:19-05:00",
  "sc_id": "21",
  "warehouse_id": "1",
  "outStock_time": "2000-01-02T13:48:19-05:00",
  "so_weight": "1166.667",
  "order_weight": "1166.667",
  "so_shipping_fee": "2571.870",
  "property_label": "",
  "item": [
    { "product_barcode": "G1038-H3166419678", "product_sku": "GR580010", "qty": 1 }
  ],
  "fee_details": { "totalFee": "5618.340", "SHIPPING": "2571.870", "currency_code": "USD" },
  "order_box_info": [
    {
      "box_no": "1",
      "box_mark": "Prepack Carton Number",         // ← NEW  prepack carton number
      "fn_box_no": "Carton Reference Number",      // ← NEW  carton reference number
      "sscc_code": "SSCC",                         // ← NEW  carton SSCC
      "ob_qty": 1,
      "ob_length": "50.00", "ob_width": "40.00", "ob_height": "30.00", "ob_weight": "30.00",
      "tracking_number": "MO201017498MO",
      "product_barcode": "EL122-F5846293533"
    }
  ],
  "pallet_info": [                                 // ← NEW  pallet → carton hierarchy
    {
      "pallet_sscc": "system generated pallet SSCC",
      "shipping_mark": "CIRRO Pallet number",
      "customer_shipping_mark": "User manual upload pallet sscc",
      "length": "", "width": "", "height": "", "weight": "",
      "order_box_info": [ { "box_no": "1" } ]      // refs the cartons above by box_no
    }
  ]
}

Wrapper unchanged

The callback still arrives as { app_token, sign, message_type: "StockChangeRecord", message_id, send_time, message: { … } } — only the message body above gains fields.

The new content breaks into four groups.

The B2B/B2C indicator — Order_type:

Value Meaning
0 standard B2C
10 FBA
20 disposal
30 self pickup
50 VC
60 WFS
70 standard B2B

70 is our B2B case and 0 is B2C — that pair replaces today's edi_docs lookup. How the other channels (10 / 20 / 30 / 50 / 60) should route is a decision we still owe; the current binary check has no equivalent (see Open items).

Carton / SSCC data for the 856/945 — order_box_info[] gains three fields:

Field Meaning
box_mark prepack carton number
fn_box_no carton reference number
sscc_code SSCC

These give each shipped carton its prepack carton number, carton reference number, and SSCC — exactly the carton-level data the 856 and 945 need.

Carrier / routing — dispatch_info[] carries pro_number, Carrier, carrier_scac, bol, load_id, and arn. This is the carrier transport method and routing we hardcode in the documents today; the callback now lets us source it properly.

Pallet → carton hierarchy — pallet_info[] (new — not in the earlier payload version). Each pallet carries its own SSCC, shipping marks, and dimensions, and lists the cartons sitting on it:

Field Meaning
pallet_sscc pallet SSCC (system-generated)
shipping_mark Cirro pallet number
customer_shipping_mark customer-uploaded pallet SSCC
length / width / height / weight pallet dimensions
order_box_info[] the cartons on this pallet — box_no references only, pointing back to the full carton objects in the top-level order_box_info[] (not duplicated)

This adds pallet-level grouping (which cartons sit on which pallet) on top of the carton/SSCC detail — the structure the 856/945 need for palletized shipments.

The callback also carries the fields the flow already uses — order_code, reference_no, order_status, tracking_number, outStock_time, weights, fees, and the item[] array.

The changes

Parse defensively first — finish the unknown-field tolerance (CI-447)

Before any of the below can land, the confirmation parser has to tolerate unknown fields at every level. Cirro adds fields to the callback without warning — 6 new top-level fields on 2026-06-03 dropped every confirmation in prod until patched. CI-447 added top-level tolerance (@JsonIgnoreProperties(ignoreUnknown=true) on OrderConfirmationDTO), but the exp-layer parsers still build a raw new ObjectMapper() and the nested DTOs reject unknowns. The new box_mark / fn_box_no / sscc_code land inside order_box_info[], so they'd re-trigger the same failure one level down.

Fix it at the mapper level, not with per-class annotations (whack-a-mole): inject the Spring-managed ObjectMapper (as cm-order-prc already does) or set FAIL_ON_UNKNOWN_PROPERTIES=false, in both exp confirmation controllers (cm-ext-service-exp live, cm-fulfill-exp legacy). Then the whole tree — current fields, the new dispatch_info / pallet_info, and anything Cirro adds later — parses safely. Ships bundled with the field additions, not as a separate PR.

Use Cirro's B2B/B2C flag, remove the DB check — CI-445

Read B2B vs B2C from the new Order_type flag (70 = standard B2B) instead of calling the is-b2b endpoint. The removal is clean — nothing else depends on that check:

  • cm-order-prc is the only consumer of the is-b2b endpoint. Its order-type lookup (called in three places, all in confirmation processing) reads the flag instead, and the lookup method goes away.
  • cm-int-service-sys loses the now-dead is-b2b endpoint and the two database queries behind it that nothing else uses. (A third, similar query used elsewhere stays.)
  • cm-edi-prc has an is-b2b route defined but never called — dead code we delete while we're here.

Persist the new fields — CI-441

Store what Cirro sends so the 856/945 generators can read it. Scope depends on the final field list (CI-440).

Complete the 856 and 945 — CI-442, CI-443

Both documents are generated today but are incomplete without the new data. Fill them out properly once the fields are persisted. They're separate documents with separate consumers, so they're tracked separately.

Decisions made

  • B2B/B2C comes from Cirro's flag, not our database — single source of truth, set by the party that actually knows.
  • The DB check is removed, not left as a fallback — keeping a dead lookup around invites drift. Verified there are no other callers.

Open items

  • Final structure confirmation (CI-440) — the fields are confirmed, but Cirro says the structure is still being double-confirmed internally. Lock it before we build against the exact names.
  • Field-name casing (CI-440) — the latest payload sample shows lower-case order_type and carrier, vs. the Order_type / Carrier used in the field breakdown above (from the earlier transcription). Only one binds; confirm against the final structure.
  • pallet_info[] shape (CI-440) — confirm whether it's present on every shipment or only palletized B2B (B2C small-parcel likely has none), and that the nested order_box_info is a box_no reference back to the top-level cartons, not a duplicated carton object.
  • Order_type mapping for non-B2B channels70 is B2B and 0 is B2C, but how FBA / VC / WFS / disposal / self-pickup (10 / 50 / 60 / 20 / 30) should route through our flow needs a decision. The binary is-b2b check has no equivalent today.

Out of scope

  • Building the per-vendor label_template send — that's the other half of Phase 1.5, CI-433, with its own design doc.
  • Any change to how non-label parts of the B2B Create payload are built.

Design authored 2026-06-02 by cm-edi-prc (CI-434, Phase 1.5). Updated 2026-06-07: added the before → after payload sample, the pallet_info[] pallet→carton hierarchy, and the parser-tolerance change (CI-447). Proposed — not yet implemented; enriched-payload fields confirmed by Cirro, final structure being double-confirmed (CI-440).