Every server record is a BaseModel<T> envelope — { sk, name, datatype, data, owner, createdate, modifydate } — and the app unwraps data on the client so screens see flat objects. Models exist only for the records the app edits with logic; everything else is handled as raw maps through DataProvider('<datatype>') and RepositoryService.
Models → datatypes
| File | Classes | Datatype / source |
|---|---|---|
base_model.dart | BaseModel<T>, BaseModelDTO<T> | The generic envelope |
user_model.dart | UserModel | /profile, auth responses |
auth_response.dart | AuthResponse | signin / signup / refresh / passcode responses |
lead_model.dart | LeadModel | lead via /crm/leads/detail |
contact_model.dart | ContactModel | customer |
activity_model.dart | ActivityModel | /crm/activity/* |
message_model.dart | MessageModel, ConversationModel | message, /crm/inbox/conversations |
inbox_message_model.dart | InboxMessageModel, InboxMessageData, InboxConversationModel | message plus server-threaded conversations (mirrors the web inbox-conversation-item.tsx) |
chat_message_model.dart | ChatMessageModel, ChatSession | appengine ChatMessageModel; text, image, video, file |
event_model.dart | EventModel, EventData | /events |
operations_reservation_model.dart | OpsReservation, OpsReservationCustomer, ServicePoint | reservation, service_point |
pos_order_model.dart | PosOrder, PosLineItem, PosLineFire, PosProduct, PosProductAttribute, PosProductAttributeOption, PosProductVariation | sf_order, sf_product |
Handled without a model class: task, ticket, bm_location_product, location, setting, social_activity, access_card, workflow records.
POS: sf_order and PosOrder
A POS tab is an sf_order, opened with status: 'new', so web orders and counter sales share one shape and reporting sees one model.
| Field | Notes |
|---|---|
businessLocationId | The location slug |
servicePointId | The service point's name, filtered by location |
alias | Free-form floor handle ("T7", "Smith — 4"); auto-numbered "Walk-up #N" when omitted |
number | Unique, read-only human invoice number; settle refuses a tab without one |
status | new, awaiting-payment, paid-partial, paid, confirmed, processing, shipped, partially-shipped, delivered, partially-delivered, completed, failed, returned, refunded, cancelled |
productItems[] | OrderItemSchema: id (stable per line, used for fire bookkeeping), sku, name, quantity, unitPrice, amount, discount, itemType (product, rental, fee, addon), options[{name, value}] — prepStation rides here |
subtotal, discount, tax, total, amountPaid | Server-owned; re-priced on every item change and every settle. Open tabs carry subtotal; total is stamped at settle |
payments[] | {amount, gateway, ref, method, date, transactionId} |
firedTaskIds[] | Mirrors sf-order.ts:189-207; which lines went to which prep task |
The server has returned money fields as strings (amountPaid: "5368" on a live order). Every money and quantity field in PosOrder and PosLineItem goes through a tolerant _num() parser. Before that, one bad record threw on as num and emptied the entire open-tabs list. Cards display displayTotal — total, else subtotal, else the line sum — because open tabs have no total yet.
PosProduct is built from sf_product: sku, price, taxable (drives the tax base), stock, attributes[] (option groups with priceDelta), variations[] (matched on every option group), image. Category chips come from /storefront/pos-categories.
service_point
| Field | Notes |
|---|---|
name | Unique key; orders and check-in reference points by name |
displayName, kind | kind is a free string in practice: table, chair, bay, room, seat, section |
parentName, level, sortOrder | Hierarchy: floor → section → table → seat |
capacity, minCapacity, maxCapacity | |
x, y, width, height, rotation, color, shape | Floor-plan geometry used by FloorPlanScreen |
bookable, isActive | Both default true |
status | SDK enum available, occupied, reserved, dirty, blocked, closed; the storefront status endpoint writes it as a free string |
currentReservationId, currentTabId, currentServerId, currentPartySize, seatedAt, lastStatusChangeAt, lastStatusReason | Live occupancy, owned by the tab and check-in flows |
The Dart ServicePoint echoes currentTabId, lastStatusChangeAt, lastStatusReason and version in toCreateBody() so an edit from the form does not erase the table-to-tab link.
reservation and reservation_definition
OpsReservation (datatype: 'reservation'):
| Field | Notes |
|---|---|
status | Read-only on the server: new, pending, confirmed, used, rescheduled, cancelled, no_show |
reservationDefinitionId, definition, service | Service is the definition's service name |
targetType | service_point, pipeline, event, interval |
servicePointId, workflowName, currentStageId | |
source | walk-in, phone, online, partner, kiosk, agent, email |
customer{name, email, phone}, partySize, notes | |
startTime, endTime, timezone | Slot bookings store a UTC instant; older records a zone-less wall clock. The row formatter converts only values that carry a zone |
checkedInAt, checkInTaskId | Stamped by the server when the booking is handed to the check-in queue |
businessLocationId | Often empty on older bookings; the operator's picker location wins at check-in |
toCreateBody() wraps the record as {datatype, data} for updates; toCreatePayload() sends the flat fields to POST /crm/reservations/create, which builds the record server-side and rejects a pre-wrapped body.
reservation_definition fields the app reads: name, title, type (service, interval, event, service_point, pipeline), services[] {name, duration, price, breakAfter}, workDays[], officeHours[] with timezone, blockedTime[], spots, checkInBy, bookingType (unassigned, round-robin), notifications[] {offset, unit, channel}, notificationTemplate, cancellationTemplate, form. When a definition has no services, the app synthesizes {name: definition name, duration: definition.duration ?? 30} — the same implicit service the server uses.
task payloads
Workflow tasks share one schema (status: new, pending, inprogress, blocked, done, rejected, approved, canceled; stageId, dueDate, escalationTier, workflowName, history[]). What differs is payload:
| Workflow | Payload |
|---|---|
reservation-checkin-pipeline (stage 0 Waiting, stage 1 Assigned) | customer{name, email, phone}, reservationId, servicePointId, partySize, preferences, businessLocationId, source, arrivalTime |
prep-pipeline (Received → Prepping → Ready → Delivered) | orderId, orderNumber, alias, businessLocationId, servicePointId, course, seat, specialInstructions, items[], itemRefs[] |
Queue reads come back with an enrich block: waitedMs, isOverdue, queuePosition (1-indexed by createdate), displayName.
CRM records
| Datatype | Fields the app uses |
|---|---|
crm_lead (LeadModel) | fullName/firstName/lastName, email, phone, company, pipelineId, stageId, source, status (new, contacted, qualified, unqualified, disqualified, converted, lost, nurturing, follow_up), priority, temperature (cold, warm, hot), value, score, assignedTo |
customer (ContactModel) | email (identity key), firstName, lastName, phone, company, jobTitle, address, groups[], leadStatus, leadStage |
crm_ticket | status (new, open, pending, inprogress, blocked, done, canceled), priority (low…urgent), severity, channel, reportedBy, title, description, assignTo, resolution; replies are customer-facing, comments are internal |
message (InboxMessageModel) | Channel-tagged messages; inbox channels email, sms, whatsapp, internal, website; AI and chat types are filtered out |
chat (ChatMessageModel, ChatSession) | Socket messages with chatId, sender, type, status, AI stream segments |
About bm_location_product
MenuManagementScreen and the POS read and write a per-location overlay named bm_location_product: eightySixed plus eightySixReason, priceOverride, and menuIds for day-parts. It behaves like any repository collection, but there is no schema for it in the SDK and no server code path reads it. Treat it as a mobile-side collection that the SDK has not published yet; the demo org has no records, which is why day-part chips show every product there.