137 lines
4.3 KiB
Dart
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);
|
|
}
|