Skip to content

iSeller — order_dispatch

Part of the iSeller adapter guide — see it for authentication, endpoints, pagination, rate limits and shared quirks.

Contract: transformer output. Runs as the provider transformer on the two-tier pipeline (ADR 0002) — owns URL/method/ headers/body and auth.

Endpoint: POST {baseUrl}/api/v4/CreateOrder (ERP API v4 — "Support Online Ordering"), header Authorization: Bearer {access_token} (same OAuth token).

Outlet code resolution

The iSeller outlet_code (string, e.g. "MO") is sent as CreateOrder.outlet_code. Its authoritative source is the Cata store's ref_code, set from GetOutlets during list_remote_outlets (the outlet external_id). The pipeline surfaces it to the transformer as input.outlet.refCode, so no per-outlet setting is required.

  • input.outlet.refCode — preferred. Loaded from the store row by the dispatch pipeline (buildTransformerInput); no drift, single source of truth.
  • outlet_providers.settings.outletCodedeprecated fallback for older mappings that set the code by hand. Used only when refCode is empty.

Cata order → CreateOrder body

iSeller field Source / value
order_reference Cata orderRefNo
channel "web" (Cata = online-ordering channel)
online_order_state "new"
order_type map deliveryMethodtake_away / pick_up / delivery
order_date createdAt (ISO-8601)
pickup_time expectedPickupAt (required when order_type=pick_up)
status "completed"
currency "IDR"
total_amount totalPay (whole IDR); tax_inclusive / tax_amount from tax
outlet_code settings.outletCode
order_details[] per item (below)
transactions[] [{ payment_type_name, type:"Sale", amount: totalPay, transaction_date }]
customer { first_name, last_name, phone_number, email, external_id? }

OrderDetail (per item): sku (= Cata plu), type (standard/variant/composite/comboset), product_name, quantity, base_price (qty=1, whole IDR), total_price, notes, modifiers[] ({sku,name,group,quantity,price}), combosets[] ({sku,quantity,base_price,price,category,modifiers}) when comboset.

Response → reconcile

{ "order_id": "e47696a0-…", "error_message": null, "status": true, "time": "…" }
  • External order id = order_id — persist on the dispatch row (the webhook's order_id reconciles back here).
  • iSeller returns HTTP 200 with status:false on a body-level error → treat as a failed dispatch (matches Cata's "2xx body indicated error" handling).

Implemented

  • Script: scripts/adapters/iseller/order_dispatch.js (v1.0.0). Mints a Client-Credentials bearer token per run (same as the other iSeller scripts), resolves the base URL from the token's resource_url (falling back to input.baseUrl, then input.standard.url for Hookdeck testing), and returns the CreateOrder spec with Authorization: Bearer {token}. The Go pipeline performs the POST.
  • Outlet code reaches the transformer: the pipeline loads the store's ref_code by DB ID and surfaces it as input.outlet.refCode (buildTransformerInput in internal/connectors/dispatch_pipeline.go, via a narrow StoreReader). The script reads refCode first, falling back to settings.outletCode. ref_code stays the single source of truth so the dispatched outlet_code can't drift from what list_remote_outlets synced.
  • Credentials reach the transformer: unlike header-auth providers (Revel, which reads outlet.settings), iSeller needs the tenant OAuth credentials to mint a token. The dispatch pipeline now surfaces the provider_connections apiKey/apiSecret/baseUrl at the top level of the transformer input (buildTransformerInput in internal/connectors/dispatch_pipeline.go), mirroring the universal adapter-script input the other topics receive.
  • Discount (single combined object): iSeller reconciles total_amount against Σ line totals − discount.amount; a discounted total with no matching discount is rejected (final amount … not match with total items amount …). iSeller's order-level discount is a single Object — empirically a discount array is ignored (total not reduced), and the promotion object is not deducted (it expects a real server-side promotion record), so promotion stays null. In Cata both "promotion" and loyalty "points" are really discounts, so the transformer sums the structured order.discounts[] breakdown ({type, name, amount, externalId}) into one discount object (amount = Σ, name = joined entry names, external_id = first entry's). When discounts[] is absent it falls back to the aggregate discountTotal. external_id is required by iSeller (rejected if blank despite the spec marking it optional).
  • Fees (provisional, line-item): iSeller v4 has no fee/service-charge field, and total_amount reconciles against the order_details totals — so a fee baked into the total with no representation is rejected. The transformer emits each fee as a standard order_detail line so it adds to the item total: order.serviceCharge uses settings.serviceChargeSku, order.deliveryFee uses settings.deliveryFeeSku, order.packagingFee uses settings.packagingFeeSku, and each order.additionalFees[] uses settings.feeSkus[feeCode] (falling back to the Cata feeCode). Each fee SKU must be a real iSeller fee product — a fee with no resolvable SKU is skipped (and the total then won't reconcile). Flash Coffee fee products: SERVICEFEE (Service Fee), DELIVERYFEE (Delivery Fee), PACKAGINGFEE (Packaging Fee).
  • Combosets / bundles — decomposed by default (settings.bundleStrategy): iSeller rejects the bundle SKU (BDL001) for every order_detail.typecomboset/compositeunsupported type, standardtype not match — so the default bundleStrategy:"decompose" drops the bundle SKU and emits each component as its own standard order_detail line (the TC-001/TC-004 shape iSeller accepts). The bundle line total is distributed across the components (each carries its surcharge; the remainder rides the first line) so total_amount reconciles. Each component line's notes is stamped Bundle: <name> so the bundle origin is visible on every line (iSeller has no native bundle-grouping field for online orders). ✅ Sandbox-validated 2026-06-26 (dispatched true, real externalOrderId); iSeller accepts the resulting Rp 0 component line, so no even-distribution fallback is needed. The sample-shaped wrapper (bundleStrategy:"comboset": one type:"standard" line with combosets[], component price field comboset_price, nested modifiers modifier_name/modifier_group — per iSeller's v4 "Sample Usage (Comboset)", which overrides the parameter table for this path) is retained as a fallback for when iSeller confirms a working bundle wrapper.
  • Tip — not supported by iSeller (dropped): iSeller's Transaction object has no tip field, so the tip is not sent. Cata's totalPay includes the tip, so the transformer sends total_amount = transaction.amount = totalPay − tip (the pre-tip total) — otherwise it would over-state the total and fail reconciliation against the (tip-less) order_details. The tip remains a Cata-side concept; it is not recorded in iSeller.
  • Response reconcile: the Go probe recognizes iSeller's snake_case order_id, and detectBodyError treats boolean status:false + error_message as a failed dispatch.
  • Reference output: matches docs/guides/adapters/samples/iseller/order_dispatch_request.iseller.json.

Sandbox validation & open items

✅ Sandbox-validated 2026-06-17 — full end-to-end pass against the Flash Coffee iSeller sandbox: a Cata PAID order dispatched and iSeller returned order_id (captured as externalOrderId). Gates cleared in this order — a useful checklist when onboarding another outlet (see ADR 0011):

  1. outlet_code resolves (from stores.ref_code) — else outlet not found.
  2. customer pre-registered in iSeller (matched by email) — else customer not found.
  3. line sku exists in the iSeller catalogue — else product SKU … not found.
  4. payment_type_name matches a registered iSeller payment type — else … not found.

⚠️ Open: real payment_type_name pending vendor. Validation used "Cash" only to unblock the pipeline — do not ship it. In iSeller, Cash denotes a not-yet-paid (cash-on-delivery) order, which contradicts the invariant that every Cata order is already paid before dispatch (ADR 0011 §C/D). The production value must be a prepaid / online payment type — awaiting confirmation from iSeller, then set settings.paymentTypeName to it.

⚠️ Open: customer auto-registration. Customers must currently be registered in iSeller out of band (no create step exists). Tracked in docs/guides/todo.md.