Live bill INV-2608-T5EDD-00116 carried billedat 2026-08-05T12:49:28Z beside
receivedat 2026-08-05T07:19:28Z — the sale appearing to have been rung five and
a half hours after the back office received it. Exactly the IST offset, on every
bill.
Nothing was lying. DateTime.toIso8601String() on a local time emits no zone
marker at all, and Go's time.Parse fills that silence with UTC, so a Coimbatore
wall clock was recorded as though it had been read in London.
The daily figures survived by luck: businessdate is derived from the wall clock
either way, and the wall clock was always the till's own, so a day's takings
landed on the right day even while the instant was wrong. Anything comparing
billedat against real time did not.
Formatters.isoWithOffset attaches the offset, which fixes both readings at once
— the instant parses correctly and the local date still formats correctly. The
minutes come from the real offset rather than being assumed zero, because India
is +05:30 and a whole-hour implementation would be wrong in a way that looks
almost right.
Applied to all six timestamps the till sends. date_of_birth is left alone: a
birthday is a date, not an instant, and giving it a zone would be meaningless.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A shop configured for the HTTP route uploaded 17 bills correctly today and
never once appeared on the fleet board. Nothing logged it, because from the
terminal's point of view nothing had failed: the reporter was typed against
MqttOrderTransport and started behind an `is MqttOrderTransport` check, so on
HTTP it was silently never constructed. A monitoring feature that quietly does
not exist on one of two supported routes is worse than no feature, because the
blank square reads as "no terminals" rather than "not wired up".
publishHealth moves onto the OrderTransport interface. The broker publishes to
the health topic as before; HTTP posts the same payload to POST /pos/health;
the simulated route does nothing, which is the honest answer for a till with no
back office configured. The reporter is now started for every route.
There was no test for the reporter at all, which is why this shipped. There are
five now, including one that fails on the old code.
Also removes test/widget_test.dart — the stock `flutter create` counter test,
referencing a MyApp that never existed here. It has never compiled and was the
only red in the suite.
263 tests pass, analyzer clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>