Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.laportenard.com/llms.txt

Use this file to discover all available pages before exploring further.

Package

nu_pos_sync_protocol/ — Shared TypeScript package consumed by the hub (@nu/sync-protocol).
nu_pos_sync_protocol/src/
├── messages.ts       # TerminalMessage | HubMessage discriminated unions
├── snapshots.ts      # OrderSnapshot, TableStatus types
├── constants.ts      # Timing constants
├── validators.ts     # Zod schemas + parse helpers
└── index.ts          # Re-exports

Message types

All WebSocket communication uses JSON-encoded messages with a type discriminator field.

Terminal to hub (13 message types)

TypePurposeKey fields
AUTHAuthenticate on connecttoken, deviceId
LEASE_ACQUIRERequest exclusive edit rightsposReference, baseVersion, force?
LEASE_RELEASERelease edit rightsposReference, finalSnapshot?
LEASE_HEARTBEATKeep lease aliveposReference
ORDER_SNAPSHOTSend full order stateposReference, baseVersion, snapshot
ORDER_FINALIZEFinalize order through hubposReference, snapshot, configId
TABLE_UPDATEUpdate table statustableId, status, posReference?
TABLE_LOCK_ACQUIRERequest table locktableId
TABLE_LOCK_RELEASERelease table locktableId
TABLE_LOCK_HEARTBEATKeep table lock alivetableId
TABLE_LOCK_FORCE_ACQUIREManager table lock overridetableId
TICKET_NUMBER_REQUESTRequest sequential ticket numberorderType ("takeout" or "delivery")
FLOOR_PLAN_UPDATEBroadcast floor plan changesfloorId, tables, layoutMode?

Hub to terminal (19 message types)

TypePurposeKey fields
AUTH_OKAuthentication successterminalId, role
AUTH_FAILAuthentication failurereason
LEASE_GRANTEDLease acquiredposReference, version, expiresAt
LEASE_DENIEDLease rejectedposReference, holder, reason
LEASE_REVOKEDLease taken awayposReference, reason, snapshot?
ORDER_UPDATEDOrder changed by another terminalposReference, version, snapshot, sourceTerminal
ORDER_FINALIZEDOrder finalization successposReference, orderId, lineIdMap, fiscal?
ORDER_FINALIZE_ERROROrder finalization failureposReference, error, retryable
SYNC_INITFull order state on connectorders: [{ posReference, version, snapshot }]
TABLE_UPDATEDTable status changedtableId, status, posReference?
CONFLICTVersion conflict detectedposReference, hubVersion, hubSnapshot
SYNC_STATUSPeriodic hub health broadcastconnectedTerminals, pendingForward, lastOdooSync
TABLE_LOCK_GRANTEDTable lock acquiredtableId
TABLE_LOCK_DENIEDTable lock rejectedtableId, lockedBy: { userId, userName }
TABLE_LOCK_RELEASEDTable lock releasedtableId
TABLE_LOCK_REVOKEDTable lock taken awaytableId, reason
TABLE_LOCKS_STATEFull table lock snapshotlocks: [{ tableId, terminalId, userId, userName }]
TICKET_NUMBER_RESPONSETicket number assignedticketNumber, orderType
TICKET_NUMBER_ERRORTicket number failurereason, orderType?
FLOOR_PLAN_CHANGEDFloor plan changed by another terminalfloorId, tables, layoutMode?, sourceTerminal

OrderSnapshot

The canonical representation of an order’s state, transmitted in ORDER_SNAPSHOT, ORDER_UPDATED, CONFLICT, and LEASE_REVOKED messages.
interface OrderSnapshot {
  posReference: string;          // Required — idempotency key
  lines: OrderLineSnapshot[];    // Order lines
  totals: OrderTotalsSnapshot;   // { subtotal, tax, total }
  updatedAt: string;             // ISO timestamp

  // Optional
  id?: string;                   // Client-side UUID
  odooId?: number;               // Odoo pos.order ID (set after first sync)
  lineIdMap?: Record<string, number>;  // Client line ID → Odoo line ID
  partnerId?: string | null;
  tableId?: number;
  status?: "open" | "sent" | "changed" | "checkout" | "paid" | "closed";
  notes?: string;
  paymentLines?: PaymentLineSnapshot[];
  amountPaid?: number;
  amountReturn?: number;
  terminalId?: string;

  // Order metadata
  guestCount?: number;
  alias?: string;               // Check alias (e.g. "Bar", "Patio")
  staffName?: string;           // Staff member name
  staffUserId?: string;         // Staff member user ID
  createdAt?: string;           // ISO timestamp — order creation time
  orderType?: "dine_in" | "takeout" | "delivery";
  ticketNumber?: string;        // Sequential ticket number for takeout/delivery
  fiscalPositionId?: number | null;

  // Check-level discount audit
  checkDiscount?: {
    presetId?: number;
    presetName?: string;
    type: "percent" | "amount";
    originalValue: number;
    discountPct: number;
    appliedByUserId: string;
    appliedByName: string;
    approvedByUserId?: string;
    approvedByName?: string;
  };
}

OrderLineSnapshot

interface OrderLineSnapshot {
  id: string;              // Client-generated UUID
  productId: string;
  quantity: number;
  unitPrice: number;
  total: number;
  discount?: number;
  taxIds?: string[];
  totalExcluded?: number;
  totalIncluded?: number;
  taxAmount?: number;
  note?: string;            // Modifier text (side dishes, etc.)
  specialRequest?: string;  // Special request tags

  // Void tracking
  isVoided?: boolean;
  voidReasonId?: number;
  voidedQty?: number;

  // Kitchen provenance
  kitchenSentAt?: string;    // ISO — when item was first sent to kitchen
  sentQuantity?: number;     // Qty sent (detects post-send quantity increases)
  kitchenTicketId?: string;  // Original order ID (split check provenance)

  // Per-line user attribution
  addedByUserId?: string;
  addedByName?: string;

  // Price override
  priceOverride?: number;

  // Discount audit trail
  discountState?: {
    presetId?: number;
    presetName?: string;
    type: "percent" | "amount";
    originalValue: number;
    discountPct: number;
    appliedByUserId: string;
    appliedByName: string;
    approvedByUserId?: string;
    approvedByName?: string;
  };

  // Price override audit trail
  priceOverrideState?: {
    originalPrice: number;
    overriddenByUserId: string;
    overriddenByName: string;
    approvedByUserId?: string;
    approvedByName?: string;
  };
}
Voided lines (isVoided: true) are included in snapshots for audit purposes but filtered out before forwarding to Odoo.

FiscalData

Returned in ORDER_FINALIZED messages for Dominican tax (NCF/DGII) integration:
interface FiscalData {
  ncf?: string;
  ncfCreationDate?: string;
  ncfAffected?: string;
  fiscalPositionName?: string;
  ncfExpiration?: string;
  stampUrl?: string;
  securityCode?: string;
  validationDate?: string;
  customerName?: string;
  customerRnc?: string;
}

Protocol constants

Defined in nu_pos_sync_protocol/src/constants.ts:
ConstantValuePurpose
PROTOCOL_VERSION1Bump on breaking wire format changes
LEASE_TTL_MS60,000 (60s)Lease validity without heartbeat
HEARTBEAT_MS20,000 (20s)Terminal heartbeat frequency
GRACE_MS10,000 (10s)Grace period after disconnect before lease revocation
FORWARD_INTERVAL_MS5,000 (5s)Hub event drain to Odoo interval
FORWARD_BATCH_SIZE20Max events per drain batch
RECONNECT_BASE_MS1,000 (1s)Reconnect backoff base (doubles each attempt)
RECONNECT_MAX_MS30,000 (30s)Max reconnect delay
TABLE_LOCK_TTL_MS45,000 (45s)Table lock validity without heartbeat
TABLE_LOCK_HEARTBEAT_MS15,000 (15s)Table lock heartbeat frequency

Validation

Every message is validated at runtime using Zod schemas (validators.ts). The hub validates incoming terminal messages before routing; the frontend can optionally validate hub messages.
import { parseTerminalMessage, parseHubMessage } from "@nu/sync-protocol";

const result = parseTerminalMessage(rawData);
if (result.ok) {
  // result.message is typed as TerminalMessage
} else {
  // result.error describes what failed
}
The schemas use z.discriminatedUnion on the type field for efficient runtime dispatch.

Connection lifecycle

  Terminal                              Hub
     │                                   │
     ├──── WS Connect ──────────────────►│
     │                                   │
     ├──── AUTH { token } ──────────────►│
     │                                   │  validate token → Odoo
     │◄─── AUTH_OK { terminalId } ───────┤
     │                                   │
     ├──── LEASE_ACQUIRE { ref } ───────►│
     │◄─── LEASE_GRANTED { version } ────┤
     │                                   │
     ├──── ORDER_SNAPSHOT { snap } ─────►│
     │                                   ├──► broadcast ORDER_UPDATED to others
     │                                   ├──► append to event log
     │                                   │
     │         (every 20s)               │
     ├──── LEASE_HEARTBEAT { ref } ─────►│
     │                                   │  renew lease expiry
     │                                   │
     ├──── LEASE_RELEASE { ref } ───────►│
     │                                   │
     ├──── WS Close ─────────────────────►│
     │                                   │  grace period → revoke leases