Wires the last two dead buttons in Settings and closes the loop on the
credential work: hashed PINs are only worth having if a shop can actually
change them.
Users & roles (Manage)
- Add, rename, re-role and remove staff. Admin-only at the door, because
anyone who can edit staff can make themselves an admin.
- PIN and confirmation are both required and must match. There is no email to
reset a PIN with, so a typo nobody can verify locks the account out until an
admin intervenes.
- Editing someone leaves their PIN alone unless a new one is typed. An admin
setting another person's PIN counts as a reset and re-arms must-change.
- Removal is a deactivation with a confirmation that explains why: bills
already rung keep the cashier's name, so shift reports stay correct.
- Anyone still on a shipped PIN is flagged in the list and in Settings.
Store details (Edit)
- Name, address, GSTIN and phone now editable and persisted. GSTIN is format
and state-code validated; it prints on every invoice as a legal requirement,
so a typo is a compliance problem across hundreds of bills.
- Admin-only: changing the GSTIN changes what every future invoice claims
about who collected the tax.
Forced PIN change
- Shown once after sign-in while must-change is set, and not dismissable. The
seeded PINs are in the source of the build, so a terminal still running one
is effectively unprotected.
Fixed while testing: the role dropdown laid its items out at natural width and
"Manager — Sales, inventory and reports" overflowed the dialog by 222px. Now
isExpanded with the description spelled out below, where it is readable.
Tests: 160 -> 168. Covers both role guards, the mismatched and too-short PIN
paths, the default-PIN flag, and GSTIN and seller-name validation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings > Connectivity > Configure now points a terminal at a back office.
Until this existed a store was wired up by editing syncConfigProvider and
rebuilding, which made every terminal in a fleet its own build.
- Transport picker (offline demo / HTTP / MQTT) with only the relevant fields
shown, validated: an MQTT route with no host is refused rather than silently
saved, because a terminal pointed at nothing looks exactly like one that is
merely offline.
- Terminal name and store id are editable and persist to the database. The
device id and terminal code are shown but not editable, with a copy button —
they are what a support call needs, and re-coding a till must not orphan the
bills already written under the old code.
- TLS defaults on, with a note that bills carry customer names and numbers.
- About card now shows the real terminal, device id and store instead of the
literal TERM-01, and the dead "Check for updates" button is now the entry
point to this dialog.
Lints cleared, analyzer now reports zero issues:
- SoundService wrapped a plain bool in a getter and setter that did nothing.
- Two post-await guards used context.mounted inside a State, which the
analyzer cannot relate to the State's own lifetime. Both are now `mounted`.
Tests: 140 -> 141. The new widget test drives the dialog end to end and asserts
that saving an MQTT route with no host keeps the dialog open with the error
visible. Suite run three times clean.
Known gap, documented in docs/sync-contract.md: broker credentials live in
memory and must be re-entered after a restart. Persisting them means
encrypting at rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bills were persisted correctly but read back wrong. The read path rebuilt a
cart from its lines alone, dropping bill-level discounts and loyalty, so every
figure derived from a stored bill was overstated: the upload payload, the day
archive and the shift report. A discounted 529 bill read back as 620.
Money and data integrity
- order_dao: restore bill_discount and points_redeemed when rebuilding a cart;
keep the reconstruction tier-less so the membership discount is not applied
twice. Trust the recorded total and points via SaleTransaction.storedTotal.
- checkout_sale + order_dao.commitSale: write the bill, its stock movement and
the loyalty update in one transaction. Previously a failure part-way through
left a persisted bill the cashier believed had failed, inviting a duplicate.
- checkout_sale: re-check every line against live stock. A parked bill resumed
after its stock was sold passed validation and oversold.
- catalogue_dao: allocate the invoice sequence in one transaction; the previous
read-modify-write could hand two sales the same number and fail UNIQUE.
- local_store: replay unsynced sales after a catalogue import, so a mid-shift
re-import cannot restore stock that has already been sold.
- payment_controller: stamp the signed-in operator on the bill instead of the
hardcoded seed session, and pass the terminal id through.
- cart: reconcile per-slab GST against the bill total so the parts sum to the
whole on a tax invoice.
Sync and reporting
- sync_repository: drain unsynced bills in a loop rather than silently capping
at one page; stop on rejection so rejected rows cannot loop forever.
- sync_log_dao (new): persist the sync history to the sync_log table, which the
schema already defined but nothing used. It was in memory, so the only record
that bills had been uploaded died at restart.
- Scope shift reports by cashier. day_archive is re-keyed to
(business_date, cashier_name) so a till stays settleable after its bills are
uploaded and deleted. Schema v4 with a migration that carries v3 rows across.
Input and UI
- barcode_service: consume machine-paced keystrokes so a scan cannot also land
in the focused field, and raise the bar to 60ms/char while a text field has
focus so typing a mobile number is not read as a scan. Clock and focus check
injected so the behaviour is testable.
- primary_button: make the label flexible; label plus trailing total overflowed
the Charge button by up to 131px.
- app_router: redirect instead of null-casting when the receipt route is
entered without its transaction.
- customer_repository: reduce the search query to digits so a punctuated mobile
number matches.
Cleanup
- Remove TransactionRepository.save, CustomerRepository.recordSale and
OrderDao.insertOrder, all superseded by commitSale.
- dart fix across the tree; 251 analyzer issues down to 3 info-level.
Tests: 23 passing / 15 failing -> 90 passing. Fixed the two defects that broke
the existing suite (containsAll type argument, reset() needing a catalogue) and
deleted the leftover template test. Added coverage for the order round trip,
the day archive after a real sync, stock safety, checkout atomicity, the v3->v4
migration, scanner-versus-human input, and an app-level smoke test that renders
every module.
Note: bills already uploaded with a discount went up overstated. This stops it
happening again but does not correct historical server data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>