DESIGN.md: Hotel8:POS — Detailed Design Document
1. Scope & Context
Owned by POS: database hotel8, schema pos — 14 tables (pos.checks,
pos.orders, pos.menu_items, pos.menu_categories, pos.modifiers,
pos.modifier_groups, pos.payments, pos.tables, pos.service_areas,
pos.printer_groups, pos.revenue_centers, pos.tax_groups, pos.outlets,
- producer tables
pos.outbox/pos.audit_journal); event familypos; RFW admin pages; the check/order lifecycle machines.
NOT owned: room inventory and rates (hotel8:pms), folio settlement (hotel8:pms cashiering) beyond the room-charge hand-off, money movement (Bill8:Ledger via balance-transactions), card capture (Bill8:Payments), identity/authorization decisions (Sentin8:Auth + Shield), email (Sentin8:Post).
System context
Container view
2. Runtime Architecture
Shell-owned boot supplies pool, Shield engine, relay, journal pusher and the
PQ listener; the crate contributes registration, state, routes and one direct
consumer task (event_consumer::spawn) for the PMS check-in event.
Request lifecycle — pay a check
Request lifecycle — PMS check-in consumed
3. Data Model
- All tables env-scoped, FORCE-RLS to the standard predicate; audit journal + outbox included.
- Money columns carry decimal amounts with the property currency from PMS settings; no currency state is invented here.
- Terminal/register identifiers ride Shield ABAC attributes rather than columns (tenant-authored).
State machines
4. API Surface
Public (Shield-gated, hotel8:pos:*:{read,write,admin} scope-mirror):
- Outlets, service areas, tables (combined/separated/status), revenue centers.
- Menu categories/items, modifier groups/modifiers, tax groups, printer groups.
- Checks: open, hold, recall, split, merge, transfer, void (refund flow enforced post-payment).
- Orders: add per seat, void, kitchen routing
(
hotel8:pos:kitchen:order_routed). - Payments: post (incl. room charge to PMS folio), split, request; payment-method configuration.
- Discounts; manager approvals for overrides (ABAC-backed).
- Package redemption (redeem hotel8:loyalty rewards at the table).
- Reports incl.
report:eod_generated+ export request; offline terminal sync (hotel8:pos:offline:synced). - Guest ordering (room service) — guest-scoped routes the eGuest app
calls through the BFF:
GET /v1/guest/{property_id}/menu(menu items + categories from the outlet flaggedROOM_SERVICE),POST /v1/guest/{property_id}/orders(creates aROOM_SERVICEcheck bound to the in-house reservation + room, opens with the ordered lines),GET /v1/guest/{property_id}/orders/{check_id}(status read-back). Guest ordering reuses the same check/order machines — the only differences are the auto-created check attributes (room number + reservation id) and the scope gate (hotel8:pos:guest:write), which the guest session carries via the BFF instead of staff scopes.
Admin/RFU: RFW pages + shared policy admin trio (same construction as PMS).
Cross-service data access — decision order (membus → bus → owner API)
POS consumes PMS state reactively: hotel8:pms:reservation:checked_in on
the pms stream (consumer group pos-table-prep) opens table-prep for
arrivals — the bus path. Synchronous reads (check-in verification before a
room-charge payment, package redemption eligibility) use the PMS owner API
(hotel8::pms::client::H8pmsClient — get_reservation,
search_reservations) with a POS M2M token: read-your-writes/validation
paths that must block the caller before committing, the narrow internal-API
exception. MemBus: none — terminal state is local, nothing POS consumes
ticks fast enough for newest-wins. There is no pms-schema GRANT — a
cross-service table grant is a defect, not an access path (principles §6.6a).
Event flow
5. Cross-Cutting Concerns
- Authentication —
auth::verify_tokenon every route (user + M2M); terminal credentials map to ABAC attributes for location-bound authority. - Tenant resolution + RLS — ingress context headers only; context +
explicit
env_idclauses co-applied on the same connection. - Idempotency — payment submission dedupes on client-supplied key; room
charge idempotency key minted as
hotel8:pos:{transaction_id}toward Bill8:Payments. - Audit journaling — every mutating handler rows
pos.audit_journalin-transaction. - Drain/TLS/pool — identical to the platform contract (shell-owned).
6. Failure Modes & Recovery
- Void-after-payment is refused with the refund path instead of mutating history.
- Relay/journal-pusher fail-open exactly like PMS.
7. Concurrency & Performance Model
- Same single-runtime shape as PMS; pool cap 3.
- Check mutation serializes naturally per check (single-row UPDATEs; state machine pre-SQL).
- Hot path: order-add during service — single INSERT + outbox row.
- Kitchen display consumes the stream; a slow kitchen state never blocks the server path (consumer lag is bounded by the stream, not by POS tasks).
8. Invariants, Insecure-Defaults Rejected, Unsafe Rationale
- A check's payments total never exceeds its total without a refund flow.
- Room-charge settlement is idempotent per transaction id (minted deterministically).
- Void requires the check to be in a voidable state AND the manager-approval authority (ABAC attribute) where configured.
- No
unsafe; no platform-schema access; no secrets stored. - Insecure defaults rejected: no direct PMS tables access on room charge (only the documented API); no unsigned webhook-style money mutations.
9. Testing Strategy
- Unit covers the full lifecycle matrices incl. held/void/refund gates.
- Integration asserts scope-mirror coverage and cross-service deny behavior.
- E2E covers table-side flows; payments instruction verification rides the platform's Payments tests.
- NOT tested here: Shield engine internals, TLS, Auth issuance.
10. Open Questions
- Pre-auth card handling at the table.
- KDS batching beyond per-order routing.
- Gratuity distribution rules.
Design attribution: hotel8:pos and its underlying concepts were designed by Hamid Jahanzad Hamid@Jahanzad.com.