Files
nearle_pos/README.md
Suriya 467d5eee75 Add an integration guide for wiring a terminal to a back office
sync-contract.md specifies what the back office must implement. It does not
say how to get a terminal talking to one, which is the question anyone
actually deploying this hits first.

Five hops, in the order they should be proved:
1. NATS with the MQTT gateway on — config snippet, narrow per-topic
   permissions, and a file-backed JetStream stream, because a memory stream
   loses a shop's bills on restart after the terminal has been told they
   landed.
2. Pointing the terminal at it from Settings.
3. Watching a bill publish.
4. Consuming, committing, and acknowledging — with the table schema, an
   idempotent insert, and the rule that the ack comes from the consumer after
   the commit rather than from an ingest handler that merely queued the work.
5. The catalogue pull, and the mid-day push that triggers it.

Each hop has a command that proves it works, because a failure at hop 4 looks
identical to a failure at hop 2 from the terminal's side — it just keeps
queueing.

Plus a troubleshooting table mapping symptoms to causes (queue refilling with
the same bills means the ack arrived after the 20s timeout; two terminals
fighting for the connection means they share a client id), and a pre-rollout
checklist: back up the database before the one-way v7 migration, build
per-ABI to cut 69MB to ~23MB, change the seed PINs, turn TLS on.

Every topic, timeout, query parameter and pill label in the guide was checked
against the code rather than written from memory.

README now points at both documents and states the default: the terminal runs
against a local stub until it is configured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:06 +05:30

165 lines
9.2 KiB
Markdown

# Nearle POS
An enterprise Point of Sale terminal for supermarkets, grocery stores, pharmacies and retail shops. Built for **Flutter Desktop** (Windows/macOS/Linux) and **Android tablets**.
Brand colour `#662582` · Inter typeface · 16px corner radius · touch-first targets.
---
## Getting started
```bash
flutter pub get
flutter run -d windows # or macos, linux, or a connected tablet
flutter test # 234 unit and widget tests
flutter analyze
```
Out of the box the terminal runs against a local stub: products are seeded,
bills queue and drain, and nothing leaves the device. Connecting it to a real
back office is a Settings change, not a rebuild — see below.
### Connecting to a back office
| Document | For |
|---|---|
| [Integration guide](docs/integration-guide.md) | Wiring a terminal to NATS/MQTT and an HTTP catalogue, hop by hop, with commands to prove each one |
| [Sync contract](docs/sync-contract.md) | What the back office must implement: topics, payloads, acknowledgement rules, field handling |
The short version: bills are written to SQLite first and uploaded in the
background, so the till never waits on the network. A bill is only marked
synced when the *back office* names its id — a broker acknowledging receipt is
not the ledger accepting the sale.
Requires Flutter 3.27 / Dart 3.6 or newer. See [Version compatibility](#version-compatibility) if `flutter analyze` complains about theme types.
---
## Architecture
Clean architecture in three layers. Dependencies point inward only: `presentation → domain ← data`. The domain layer imports nothing from Flutter, which is what makes the pricing engine testable in isolation.
```
lib/
├── main.dart Entry point: orientation lock, store + audio warmup
├── app/
│ ├── app.dart MaterialApp.router, global shortcuts, text-scale lock
│ └── providers.dart Dependency injection graph, cashier session, clock
├── core/
│ ├── constants/ Store details, GST rates, loyalty ratios, asset paths
│ ├── theme/ Colours, 4pt spacing scale, typography, ThemeData
│ ├── router/ GoRouter routes and page transitions
│ ├── services/ Barcode capture, scanner beeps, thermal receipt PDF
│ ├── utils/ Formatters, validators, BuildContext extensions
│ └── widgets/ GlassCard, PrimaryButton, NumericKeypad, StatusPill
├── domain/ Pure Dart — no Flutter imports
│ ├── entities/ Product, Customer, Cart, SaleTransaction
│ ├── repositories/ Abstract contracts
│ └── usecases/ CheckoutSale
├── data/
│ ├── datasources/ LocalStore (swappable), seed catalogue
│ └── repositories/ Concrete implementations
└── presentation/ One folder per feature: screens / widgets / providers
├── welcome/ customer/ pos/ payment/ receipt/
```
### Why the domain layer is pure
`Cart` is an immutable value object that computes every figure on the bill as a getter — subtotal, per-slab GST, membership discount, loyalty redemption, round-off, points earned. No widget, repository or provider participates in the arithmetic. That means the money maths is covered by fast unit tests with no `pumpWidget` and no mocking, and swapping `LocalStore` for Hive or a REST backend changes nothing above the data layer.
---
## The five screens
| Screen | What it does |
|---|---|
| **Welcome** | Three entry paths: New Customer, Existing Customer, Skip (walk-in). Vector illustration painted in code, so it stays crisp at 4K. |
| **Customer Registration** | Mobile + name required; email, gender, DOB optional. Saves and drops straight into billing. |
| **Existing Customer** | Large numeric keypad, auto-searches on the 10th digit. Shows name, tier, points and lifetime spend, or offers Register / Walk-in when nothing matches. |
| **POS Dashboard** | Three columns: navigation, product grid, always-visible bill. Category chips, live search, barcode billing. |
| **Payment** | Cash, Card, UPI, Wallet, Gift Card and arbitrary splits. Live change calculation with denomination shortcuts. |
| **Receipt** | Success summary beside a paper-style preview. Counts down and starts the next sale on its own. |
---
## Fast cashier workflow
**Barcode billing has no dialogs.** `BarcodeService` listens to `HardwareKeyboard` globally rather than depending on a text field holding focus. It distinguishes a scanner from a human by keystroke timing — characters arriving faster than `barcodeScanTimeout` (120ms apart) are treated as a scan, so the cashier can still type into the same search box by hand. On a hit the item is added or its quantity incremented, a beep plays, and a floating toast confirms it. Nothing ever needs dismissing between items.
**Other workflow details:**
- Tapping a product card bills it immediately; the card shows a live quantity badge.
- Newest cart line renders first, mirroring what was just scanned.
- Every mutation goes through `CartController`, so scanner input, taps and shortcuts share one code path — and one undo stack (F8).
- Swipe a cart line to remove it.
- `F2` focuses search, `Esc` releases it.
- Bills can be parked and resumed.
- Stock is checked before adding, not after — an over-scan beeps and refuses rather than failing at checkout.
---
## Pricing engine
Prices are GST-inclusive, per Indian retail convention, so tax is extracted rather than added.
- **Per-product GST slabs.** Vegetables at 0%, milk at 5%, biscuits at 18%, aerated drinks at 28%. The receipt breaks GST out per slab, split into CGST and SGST.
- **Bill-level discounts are apportioned.** When a membership discount or manual discount reduces the bill, the GST charged is scaled by the same factor rather than left at the pre-discount figure.
- **Membership tiers are automatic.** Bronze / Silver / Gold / Platinum derive from lifetime spend and carry 0 / 2 / 5 / 8 percent off, applied without cashier action.
- **Loyalty.** One point per ₹10 spent; each point is worth ₹0.25 on redemption. Redemption is capped at both the balance held and the bill value, and re-clamps automatically if the bill shrinks after points were applied.
- **Round-off** to the nearest rupee is shown as its own line.
- All money passes through an `asMoney` extension that rounds to two decimals, so floating-point drift never reaches a total.
---
## Testing
```bash
flutter test
```
- `test/unit/cart_test.dart` — the pricing engine: GST extraction, discount stacking, apportionment, loyalty caps, tier thresholds, round-off, and the guarantee that a payable never goes negative.
- `test/unit/checkout_test.dart` — end-to-end sale completion against real repositories: stock decrement, invoice sequencing, split tenders, loyalty movement, and every rejection path. `LocalStore.reset()` re-seeds between tests so fixtures never leak.
- `test/unit/validators_test.dart` — mobile/email/name validation and formatter output.
- `test/widget/primary_button_test.dart` — button and status pill rendering, tap and busy states.
---
## Swapping the data layer
`LocalStore` is a process-local map that stands in for a database. To move to Hive, SQLite or an HTTP API, implement the three interfaces in `domain/repositories/` and rebind them in `app/providers.dart`:
```dart
final productRepositoryProvider = Provider<ProductRepository>(
(ref) => HiveProductRepository(ref.watch(hiveBoxProvider)),
);
```
Nothing in `domain/` or `presentation/` changes.
---
## Version compatibility
Two Flutter APIs used here moved recently:
- `ThemeData.cardTheme` / `dialogTheme` take `CardThemeData` / `DialogThemeData` on Flutter 3.29+. On older versions, drop the `Data` suffix in `lib/core/theme/app_theme.dart`.
- `Color.withValues(alpha:)` requires Flutter 3.27. On older versions substitute `withOpacity()`.
---
## Status and known gaps
**This project has not been compiled.** It was written in an environment without a Dart SDK or network access, so `flutter pub get`, `flutter analyze` and `flutter test` have never run against it. Verification was static: the import graph resolves with no missing or orphaned files, and every symbol referenced in the tests exists in `lib/`. That cannot catch type errors, signature mismatches, or drift in the pinned package APIs. Expect to fix a handful of issues on first build — the theme types above are the most likely.
Deliberately out of scope, stubbed as clear extension points:
- **Cash drawer** — `ReceiptService.openCashDrawer()` logs the ESC/POS kick sequence but does not send it. Wire in your printer's serial passthrough.
- **AI product search** — the search bar ranks by exact barcode, SKU, prefix then fuzzy match. No model is called.
- **Bottom navigation modules** — Dashboard, Products, Inventory, Customers, Promos, Reports, Suppliers and Settings render with live badge counts but are not routed.
- **Authentication** — `cashierSessionProvider` holds a hardcoded session.
- **Sounds** — the three bundled WAVs are synthesised placeholders. Replace with your own; `SoundService` swallows playback failures so a missing file never blocks billing.