> ## 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.

# Protocol

> WebSocket message types, snapshot format, Zod validation, and connection lifecycle.

## 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)

| Type                       | Purpose                          | Key fields                                |
| -------------------------- | -------------------------------- | ----------------------------------------- |
| `AUTH`                     | Authenticate on connect          | `token`, `deviceId`                       |
| `LEASE_ACQUIRE`            | Request exclusive edit rights    | `posReference`, `baseVersion`, `force?`   |
| `LEASE_RELEASE`            | Release edit rights              | `posReference`, `finalSnapshot?`          |
| `LEASE_HEARTBEAT`          | Keep lease alive                 | `posReference`                            |
| `ORDER_SNAPSHOT`           | Send full order state            | `posReference`, `baseVersion`, `snapshot` |
| `ORDER_FINALIZE`           | Finalize order through hub       | `posReference`, `snapshot`, `configId`    |
| `TABLE_UPDATE`             | Update table status              | `tableId`, `status`, `posReference?`      |
| `TABLE_LOCK_ACQUIRE`       | Request table lock               | `tableId`                                 |
| `TABLE_LOCK_RELEASE`       | Release table lock               | `tableId`                                 |
| `TABLE_LOCK_HEARTBEAT`     | Keep table lock alive            | `tableId`                                 |
| `TABLE_LOCK_FORCE_ACQUIRE` | Manager table lock override      | `tableId`                                 |
| `TICKET_NUMBER_REQUEST`    | Request sequential ticket number | `orderType` (`"takeout"` or `"delivery"`) |
| `FLOOR_PLAN_UPDATE`        | Broadcast floor plan changes     | `floorId`, `tables`, `layoutMode?`        |

### Hub to terminal (19 message types)

| Type                     | Purpose                                | Key fields                                              |
| ------------------------ | -------------------------------------- | ------------------------------------------------------- |
| `AUTH_OK`                | Authentication success                 | `terminalId`, `role`                                    |
| `AUTH_FAIL`              | Authentication failure                 | `reason`                                                |
| `LEASE_GRANTED`          | Lease acquired                         | `posReference`, `version`, `expiresAt`                  |
| `LEASE_DENIED`           | Lease rejected                         | `posReference`, `holder`, `reason`                      |
| `LEASE_REVOKED`          | Lease taken away                       | `posReference`, `reason`, `snapshot?`                   |
| `ORDER_UPDATED`          | Order changed by another terminal      | `posReference`, `version`, `snapshot`, `sourceTerminal` |
| `ORDER_FINALIZED`        | Order finalization success             | `posReference`, `orderId`, `lineIdMap`, `fiscal?`       |
| `ORDER_FINALIZE_ERROR`   | Order finalization failure             | `posReference`, `error`, `retryable`                    |
| `SYNC_INIT`              | Full order state on connect            | `orders: [{ posReference, version, snapshot }]`         |
| `TABLE_UPDATED`          | Table status changed                   | `tableId`, `status`, `posReference?`                    |
| `CONFLICT`               | Version conflict detected              | `posReference`, `hubVersion`, `hubSnapshot`             |
| `SYNC_STATUS`            | Periodic hub health broadcast          | `connectedTerminals`, `pendingForward`, `lastOdooSync`  |
| `TABLE_LOCK_GRANTED`     | Table lock acquired                    | `tableId`                                               |
| `TABLE_LOCK_DENIED`      | Table lock rejected                    | `tableId`, `lockedBy: { userId, userName }`             |
| `TABLE_LOCK_RELEASED`    | Table lock released                    | `tableId`                                               |
| `TABLE_LOCK_REVOKED`     | Table lock taken away                  | `tableId`, `reason`                                     |
| `TABLE_LOCKS_STATE`      | Full table lock snapshot               | `locks: [{ tableId, terminalId, userId, userName }]`    |
| `TICKET_NUMBER_RESPONSE` | Ticket number assigned                 | `ticketNumber`, `orderType`                             |
| `TICKET_NUMBER_ERROR`    | Ticket number failure                  | `reason`, `orderType?`                                  |
| `FLOOR_PLAN_CHANGED`     | Floor plan changed by another terminal | `floorId`, `tables`, `layoutMode?`, `sourceTerminal`    |

## OrderSnapshot

The canonical representation of an order's state, transmitted in `ORDER_SNAPSHOT`, `ORDER_UPDATED`, `CONFLICT`, and `LEASE_REVOKED` messages.

```typescript theme={null}
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

```typescript theme={null}
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;
  };
}
```

<Note>
  Voided lines (`isVoided: true`) are included in snapshots for audit purposes but filtered out before forwarding to Odoo.
</Note>

### FiscalData

Returned in `ORDER_FINALIZED` messages for Dominican tax (NCF/DGII) integration:

```typescript theme={null}
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`:

| Constant                  | Value        | Purpose                                               |
| ------------------------- | ------------ | ----------------------------------------------------- |
| `PROTOCOL_VERSION`        | `1`          | Bump on breaking wire format changes                  |
| `LEASE_TTL_MS`            | 60,000 (60s) | Lease validity without heartbeat                      |
| `HEARTBEAT_MS`            | 20,000 (20s) | Terminal heartbeat frequency                          |
| `GRACE_MS`                | 10,000 (10s) | Grace period after disconnect before lease revocation |
| `FORWARD_INTERVAL_MS`     | 5,000 (5s)   | Hub event drain to Odoo interval                      |
| `FORWARD_BATCH_SIZE`      | 20           | Max events per drain batch                            |
| `RECONNECT_BASE_MS`       | 1,000 (1s)   | Reconnect backoff base (doubles each attempt)         |
| `RECONNECT_MAX_MS`        | 30,000 (30s) | Max reconnect delay                                   |
| `TABLE_LOCK_TTL_MS`       | 45,000 (45s) | Table lock validity without heartbeat                 |
| `TABLE_LOCK_HEARTBEAT_MS` | 15,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.

```typescript theme={null}
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
```
