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:
@@ -62,15 +62,21 @@ connection or return 5xx instead, and the terminal will back off and retry.
|
||||
|
||||
| Topic | Direction | QoS | Retained |
|
||||
|---|---|---|---|
|
||||
| `pos/{store}/{terminal}/order` | till → cloud | 1 | no |
|
||||
| `pos/{store}/{terminal}/customer` | till → cloud | 1 | no |
|
||||
| `pos/{store}/{terminal}/ack` | cloud → till | 1 | no |
|
||||
| `pos/{store}/{terminal}/status` | till → cloud | 1 | **yes** |
|
||||
| `pos/{store}/{terminal}/command` | cloud → till | 1 | no |
|
||||
| `pos/{store}/catalogue` | cloud → all tills | 1 | **yes** |
|
||||
| `nearle/pos/{loc}/{terminal}/order` | till → cloud | 1 | no |
|
||||
| `nearle/pos/{loc}/{terminal}/customer` | till → cloud | 1 | no |
|
||||
| `nearle/pos/{loc}/{terminal}/health` | till → cloud | 1 | no |
|
||||
| `nearle/pos/{loc}/{terminal}/ack` | cloud → till | 1 | no |
|
||||
| `nearle/pos/{loc}/{terminal}/status` | till → cloud | 1 | **yes** |
|
||||
| `nearle/pos/{loc}/{terminal}/command` | cloud → till | 1 | no |
|
||||
| `nearle/pos/{loc}/catalogue` | cloud → all tills | 1 | **yes** |
|
||||
|
||||
`{store}` and `{terminal}` come from the device's own identity, minted on first
|
||||
run and stored in its database. They are never literals — 100 tills sharing one
|
||||
Namespaced under `nearle/` alongside the rider fleet's `nearle/riders/…`, so one
|
||||
broker ACL rule covers each system.
|
||||
|
||||
`{loc}` is the back office's numeric location id, entered once in Settings; the
|
||||
tenant is resolved from it server-side and never taken from the wire.
|
||||
`{terminal}` comes from the device's own identity, minted on first run and
|
||||
stored in its database. They are never literals — 100 tills sharing one
|
||||
id would collide on every topic and evict each other from the broker, since a
|
||||
second connection with the same client id kicks the first off.
|
||||
|
||||
@@ -79,28 +85,28 @@ second connection with the same client id kicks the first off.
|
||||
dark" board possible, and it is the only way to tell *closed for the night*
|
||||
from *unplugged*.
|
||||
|
||||
### Running this on NATS
|
||||
### Running this on Mosquitto
|
||||
|
||||
The MQTT gateway maps `/` to `.`, so the topics above arrive as subjects and a
|
||||
JetStream consumer binds to them directly:
|
||||
The deployed broker is Eclipse Mosquitto 2.1.2. A consumer binds to the topics
|
||||
above directly, using `+` as the single-level wildcard:
|
||||
|
||||
| Purpose | Subject |
|
||||
| Purpose | Filter |
|
||||
|---|---|
|
||||
| Every till's bills | `pos.*.*.order` |
|
||||
| Every till's presence | `pos.*.*.status` |
|
||||
| One store's bills | `pos.store-01.*.order` |
|
||||
| Ack back to one till | `pos.store-01.T4A9.ack` |
|
||||
| Every till's bills | `nearle/pos/+/+/order` |
|
||||
| Every till's heartbeat | `nearle/pos/+/+/health` |
|
||||
| One shop's bills | `nearle/pos/12/+/order` |
|
||||
| Ack back to one till | `nearle/pos/12/T4A9/ack` |
|
||||
|
||||
`SyncConfig.asNatsSubject()` does the translation, so a consumer's subject can
|
||||
be read off the terminal rather than guessed.
|
||||
Two things to get right:
|
||||
|
||||
Two things to get right on the NATS side:
|
||||
|
||||
- **The stream must be durable and file-backed.** A memory stream loses a shop's
|
||||
bills on a server restart, and the till has already been told they landed.
|
||||
- **Publish the ack from the consumer, after the database commit** — not from an
|
||||
ingest handler that has merely queued the work. The ack is the terminal's
|
||||
only evidence, and it deletes its copy seven days later on the strength of it.
|
||||
ingest handler that has merely queued the work. The ack is the terminal's only
|
||||
evidence, and it deletes its copy seven days later on the strength of it.
|
||||
- **Do not treat the broker as durable storage.** Mosquitto's default
|
||||
`max_queued_messages` is 1000 and its `autosave_interval` is 30 minutes, so a
|
||||
long outage or a hard kill can drop queued messages. Nothing is lost, because
|
||||
an undelivered batch is simply never acked and the till sends it again — but
|
||||
only as long as nobody acknowledges on the broker's behalf.
|
||||
|
||||
### Fleet presence
|
||||
|
||||
@@ -127,7 +133,7 @@ so.
|
||||
|
||||
## Payloads
|
||||
|
||||
**Uplink** — `pos/{store}/{terminal}/order`
|
||||
**Uplink** — `nearle/pos/{loc}/{terminal}/order`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -140,7 +146,7 @@ so.
|
||||
}
|
||||
```
|
||||
|
||||
**Ack** — `pos/{store}/{terminal}/ack`. Must echo `batch_id`; anything else is
|
||||
**Ack** — `nearle/pos/{loc}/{terminal}/ack`. Must echo `batch_id`; anything else is
|
||||
ignored as belonging to a batch the terminal is no longer waiting on.
|
||||
|
||||
```json
|
||||
@@ -160,7 +166,7 @@ the same bills.
|
||||
|
||||
### Shopper registrations
|
||||
|
||||
**Uplink** — `pos/{store}/{terminal}/customer`, or `POST {base}/customers`.
|
||||
**Uplink** — `nearle/pos/{loc}/{terminal}/customer`, or `POST {base}/customers`.
|
||||
Acked on the same topic and by the same rules: only ids you name are marked
|
||||
sent.
|
||||
|
||||
@@ -269,7 +275,7 @@ Dates are ISO 8601 or epoch milliseconds; both are accepted.
|
||||
|
||||
### Pushing a change mid-day
|
||||
|
||||
Publish to `pos/{store}/catalogue` (retained) and every terminal in the shop
|
||||
Publish to `nearle/pos/{loc}/catalogue` (retained) and every terminal in the shop
|
||||
pulls immediately instead of waiting for tomorrow morning. The message body is
|
||||
only a nudge — the catalogue itself still comes over HTTP, because a broker is
|
||||
the wrong shape for tens of thousands of rows.
|
||||
|
||||
Reference in New Issue
Block a user