Files
nearle_pos/lib/data/datasources/local_store.dart
2026-07-29 11:41:53 +05:30

137 lines
4.3 KiB
Dart

import '../../domain/entities/customer.dart';
import '../../domain/entities/product.dart';
import '../../domain/entities/sync_event.dart';
import '../../domain/entities/transaction.dart';
import 'seed_data.dart';
/// On-terminal storage.
///
/// The terminal starts with an **empty catalogue**: nothing can be billed
/// until the cashier imports products. Everything after that — sales, parked
/// bills, queued events — lives here and survives without a connection.
///
/// Swapping this for Hive or SQLite changes nothing above the data layer.
class LocalStore {
LocalStore._();
static final LocalStore instance = LocalStore._();
final Map<String, Product> _products = {};
final Map<String, Customer> _customers = {};
final List<SaleTransaction> _transactions = [];
final List<ParkedBill> _parked = [];
final List<SyncEvent> _events = [];
int _invoiceSequence = 0;
DateTime? _lastImportAt;
String? _catalogueRevision;
/// Nothing to seed — the catalogue arrives via import.
Future<void> init() async {}
/// Test helper. Clears everything and optionally loads the demo catalogue
/// so fixtures don't have to run an import first.
Future<void> reset({bool withCatalogue = false}) async {
_products.clear();
_customers.clear();
_transactions.clear();
_parked.clear();
_events.clear();
_invoiceSequence = 0;
_lastImportAt = null;
_catalogueRevision = null;
if (withCatalogue) {
importCatalogue(
products: SeedData.products(),
customers: SeedData.customers(),
revision: 'seed',
at: DateTime.now(),
);
}
}
// -------------------------------------------------------------- Catalogue
/// True once a catalogue has been pulled. The POS refuses to bill until so.
bool get hasCatalogue => _products.isNotEmpty;
DateTime? get lastImportAt => _lastImportAt;
String? get catalogueRevision => _catalogueRevision;
/// Replaces the catalogue wholesale.
///
/// Stock levels already adjusted by local sales are preserved for products
/// that survive the re-import, so importing mid-shift does not resurrect
/// stock that has been sold.
void importCatalogue({
required List<Product> products,
required List<Customer> customers,
required String revision,
required DateTime at,
}) {
final priorStock = {
for (final p in _products.values) p.id: p.stock,
};
_products
..clear()
..addEntries(products.map((p) {
final held = priorStock[p.id];
return MapEntry(p.id, held == null ? p : p.copyWith(stock: held));
}));
// Locally registered customers must not be wiped by a server pull.
for (final c in customers) {
_customers.putIfAbsent(c.id, () => c);
}
_lastImportAt = at;
_catalogueRevision = revision;
}
// --------------------------------------------------------------- Products
List<Product> get products => _products.values.toList(growable: false);
void putProduct(Product p) => _products[p.id] = p;
Product? productById(String id) => _products[id];
// -------------------------------------------------------------- Customers
List<Customer> get customers => _customers.values.toList(growable: false);
void putCustomer(Customer c) => _customers[c.id] = c;
Customer? customerById(String id) => _customers[id];
// ----------------------------------------------------------- Transactions
List<SaleTransaction> get transactions =>
List.unmodifiable(_transactions.reversed);
void addTransaction(SaleTransaction t) => _transactions.add(t);
int nextInvoiceSequence() => ++_invoiceSequence;
// ------------------------------------------------------------ Parked bills
List<ParkedBill> get parked => List.unmodifiable(_parked);
void addParked(ParkedBill b) => _parked.add(b);
void removeParked(String id) => _parked.removeWhere((b) => b.id == id);
// ------------------------------------------------------------ Sync events
List<SyncEvent> get events => List.unmodifiable(_events.reversed);
void addEvent(SyncEvent e) => _events.add(e);
/// Updates in place. Events are never removed, so a failed push stays
/// visible and retryable.
void updateEvent(SyncEvent e) {
final i = _events.indexWhere((x) => x.id == e.id);
if (i >= 0) _events[i] = e;
}
bool get hasUnsyncedEvents =>
_events.any((e) => e.status != SyncStatus.synced);
}