# 0015. Event ordering and conflicts

- Status: Accepted
- Date: 2026-10-27

## Context

Events for one order can come from several devices, arrive late after time offline, arrive
twice, or contradict each other (a waiter adds an item on a phone just as the cashier closes
the order). Every device and the hub must still reach the same state from the same events,
and no event may be lost.

## Decision

- An order's state is rebuilt by `replayOrder`: drop repeated ids, apply `order.opened` first,
  then merge the devices' events. Each device's events keep that device's own order
  (`deviceSeq`), and devices are interleaved by event id: at each step, the device whose next
  event has the smallest id goes next. Ids are ULIDs, so this approximates creation time
  across devices, while a device whose clock jumps backwards still replays in its own order.
- The reducer is pure and never throws on a valid event. An event that cannot apply (a closed
  order, an unknown or voided line, a void without a reason, a repeated payment id) is kept in
  the order's `conflicts` list with its reason, instead of being dropped or failing the replay.
- A closed order accepts only refunds.
- Stored events are append-only: the SQLite schema has triggers that reject `UPDATE` and
  `DELETE` on the events table.

## Consequences

- Replaying any set of events, in any order and with duplicates, gives identical state.
- Conflicts are visible for review in the back office and in reports instead of disappearing.
- A device's own events always replay in the order it recorded them. Device clocks only
  affect how different devices' events interleave; the hub records `receivedAt` so badly wrong
  clocks can be detected later.
- Fixed-amount discounts cap at the amount they discount, so a replay never fails because
  voids made a voucher larger than the bill.
