PrintStation & BatchStation¶
The custom / personalized print pipeline — how OMS hands off printable orders to BatchStation, how batches are assembled and released to PrintStation, and how PrintStation's callback to OMS unblocks the usual Cirro fulfillment flow. This is what fires when a customer orders a CYO/CPOD/STD product from a warehouse configured for on-site print production.
What these services are¶
BatchStation is the staging and batch-assembly service for custom orders. Spring Boot 3.4 on Java 21, MySQL-backed, operated through a web Batching UI. It receives printable orders from OMS, runs image validation, and — on operator command — groups staged orders into batches destined for a specific print site.
PrintStation is the production-floor service. Java/Vert.x 3.9 backend with a Vue 2 webapp (MSAL auth). Operators drive PrintStation through the webapp to run batches through the press, manage reprint/quarantine queues for failed or held items, and mark batches complete. Built and maintained by the Sunketa team; PS owns the PopSockets-side integration surface.
Neither service sees non-printable orders — those go straight from OMS → Cirro and bypass this pipeline entirely.
When this flow fires¶
OMS routes an order through BatchStation instead of straight to Cirro when both of these are true on the partial order:
active_printable_items?— at least one line item has a printable component with image typeCPOD,CYO, orSTD.warehouse_configurations.print_station_batching = truefor the partial order's warehouse.
If either is false, NAV release transitions the order straight to CIRRO_READY_FOR_FULFILLMENT and Cirro::ExportFulfillmentRequestJob fires immediately. If both are true, the same job silently short-circuits (if active_printable_items? && batch_reference.blank? → return) until PrintStation reports a batch back. That means a qualifying order produces no log line during the wait — see gotchas.
End-to-end lifecycle¶
sequenceDiagram
participant OMS
participant BS as BatchStation
participant OP as Operator
participant PS as PrintStation
participant Cirro
OMS->>BS: order.create.request on int-prod-order-update-topic
BS->>BS: Create Order and OrderItems, status = NEW
BS->>BS: Image validation — DPI, resolution, color mode
BS->>OMS: ack via order-update-oms
OP->>BS: PUT /api/order/update-stageOrder-status — NEW to STAGE
OP->>BS: POST /api/order/batch-order with siteId
BS->>BS: Walk STAGE orders at siteId, build Batch CF-* with SubBatches CFS-*
par Dual release — both fire
BS->>PS: batch.generate.update on batching-update-topic
and
BS->>PS: PUT CAMEL_BATCH_UPDATE_API — Camel-proxied HTTP
end
Note over PS: Operators print, handle reprint and quarantine queues
PS->>OMS: POST /api/v3/print_station/batches — Bearer auth
OMS->>OMS: MultiBatch created, line items BATCHED
OMS->>OMS: PRINT_STATION_BATCHED then CIRRO_READY_FOR_FULFILLMENT
OMS->>Cirro: Normal fulfillment export
Key thing to notice: transport asymmetry. OMS → PrintStation is Azure Service Bus (via BatchStation). PrintStation → OMS is a direct HTTPS POST. The Cirro flow is Service Bus both directions; this flow isn't.
BatchStation¶
State machine¶
Happy path: NEW → READY_FOR_STAGING → STAGE → BATCHED (terminal success).
Error side-paths:
ERROR— image validation failureLACK_OF_INVENTORYVALIDATING_IMAGE/READY_FOR_REVALIDATION/MARK_AS_FIXED— revalidation loop
There is no SENT / FULFILLED / EXPORTED state on BatchStation's side; the lifecycle ends at BATCHED from BS's perspective.
Intake¶
- Azure Service Bus —
ORDER_CREATE_TOPIC_*(fans from OMS'sorder-update-topicintoorder-update-bs). - REST —
POST /api/order,POST /api/order/bulk(bulk has a JSON nesting limit of 1001 > 1000; see gotchas).
Image validation fires automatically after order create via OrderCreationEvent + @TransactionalEventListener(AFTER_COMMIT). Failures produce an ERROR status and a LogFailedOrder row.
Operator gates¶
Both staging and batch generation are operator-driven. Nothing auto-promotes STAGE → BATCHED.
| Action | Endpoint | What it does |
|---|---|---|
| Stage orders | PUT /api/order/update-stageOrder-status |
Moves NEW orders to STAGE. Capped per site by StageOrderLimit. |
| Generate batch | POST /api/order/batch-order {siteId, isUniversalSubBatch} |
Walks every STAGE order at siteId; builds one Batch (CF-xxxxx) with N SubBatches (CFS-xxxxx) grouped by product type, or by product group in universal mode. |
Batches are not scoped by ship date or print run — they're "everything staged at this site, right now."
Release to PrintStation (dual path)¶
When a batch generates, BatchStation fires both:
- Service Bus topic —
batch.generate.update→batching-update-topic→ fans tobatching-update-printstation,batching-update-oms, and ADX for audit. Test batches usebatch.generate.testand only PS consumes. - Direct HTTP —
CamelService.batchStatusUpdateInPrintStationdoes aPUTto${CAMEL_BATCH_UPDATE_API}(Camel-proxied).
Both paths fire for every batch. PrintStation consumes the topic; the HTTP call is belt-and-suspenders.
Other events BatchStation emits¶
fulfillment.edit.request→bffid-edit-topicwhen FFID config changes (PrintStation updatesprint_product_item).- Order cancel ack on
order-update-topic(shipped in BS 2.7.2, April 2026).
PrintStation¶
Stack¶
- Backend: Java / Vert.x 3.9
- Webapp: Vue 2 + MSAL auth
- Team ownership: Sunketa team builds/maintains; PopSockets owns the integration contract
Operator flow¶
Operators work entirely through the Vue webapp. Relevant internal concepts:
DOReprintQueue— items that need another print passDOQuarantineQueue— held / failed items pending review
Callback to OMS (the thing that unblocks Cirro)¶
- Endpoint:
POST /api/v3/print_station/batches - Auth: Bearer token (
Rails.application.credentials[:printstation_api_token]) - Body:
{batch: "<batchRef>", subBatches: [{subBatchId, assemblyOrders: [{assemblyOrderId, componentCode}]}]}
OMS enqueues Batching::CreatePrintStationBatchJob, which creates a MultiBatch + LineItemMultiBatchAssociation per sub-batch, marks each line item BATCHED, and — for each fully batched order — transitions to PRINT_STATION_BATCHED → CIRRO_READY_FOR_FULFILLMENT and enqueues Cirro::ExportFulfillmentRequestJob. Partial batching writes an Order partially batched by PrintStation audit and leaves the order waiting for the remaining items.
Physical print assets (separate track)¶
OMS also runs Batching::PrintStationUploadJob which uploads TIFFs + CSVs to the PrintStation SFTP in warehouse-specific directories. This is the physical-asset pipeline and lives outside the order state machine — don't conflate the two.
OMS status progression for a custom-print order¶
PRINT_STATION_READY_FOR_BATCHING— set by NAV releasePRINT_STATION_WAITING_FOR_BATCHING— after Service Bus publish succeedsPRINT_STATION_FAILED_EXPORT/FAILED_TO_BATCH— publish or batch-job errorPRINT_STATION_BATCHED— set on batch callback from PSCIRRO_READY_FOR_FULFILLMENT— set immediately afterBATCHED; unblocks Cirro exportWAITING_FULFILLMENT_CONFIRMATION→EXPORTED_TO_TPL→SHIPPED— normal Cirro flow
Core identifiers¶
| ID | Meaning |
|---|---|
FFID (FF-xxxxx) |
Layer definitions |
FFT (FFT-xxxxx) |
Templates |
Batch (CF-xxxxx) |
BatchStation-assembled batch |
SubBatch (CFS-xxxxx) |
Product-group partition of a batch |
CSTM SKU |
Custom uploads |
PPID |
Product part ID |
| Jig slot | Always OMS designation (A1, B1, J2) — not Streamline 0-indexed |
Gotchas¶
- The wait is silent. When an order qualifies for batching, the Cirro export job returns early with no log line if
batch_referenceis blank. Orders stuck inPRINT_STATION_WAITING_FOR_BATCHINGproduce zero noise until someone looks. - Operator bottleneck. Staging and batch generation are both manual. Nothing auto-promotes on a cadence — if no one at the print site drives the UI, nothing moves.
Fulfillment is not created for SKU: XXXmeans a missingFulfilmentProductMappingrow. The order is rejected at batching time, not at intake, so it sits inSTAGEuntil resolved.- STAGE can desync with OMS. If the batch-generation ack back to OMS fails, OMS still sees the order as pending. Reconcile with
generate-batchstation-report.sh. - Partial batching is real. An order with multiple printable items can be partially batched — some items flip to
BATCHED, the rest sit inSTAGE, and the order stays in limbo until everything lands. - BatchStation's fulfillment listener is one-sided.
FULFILLMENT_QUEUEonly processesstatus=FAILED;SUCCESS+ shipping confirmations are silently dropped (noelsebranch). Don't assume BS sees fulfillment completions. - HikariPool staleness. Max pool 40, 30-minute lifetime. Goes stale and recovers, but
catch (Exception)swallows DB errors during recovery. - Bulk intake JSON nesting limit.
/api/order/bulkhits the 1001 > 1000 JSON parser limit under load. - 2.7.2 env rename.
FULFILLMENT_CONNECTION_ORDER_UPDATE → ORDER_PROCESSING_CONNECTION(and_QUEUEequivalents). Old var names fail startup. - No bespoke audit layer. Every topic message is auto-logged to Azure Data Explorer via the
cm-*Camel services — don't add service-level audit on top.
Open questions¶
- Cancellation topic wiring on the PrintStation side. The 2.7.2 cancellation flow (shipped April 2026) has OMS publishing cancellations, both services consuming independently, PrintStation acking, and BatchStation orchestrating the final response to OMS. The exact topics PrintStation subscribes to aren't visible from the PS integration-side code — listeners may live in a sibling service (
picking-list-web?) or be post-snapshot. Ask the Sunketa team if you're touching cancellations.