import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../app/providers.dart'; import '../../../core/utils/extensions.dart'; import '../../../domain/entities/transaction.dart'; import '../../../domain/usecases/checkout_sale.dart'; import '../../pos/providers/cart_controller.dart'; import '../../pos/providers/catalog_providers.dart'; /// UI state for the payment screen. class PaymentState { const PaymentState({ this.splits = const [], this.activeMethod = PaymentMethod.cash, this.cashTendered = 0, this.reference = '', this.isProcessing = false, this.error, this.result, }); final List splits; final PaymentMethod activeMethod; final double cashTendered; final String reference; final bool isProcessing; final String? error; final CheckoutResult? result; double get settled => splits.fold(0.0, (sum, s) => sum + s.amount).asMoney; bool get isComplete => result != null; PaymentState copyWith({ List? splits, PaymentMethod? activeMethod, double? cashTendered, String? reference, bool? isProcessing, String? error, bool clearError = false, CheckoutResult? result, }) { return PaymentState( splits: splits ?? this.splits, activeMethod: activeMethod ?? this.activeMethod, cashTendered: cashTendered ?? this.cashTendered, reference: reference ?? this.reference, isProcessing: isProcessing ?? this.isProcessing, error: clearError ? null : (error ?? this.error), result: result ?? this.result, ); } } class PaymentController extends StateNotifier { PaymentController(this._ref) : super(const PaymentState()); final Ref _ref; double get _billTotal => _ref.read(cartControllerProvider).grandTotal; /// Amount still outstanding after the tenders recorded so far. double get balanceDue => (_billTotal - state.settled).clamp(0, double.infinity); double get changeDue { if (!state.activeMethod.needsChange) return 0; final diff = state.cashTendered - balanceDue; return diff > 0 ? diff.asMoney : 0; } bool get canConfirm { if (state.activeMethod.needsChange) { return state.cashTendered >= balanceDue && balanceDue > 0; } return balanceDue > 0; } void selectMethod(PaymentMethod method) { state = state.copyWith( activeMethod: method, cashTendered: 0, reference: '', clearError: true, ); } void setCashTendered(double amount) => state = state.copyWith(cashTendered: amount, clearError: true); /// Adds to the tendered amount — powers the quick-cash denomination chips. void addCash(double amount) => setCashTendered(state.cashTendered + amount); /// Fills the exact balance, the most common cash case. void tenderExact() => setCashTendered(balanceDue); void setReference(String value) => state = state.copyWith(reference: value, clearError: true); /// Records the active tender. For a split payment, call this once per part. void addSplit({double? amount}) { final value = (amount ?? balanceDue).clamp(0, balanceDue).toDouble(); if (value <= 0) return; final split = PaymentSplit( method: state.activeMethod, amount: value.asMoney, tendered: state.activeMethod.needsChange ? (state.cashTendered > 0 ? state.cashTendered : value) : null, reference: state.reference.trim().isEmpty ? null : state.reference.trim(), ); state = state.copyWith( splits: [...state.splits, split], cashTendered: 0, reference: '', clearError: true, ); } void removeSplit(int index) { final next = [...state.splits]..removeAt(index); state = state.copyWith(splits: next); } void clearSplits() => state = state.copyWith(splits: const []); /// Finalises the sale. On success the caller navigates to the receipt. Future confirm() async { if (state.isProcessing) return null; // A single-tender sale needn't be staged first — fold it in automatically. var splits = state.splits; if (splits.isEmpty || balanceDue > 0.01) { addSplit(); splits = state.splits; } state = state.copyWith(isProcessing: true, clearError: true); try { final cart = _ref.read(cartControllerProvider); final session = _ref.read(cashierSessionProvider); final result = await _ref.read(checkoutSaleProvider)( cart: cart, payments: splits, cashierName: session.name, ); state = state.copyWith(isProcessing: false, result: result); // Fire and forget — printing must never block the next sale. final receipts = _ref.read(receiptServiceProvider); unawaited(receipts.printDirect(result.transaction)); unawaited(receipts.openCashDrawer()); unawaited(_ref.read(soundServiceProvider).saleComplete()); // Stock changed, so the grid must refresh. _ref.invalidate(allProductsProvider); _ref.invalidate(visibleProductsProvider); return result; } on CheckoutFailure catch (e) { state = state.copyWith(isProcessing: false, error: e.message); return null; } catch (e) { state = state.copyWith( isProcessing: false, error: 'Could not complete the sale. $e', ); return null; } } void reset() => state = const PaymentState(); } final paymentControllerProvider = StateNotifierProvider.autoDispose( (ref) => PaymentController(ref), );