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>
This commit is contained in:
Suriya
2026-08-03 17:47:24 +05:30
parent 09edce5dc6
commit 33b4337933
9 changed files with 435 additions and 185 deletions

View File

@@ -73,7 +73,14 @@ class SyncConfig {
};
// ------------------------------------------------------------------ Topics
String get _base => 'pos/$storeId/$terminalId';
/// Namespaced under `nearle/` alongside the rider fleet's
/// `nearle/riders/{riderId}/…`, so one broker ACL rule covers each system and
/// a topic says at a glance which one it belongs to.
///
/// [storeId] carries the back office's numeric location id. The tenant is
/// resolved from it server-side and never taken from the wire — a till that
/// could name its own tenant could post sales into another shop's books.
String get _base => 'nearle/pos/$storeId/$terminalId';
/// Uplink. Completed bills, QoS 1.
String get orderTopic => '$_base/order';
@@ -99,8 +106,16 @@ class SyncConfig {
/// what makes a head-office "which tills are dark" board possible.
String get statusTopic => '$_base/status';
/// Liveness, published on a timer rather than on an event.
///
/// Separate from [statusTopic]: that one is retained and doubles as the Last
/// Will, so it must stay small and rarely written. This carries queue depth,
/// today's trading and device state — the things a head-office board needs to
/// tell a till that is merely quiet from one that is in trouble.
String get healthTopic => '$_base/health';
/// Store-wide downlink: catalogue changes land here for every terminal.
String get catalogueTopic => 'pos/$storeId/catalogue';
String get catalogueTopic => 'nearle/pos/$storeId/catalogue';
/// Addressed to this terminal alone.
String get commandTopic => '$_base/command';
@@ -118,7 +133,8 @@ class SyncConfig {
/// NATS' MQTT gateway maps `/` to `.`, so this is what a JetStream stream or
/// consumer is configured against. Provided so the wildcard a back-office
/// consumer needs can be read off the terminal rather than guessed:
/// `pos.*.*.order` for every till's bills, `pos.*.*.status` for presence.
/// `nearle.pos.*.*.order` for every till's bills, `nearle.pos.*.*.health`
/// for presence.
static String asNatsSubject(String topic) => topic.replaceAll('/', '.');
SyncConfig copyWith({