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:
- Consume and persist the new fields so downstream documents can use them.
- Complete the 856 (ASN) and 945 (Ship Advice) — both are wired up already (CI-252) but are incomplete without the new data.
- 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-prcis the only consumer of theis-b2bendpoint. 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-sysloses the now-deadis-b2bendpoint and the two database queries behind it that nothing else uses. (A third, similar query used elsewhere stays.)cm-edi-prchas anis-b2broute 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_typeandcarrier, vs. theOrder_type/Carrierused 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 nestedorder_box_infois abox_noreference back to the top-level cartons, not a duplicated carton object.Order_typemapping for non-B2B channels —70is B2B and0is B2C, but how FBA / VC / WFS / disposal / self-pickup (10/50/60/20/30) should route through our flow needs a decision. The binaryis-b2bcheck has no equivalent today.
Out of scope¶
- Building the per-vendor
label_templatesend — 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.
Related¶
- Design: Cirro B2B
label_template— config-driven vendor setup (CI-433) — the sibling half of Phase 1.5 (the send side). - Cirro B2B by Vendor — Overview — per-vendor ground truth (real 850 + 940 + what we send Cirro).
- EDI Pipeline — where the 856 and 945 sit in the full 850→810 lifecycle (steps 10–12).
- Design Docs — all proposed engineering designs.
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).