import 'package:flutter_test/flutter_test.dart'; import 'package:nearle_pos/data/datasources/local_store.dart'; import 'package:nearle_pos/data/datasources/seed_data.dart'; import 'package:nearle_pos/data/repositories/customer_repository_impl.dart'; import 'package:nearle_pos/data/repositories/product_repository_impl.dart'; import 'package:nearle_pos/data/repositories/transaction_repository_impl.dart'; import 'package:nearle_pos/domain/entities/cart.dart'; import 'package:nearle_pos/domain/entities/customer.dart'; import 'package:nearle_pos/domain/entities/transaction.dart'; import 'package:nearle_pos/domain/usecases/checkout_sale.dart'; void main() { late LocalStore store; late ProductRepositoryImpl products; late CustomerRepositoryImpl customers; late TransactionRepositoryImpl transactions; late CheckoutSale checkout; setUpAll(() { // The store only reaches for demo rows through these hooks, so the seed has // to be registered before any test asks for a catalogue. LocalStore.registerSeed( products: SeedData.products, customers: SeedData.customers, ); }); setUp(() async { store = LocalStore.instance; // Fresh seed for every test so stock and invoice sequence never leak. await store.reset(withCatalogue: true); products = ProductRepositoryImpl(store); customers = CustomerRepositoryImpl(store); transactions = TransactionRepositoryImpl(store); checkout = CheckoutSale( productRepository: products, customerRepository: customers, transactionRepository: transactions, ); }); /// Amul Milk 1L: 62.00, 5% GST, 50 in stock. Future milkCart({int qty = 2, String? customerId}) async { final milk = (await products.findByBarcode('8901234500011'))!; final customer = customerId == null ? null : await customers.findById(customerId); return Cart( lines: [CartLine(product: milk, quantity: qty.toDouble())], customer: customer, ); } group('successful checkout', () { test('records the sale and returns an invoice', () async { final cart = await milkCart(); expect(cart.grandTotal, 124); final result = await checkout( cart: cart, payments: const [ PaymentSplit(method: PaymentMethod.cash, amount: 124, tendered: 200), ], cashierName: 'Suriya', terminalId: 'T0TEST', ); final txn = result.transaction; expect(txn.total, 124); expect(txn.amountPaid, 124); expect(txn.changeDue, 76); expect(txn.isFullySettled, isTrue); expect(txn.isSplit, isFalse); expect(txn.invoiceNumber, contains('00001')); expect(txn.status, TransactionStatus.completed); }); test('decrements stock by the quantity sold', () async { final cart = await milkCart(); await checkout( cart: cart, payments: const [ PaymentSplit(method: PaymentMethod.cash, amount: 124, tendered: 124), ], cashierName: 'Suriya', terminalId: 'T0TEST', ); final milk = await products.findByBarcode('8901234500011'); expect(milk!.stock, 48); }); test('persists the transaction to history', () async { final cart = await milkCart(); await checkout( cart: cart, payments: const [ PaymentSplit(method: PaymentMethod.cash, amount: 124, tendered: 124), ], cashierName: 'Suriya', terminalId: 'T0TEST', ); final history = await transactions.history(); expect(history, hasLength(1)); expect(await transactions.salesTotalForDay(DateTime.now()), 124); }); test('accepts a split across two tenders', () async { final cart = await milkCart(); final result = await checkout( cart: cart, payments: const [ PaymentSplit(method: PaymentMethod.cash, amount: 100, tendered: 100), PaymentSplit( method: PaymentMethod.upi, amount: 24, reference: 'UPI-991', ), ], cashierName: 'Suriya', terminalId: 'T0TEST', ); expect(result.transaction.isSplit, isTrue); expect(result.transaction.amountPaid, 124); expect(result.transaction.paymentSummary, 'Cash + UPI'); }); test('moves loyalty points and lifetime spend for a member', () async { final cart = await milkCart(customerId: 'c001'); // 124 less the 2% silver discount, rounded. expect(cart.membershipDiscountAmount, 2.48); expect(cart.grandTotal, 122); expect(cart.pointsEarned, 12); final result = await checkout( cart: cart, payments: const [ PaymentSplit(method: PaymentMethod.cash, amount: 122, tendered: 200), ], cashierName: 'Suriya', terminalId: 'T0TEST', ); final updated = result.updatedCustomer!; expect(updated.loyaltyPoints, 332); // 320 + 12 expect(updated.lifetimeSpend, 24622); // 24500 + 122 expect(updated.visitCount, 42); }); }); group('rejected checkout', () { test('refuses an empty cart', () async { expect( () => checkout( cart: Cart.empty, payments: const [ PaymentSplit(method: PaymentMethod.cash, amount: 0), ], cashierName: 'Suriya', terminalId: 'T0TEST', ), throwsA(isA()), ); }); test('refuses a bill with no tender', () async { final cart = await milkCart(); expect( () => checkout( cart: cart, payments: const [], cashierName: 'Suriya', terminalId: 'T0TEST', ), throwsA(isA()), ); }); test('refuses an underpayment', () async { final cart = await milkCart(); expect( () => checkout( cart: cart, payments: const [ PaymentSplit(method: PaymentMethod.cash, amount: 100, tendered: 100), ], cashierName: 'Suriya', terminalId: 'T0TEST', ), throwsA(isA()), ); }); test('refuses to sell more than is in stock', () async { final cart = await milkCart(qty: 999); expect( () => checkout( cart: cart, payments: [ PaymentSplit( method: PaymentMethod.cash, amount: cart.grandTotal, tendered: cart.grandTotal, ), ], cashierName: 'Suriya', terminalId: 'T0TEST', ), throwsA(isA()), ); }); test('leaves stock untouched when validation fails', () async { final cart = await milkCart(); try { await checkout( cart: cart, payments: const [], cashierName: 'Suriya', terminalId: 'T0TEST', ); } on CheckoutFailure { // expected } final milk = await products.findByBarcode('8901234500011'); expect(milk!.stock, 50); }); }); group('repositories', () { test('finds a customer by mobile number', () async { final found = await customers.findByMobile('9876543210'); expect(found?.name, 'Abhishek'); expect(await customers.findByMobile('0000000000'), isNull); }); test('rejects a duplicate mobile number on create', () async { // 9876543210 already belongs to the seeded customer c001. expect( () => customers.create( const Customer(id: '', name: 'Impostor', mobile: '9876543210'), ), throwsA(isA()), ); }); test('creates a customer with a generated id and zeroed loyalty', () async { final created = await customers.create( const Customer(id: '', name: 'New Shopper', mobile: '9000011111'), ); expect(created.id, isNotEmpty); expect(created.loyaltyPoints, 0); expect(created.lifetimeSpend, 0); expect(created.tier, MembershipTier.bronze); expect(await customers.findByMobile('9000011111'), isNotNull); }); test('ranks an exact barcode above a fuzzy name match', () async { final results = await products.search('8901234500011'); expect(results.first.name, 'Amul Milk 1L'); }); test('parks and resumes a bill', () async { final cart = await milkCart(); await transactions.park( ParkedBill(id: 'park-1', cart: cart, parkedAt: DateTime.now()), ); expect(await transactions.parkedBills(), hasLength(1)); await transactions.removeParked('park-1'); expect(await transactions.parkedBills(), isEmpty); }); }); }