# Connecting a terminal to your back office Step-by-step wiring. The companion to [`sync-contract.md`](sync-contract.md), which specifies *what* the back office must implement — this one covers *how* to get a terminal talking to it, and how to prove each hop works before moving to the next. Nothing here needs a rebuild. A terminal is pointed at a back office from Settings. --- ## The five hops ``` 1. Mosquitto reachable, with a scoped account for the tills 2. Terminal connects and shows LIVE 3. Terminal publishes a bill 4. Your consumer commits it and acks 5. Terminal marks it synced and stops re-sending ``` Do them in order. A failure at hop 4 looks identical to a failure at hop 2 from the terminal's side — it just keeps queueing — so proving each one saves a lot of guessing. --- ## Hop 1 — Mosquitto, with an account for the tills The deployed broker is **Eclipse Mosquitto 2.1.2** at `66.116.225.226:1883`, already carrying `nearle/riders/#` and `doormile/#`. There is no NATS in this estate — the NATS servers that exist belong to other projects, on hosts whose ports are closed, and their configs expose no MQTT gateway. Current config has `allow_anonymous false` and a password file, but **no `acl_file`** — so every authenticated user, including the `admin` account hardcoded in the rider APK, has full run of every topic. Add scoped accounts before a hundred tills start publishing takings: ```bash mosquitto_passwd -b /mosquitto/config/passwd pos_terminal '' mosquitto_passwd -b /mosquitto/config/passwd pos_ingest '' ``` ```conf # /mosquitto/config/acl, then add `acl_file /mosquitto/config/acl` to mosquitto.conf user pos_terminal topic write nearle/pos/+/+/order topic write nearle/pos/+/+/customer topic write nearle/pos/+/+/status topic write nearle/pos/+/+/health topic read nearle/pos/+/+/ack topic read nearle/pos/+/+/command topic read nearle/pos/+/catalogue user pos_ingest topic read nearle/pos/+/+/order topic read nearle/pos/+/+/customer topic read nearle/pos/+/+/health topic write nearle/pos/+/+/ack topic write nearle/pos/+/catalogue user admin topic readwrite nearle/riders/# topic readwrite doormile/# ``` Prove it: ```bash mosquitto_sub -h 66.116.225.226 -p 1883 -u pos_ingest -P '' \ -t 'nearle/pos/#' -v & mosquitto_pub -h 66.116.225.226 -p 1883 -u pos_terminal -P '' \ -t nearle/pos/12/T0000/order -m 'hello' ``` **The broker is a transport, not a ledger.** `max_queued_messages` defaults to 1000 and `autosave_interval` to 30 minutes, so a long outage or a hard kill can drop queued messages. That costs nothing here — an undelivered batch is never acked, so the till keeps it and sends again — but only while nobody acknowledges on the broker's behalf. **TLS is not configured** and 8883 is closed. Bills carry customer names and mobile numbers; worth adding a listener before rollout rather than after. ## Hop 2 — Point the terminal at it On the terminal: **Settings → Connectivity & sync → Configure**. | Field | Value | |---|---| | Terminal name | What staff call this till, e.g. "Counter 2" | | Store ID | **The numeric `locationid`.** The tenant is resolved from it server-side | | Transport | MQTT | | Broker host / port | `66.116.225.226`, port `1883`, **TLS off** | | Username / password | The `pos_terminal` account from hop 1 | | Use TLS | **Off** — 8883 is not configured on this broker yet | The dialog also shows a **Device ID** and a terminal code like `T4A9`. Neither is editable. They are minted on first run and stay with the physical machine, which is what keeps 100 terminals from colliding on topics, client ids and invoice numbers. Note the code down — it is what a support call needs. Credentials go to the OS keystore (Keychain / Credential Manager / Android Keystore), not into the database alongside the bills. Prove it: the header pill switches from `OFFLINE (SIM)` to `LIVE`, and ```bash mosquitto_sub -h 66.116.225.226 -u pos_ingest -P '' -t 'nearle/pos/+/+/status' -v ``` should immediately show a retained presence record for the terminal. If it doesn't, the terminal never connected — check the broker log for an auth rejection before looking anywhere else. --- ## Hop 3 — Ring a sale and watch it publish ```bash mosquitto_sub -h 66.116.225.226 -u pos_ingest -P '' -t 'nearle/pos/+/+/order' -v ``` Ring a bill on the terminal. Within a couple of seconds you should see the envelope from [`sync-contract.md`](sync-contract.md#payloads) — a `batch_id`, the store and terminal, and an `orders` array. The header pill will show `1 QUEUED` and stay there, because nothing has acknowledged it yet. That is correct behaviour, not a fault. --- ## Hop 4 — Consume, commit, acknowledge **This is already built**, in the Fiesta backend. See `backend_fiesta/POS_TERMINAL_INGEST.md` for how to turn it on; what follows is what it guarantees, so you can check it still holds if anyone changes it. Set `MQTT_URL` and it subscribes to `nearle/pos/+/+/{order,customer,health}`, commits, and acknowledges. Bills land in `pos_orders` / `pos_order_items`, and the stock they consumed goes through the same `productstocks` ledger an app order uses. Three rules the implementation is built around, and that any replacement must also keep: **Acknowledge from the consumer, after the database commit.** Not from a handler that has merely queued the work. That ack is the terminal's only evidence, and it deletes its own copy seven days later on the strength of it. **A duplicate is accepted, not rejected.** QoS 1 is at-least-once and a lost ack makes the terminal re-send the whole batch. Reporting those as failures would strand a day of takings on the till. Deduplication is a unique index on the till's UUID plus a Postgres advisory lock. **Read the store and terminal from the topic, never the body.** A till that could name its own store in a payload could redirect another counter's acknowledgements. Prove the ack path by hand before trusting the consumer: ```bash # copy batch_id and the order id from the hop-3 output mosquitto_pub -h 66.116.225.226 -u pos_ingest -P '' \ -t 'nearle/pos/12/T4A9/ack' \ -m '{"batch_id":"","accepted":[""]}' ``` The pill should flip to `LIVE` and the bill disappear from the queue. ### Shopper registrations A second uplink runs on `nearle/pos/{loc}/{terminal}/customer`, acked on the same topic by the same rules, and handled by the same consumer. The id is a UUIDv5 over the shopper's normalised ten-digit mobile, so two tills registering the same person independently produce the same row. It is stored insert-if-absent — never an update, so a profile corrected at head office is not reverted by a terminal replaying an old capture. No loyalty figures travel upward: those are derived from the bill stream, which is idempotent and sees every counter. ```bash mosquitto_sub -h 66.116.225.226 -u pos_ingest -P '' -t 'nearle/pos/+/+/customer' -v ``` Add a shopper on the terminal — no sale needed — and it should appear. ### Terminal health Every till publishes to `nearle/pos/{loc}/{terminal}/health` every 30 seconds. The consumer writes it to Redis as `pos:terminal:{code}` under a 90-second TTL, so a till that loses power ages off the board by itself. Read it back at `GET /live/api/v1/pos/health/location?location_id=12`. Heartbeats are never acknowledged — a till that could be blocked by a busy dashboard would be a self-inflicted outage. ## Hop 5 — Catalogue down The catalogue is a bulk pull over HTTP, not MQTT — a broker is the wrong shape for tens of thousands of rows. Set the **Base URL** in the same Configure dialog and implement: ``` GET {base}/catalogue?since={revision}&page={n}&store_id=…&terminal_id=… Authorization: Bearer {apiKey} ``` Full field-by-field behaviour, including what happens when something is missing, is in [`sync-contract.md`](sync-contract.md#catalogue-pull). The two things easiest to get wrong: - **`is_delta` is load-bearing.** A full snapshot withdraws every product it does not mention. Answer `is_delta: true` for a change set, or the first morning price change empties the shelf. - **Send `stock` only when you mean it.** Any product in the payload gets its count overwritten with your figure, which predates sales the terminal has rung but not uploaded. The terminal replays those — but only for products the payload carried. To push a change mid-day rather than waiting for the next pull: ```bash mosquitto_pub -h 66.116.225.226 -u pos_ingest -P '' \ -t 'nearle/pos/12/catalogue' -m '{"revision":"rev-8822"}' ``` Every terminal in that store pulls immediately. --- ## Troubleshooting | Symptom | Where to look | |---|---| | Pill stuck on `OFFLINE (SIM)` | Simulate offline is still on in Settings | | Pill shows `LIVE`, no presence on `nearle/pos/+/+/status` | Terminal never connected — check broker auth logs | | Bills publish, queue never empties | You are acking the wrong `batch_id`, or not acking at all | | Queue empties then refills with the same bills | Ack arriving after `ackTimeout` (20s default) — the terminal gave up and re-sent | | Two terminals fighting for the connection | They share a client id. Each device mints its own; check they have different terminal codes | | `SYNC HALTED` | You named an id in `rejected`. The reason is on the pill tooltip and in Events | | Duplicate rows server-side | No unique index on `order.id`. At-least-once delivery makes it mandatory | | Shelf empties after a price change | You sent a delta with `is_delta: false`, or a stale `stock` | The **Events** module on the terminal shows every sync attempt with its error, and per-bill state — start there before the broker logs. --- ## Before a fleet rollout - **Add the broker ACL first** (hop 1). Today every authenticated user is unrestricted on every topic, including the `admin` account hardcoded in the rider APK. - **Back up `nearle_pos.db` on any terminal already trading.** The schema goes to v8 on first launch and the migration is one-way. It also queues every shopper already on the terminal for upload, so expect one burst of registrations from each existing store — collapse those onto the mobile number. - **Build per-ABI.** `flutter build apk --split-per-abi` gives ~23MB per architecture instead of a 69MB universal APK — worth it over shop wifi. - **Change the seed PINs.** `4821` / `5093` / `6274` are in the source. Every account is flagged to force a change at first sign-in, but a shop that dismisses it is running a published credential. - **Turn TLS on.** Bills carry customer names and mobile numbers.