Skip to content

POS Adapter Transformation Guidelines

Overview

Every POS integration requires 4 transformation scripts that convert between the external POS format and Cata's Standard API. These scripts are generated by an AI Agent and go through a review pipeline before activation.

AI Agent
  ├── Input:  Cata Standard API spec (this document)
  ├── Input:  External POS API docs / sample payloads
  ├── Input:  Guideline Principles (this document)
  ├── Output: JS transformation script
  └── Status: "candidate" → human review → "active"

Runtime: External POS payload → JS engine (goja) executes active script → Cata Standard API format.


The 4 Integration Topics

# Topic Direction Trigger
1 SyncOutlet Inbound (POS → Cata) POS pushes location data or manual pull
2 SyncProducts Inbound (POS → Cata) POS pushes product catalog or manual pull
3 OrderDispatch Outbound (Cata → POS) OrderPaid event from kds-management-service
4 OrderStatusUpdate Inbound (POS → Cata) POS sends webhook with status change

Topic 1: SyncOutlet

Purpose: Transform external POS location data into a Cata outlet.

Two Modes

Mode When Behavior
Import (create) No matching Cata outlet exists Create a new outlet from POS data
Map (link) Outlet with same name already exists Link POS external_id to existing outlet, update fields

Cata Target Format

POST /api/v1/outlets/sync
{
  "externalId": "string (REQUIRED — POS location ID)",
  "name": "string (REQUIRED)",
  "channelLinkId": "string (optional — Deliverect-specific)",
  "address": {
    "street1": "string",
    "street2": "string",
    "city": "string",
    "stateProvince": "string",
    "postalCode": "string",
    "country": "string",
    "latitude": 0.0,
    "longitude": 0.0
  },
  "contact": "string (phone or email)",
  "timezone": "string (IANA, e.g. Asia/Singapore)",
  "currency": "string (ISO 4217, e.g. SGD)",
  "openingHours": [
    {
      "dayOfWeek": 1,
      "openTime": "08:00",
      "closeTime": "22:00"
    }
  ]
}

Transform Rules

  1. externalId — REQUIRED. Must be the POS system's unique location identifier. This is the idempotency key for upsert.
  2. name — REQUIRED. Use the POS location display name. Used for matching in Map mode.
  3. timezone — Must be IANA format (e.g. Asia/Singapore, NOT SGT or +08:00).
  4. openingHours.dayOfWeek — 0=Sunday through 6=Saturday. Convert from POS format if needed.
  5. openingHours times — 24-hour HH:MM format.
  6. Address fields — Map whatever the POS provides. Missing fields are OK (omit, don't send empty strings).
  7. Do NOT fabricate data — if the POS doesn't provide a field, omit it entirely.

Topic 2: SyncProducts

Purpose: Transform external POS product catalog into Cata's flat product structure.

Strategy: Flat Only

  • Pull ALL products from POS as a flat list
  • Map: Product → Modifier Groups → Modifier Options (one level)
  • IGNORE the POS menu tree / categories / sections — Cata owns menu composition via Menu V2 drafts
  • Full-replace semantics: every sync replaces the entire catalog for the outlet

Cata Target Format

POST /api/v1/products/sync
{
  "products": [
    {
      "itemCode": "string (REQUIRED — unique product ID from POS)",
      "name": "string (REQUIRED)",
      "description": "string",
      "basePrice": 15.50,
      "specialPrice": null,
      "visible": true,
      "pickupAvailable": true,
      "deliveryAvailable": true,
      "eatInAvailable": true,
      "isBundle": false,
      "isVariant": false,
      "taxOverride": null,
      "posCategory": "string (metadata only, not used for menu structure)",
      "modifierGroups": [
        {
          "itemCode": "string (REQUIRED — unique modifier group ID)",
          "name": "string (REQUIRED — e.g. Size, Milk Type)",
          "inputType": "SINGLE or MULTIPLE",
          "minSelect": 0,
          "maxSelect": 1,
          "sortNum": 0,
          "options": [
            {
              "itemCode": "string (REQUIRED — unique option ID)",
              "name": "string (REQUIRED — e.g. Large, Oat Milk)",
              "additionalPrice": 1.50,
              "sortNum": 0,
              "asDefault": false,
              "visible": true,
              "taxOverride": null
            }
          ]
        }
      ],
      "bundleSections": []
    }
  ]
}

Transform Rules

  1. itemCode — REQUIRED on products, modifier groups, AND options. Must be unique within their scope. Use the POS system's native ID/PLU/SKU.
  2. Prices are decimals, NOT cents15.50 not 1550. If POS sends cents, divide by 100.
  3. inputType"SINGLE" (radio/select one) or "MULTIPLE" (checkbox/select many). Determine from POS min/max selection rules.
  4. minSelect / maxSelect — Preserve from POS. If POS doesn't specify, default: minSelect=0, maxSelect=1 for SINGLE, maxSelect=999 for MULTIPLE.
  5. isBundle — True only if the product is a combo/meal deal with sub-product choices. Populate bundleSections for bundles.
  6. visible — Default true. Set false only if POS explicitly marks the product as hidden/disabled.
  7. posCategory — Store the POS category name as metadata. NOT used for Cata menu structure.
  8. Flatten nested structures — If POS sends a tree (menu → category → product), ignore the tree. Extract products as a flat list.
  9. Tax valuestaxOverride is optional. Only set if POS provides explicit tax rate. Value is a percentage (e.g. 10.0 for 10%). Use null (omit) if no override.
  10. Do NOT duplicate products — If the same product appears in multiple POS menus/categories, include it once.

Topic 3: OrderDispatch

Purpose: Transform a Cata order into the external POS's order creation format.

Flow

OrderPaid event → Load Cata order → Transform via adapter → POST to external POS API

Cata Source Format (input to transform)

{
  "uuid": "order-uuid",
  "orderRefNo": "ORD-001",
  "dailyQueueNo": "A001",
  "storeUuid": "store-uuid",
  "storeName": "Main Street Store",
  "deliveryMethod": "PICKUP | DELIVERY | EATIN",
  "status": "PAID",
  "currency": "SGD",
  "isPreorder": false,
  "customer": {
    "fullName": "John Doe",
    "email": "john@example.com",
    "phone": "+6591234567",
    "address": "123 Main St"
  },
  "items": [
    {
      "itemUuid": "item-uuid",
      "plu": "LATTE-001",
      "name": "Cafe Latte",
      "quantity": 2,
      "itemOnlyPrice": 5.50,
      "modifierOnlyPrice": 1.00,
      "itemPrice": 6.50,
      "itemSubTotal": 13.00,
      "notes": "Extra hot",
      "modifiers": [
        {
          "modifierHeaderId": "mod-group-id",
          "modifierOptionId": "mod-option-id",
          "modifierHeaderName": "Size",
          "modifierOptionCode": "LARGE",
          "modifierOptionName": "Large",
          "price": 1.00,
          "quantity": 1
        }
      ],
      "subItems": []
    }
  ],
  "subtotal": 13.00,
  "discountTotal": 0.00,
  "serviceCharge": 0.00,
  "deliveryFee": 0.00,
  "totalPay": 13.00,
  "payment": {
    "amount": 13.00,
    "method": "CARD"
  },
  "expectedPickupAt": "2026-03-16T15:30:00Z",
  "createdAt": "2026-03-16T15:00:00Z"
}

Transform Rules

  1. Prices — Cata uses decimals. If POS expects cents, multiply by 100 and round.
  2. plu (itemCode) — This is the product identifier synced during SyncProducts. The POS should recognize it.
  3. modifierOptionCode — Same itemCode synced during SyncProducts for modifier options.
  4. deliveryMethod — Map PICKUP/DELIVERY/EATIN to the POS equivalent.
  5. currency — Pass through. POS should validate.
  6. Customer data — Map available fields. POS may require different structure.
  7. Order reference — Use orderRefNo or uuid as the external order reference, depending on POS requirements.
  8. Bundle items — If subItems is populated, transform as nested/combo items per POS format.
  9. Additional fees — Map serviceCharge, deliveryFee, packagingFee, additionalFees[] to POS fee structure.
  10. Timestamps — Cata uses RFC 3339 UTC. Convert to POS timezone/format if needed.

Topic 4: OrderStatusUpdate

Purpose: Transform external POS order status webhooks into Cata's order status format.

Flow

External POS → webhook → Cata webhook endpoint → adapter transforms → update Cata order status

Cata Order Status Values

Status Description
PENDING Order received, awaiting POS acceptance
ACCEPTED POS accepted the order
PREPARING Kitchen is preparing
READY Ready for pickup/delivery
COMPLETED Order fulfilled
CANCELLED Order cancelled

Status Transition Diagram

PENDING → ACCEPTED → PREPARING → READY → COMPLETED
   │          │          │          │
   └──────────┴──────────┴──────────┴──→ CANCELLED

Valid transitions: - PENDINGACCEPTED, CANCELLED - ACCEPTEDPREPARING, CANCELLED - PREPARINGREADY, CANCELLED - READYCOMPLETED, CANCELLED - COMPLETEDCOMPLETED (special case: ACK only, see below)

Special Case: COMPLETED → COMPLETED

Cata has an auto-complete feature: orders are automatically marked COMPLETED after X minutes. If the POS sends a COMPLETED status update after Cata already auto-completed, bypass validation and ACK — do not reject as an invalid transition.

Transform Rules

  1. Map POS statuses to Cata statuses — Each POS uses different status names. The transform must map them to the 6 Cata values above.
  2. Validate transitions — The transform should output the target Cata status. The service layer validates whether the transition is allowed.
  3. Unknown POS statuses — Log a warning and skip (do not fail). Some POS systems send intermediate statuses that have no Cata equivalent.
  4. Order identification — The POS webhook must include the order reference (orderRefNo or externalId) so Cata can match it to the original order.
  5. Timestamps — If POS provides a status change timestamp, include it. Otherwise Cata uses NOW().
  6. Cancellation reasons — If POS provides a reason, pass it through as metadata.

Cross-POS Complexity

These differences MUST be handled by each adapter — they cannot be abstracted away generically.

Pricing

POS Behavior
Revel Same PLU can have different pricing per establishment
Lightspeed Same PLU can only have different pricing via Order Profiles — cannot customize externally
Deliverect + Lightspeed Pricing must be changed in Lightspeed directly, not through Deliverect

Product Structure

POS Modifiers Bundle Handling
Revel Native modifier groups + options Standard bundles
Lightspeed No Modifiers — all items treated as Items. Modifiers must be created as Combos.
Deliverect Complex nested structure (categories → subcategories → products → modifiers)

Bundle-only items: Items that exist only inside a bundle should NOT be required as standalone products in a menu section. The adapter must distinguish between standalone items and bundle-only items.

Fee Handling

POS Fee Structure
Deliverect Separate fee fields in order payload
Revel Own fee structure (different from Deliverect)
Lightspeed Fees treated as Items with open price (positive or negative)

Adapters must map Cata's fee fields (serviceCharge, deliveryFee, packagingFee, additionalFees[]) to each POS's native fee handling.


General Principles for AI Agent Script Generation

DO

  • Use the POS system's official API documentation as the source of truth for field names and formats
  • Preserve all data — transform structure, don't discard fields
  • Handle missing/null fields gracefully — omit from output rather than sending empty strings or zeros
  • Add comments in the JS script explaining non-obvious mappings
  • Include the POS API version the script was generated for
  • Test with real sample payloads from the POS system

DON'T

  • Don't fabricate data — if a field isn't in the source, don't invent a value
  • Don't hardcode tenant-specific values — scripts must work for any tenant using that POS
  • Don't convert currencies — pass through as-is, Cata handles currency display
  • Don't assume field presence — always check before accessing nested fields
  • Don't modify itemCodes — they must match exactly between SyncProducts and OrderDispatch

Script Metadata

Every generated script must include:

/**
 * @provider   deliverect
 * @topic      SyncProducts
 * @version    1.0.0
 * @generated  2026-03-16
 * @status     candidate
 * @apiVersion Deliverect API v2
 */

Status Lifecycle

candidate → (human review) → active
candidate → (rejected) → archived
active → (new version generated) → deprecated → archived

Only one active script per provider per topic at any time.