Integration & ingestion

Commerce integration is built from source-agnostic event ingestion and CMS → Shopify bidirectional sync. Every event is logged with its raw payload, so any event can be replayed.

Event ingestion

backend adapter (verify signature, build the envelope)
  → direct:commerce-ingest
     ├─ logEvent (record the raw payload, status=received)
     ├─ by topic: a dedicated route (order-paid, …)
     └─ otherwise: normalizeEvent → /content/commerce/entities/{source}/{collection}/{id}.json
  → markEvent (processed / error)
  • all-topics ingestion: every webhook is accepted (no allow-list); topics without a dedicated workflow are normalized into entities
  • multi-backend: if an adapter sends the same envelope (event_source / event_topic / event_id / received_at, body = raw payload) to direct:commerce-ingest, nothing downstream needs to change
  • config: /etc/commerce/config/ingest.yml

Replay

  • automatic: the commerce-replay timer (every 5 min by default) re-runs error events up to replay.maxAttempts (with backoffMinutes); processed events are pruned by retentionDays
  • manual: POST …/endpoints/events.groovy with a single event ({source, eventId}) or a filter ({status, topic, source, sinceDays}); returns {matched, replayed}
  • mechanism: the envelope is re-dispatched with replay=true — the webhook idempotency guard is bypassed while per-workflow guards still prevent duplicates

Read: GET …/endpoints/events.groovy?status=error&limit=100. UI: Commerce Operations → Events.

Bidirectional sync (CMS → Shopify)

GET  …/endpoints/sync.groovy   # capability / status
POST …/endpoints/sync.groovy   # perform an action
Action Purpose Shopify mutation
inventory set a location's available quantity inventorySetQuantities
price set a variant price productVariantsBulkUpdate
publish publish/unpublish (ACTIVE/DRAFT) productUpdate
metafields upsert metafields metafieldsSet
{ "action": "inventory", "inventoryItemId": 123, "locationId": 456, "quantity": 10 }
{ "action": "price", "productId": 1, "variantId": 2, "price": "19.99", "dryRun": true }
  • gated by adminApi.enabled (shopify.yml); when disabled it returns 409 and never calls Shopify
  • dryRun: true validates and echoes the target and values without calling Shopify
  • every attempt (including dry runs and failures) is audited to /content/commerce/sync/{yyyy}/{MM}/sync_{ts}.json
  • implemented in commerce.ShopifyWrite (on commerce.ShopifyAdmin); reconciliation's auto-heal uses the same write primitives

UI: Commerce Operations → Sync (a dry-run toggle and a recent-writes table).

Commerce i18n

Commerce messages live in /etc/i18n/commerce.<locale>.json and each app's <appId>.<locale>.json, merged per locale. Keys are app.<appId>.* (shared ones are common.*); the format is ICU. Keep en and ja in sync, and write one sentence per key rather than concatenating fragments.

Currency and locale follow the user's effective settings (Preferences → Localization), with store.json's currency as the fallback. For platform-wide i18n, see "Localization & i18n".