Primitive spec

Hober

The live-order primitive. Hober holds the runtime state of a sale-in-progress — the dine-in tab, the retail cart, the takeout ticket — from open through settle to close, when it becomes an immutable Mallow Invoice.

Status: Entity model and lifecycle designed; controllers, entities, and event subscribers exist for Order, OrderLine, OrderLineModifier, OrderLineCompositeFill, OrderPayment, and OrderEvent. KDS workflow integration (split, merge, transfer, coursing) and the Mallow Invoice generation path are the next slices.

What it owns

Hober owns mutable runtime state. Mallow owns the finalised ledger — the Invoice that the Order becomes on close. They are deliberately different entities: different audiences (KDS reads Order, finance reads Invoice), different lifecycles (Order mutable, Invoice immutable), different timing (Order exists from open to close, Invoice exists from close forever).

On close, Hober triggers: Mallow Invoice generation, Trantor inventory consume per RecipeIngredient, Payments transaction reconciliation, and a Speaker receipt — all in one transaction. Any failure rolls back the close.

Concepts

Order
The live tab. Carries orderType (dine_in, takeout, delivery, retail_quick_sale, online_pickup, online_delivery), optional table ref (a Trantor DiscreteResource), party size, server, customer, and a denormalised total cache.
OrderLine
One item on the order. Snapshots unitPriceCents, displayName, and kitchenName at add-time, locking the price the customer saw. Status moves pending → fired → ready → served, with branches for cancelled, comped, and returned.
OrderLineModifier & CompositeFill
Modifiers attach to a line with a snapshotted price delta. Combo slots create sub-lines (parentLineId) rather than embedded data — KDS routes naturally, modifiers on the side work via the normal modifier table.
OrderPayment
An applied tender. Multiple per order for split-tender. Carries the tip (tip lives on the payment, not the order, so split tenders each carry their own tip). Optional appliedToLineIds for split-by-item.
Lifecycle
OPEN → CLOSING → CLOSED, with VOIDED as the abandon branch. CLOSING blocks new lines while payment is in flight. CLOSED is terminal — voids, comps, and returns on closed orders become status changes on lines or new compensating orders, never deletes.
Table state
A dine-in table is held "until the tab closes," not "for a 90-minute window." Modelled as a state flag on the Trantor Resource with the Order id in linkRef, not as a ResourceHold. Different physics, different storage.
Snapshot at add-time
Every line, modifier, and composite fill captures priceCents and display names at the moment it was added. Catalog changes mid-order never move the goalposts on what the customer agreed to pay.

API surface

Endpoints are versioned under /order/v1/ — the wire namespace uses the descriptive name even though the primitive is "Hober," to match how POS UIs and integrations talk about orders.

MethodPathPurpose
POST/order/v1/ordersOpen an Order.
GET / PATCH/order/v1/orders/{id}Fetch or modify metadata (party size, table, course pacing).
POST / PATCH / DELETE/order/v1/orders/{id}/linesAdd, modify, or void OrderLines (voids keep the audit row).
POST/order/v1/orders/{id}/fireFire pending lines. Daneel routes them to stations.
POST/order/v1/orders/{id}/transferTransfer server (audit only).
POST/order/v1/orders/{id}/splitMove lines into N new child orders.
POST/order/v1/orders/{id}/mergeMove lines from N source orders into this one; sources void.
POST/order/v1/orders/{id}/paymentsApply a tender.
POST/order/v1/orders/{id}/closeFinalise. Generates Mallow Invoice, consumes Trantor inventory, fires Speaker receipt.
POST/order/v1/orders/{id}/voidAdmin abandon. No Invoice generated.
GET / PATCH/order/v1/lines?station=&status=KDS read path. Filter by station, status. Bump / recall / serve via line status PATCH.

Example: open, add, fire, pay, close

POST /order/v1/orders
{
  "orderType":  "dine_in",
  "tableId":    "res_table_07",
  "serverId":   "per_alice…",
  "partySize":  2
}
→ 201 Created  Order  ord_01JAZB…
POST /order/v1/orders/ord_01JAZB…/lines
{
  "productVariantId": "pvar_burger_single",
  "quantity": 2,
  "modifiers": [
    { "modifierId": "mod_medium_rare" },
    { "modifierId": "mod_add_bacon" }
  ]
}
POST /order/v1/orders/ord_01JAZB…/fire
POST /order/v1/orders/ord_01JAZB…/payments
{ "tenderType": "card", "amountCents": 3500, "tipCents": 500,
  "paymentMethodId": "pm_visa_4242" }
POST /order/v1/orders/ord_01JAZB…/close
→ 200 OK  Invoice  in_01JAZC…   (Mallow)

How it fits with the rest

flowchart LR
  Server[Server / customer] --> Ho(Hober Order)
  Ho -. catalog .-> Ha[Hardin]
  Ho -. table / inventory .-> T[Trantor]
  Ho -. identity .-> Te[Terminus]
  Ho -- close --> M[Mallow Invoice]
  Ho -- tender --> Pa[Payments]
  Ho -- receipt --> Sp[Speaker]
  Ho -- KDS --> D[Daneel]
            

Hardin is the catalog that lines reference and snapshot from. Trantor provides the table (as DiscreteResource state) and the inventory that close-time consume decrements. Terminus is the identity ref for server and customer. Payments processes each tender via an OrderPayment. Mallow generates the Invoice on close. Daneel runs the kitchen routing, coursing, and comp-approval workflows wired to OrderLine status events. Speaker sends the receipt, gated by Terminus consent.