import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/data/accepted_store.dart'; import 'package:miler/data/bag_manifest.dart'; import 'package:miler/data/service_profile.dart'; /// ───────────────────────────────────────────────────────────────────────── /// THE DELIVERY QUEUE, AND THE BAG THAT SURVIVES IT /// /// Two promises are made at a kitchen counter and both have to hold all the way /// to a doorstep: /// /// 1. **Five orders picked is five deliveries** — no order goes missing in the /// hand-off, and nothing appears that was not collected. /// 2. **Joe is Bag 1 and stays Bag 1** — even after Joe is delivered and the /// list underneath him shrinks. /// /// The second one is the subtle one, and it is why bag identity is *stored* at /// pickup rather than recomputed from whatever is on screen. A position-derived /// bag number silently renames every remaining order the moment one leaves the /// list, which sends the rider looking for a bag that is already in somebody's /// hallway. /// ───────────────────────────────────────────────────────────────────────── void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() => SharedPreferences.setMockInitialValues({})); tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); Map order(String id, String customer) => { 'orderid': id, 'pickupcustomer': customer, 'kitchenname': 'Vidhya Kitchen', 'kitchenid': 'K1', 'quantity': 3, }; final five = [ order('1', 'Joe'), order('2', 'Arun'), order('3', 'Priya'), order('4', 'Kumar'), order('5', 'Ravi'), ]; group('the hand-off', () { test( 'five picked orders become five deliveries, each with its bag', () async { ServiceProfile.setActive(ServiceProfile.meals); // What the pickup confirmation writes when the slide lands. final manifest = BagManifest.forGroup(five); await addCollectedOrderIds([for (final l in manifest) l.orderId]); await saveBagLabels({for (final l in manifest) l.orderId: l.bag}); final carrying = await getCollectedOrderIds(); final bags = await getBagLabels(); expect(carrying.length, 5, reason: 'five picked is five to deliver'); expect(bags.length, 5, reason: 'and five bags, one each'); expect(bags['1'], 'Bag 1'); expect(bags['5'], 'Bag 5'); // No aggregate anywhere: the count of deliveries and the count of bags // are the same set, not two numbers that can drift. expect(bags.keys.toSet(), carrying); }, ); test('a bag the counter could not supply never becomes a delivery', () async { ServiceProfile.setActive(ServiceProfile.meals); // Four handed over, one short — the shortfall path the pickup sheet owns. final loaded = five.where((o) => o['orderid'] != '3').toList(); await addCollectedOrderIds([ for (final o in loaded) o['orderid'] as String, ]); await saveBagLabels({ for (final l in BagManifest.forGroup(loaded)) l.orderId: l.bag, }); await addNotLoadedOrderIds(['3']); expect(await getCollectedOrderIds(), {'1', '2', '4', '5'}); expect(await getNotLoadedOrderIds(), {'3'}); // A short pick is its own record. It is not a skip: nobody was visited // and the customer did nothing. expect(await getSkippedOrderIds(), isEmpty); // And it is not carrying a bag, because it was never in the box. expect((await getBagLabels()).containsKey('3'), isFalse); }); }); group('bag identity outlives the queue', () { test('delivering the first does not renumber the rest', () async { ServiceProfile.setActive(ServiceProfile.meals); await saveBagLabels({ for (final l in BagManifest.forGroup(five)) l.orderId: l.bag, }); // Joe is delivered and leaves the carrying set. await addCollectedOrderIds([ for (final o in five) o['orderid'] as String, ]); await removeCollectedOrderIds(['1']); final bags = await getBagLabels(); expect(await getCollectedOrderIds(), {'2', '3', '4', '5'}); // Arun does *not* become Bag 1 because the list in front of him shrank. expect(bags['2'], 'Bag 2'); expect(bags['3'], 'Bag 3'); expect(bags['5'], 'Bag 5'); }); test('a second kitchen adds to the map rather than replacing it', () async { ServiceProfile.setActive(ServiceProfile.meals); await saveBagLabels({ for (final l in BagManifest.forGroup(five.take(2).toList())) l.orderId: l.bag, }); // A different counter, its own Bag 1 and Bag 2. final second = [order('9', 'Meena'), order('10', 'Sai')]; await saveBagLabels({ for (final l in BagManifest.forGroup(second)) l.orderId: l.bag, }); final bags = await getBagLabels(); expect(bags.length, 4, reason: 'the first load must survive the second'); expect(bags['1'], 'Bag 1'); expect(bags['9'], 'Bag 1', reason: 'each counter counts its own shelf'); expect(bags['10'], 'Bag 2'); }); test('a skipped delivery keeps its bag so it can be resumed', () async { ServiceProfile.setActive(ServiceProfile.meals); await saveBagLabels({ for (final l in BagManifest.forGroup(five)) l.orderId: l.bag, }); await addSkippedBooking(five[2], reason: 'Customer unavailable'); expect(await getSkippedOrderIds(), contains('3')); // And the reason survives with it — the hub needs to know why, and // "Failed" is not a reason. final skipped = await getSkippedBookings(); expect( skipped.firstWhere((b) => b['orderid'] == '3')['skipreason'], 'Customer unavailable', ); // He is still carrying Priya's lunch — the bag identity is exactly what // he needs when he comes back to it. expect((await getBagLabels())['3'], 'Bag 3'); }); test("a day's run is wiped so bags never leak into tomorrow", () async { await saveBagLabels({'1': 'Bag 1'}); await clearServiceRunState(); expect(await getBagLabels(), isEmpty); }); }); test('a demo bag never outlives the build that wrote it', () async { // The mock day's ids are `MOCK-…` so the launch purge can find them. The // bag map is keyed by order id, so it is swept by the same rule — a stored // pairing is trusted later *because* it is stored, and a stale one is worse // than none. await saveBagLabels({'MOCK-M-1001': 'Bag 1', '77': 'Bag 2'}); await purgeDemoRecords(); final bags = await getBagLabels(); expect(bags.containsKey('MOCK-M-1001'), isFalse); expect(bags['77'], 'Bag 2', reason: 'real work is untouched'); }); }