Skip to content
Build bf98f58

OMS

The Order Management System — a Rails app that owns the post-checkout order lifecycle for PopSockets. Orders arrive from storefronts, move through NAV release, batching, Cirro fulfillment, and payment capture — OMS orchestrates every step.

At a Glance

Purpose Single source of truth for where an order is in the post-checkout pipeline
Owner Diff Agency (built + maintained + hosted in AWS)
Stack Ruby 3.4.8 · Rails 8.1.3 · MySQL · Sidekiq + Redis · Flipper · RailsEventStore
Repo PopSockets/oms (originally diffagency/popsockets-si)
APM Datadog (service name: ruby)
Deploy Capistrano + Passenger

Environments

Env Host App path
Prod oms.popsockets.com /srv/popsockets_oms-production/current (oms-production-1..4)
Staging popsockets-staging.diffagency.com /srv/pop_sockets_oms_merge_staging-staging/current
Sandbox popsockets-sandbox.diffagency.com /srv/pop_sockets_sandbox-sandbox/current

OMS vs. OSOR

They're the same thing. "OSOR" is a legacy name for the v3 marketplace API namespace (/api/v3/osor_order/..., controller Api::V3::OsorController). It's where MuleSoft/Camel integrations call in. Don't treat it as distinct from OMS.

Integrations at a Glance

flowchart LR
    subgraph Storefronts
        SFCC[SFCC]
        Shopify
        Amazon[Amazon ADF]
        MP[Marketplace<br/>Lazada/Jackyun]
    end

    subgraph Fulfillment
        Cirro[Cirro 3PL]
        PS[PrintStation]
        BS[BatchStation]
    end

    subgraph ERP
        NAV[NAV Dynamics]
    end

    subgraph Support
        Smarty
        Klaviyo
        Arena
        DD[Datadog]
    end

    Camel[Camel<br/>cm-* services]
    ASB[(Azure<br/>Service Bus)]
    OMS((OMS))

    SFCC <-->|OCAPI/SCAPI<br/>OAuth2| OMS
    Shopify <-->|REST + GraphQL<br/>webhooks| OMS
    Amazon <-->|SP-API| OMS
    MP -->|Marketplace<br/>GeneralizeOrderJob| OMS

    OMS <-->|SOAP + NTLM<br/>PSCORP domain| NAV
    NAV -.->|release callback<br/>via Camel| OMS

    OMS <-->|APIM<br/>Basic + key| Camel
    Camel <-->|Bearer| Cirro
    OMS <-->|SAS<br/>raw sockets| ASB
    ASB <--> BS
    BS <--> PS

    OMS -->|address validate| Smarty
    OMS -->|transactional events| Klaviyo
    OMS -->|item master sync| Arena
    OMS -->|APM + StatsD| DD

    style OMS fill:#ffe066,stroke:#333,stroke-width:3px
    style Camel fill:#74c0fc,stroke:#333
    style ASB fill:#d0bfff,stroke:#333

Responsibilities

OMS owns:

  • Order lifecycle orchestration — ~80 statuses (see Order Status Lifecycle) driven implicitly by job outcomes. No formal state machine DSL (no AASM/statesman) — statuses are string constants on OrderStatus, enforced loosely.
  • Payment capture triggering — SFCC is atomic with the fulfill call (payment_instructions: {execute: Capture}); Shopify uses a separate CapturePaymentJob. Gateway logic lives in Store#capture_payment.
  • Retained order review — fraud/risk holds, address validation via Smarty, max-currency checks. Orders go RETAINEDRETAIN_APPROVED / RETAIN_REJECTED.
  • Refunds, cancellations, duplicates, manual fulfillments — all through OrderAction::* services.
  • Admin UI — orders, custom approvals, retained orders, error dashboard, bulk actions, Sidekiq/Flipper/EventStore access.
  • Reports & reconciliation/order_lifecycle_reports, /dashboard/stats, /error_dashboard, /orders/bulk_reports.

OMS does not own inventory allocation (NAV does), storefront checkout (SFCC/Shopify), printing (PrintStation), packing/shipping (Cirro), carrier integration, customer notifications (Klaviyo/storefronts), returns/RMA, product catalog (Arena), tax calculation, or pricing/promotions. See What OMS Doesn't Do.

What OMS Talks To

System Direction Mechanism
SFCC (Demandware) Pull orders; push status + fulfillment + capture OCAPI or SCAPI (Flipper sfcc_scapi_orders), OAuth2 client-credentials. Demandware::SyncOrdersJob, Demandware::Api::FulfillOrder
Shopify Pull orders/products; push fulfillments/cancels/refunds REST + GraphQL Admin API. Shopify::ShopSync::SyncOrdersJob, Shopify::CreateFulfillment. Webhook receipt via ShopifyAppAdmin::Engine
Amazon (ADF) Pull orders; push ack + labels SP-API. Amazon::SyncOrdersJob, Amazon::Api.fetch_shipping_label
NAV (Dynamics) Push (SOAP); receive NAV callbacks via Camel Savon + NTLM (domain PSCORP). app/services/nav/api.rb. 4 URLs: US, EMEA, WriteON Promo, Japan. DocTypes: 1=SalesOrder, 3=CreditMemo, 4=Product, 6=ShipmentNotice, 7=Cancellation
Camel Outbound REST to APIM; Camel pushes back via Service Bus + REST Outbound: POST CONFIG[:cirro_fulfillment_request_url] (APIM key + Basic auth). Inbound: PATCH /api/v3/osor_order/status/:order_id, POST /osor_order/fulfill_orders, POST /api/service_bus/events
Cirro (3PL) Only via Camel, never direct OMS → Camel APIM → ASB fulfillment-request-amer → Cirro. Cirro → webhook to Camel → OMS
BatchStation / PrintStation Bi-directional via ASB topics OMS publishes order.create.request / order.update.repush to int-prod-order-update-bs. Consumes int-prod-batching-update-oms. Events: Event::PrintStationOrderCreated, Event::PrintStationCancellationAcknowledged
Azure Service Bus Both directions Raw HTTPS + SAS auth in app/services/azure/service_bus/client.rb. Handwritten sockets, no Azure SDK.
Smarty Streets Outbound Address validation. app/services/smarty/validate_address.rb
Klaviyo Outbound Transactional events. app/services/klaviyo/create_event.rb
Arena Outbound Product / BOM sync. app/services/item_master/fetch_info.rb. Flipper use_prod_arena
Datadog Outbound APM + StatsD metrics + custom per-order log streams

Gladly, Snowflake, and analytics are not integrated. The OMS DB is replicated by a separate ETL owned by another team.

Key Subsystems

Sidekiq Jobs (the important ones)

Job What it does
Demandware::SyncOrdersJobSyncOrderJobGeneralizeOrderJob SFCC intake pipeline
Shopify::ShopSync::SyncOrdersJob Shopify intake
Amazon::SyncOrdersJob Amazon SP-API intake
Marketplace::GeneralizeOrderJob Marketplace (Lazada, Jackyun) intake
SplitOrderJob / SplitOrdersJob Splits orders by fulfillment path (printable vs finished goods vs vendor)
Nav::CreateOrderJob SOAP-creates sales order (DocType 1)
Nav::ExportOrderJob / Nav::ExportOrdersJob SOAP-sends shipment notice (DocType 6)
Nav::ReleaseFromNavJob Processes Camel's NAV-release callback
Nav::ExportCancellationJob SOAP-sends cancellation (DocType 7)
Cirro::ExportFulfillmentRequestJob POSTs fulfillment request to Camel → Cirro
Cirro::CheckInventoryJob Inventory verification sidecar
PrintStation::CreateOrderJob Publishes order.create.request to ASB
Orders::ConfirmFulfillmentJob Cirro acceptance → EXPORTED_TO_TPL
Orders::RevertFulfillmentJob Cirro rejection handling
CapturePaymentJob Shopify-only payment capture
BulkActionJob Processes bulk-action CSVs
UpdateStaleOrdersJob Marks stale orders for re-sync
ResumeDelayedOrdersJob Kicks DELAYED orders back into the pipeline

Recurring Jobs (DB-driven schedule)

There is no schedule.yml. Recurring job schedules live in the recurring_jobs MySQL table. Each row: job_class, store_url, warehouse_id, cron, enabled. UI at /recurring_jobs. Sidekiq-Scheduler reads RecurringJob.schedule and rebuilds on change. To change SFCC poll frequency for a store, edit the DB row, not code. recurring_job_scheduled_downtimes lets ops temporarily disable jobs without deleting cron.

Flipper Flags That Matter

Flag Meaning
sfcc_scapi_orders Use SCAPI instead of OCAPI for SFCC pulls
cirro_fulfillment_request Master switch for Cirro exports
cirro_inventory_check Enable CheckInventory sidecar + OOS handling
cirro_fulfillment_sequence Include TPL export sequence in Cirro payload
fulfillment_confirmation New two-step flow (WAITING_FULFILLMENT_CONFIRMATIONEXPORTED_TO_TPL) vs legacy straight-to-EXPORTED_TO_TPL
print_station_create_order Master switch for PrintStation batching exports
use_prod_arena Point Arena calls at prod cluster in non-prod envs

Core Models

  • Order — polymorphic-ish: has_one of shopify_order, marketplace_order, amazon_order, etc. pointing at platform-specific details. store_type is the discriminator.
  • OrderStatus — 1:1 with Order. Holds current status + per-stage booleans (generalized, nav_created, nav_exported, tpl_exported, payment_capture, etc.) and error text. after_save triggers OrderAudit.
  • OrderAudit — append-only log of every transition. Indexed on (status, created_at, order_id). Some statuses marked MULTIPLE_AUDITS_PERMITTED can repeat.
  • OrderDetailwarehouse_id, nav_buffer_id, shipping rate data.
  • PartialOrder — when an order splits into multiple fulfillment paths, each path gets one.
  • LineItem / LineItemDetail / LineItemStatus / LineItemFulfillment — item-level state.
  • LineItemAsset — printable artwork/positioning data.
  • Demandware::Order, Shopify::ShopifyOrder, Marketplace::Order, Amazon::Order — raw platform data, kept separately from the generalized Order.
  • Demandware::Client — SFCC credentials/config per instance (dev AMER, SBX3, staging APAC, India, etc.)
  • Store — a saleable storefront with warehouse_id, export_to_nav?, cirro?, shopify?.
  • Warehouse — fulfillment destination. Drives NAV URL routing + Cirro region.
  • Nav::CustomerCard — maps storefront source → NAV customer number + payment method. No SFCC entry — SFCC uses customer.platform_id directly.
  • RecurringJob / BulkActionReport — see above.

Controllers / API Namespaces

  • Api::V3::OsorController/api/v3/osor_order/*. The primary integration surface for Camel despite the "osor" name.
  • Api::V3::CustomersController — customer lookup.
  • Api::V3::PrintStation::BatchesController — PrintStation batch webhooks.
  • Api::V3::ItemMaster::ProductsController — product master sync.
  • Api::ServiceBus::EventsController — Service Bus push-pull (Cirro fulfillment + PrintStation cancellation).
  • Api::V1::BaseApiController — Shopify Proxy + legacy v1.
  • Admin: OrdersController, CustomApprovalsController, RetainedOrdersController, ErrorDashboardController, PortalController, RecurringJobsController.
  • Shopify app: mounted via ShopifyAppAdmin::Engine at /.

Order Status Lifecycle

OrderStatus::STATUSES holds ~80 values. They're strings, not enums. The happy-path flow for a typical SFCC/Shopify order looks like this:

stateDiagram-v2
    [*] --> READY_FOR_PROCESSING: Storefront sync

    READY_FOR_PROCESSING --> RETAINED: Risk/address flag
    RETAINED --> RETAIN_APPROVED: CS approves
    RETAINED --> RETAIN_REJECTED: CS rejects
    RETAIN_REJECTED --> [*]

    READY_FOR_PROCESSING --> READY_TO_CREATE_IN_NAV
    RETAIN_APPROVED --> READY_TO_CREATE_IN_NAV

    READY_TO_CREATE_IN_NAV --> PENDING_NAV_RELEASE: Nav::CreateOrderJob
    READY_TO_CREATE_IN_NAV --> FAILED_TO_CREATE_IN_NAV: SOAP error

    PENDING_NAV_RELEASE --> RELEASED_FROM_NAV: Camel callback<br/>PATCH /osor_order/status
    PENDING_NAV_RELEASE --> FAILED_TO_RELEASE_FROM_NAV

    RELEASED_FROM_NAV --> READY_FOR_BATCHING: Printable
    RELEASED_FROM_NAV --> CIRRO_READY_FOR_FULFILLMENT: Finished goods

    READY_FOR_BATCHING --> BATCHED: PrintStation<br/>batch_reference set
    BATCHED --> CIRRO_READY_FOR_FULFILLMENT

    CIRRO_READY_FOR_FULFILLMENT --> WAITING_FULFILLMENT_CONFIRMATION: Cirro::ExportFulfillmentRequestJob
    CIRRO_READY_FOR_FULFILLMENT --> FAILED_TO_EXPORT_TO_TPL

    WAITING_FULFILLMENT_CONFIRMATION --> EXPORTED_TO_TPL: Cirro acceptance<br/>(via ASB)
    EXPORTED_TO_TPL --> SHIPPED: Cirro fulfill_orders<br/>(tracking)

    SHIPPED --> EXPORTED_TO_NAV: Nav::ExportOrdersJob (cron)
    EXPORTED_TO_NAV --> FULFILLED: Platform marked shipped
    FULFILLED --> [*]

    FULFILLED --> PAYMENT_CAPTURED: Shopify only<br/>(SFCC is atomic)

The main branch points are retention (risk holds), printable vs finished goods (batching fork), and new vs legacy Cirro flow (fulfillment_confirmation flag — legacy went straight to EXPORTED_TO_TPL). Cancellation, refund, and Amazon-specific states branch off most of these but aren't shown here to keep the diagram legible.

All statuses by phase

Intake / generalization

READY_FOR_PROCESSING · WAITING_FOR_RECIPE_IDS (Jackyun) · PRE_ORDER · DELAYED · AGING_ORDER · GENERALIZE_ERROR

Approval / retention

PENDING_APPROVALS · ALL_LINE_ITEMS_REJECTED · RETAINED · RETAIN_APPROVED · RETAIN_REJECTED

READY_TO_CREATE_IN_NAVPENDING_NAV_RELEASERELEASED_FROM_NAV Error branches: FAILED_TO_CREATE_IN_NAV, FAILED_TO_RELEASE_FROM_NAV Special: STROOPWAFEL_EXCEPTION (real status — orders caught mid-lifecycle during a past refactor; not a joke)

Batching (printable only)

READY_FOR_BATCHINGWAITING_BATCHING_CAPACITYBATCHED PrintStation variants: PRINT_STATION_READY_FOR_BATCHING, _WAITING_FOR_BATCHING, _BATCHED, _FAILED_EXPORT, FAILED_TO_BATCH

Cirro export & fulfillment

CIRRO_READY_FOR_FULFILLMENTCIRRO_PENDING_INVENTORY_CHECKCIRRO_WAITING_INVENTORYCIRRO_INVENTORY_CONFIRMEDCIRRO_WAITING_FOR_FULFILLMENTWAITING_FULFILLMENT_CONFIRMATIONEXPORTED_TO_TPL Error: FAILED_TO_EXPORT_TO_TPL

Shipped + post-ship

SHIPPED (Cirro fulfill_orders) · FULFILLED · OMS_CREATED_ORDER_FULFILLMENT · EXPORTED_TO_NAV · FAILED_TO_EXPORT_TO_NAV · PAYMENT_CAPTURED (Shopify only) · FAILED_TO_CAPTURE · FULFILLED_PAYMENT_ERROR

Cancellations & refunds

CS_CANCELLED · CUSTOMER_CANCELLED · OMS_CANCELLED · PLATFORM_CANCELLED · SHOPIFY_CANCELLED / _PARTIALLY_CANCELLED · SFCC_CANCELLED / _PARTIALLY_CANCELLED · EXPORTED_CANCELLATION_TO_NAV · FAILED_TO_CANCEL / _ON_NAV · CS_REFUNDED · CS_REFUNDED_ARBITRARY · EXPORTED_REFUND_TO_NAV · FAILED_TO_REFUND · FAILED_TO_EXPORT_REFUND · PROCESSED_EXTERNALLY ("we fixed it manually, stop trying") · VOIDED

Amazon-specific

PENDING_ACKNOWLEDGMENT · ORDER_ACKNOWLEDGED / FAILED_TO_ACKNOWLEDGE_ORDER · PENDING_SHIPPING_LABEL · AMZ_SHIPPING_LABEL_SUCCESS / _FAILED · PACKING_SLIP_GENERATED / _FAILED · AMZ_PACKING_SLIP_SUCCESS / _FAILED

Useful grouping constants

OrderStatus::CANCELLED_STATUSES (5 variants) · REFUNDED_STATUSES (2) · ERROR_STATUSES (~20, drives /errors) · PENDING_STATUSES (manual-kick candidates)

Authentication

Inbound (callers → OMS)

Caller Endpoint Auth
MuleSoft / Camel /api/v3/osor_order/* X-USER-TOKEN vs CONFIG[:mulesoft_api_access_token]
Service Bus push /api/service_bus/events X-USER-TOKEN vs CONFIG[:service_bus_api_access_token]
PrintStation /api/v3/print_station/batches HTTP Bearer vs credentials[:printstation_api_token]
Shopify Proxy (v1) HMAC-SHA256 (shopify_secret / shopify_deprecated_secret); only enforced in prod
Admin UI / Session via ShopifyAppAdmin::User + roles (system_admin, admin, supervisor, warehouse_admin)
Sidekiq / Flipper / EventStore Internal CanAccessInternalConfigUI constraint

Outbound (OMS → others)

  • SFCC — OAuth2 client-credentials per Demandware::Client
  • NAV — NTLM (PSCORP\DIFFOMS staging / PSCORP\PRODOMS prod) + cert pinning (/home/ubuntu/nav_cert/star.popsockets.com.ca-bundle.pem)
  • Camel — APIM subscription key + Basic auth (cirro_username / cirro_password)
  • Azure Service Bus — SAS signature (handwritten HMAC, no SDK)
  • Shopify — OAuth access tokens via shopify_app gem
  • Amazon SP-API — LWA client credentials + STS-assumed role
  • Arena — API key. Smarty — auth-id/auth-token

Rails credentials (config/credentials/<env>.yml.enc) hold all outbound creds. Master key lives on server at <shared>/config/credentials/<env>.key.

Cirro fulfillment payloads are NOT encrypted by OMS. Encryption happens in Camel when the message is published to Service Bus.

Gotchas

  1. Recurring schedule is DB-driven. No schedule.yml. If SFCC sync stops, check /recurring_jobs UI, not configs. recurring_job_scheduled_downtimes can silently disable jobs.
  2. Two separate "Cirro confirmations." Acceptance (ASB events → ConfirmFulfillmentJobEXPORTED_TO_TPL) vs Shipping (REST fulfill_ordersSHIPPED). Camel has endpoints for both — don't route the wrong one.
  3. Printable orders silently skip Cirro without batch_reference. Cirro::ExportFulfillmentRequestJob guard: active_printable_items? && batch_reference.blank? → return. No error, no audit — just a log line. If an order never hits Cirro, check batching first.
  4. Status transitions are string-based. No state machine guards — nothing stops an impossible transition. Always check current status before calling order.order_status.update!(status: ...).
  5. OrderStatus#update! auto-creates an audit. after_save → OrderAudit.create_or_update_audit. Don't build parallel audit logging in jobs — you'll duplicate.
  6. NAV release comes via Camel, not direct from NAV. Path: NAV → ASB nav-order-releasecm-order-prccm-osor-sysPATCH /api/v3/osor_order/status/:order_id. If NAV "isn't releasing," check Camel logs before blaming NAV or OMS.
  7. Nav::CustomerCard has no SFCC entry. SFCC orders bypass the mapping table and use customer.platform_id[0..19] as the NAV billing number. OCAPI test orders frequently miss customer_id in the payload — that breaks this.
  8. Shopify vs SFCC payment capture are not symmetric. SFCC = one atomic call embedded in FulfillOrder. Shopify = separate CapturePaymentJob after platform fulfill.
  9. Datadog may index ERROR logs at status: info. Logstash JSON encoder quirk. Search by message content, not the status: field.
  10. MySQL is IP-restricted to AKS. You cannot query prod DB from your laptop even on VPN. Use the single persistent oms-console tmux session on a prod box. rails runner and new consoles are banned on remote servers.
  11. Order names have prefixes. SFCC 00XXXX, Amazon uses PO directly. Non-prod test orders prepend DEV_ or OMS_. Strip cautiously.
  12. Store.url is the join key for recurring jobs, configs, and credentials. Changing it silently orphans everything.
  13. STROOPWAFEL_EXCEPTION is a real status, not a joke. Orders caught mid-lifecycle during a past refactor land here.
  14. ASB client is custom TCP sockets. Azure::ServiceBus::Client#raw_https_post hand-rolls HTTPS. If timeouts happen, look at socket-level handling, not SDK config.
  15. order_business_type: 1 = "warehouse only — PopSockets ships, not Cirro." Used for GlobalE and Amazon. Not an internal category — it drives who physically handles shipment.
  16. Bulk actions are Sidekiq jobs, not inline. 500 orders cancelled in the UI = 500 BulkActionJobs.

What OMS Doesn't Do

  • Storefront checkout, cart, or payment authorization — SFCC/Shopify. OMS receives the finalized order.
  • Inventory allocation — NAV. OMS asks and waits.
  • Printing or label generation — PrintStation/BatchStation own printables; Cirro owns fulfillment labels (Amazon labels come from Amazon).
  • Packing and warehouse ops — Cirro, or internal PopSockets warehouse for order_business_type=1.
  • Carrier integration — Cirro picks carrier + rates.
  • Customer notifications (email/SMS) — Storefronts + Klaviyo. OMS only emits specific Klaviyo events.
  • CS ticketing — Not integrated with Gladly or Zendesk.
  • Analytics / BI — Separate ETL team replicates the DB.
  • Returns / RMA — Refunds are handled (CS UI + platform webhooks) but RMA workflow isn't here.
  • Product catalog management — Arena is the master; OMS syncs a slice.
  • Tax calculation — Storefronts calculate; OMS records tax_lines.
  • Pricing / promotions — Storefronts own it; OMS records discount codes.
  • Supply chain / purchasing — Not here.
  • Amazon listing management — SP-API is read-only for orders + labels + acks.