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/datasources/remote_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/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); /// Simulated back-office endpoints. Held as singletons so the offline toggle /// in Settings affects every call. final remoteCatalogueProvider = Provider( (ref) => RemoteCatalogueSource( isOffline: () => ref.read(simulateOfflineProvider), ), ); // ------------------------------------------------------------------- Sync /// How this terminal reaches the back office. /// /// Defaults to the simulated route so a fresh install is usable with no broker /// and no endpoint; Settings re-points it. /// /// Store and terminal ids always come 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( 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(remoteCatalogueProvider), 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, ), ); // ------------------------------------------------------------- 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()); });