Skip to content
Build bf98f58

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:

  1. active_printable_items? — at least one line item has a printable component with image type CPOD, CYO, or STD.
  2. warehouse_configurations.print_station_batching = true for 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 failure
  • LACK_OF_INVENTORY
  • VALIDATING_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 BusORDER_CREATE_TOPIC_* (fans from OMS's order-update-topic into order-update-bs).
  • RESTPOST /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:

  1. Service Bus topicbatch.generate.updatebatching-update-topic → fans to batching-update-printstation, batching-update-oms, and ADX for audit. Test batches use batch.generate.test and only PS consumes.
  2. Direct HTTPCamelService.batchStatusUpdateInPrintStation does a PUT to ${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.requestbffid-edit-topic when FFID config changes (PrintStation updates print_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 pass
  • DOQuarantineQueue — 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_BATCHEDCIRRO_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

  1. PRINT_STATION_READY_FOR_BATCHING — set by NAV release
  2. PRINT_STATION_WAITING_FOR_BATCHING — after Service Bus publish succeeds
  3. PRINT_STATION_FAILED_EXPORT / FAILED_TO_BATCH — publish or batch-job error
  4. PRINT_STATION_BATCHED — set on batch callback from PS
  5. CIRRO_READY_FOR_FULFILLMENT — set immediately after BATCHED; unblocks Cirro export
  6. WAITING_FULFILLMENT_CONFIRMATIONEXPORTED_TO_TPLSHIPPED — 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_reference is blank. Orders stuck in PRINT_STATION_WAITING_FOR_BATCHING produce 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: XXX means a missing FulfilmentProductMapping row. The order is rejected at batching time, not at intake, so it sits in STAGE until 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 in STAGE, and the order stays in limbo until everything lands.
  • BatchStation's fulfillment listener is one-sided. FULFILLMENT_QUEUE only processes status=FAILED; SUCCESS + shipping confirmations are silently dropped (no else branch). 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/bulk hits the 1001 > 1000 JSON parser limit under load.
  • 2.7.2 env rename. FULFILLMENT_CONNECTION_ORDER_UPDATE → ORDER_PROCESSING_CONNECTION (and _QUEUE equivalents). 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.