Commit Graph

2 Commits

Author SHA1 Message Date
Suriya
33b4337933 Publish under nearle/pos, add a health heartbeat, send the GST slab split
Three changes, all driven by what the back office turned out to need.

The broker is shared with the rider fleet on nearle/riders/…, so topics
move under nearle/pos/{locationid}/{terminal}/… — one ACL rule per
system, and it is obvious from a topic which one owns it. Store ID now
carries the back office's numeric location id; the tenant is resolved
from it server-side and never taken from the wire.

A till publishes a heartbeat every 30 seconds on its own topic. The Last
Will already answers "is it dead", which is not enough to run a hundred
shops on: the failure that costs money is a terminal that is connected,
selling, and quietly holding two hundred bills it has never uploaded. So
the beat carries queue depth, the age of the oldest thing waiting,
today's trading, and printer reachability. Not retained — the back
office holds it under a TTL, and a retained beat would leave an
unplugged till looking alive until something overwrote it.

Bills now carry tax_breakdown, the GST slab split the cart already
computes. A tax return is filed per slab, and recomputing the split
server-side would mean redoing the discount apportionment and getting
exactly the same answer — or else the filed figure stops matching the
paper the shopper was handed.

Docs rewritten against the real deployment: Eclipse Mosquitto 2.1.2, no
NATS anywhere reachable, no TLS, and a broker whose queue and autosave
defaults mean it must not be treated as durable storage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:47:24 +05:30
Suriya
0a49323858 Drain bills to the back office automatically, over MQTT or HTTP
Turns the orders table into a queue that empties itself. Bills were only
uploaded when a cashier pressed Sync at end of day; a till that was never
pressed held a day's takings indefinitely.

Drain engine (lib/data/sync/sync_engine.dart)
- Triggers on sale committed, network regained, 5-minute poll, head-office
  request, and the manual button.
- Single flight: a busy till firing a trigger per sale would otherwise have
  several passes reading the same pending rows and send every bill twice.
  A trigger arriving mid-drain is queued and replayed, so nothing is dropped.
- Exponential backoff with +/-20% jitter to a 5-minute ceiling. The jitter
  matters: a store's terminals all fail at the same instant when the line
  drops, and would retry in lockstep without it.
- Halts rather than loops on a failure retrying cannot fix (bad credential,
  refused batch). Pressing Sync clears the halt.

Transports (lib/data/remote/)
- OrderTransport interface; MQTT, HTTP and simulated implementations. The
  repository does not know which is in use.
- MQTT: QoS 1 uplink, application-level ACK correlated by batch_id on a return
  topic, retained Last Will for terminal-offline detection, downlink for
  catalogue pushes and remote sync requests.
- A broker PUBACK is never treated as acceptance. It means the broker holds
  the bytes, not that the ledger took the sale. Only ids the back office names
  are marked synced; silence leaves a bill pending.
- HTTP carries a stable idempotency key across retries of the same bills.

Retention
- Accepted bills are kept 7 days instead of deleted, so a batch the back
  office later loses can be re-sent in full. Purged after that; archived
  totals stay forever.
- forBusinessDate now reads pending rows only. A retained bill exists in both
  the orders table and day_archive, and summing both would overstate the day.

Fixes found while building this
- SyncEngine._refreshPending wrote state.copyWith(pending: await ...). Dart
  evaluates the receiver before the awaited argument, so a connectivity drop
  during the wait was silently overwritten by the stale snapshot. Caught by
  the first run of the new engine tests.
- PrinterSettingsController wrote state after four awaits with no mounted
  check, throwing "used after dispose" when Settings was left mid-load. This
  was pre-existing and reached the cashier as a red screen.

Also
- Header pill now reports real sync state: LIVE / n QUEUED / SYNCING /
  SYNC HALTED, with an explanation of where the bills are.
- Settings shows the route, last upload, next retry and retention window.
- docs/sync-contract.md states what the back office must implement, including
  the idempotency requirement that at-least-once delivery makes mandatory.

Tests: 90 -> 129 passing. New coverage for backoff shape and jitter band,
single flight, halting, ACK correlation and partial acceptance, at-least-once
duplicate handling, retention and purge, and no double-counting after a sync.
Suite run six times clean.

Not addressed: bills already synced by an older build went up overstated and
still need server-side reconciliation. Broker credentials have no Settings
editor yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:57:29 +05:30