import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/config/sync_config.dart'; import '../core/services/connectivity_service.dart'; import '../core/services/receipt_service.dart'; import '../core/services/sound_service.dart'; import '../data/datasources/local_store.dart'; import '../data/repositories/customer_repository_impl.dart'; import '../data/repositories/product_repository_impl.dart'; import '../data/remote/catalogue_source.dart'; import '../data/remote/http_catalogue_source.dart'; import '../data/remote/simulated_catalogue_source.dart'; import '../data/remote/http_order_transport.dart'; import '../data/remote/mqtt_order_transport.dart'; import '../data/remote/order_transport.dart'; import '../data/remote/simulated_order_transport.dart'; import '../data/repositories/store_repository_impl.dart'; import '../data/repositories/sync_repository_impl.dart'; import '../data/repositories/transaction_repository_impl.dart'; import '../data/local/terminal_identity.dart'; import '../data/sync/sync_engine.dart'; import '../domain/repositories/customer_repository.dart'; import '../domain/repositories/product_repository.dart'; import '../domain/repositories/sync_repository.dart'; import '../domain/repositories/transaction_repository.dart'; import '../domain/entities/promo.dart'; import '../domain/entities/store_account.dart'; import '../domain/usecases/checkout_sale.dart'; import '../presentation/auth/providers/auth_controller.dart'; /// Root data source. Overridden in tests with an in-memory double. final localStoreProvider = Provider((ref) => LocalStore.instance); // ---------------------------------------------------------- Repositories final productRepositoryProvider = Provider( (ref) => ProductRepositoryImpl(ref.watch(localStoreProvider)), ); final customerRepositoryProvider = Provider( (ref) => CustomerRepositoryImpl(ref.watch(localStoreProvider)), ); final transactionRepositoryProvider = Provider( (ref) => TransactionRepositoryImpl(ref.watch(localStoreProvider)), ); /// Mirrors the Settings "Simulate offline" switch so the header can show it. /// /// Without this the terminal claims LIVE while every call is being failed on /// purpose, which reads as a real network fault. final simulateOfflineProvider = StateProvider((ref) => false); /// Where products and customers come from. /// /// Rebuilt when the route changes, and the old one closed, so a re-pointed /// terminal does not keep a stale client alive. final catalogueSourceProvider = Provider((ref) { final config = ref.watch(syncConfigProvider); final source = switch (config.transport) { // MQTT carries the *notification* that the catalogue moved; the catalogue // itself is a bulk pull, which is an HTTP job. A broker is the wrong shape // for tens of thousands of rows. TransportKind.http || TransportKind.mqtt => config.httpBaseUrl.isEmpty ? SimulatedCatalogueSource( isOffline: () => ref.read(simulateOfflineProvider), ) : HttpCatalogueSource(config: config), TransportKind.simulated => SimulatedCatalogueSource( isOffline: () => ref.read(simulateOfflineProvider), ), }; ref.onDispose(source.dispose); return source; }); // ------------------------------------------------------------------- Sync /// How this terminal reaches the back office. /// /// Defaults to this store's live HTTP endpoint, so importing works out of the /// box against real products rather than the offline demo catalogue. /// Settings → Connectivity & sync → Configure re-points it to a different /// store, endpoint, or transport without a rebuild. /// /// Terminal id always comes from this device's own identity, never from a /// literal — two terminals publishing on the same topic is the failure this /// exists to prevent. final syncConfigProvider = StateProvider((ref) { final terminal = ref.watch(terminalIdentityProvider); return SyncConfig( transport: TransportKind.http, httpBaseUrl: 'https://fiesta.nearle.app/live/api/v1/pos', storeId: terminal.storeId, terminalId: terminal.code, ); }); /// Real network state, folded with the Settings offline switch. final connectivityServiceProvider = Provider((ref) { final service = ConnectivityService( isSimulatedOffline: () => ref.read(simulateOfflineProvider), ); ref.onDispose(service.dispose); return service; }); /// The wire itself. Rebuilt when the configuration changes, and the old one is /// closed so a re-pointed terminal does not keep a stale broker session open. final orderTransportProvider = Provider((ref) { final config = ref.watch(syncConfigProvider); final transport = switch (config.transport) { TransportKind.mqtt => MqttOrderTransport(config: config), TransportKind.http => HttpOrderTransport(config: config), TransportKind.simulated => SimulatedOrderTransport( isOffline: () => ref.read(simulateOfflineProvider), ), }; ref.onDispose(transport.dispose); return transport; }); final syncRepositoryProvider = Provider( (ref) => SyncRepositoryImpl( ref.watch(localStoreProvider), ref.watch(catalogueSourceProvider), ref.watch(orderTransportProvider), batchSize: ref.watch(syncConfigProvider).batchSize, ), ); /// Decides when bills are uploaded. Started once, by the app shell. final syncEngineProvider = Provider((ref) { final transport = ref.watch(orderTransportProvider); final engine = SyncEngine( repository: ref.watch(syncRepositoryProvider), connectivity: ref.watch(connectivityServiceProvider).onlineChanges, downlink: transport.downlink, onCatalogueChanged: () async { await ref.read(syncRepositoryProvider).importCatalogue(); ref.invalidate(localStoreProvider); }, ); ref.onDispose(engine.dispose); return engine; }); /// Live engine state for the header pill and the events screen. final syncEngineStateProvider = StreamProvider((ref) { final engine = ref.watch(syncEngineProvider); return engine.states.map((s) => s); }); /// Store details and staff, read from this terminal's database. final storeRepositoryProvider = Provider( (ref) => StoreRepositoryImpl(ref.watch(localStoreProvider)), ); /// The outlet, refreshed whenever staff or details change. final storeAccountProvider = FutureProvider( (ref) => ref.watch(storeRepositoryProvider).load( email: DemoCredentials.email, ), ); /// Campaigns stored on this terminal. final promosProvider = FutureProvider>( (ref) => ref.watch(localStoreProvider).promos.all(), ); /// Only the campaigns the till should be applying right now. final activePromosProvider = FutureProvider>( (ref) => ref.watch(localStoreProvider).promos.all(activeOnly: true), ); // ------------------------------------------------------------- Use cases final checkoutSaleProvider = Provider( (ref) => CheckoutSale( productRepository: ref.watch(productRepositoryProvider), customerRepository: ref.watch(customerRepositoryProvider), transactionRepository: ref.watch(transactionRepositoryProvider), ), ); // -------------------------------------------------------------- Services final soundServiceProvider = Provider((ref) => SoundService.instance); final receiptServiceProvider = Provider((ref) => ReceiptService.instance); // --------------------------------------------------------------- Session class CashierSession { const CashierSession({ required this.name, required this.role, required this.terminalId, }); final String name; final String role; final String terminalId; } /// Identity of the physical till, read from its own database. /// /// Falls back only before the store has opened; every real read happens after /// `LocalStore.init`, which mints the identity if this device has never run /// before. final terminalIdentityProvider = Provider((ref) { final store = ref.watch(localStoreProvider); return store.isReady ? store.terminal : const TerminalIdentity( deviceId: 'unopened', code: 'T0000', name: 'Terminal', storeId: 'store-01', ); }); final cashierSessionProvider = StateProvider((ref) { final terminal = ref.watch(terminalIdentityProvider); return CashierSession( name: 'Suriya', role: 'ADMIN', terminalId: terminal.code, ); }); /// Ticks once a minute to drive the header clock without rebuilding on every /// frame. final clockProvider = StreamProvider((ref) async* { yield DateTime.now(); yield* Stream.periodic(const Duration(seconds: 20), (_) => DateTime.now()); });