second commit
This commit is contained in:
187
lib/presentation/payment/providers/payment_controller.dart
Normal file
187
lib/presentation/payment/providers/payment_controller.dart
Normal file
@@ -0,0 +1,187 @@
|
||||
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<PaymentSplit> 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<PaymentSplit>? 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<PaymentState> {
|
||||
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<CheckoutResult?> 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<PaymentController, PaymentState>(
|
||||
(ref) => PaymentController(ref),
|
||||
);
|
||||
Reference in New Issue
Block a user