fdd90f28d96b9f58a9ae0e1a4bbf22241a8704fe
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fdd90f28d9 |
Open the cash drawer for real, and persist the back-office route
Cash drawer - openCashDrawer was a debugPrint. The drawer never opened. - It cannot go through the PDF pipeline: a PDF is rendered by the platform driver, which will not pass raw ESC/POS bytes to the device. So it goes over a socket instead — nearly every network thermal printer listens on 9100 and forwards whatever arrives straight to the print head, which makes the whole protocol five bytes. - Printer IP and port are configurable in Settings with a Test button that saves and fires immediately, because a drawer that does not open is indistinguishable from one that is not wired up. - Every failure explains itself: unreachable, refused, or simply not configured — which is the honest state for a USB printer, since there is no raw path to one from Flutter. - Now fires only on a cash tender. A card-only sale that pops the drawer is a shrinkage risk, and it is the first thing a shop notices. Back-office route - Host, port, TLS and transport persist to the database; username, password and API key go to the platform keystore (Keychain / Credential Manager / Android Keystore). Writing credentials into SQLite would put them in the same file as the bills, on a machine behind a shop counter. - Loaded at startup. Previously the dialog wrote settings that were silently ignored on the next launch, which reads exactly like they never saved — and credentials retyped every morning end up on a sticky note instead. - A saved route never overwrites the terminal's store or terminal id. Those belong to the device, and re-pointing a till at a different broker must not change who it is, or its bills and presence records stop lining up. Tests: 168 -> 176. The drawer test stands up a real socket server and asserts the exact bytes arrive. The config test asserts no credential appears anywhere in the meta table while the non-secret settings do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e17937e8f1 |
Move staff PINs out of the shipped binary into hashed database rows
Three StaffUser constants carried plaintext PINs (1234/2345/3456) in auth_controller.dart. Every build shipped every till's credentials, readable by anyone who unzipped the APK. Across 100 deployed devices that is one credential, not a hundred. - Schema v5 adds a staff table. Only a PBKDF2-HMAC-SHA256 hash and a per-user random salt are stored; the PIN itself exists nowhere, including there. 12,000 iterations, tuned so one sign-in is imperceptible while working through all 10,000 four-digit PINs against a stolen database takes ~15 minutes per account instead of milliseconds. - Verification is constant-time. String == returns at the first differing byte, and that timing leaks how much of a guess was right. - StaffUser no longer has a pin field at all, so the credential cannot drift back into memory, into widgets, or into a const declaration. - Weak PINs are refused: under four digits, non-numeric, repeated digits, and sequences. Two staff cannot share a PIN — the till identifies a cashier by PIN alone, so a shared one would attribute bills to whichever row was checked first. - The last admin cannot be demoted or deactivated. A till with no admin cannot be administered, including to appoint one, and recovering means editing the database by hand. - Staff are deactivated, never deleted, so bills already rung keep naming a real person. Seed accounts are now 4821/5093/6274 rather than 1234/2345/3456 — the weak-PIN rule refuses the old ones, and a default the rule itself would reject is not a defensible default. All three are flagged must-change-pin so they get a shop trading on day one without becoming permanent. Store details are now editable data, not compile-time constants. Name, address, GSTIN and phone persist to the database and are read back rather than falling through to the build's constants, which would silently undo a failed save. GSTIN is format-validated including the state code — it prints on every invoice as a legal requirement, so a typo is a compliance problem across hundreds of bills before anyone notices. Tests: 141 -> 160. Includes a test that reads every column of every staff row and asserts no seed PIN appears anywhere in the database. Migration test now asserts v5 and that an upgraded terminal comes up with the staff table present but empty — seeding is the store's job on first open, so an existing shop is never handed accounts it did not create. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
30d6d1080f |
Give every terminal its own identity, and report fleet presence
Answers "which of my 100 tills are alive and healthy", and fixes three things that were fine on one device and broken on a hundred. Terminal identity (lib/data/local/terminal_identity.dart) - Every device mints a UUID on first run, stored in its own database, plus a short code (T4A9) derived from it. Renaming keeps the device id, so history keeps pointing at the same physical till. - Replaces the literal 'TERM-01', which was hardcoded in five places. The whole fleet reported as one terminal: shift reports merged, MQTT topics collided, and a second connection with the same client id evicts the first from the broker — so two tills would have knocked each other offline in a loop. Invoice numbers now carry the terminal code - INV-2608-T4A9-00042. The sequence counter lives in each till's own database and starts at 1, so without this every terminal in the fleet minted INV-2608-00001 for its first sale of the month. The order UUID kept the data distinct; the number a customer quotes on a receipt was not. SQLite pragmas - WAL, so the product grid refreshing does not block the sale being written, and the file is never left mid-rewrite by a power cut. - busy_timeout 5s, so a contended lock waits instead of throwing "database is locked" — which at checkout is a failed sale with a customer standing there. - synchronous NORMAL, the right trade under WAL for a till. Fleet presence (lib/data/sync/presence_reporter.dart) - Retained status record on connect and once a minute: device id, code, name, app version, pending bill count, last upload, catalogue revision, sync halt state. Retained so a dashboard connecting at noon gets all 100 terminals immediately rather than a blank board. - The Last Will already said "reachable". A till can be connected and still be holding 200 unsent bills or running last month's prices; only pending_bills and catalogue_revision say so. NATS - The MQTT gateway maps / to . so the existing transport works unchanged. SyncConfig.asNatsSubject() exposes the translation, and the contract doc gives the JetStream subjects (pos.*.*.order, pos.*.*.status) plus the two server-side requirements: a file-backed stream, and the ack published by the consumer after commit rather than by the ingest handler. Tests: 129 -> 140. New coverage for identity minting and stability, per-device invoice uniqueness, topic and client-id separation, NATS subject mapping, and the two pragmas. Suite run three times clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0a49323858 |
Drain bills to the back office automatically, over MQTT or HTTP
Turns the orders table into a queue that empties itself. Bills were only uploaded when a cashier pressed Sync at end of day; a till that was never pressed held a day's takings indefinitely. Drain engine (lib/data/sync/sync_engine.dart) - Triggers on sale committed, network regained, 5-minute poll, head-office request, and the manual button. - Single flight: a busy till firing a trigger per sale would otherwise have several passes reading the same pending rows and send every bill twice. A trigger arriving mid-drain is queued and replayed, so nothing is dropped. - Exponential backoff with +/-20% jitter to a 5-minute ceiling. The jitter matters: a store's terminals all fail at the same instant when the line drops, and would retry in lockstep without it. - Halts rather than loops on a failure retrying cannot fix (bad credential, refused batch). Pressing Sync clears the halt. Transports (lib/data/remote/) - OrderTransport interface; MQTT, HTTP and simulated implementations. The repository does not know which is in use. - MQTT: QoS 1 uplink, application-level ACK correlated by batch_id on a return topic, retained Last Will for terminal-offline detection, downlink for catalogue pushes and remote sync requests. - A broker PUBACK is never treated as acceptance. It means the broker holds the bytes, not that the ledger took the sale. Only ids the back office names are marked synced; silence leaves a bill pending. - HTTP carries a stable idempotency key across retries of the same bills. Retention - Accepted bills are kept 7 days instead of deleted, so a batch the back office later loses can be re-sent in full. Purged after that; archived totals stay forever. - forBusinessDate now reads pending rows only. A retained bill exists in both the orders table and day_archive, and summing both would overstate the day. Fixes found while building this - SyncEngine._refreshPending wrote state.copyWith(pending: await ...). Dart evaluates the receiver before the awaited argument, so a connectivity drop during the wait was silently overwritten by the stale snapshot. Caught by the first run of the new engine tests. - PrinterSettingsController wrote state after four awaits with no mounted check, throwing "used after dispose" when Settings was left mid-load. This was pre-existing and reached the cashier as a red screen. Also - Header pill now reports real sync state: LIVE / n QUEUED / SYNCING / SYNC HALTED, with an explanation of where the bills are. - Settings shows the route, last upload, next retry and retention window. - docs/sync-contract.md states what the back office must implement, including the idempotency requirement that at-least-once delivery makes mandatory. Tests: 90 -> 129 passing. New coverage for backoff shape and jitter band, single flight, halting, ACK correlation and partial acceptance, at-least-once duplicate handling, retention and purge, and no double-counting after a sync. Suite run six times clean. Not addressed: bills already synced by an older build went up overstated and still need server-side reconciliation. Broker credentials have no Settings editor yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
af3933092f |
Fix billing data integrity, sale atomicity and stock safety
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> |
||
| d72522e737 | second commit |