import 'package:equatable/equatable.dart'; import '../../core/utils/extensions.dart'; import 'cart.dart'; import 'customer.dart'; enum PaymentMethod { cash('Cash', '💵', true), card('Card', '💳', false), upi('UPI', '📱', false), wallet('Wallet', '👛', false), giftCard('Gift Card', '🎁', false), loyalty('Loyalty Points', '⭐', false); const PaymentMethod(this.label, this.emoji, this.needsChange); final String label; final String emoji; /// Only cash tenders can be over-paid and produce change. final bool needsChange; /// Non-cash tenders normally capture a reference number. bool get needsReference => this == PaymentMethod.card || this == PaymentMethod.upi || this == PaymentMethod.giftCard; } /// A single tender against a bill. A split payment holds several of these. class PaymentSplit extends Equatable { const PaymentSplit({ required this.method, required this.amount, this.tendered, this.reference, }); final PaymentMethod method; /// Amount settled by this tender. final double amount; /// Cash handed over — may exceed [amount]. final double? tendered; /// Card approval code, UPI txn id, gift card number. final String? reference; double get change { if (!method.needsChange || tendered == null) return 0; final diff = tendered! - amount; return diff > 0 ? diff.asMoney : 0; } @override List get props => [method, amount, tendered, reference]; } enum TransactionStatus { completed, parked, voided, refunded } /// An immutable record of a finished sale. class SaleTransaction extends Equatable { const SaleTransaction({ required this.id, required this.invoiceNumber, required this.cart, required this.payments, required this.createdAt, required this.cashierName, this.status = TransactionStatus.completed, this.terminalId = 'TERM-01', this.storedTotal, this.storedPointsEarned, }); final String id; final String invoiceNumber; final Cart cart; final List payments; final DateTime createdAt; final String cashierName; final TransactionStatus status; final String terminalId; /// The figure actually charged, as recorded at sale time. /// /// Set only when a bill is read back from storage. Deriving the total from /// [cart] is correct for a live sale, but a rehydrated cart is a /// reconstruction — if it ever loses a component the money must not move with /// it. The recorded value wins whenever there is one. final double? storedTotal; /// Points issued by this sale, as recorded at sale time. See [storedTotal]. final int? storedPointsEarned; Customer? get customer => cart.customer; double get total => storedTotal ?? cart.grandTotal; double get amountPaid => payments.fold(0.0, (sum, p) => sum + p.amount).asMoney; double get amountTendered => payments .fold(0.0, (sum, p) => sum + (p.tendered ?? p.amount)) .asMoney; double get changeDue => payments.fold(0.0, (sum, p) => sum + p.change).asMoney; double get balanceDue => (total - amountPaid).clamp(0, double.infinity); bool get isFullySettled => balanceDue <= 0.001; bool get isSplit => payments.length > 1; int get pointsEarned => storedPointsEarned ?? cart.pointsEarned; int get pointsRedeemed => cart.pointsRedeemed; String get paymentSummary => payments.map((p) => p.method.label).toSet().join(' + '); @override List get props => [id, invoiceNumber, createdAt, status]; } /// A bill set aside so the cashier can serve the next shopper. class ParkedBill extends Equatable { const ParkedBill({ required this.id, required this.cart, required this.parkedAt, this.label, }); final String id; final Cart cart; final DateTime parkedAt; final String? label; String get displayLabel => label ?? cart.customer?.name ?? 'Walk-in #${id.substring(0, 4)}'; @override List get props => [id, parkedAt]; }