# 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 # 60+ unit and widget tests flutter analyze ``` 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( (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.