A till holds every bill in its own SQLite database and keeps it for seven days after we acknowledge it, marking one synced only when its id comes back in an ack. Everything here follows from that. Silence is not acceptance, so a failing ingest publishes nothing at all and the terminal simply sends again. A duplicate is a success, because at-least-once delivery means a lost ack legitimately re-delivers bills we already hold, and calling those failures would strand a day of takings on the till. Deduplication is a unique index on the terminal's UUID plus an advisory lock held for the transaction. Bills land in pos_orders / pos_order_items rather than orders: a counter bill carries a cashier, a terminal, a rounding adjustment, promos, loyalty movement and a payment split that orders has nowhere to put, and forcing one into the other loses whatever does not fit. Stock is *not* split — a counter sale writes the same productstocks rows an app order does, through helpers extracted from createOrderTx so the rule that prevents overselling has one implementation rather than two. GetRevenueSummary and GetSalesSummary were extended to union the new table in; any new report has to remember the same. Terminal health goes to Redis under a 90-second TTL, sharing the instance the express backend uses. A heartbeat is a fact with an expiry date: a till that loses power stops refreshing and ages off the board by itself, where a Postgres row would need ~288k writes a day and a reaper. Proven end to end against the live estate before commit: a bill over HTTP and one over the real Mosquitto broker, the same bill three times producing one row and one stock movement, and a heartbeat arriving on the health endpoint. All probe data was removed afterwards. Four things that only surfaced against real data. An unset jsonb column failed the very first bill. Product SKUs are unusable as barcodes — 6,245 products share 93 SKUs and "1" covers 5,794 of them — against the till's unique index, so barcodes fall back to the product id. A taxpercent of -1 exists and would have put negative GST in a filed slab. And a product with id 0 exists, which can never be billed and is now skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59 lines
2.8 KiB
Go
59 lines
2.8 KiB
Go
package models
|
|
|
|
// PosHealth is what a till reports about itself every 30 seconds.
|
|
//
|
|
// This is a liveness signal, not a record. It lives in Redis under a TTL and is
|
|
// never written to Postgres: a terminal that dies simply stops refreshing and
|
|
// disappears from the board on its own, with no reaper job and no row left
|
|
// claiming "online" three days after the shop closed.
|
|
//
|
|
// The fields exist to answer questions a person actually asks when a shop
|
|
// phones in: is the till on, is it reaching us, is it selling anything, and is
|
|
// the hardware in the way.
|
|
type PosHealth struct {
|
|
// Identity. Terminalid is the short code printed on invoices — the thing a
|
|
// support call starts with.
|
|
Terminalid string `json:"terminal_id"`
|
|
Locationid string `json:"location_id"`
|
|
Storename string `json:"store_name"`
|
|
Appversion string `json:"app_version"`
|
|
|
|
// "online" while the till is refreshing this. The broker's Last Will
|
|
// overwrites it with "offline" if the terminal loses power mid-shift, which
|
|
// is the only way to tell *closed for the night* from *unplugged*.
|
|
Status string `json:"status"`
|
|
|
|
// Queue depth — the number that matters most. A shop quietly accumulating
|
|
// unsynced takings looks completely normal from the shop floor, and this is
|
|
// the only thing that makes it visible before someone reconciles a till and
|
|
// finds a day missing.
|
|
Pendingbills int `json:"pending_bills"`
|
|
Pendingregistrations int `json:"pending_registrations"`
|
|
Oldestpendingat string `json:"oldest_pending_at"`
|
|
|
|
// Today's trading. A till that is connected but has rung nothing in three
|
|
// hours usually means a jammed printer or an absent cashier, and neither
|
|
// shows up in a plain online/offline board.
|
|
Todaybills int `json:"today_bills"`
|
|
Todayamount float64 `json:"today_amount"`
|
|
Lastbillat string `json:"last_bill_at"`
|
|
|
|
// Device state, for pre-emptive support.
|
|
//
|
|
// Pointers so that *not reported* is distinguishable from *reported as
|
|
// zero*. Not every build collects these — battery and free storage need
|
|
// platform packages a desktop till has no use for — and writing an
|
|
// uncollected reading as 0 would show a board full of terminals on a flat
|
|
// battery with an unreachable printer. A nil field is skipped entirely.
|
|
Batterylevel *int `json:"battery_level,omitempty"`
|
|
Batterycharging *bool `json:"battery_charging,omitempty"`
|
|
Storagefreemb *int `json:"storage_free_mb,omitempty"`
|
|
Printerreachable *bool `json:"printer_reachable,omitempty"`
|
|
Drawerstatus *string `json:"drawer_status,omitempty"`
|
|
|
|
// Stamped by the till. The consumer also stamps its own arrival time, and
|
|
// the two disagreeing is itself a signal — a till whose clock is wrong
|
|
// writes bills under the wrong business date.
|
|
Reportedat string `json:"reported_at"`
|
|
}
|