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¶
externalId— REQUIRED. Must be the POS system's unique location identifier. This is the idempotency key for upsert.name— REQUIRED. Use the POS location display name. Used for matching in Map mode.timezone— Must be IANA format (e.g.Asia/Singapore, NOTSGTor+08:00).openingHours.dayOfWeek— 0=Sunday through 6=Saturday. Convert from POS format if needed.openingHourstimes — 24-hourHH:MMformat.- Address fields — Map whatever the POS provides. Missing fields are OK (omit, don't send empty strings).
- 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¶
itemCode— REQUIRED on products, modifier groups, AND options. Must be unique within their scope. Use the POS system's native ID/PLU/SKU.- Prices are decimals, NOT cents —
15.50not1550. If POS sends cents, divide by 100. inputType—"SINGLE"(radio/select one) or"MULTIPLE"(checkbox/select many). Determine from POS min/max selection rules.minSelect/maxSelect— Preserve from POS. If POS doesn't specify, default:minSelect=0,maxSelect=1for SINGLE,maxSelect=999for MULTIPLE.isBundle— True only if the product is a combo/meal deal with sub-product choices. PopulatebundleSectionsfor bundles.visible— Defaulttrue. Setfalseonly if POS explicitly marks the product as hidden/disabled.posCategory— Store the POS category name as metadata. NOT used for Cata menu structure.- Flatten nested structures — If POS sends a tree (menu → category → product), ignore the tree. Extract products as a flat list.
- Tax values —
taxOverrideis optional. Only set if POS provides explicit tax rate. Value is a percentage (e.g.10.0for 10%). Usenull(omit) if no override. - 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¶
- Prices — Cata uses decimals. If POS expects cents, multiply by 100 and round.
plu(itemCode) — This is the product identifier synced during SyncProducts. The POS should recognize it.modifierOptionCode— Same itemCode synced during SyncProducts for modifier options.deliveryMethod— MapPICKUP/DELIVERY/EATINto the POS equivalent.currency— Pass through. POS should validate.- Customer data — Map available fields. POS may require different structure.
- Order reference — Use
orderRefNooruuidas the external order reference, depending on POS requirements. - Bundle items — If
subItemsis populated, transform as nested/combo items per POS format. - Additional fees — Map
serviceCharge,deliveryFee,packagingFee,additionalFees[]to POS fee structure. - 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:
- PENDING → ACCEPTED, CANCELLED
- ACCEPTED → PREPARING, CANCELLED
- PREPARING → READY, CANCELLED
- READY → COMPLETED, CANCELLED
- COMPLETED → COMPLETED (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¶
- Map POS statuses to Cata statuses — Each POS uses different status names. The transform must map them to the 6 Cata values above.
- Validate transitions — The transform should output the target Cata status. The service layer validates whether the transition is allowed.
- Unknown POS statuses — Log a warning and skip (do not fail). Some POS systems send intermediate statuses that have no Cata equivalent.
- Order identification — The POS webhook must include the order reference (
orderRefNoorexternalId) so Cata can match it to the original order. - Timestamps — If POS provides a status change timestamp, include it. Otherwise Cata uses
NOW(). - 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.