import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/Models/stop_status.dart'; import 'package:miler/data/accepted_store.dart'; import 'package:miler/data/api_config.dart'; import 'package:miler/data/milk_run.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/pickups/stop_type.dart'; /// ───────────────────────────────────────────────────────────────────────── /// THE BRIEF'S OWN SCENARIO, WALKED THROUGH /// /// Six orders, three kitchens: /// /// ``` /// Vidhya ×3 ┐ /// Priyanka ×2 ├─ accept all six (per order, across kitchens) /// ABC ×1 ┘ /// │ /// ├─ Vidhya: arrived → picked ×3 ─▶ DELIVERIES (Priyanka/ABC stay) /// ├─ Priyanka: arrived → picked ×2 ─▶ DELIVERIES /// └─ ABC: arrived → picked ×1 ─▶ DELIVERIES /// │ /// arrived → delivered ─▶ ACTIVITY /// ``` /// /// The three rules this file holds, all of which were got wrong at some point: /// /// 1. **Acceptance is per order.** Six accepted orders stay six visible cards; /// they are not replaced by three kitchen cards. /// 2. **Pickup is per kitchen.** One counter at a time — and finishing one /// kitchen must NOT wait for the others. /// 3. **`picked` is the middle of the morning.** It goes to DELIVERIES, and /// only `delivered` reaches Activity. /// ───────────────────────────────────────────────────────────────────────── void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { SharedPreferences.setMockInitialValues({}); ServiceProfile.setActive(ServiceProfile.milkMan); }); tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); Map order({ required String id, required String kitchen, required String kitchenId, String status = 'assigned', bool withDrop = true, }) => { 'orderid': id, 'pickupid': int.parse(id), 'orderstatus': status, 'sourceid': kitchenId, 'sourcename': kitchen, 'pickupaddress': '$kitchen, Trichy Road', 'pickupcustomer': 'Receiver $id', 'pickupcontactno': '98765432$id', 'quantity': 1, 'pickuplat': 11.01, 'pickuplon': 76.95, if (withDrop) 'droplat': 11.05, if (withDrop) 'droplon': 76.99, if (withDrop) 'dropaddress': 'Flat $id, Anna Nagar', }; /// The brief's six. List> sixOrders() => [ order(id: '101', kitchen: 'Vidhya Kitchen', kitchenId: 'K1'), order(id: '102', kitchen: 'Vidhya Kitchen', kitchenId: 'K1'), order(id: '103', kitchen: 'Vidhya Kitchen', kitchenId: 'K1'), order(id: '104', kitchen: 'Priyanka Kitchen', kitchenId: 'K2'), order(id: '105', kitchen: 'Priyanka Kitchen', kitchenId: 'K2'), order(id: '106', kitchen: 'ABC Kitchen', kitchenId: 'K3'), ]; Set idsOf(Iterable> s) => s.map(MilkRun.idOf).toSet(); group('1–7 · six orders arrive, all six are accepted, all six stay', () { test('acceptance is per order and crosses kitchens', () { final stops = sixOrders(); final accepted = idsOf(stops); expect( accepted.length, 6, reason: 'he takes on the whole morning in one press, whichever ' 'counters it comes from', ); }); test('six accepted orders remain six — not three kitchen cards', () { final stops = sixOrders(); final accepted = idsOf(stops); // Every accepted order is still individually addressable and still // outstanding work. Collapsing them into one card per kitchen would lose // the receiver, the drop address and the order id — the things the rider // reads on the card. final outstanding = MilkRun.outstandingPickups( stops, acceptedIds: accepted, collectedIds: const {}, ); expect(outstanding.length, 6); expect(idsOf(outstanding), accepted); for (final s in outstanding) { expect(MilkRun.sourceNameOf(s), isNotEmpty); expect(s['pickupcustomer'], isNotEmpty); expect(s['pickupcontactno'], isNotEmpty); } }); test('but they are still knowable as three kitchens', () { final stops = sixOrders(); final keys = stops.map(MilkRun.sourceKeyOf).toSet(); expect(keys.length, 3); expect(MilkRun.sameSourceAs(stops[0], stops).length, 3); // Vidhya expect(MilkRun.sameSourceAs(stops[3], stops).length, 2); // Priyanka expect(MilkRun.sameSourceAs(stops[5], stops).length, 1); // ABC }); }); group('9–11 · navigate and the details sheet', () { test('any Vidhya order navigates to Vidhya Kitchen', () { final stops = sixOrders(); // He does not need a special kitchen card — any of the three will do. for (final s in MilkRun.sameSourceAs(stops[0], stops)) { expect(MilkRun.navigatesToCustomer(s), isFalse); expect(MilkRun.navigationLabel(s), 'Vidhya Kitchen'); expect(MilkRun.navigationTarget(s)?.lat, 11.01); } }); test('the sheet has everything it needs, from the payload', () { // Nothing here is invented; each is a field the adapter carries. final s = sixOrders().first; expect(MilkRun.sourceNameOf(s), 'Vidhya Kitchen'); // pickup kitchen expect(s['pickupaddress'], isNotEmpty); // pickup address expect(s['pickupcustomer'], 'Receiver 101'); // receiver expect(s['pickupcontactno'], isNotEmpty); // phone → Call expect(s['dropaddress'], isNotEmpty); // drop address expect(MilkRun.idOf(s), '101'); // order id expect(s['quantity'], 1); // quantity expect(stopStatusOf(s), StopStatus.assigned); // status }); test('a missing phone is absent, never fabricated', () { final s = sixOrders().first..remove('pickupcontactno'); expect( s['pickupcontactno'], isNull, reason: 'the Call action hides rather than dialling a made-up number', ); }); }); group('12–17 · Vidhya: arrive, pick, and straight to DELIVERIES', () { test('the pickup set is bounded to one kitchen', () { final stops = sixOrders(); final vidhya = MilkRun.sameSourceAs(stops[0], stops); expect(idsOf(vidhya), {'101', '102', '103'}); expect( idsOf(vidhya).intersection({'104', '105', '106'}), isEmpty, reason: 'Priyanka and ABC are different rides and different counters', ); }); test('arrived is not picked', () { // §14: arrival and collection are separate events. final s = sixOrders().first..['orderstatus'] = 'arrived'; expect(stopStatusOf(s), StopStatus.arrived); expect(stopStatusOf(s).isPicked, isFalse); expect(MilkRun.nextActionLabel(StopStatus.arrived), 'Picked up'); }); test('picking Vidhya leaves Priyanka and ABC on Home', () { final stops = sixOrders(); final accepted = idsOf(stops); final collected = {'101', '102', '103'}; final stillToCollect = MilkRun.outstandingPickups( stops, acceptedIds: accepted, collectedIds: collected, ); expect(idsOf(stillToCollect), {'104', '105', '106'}); expect(stillToCollect.map(MilkRun.sourceNameOf).toSet(), { 'Priyanka Kitchen', 'ABC Kitchen', }); }); test('and the three picked ones are live deliveries immediately', () { // §24 — the rule that must NOT wait for the other kitchens. final collected = {'101', '102', '103'}; for (final id in collected) { expect(collected.contains(id), isTrue); } // They navigate to the customer now, not back to the kitchen. final s = sixOrders().first; expect(MilkRun.navigatesToCustomer(s, collectedIds: collected), isTrue); expect(MilkRun.navigationTarget(s, collectedIds: collected)?.lat, 11.05); }); }); group('18 · the picked card lands on DELIVERIES, not Activity', () { // ── Both filters, from opposite sides, on one fixture ── // // The deliveries list keeps what is NOT complete; Activity keeps what IS. // They read the same test, so a stop can only ever be on one of them — and // the bug was that `picked` answered "complete" on a line where it means // "in the rider's box". test('the deliveries list keeps a picked milk-run stop', () { ServiceProfile.setActive(ServiceProfile.milkMan); final s = sixOrders().first..['orderstatus'] = 'picked'; // `pickups.dart` builds its list with `!st.isFinishedPickup`. expect(stopStatusOf(s).isFinishedPickup, isFalse); }); test('and Activity drops it', () { ServiceProfile.setActive(ServiceProfile.milkMan); final s = sixOrders().first..['orderstatus'] = 'picked'; // `activity_page.dart` builds its list with `.isFinishedPickup`. expect(stopStatusOf(s).isFinishedPickup, isFalse); }); test('the backend\'s own post-pickup status behaves the same', () { // `pickup-complete` leaves the booking `Converted_To_Consignment`, which // the adapter maps to `picked`. The live payload must not put the order // in history either. ServiceProfile.setActive(ServiceProfile.milkMan); final mapped = ApiConfig.legacyStatusFromNew('Converted_To_Consignment'); expect(mapped, 'picked'); expect(stopStatusFromRaw(mapped).isWorkComplete, isFalse); }); test('once delivered the two swap', () { ServiceProfile.setActive(ServiceProfile.milkMan); final s = sixOrders().first..['orderstatus'] = 'delivered'; expect(stopStatusOf(s).isFinishedPickup, isTrue); }); test('on logistics picked goes to Activity, as it always did', () { ServiceProfile.setActive(ServiceProfile.parcel); final s = sixOrders().first..['orderstatus'] = 'picked'; expect(stopStatusOf(s).isFinishedPickup, isTrue); }); }); group('18b · the status vocabulary underneath', () { test('picked is live work on this line', () { final s = sixOrders().first..['orderstatus'] = 'picked'; expect( stopStatusOf(s).isWorkComplete, isFalse, reason: 'the bug: a bag collected at 11:40 was filed as history before ' 'the rider had left the counter', ); }); test('delivered is what completes it', () { final s = sixOrders().first..['orderstatus'] = 'delivered'; expect(stopStatusOf(s).isWorkComplete, isTrue); }); }); group('20–22 · the rest of the kitchens, then all six on the round', () { test('each kitchen adds its orders as it is worked', () { final stops = sixOrders(); final accepted = idsOf(stops); final collected = {}; for (final batch in [ {'101', '102', '103'}, {'104', '105'}, {'106'}, ]) { collected.addAll(batch); final left = MilkRun.outstandingPickups( stops, acceptedIds: accepted, collectedIds: collected, ); expect(left.length, 6 - collected.length); } expect(collected.length, 6); expect( MilkRun.outstandingPickups( stops, acceptedIds: accepted, collectedIds: collected, ), isEmpty, ); }); }); group('23–25 · the delivery leg', () { test('navigation targets the customer', () { final s = sixOrders().first..['orderstatus'] = 'picked'; expect(MilkRun.navigationLabel(s), 'Receiver 101'); expect(MilkRun.navigationTarget(s)?.lng, 76.99); }); test('customer arrival is a different state from kitchen arrival', () { expect(stopStatusFromRaw('arrived'), StopStatus.arrived); expect(stopStatusFromRaw('delivery_arrived'), StopStatus.deliveryArrived); expect(StopStatus.arrived, isNot(StopStatus.deliveryArrived)); expect(MilkRun.nextActionLabel(StopStatus.deliveryArrived), 'Delivered'); }); test('the delivery arrival is local, and says so', () { // §20 — documented, not faked. No call is made and no success invented. expect(MilkRun.deliveryArrivalIsLocalOnly, isTrue); }); test('delivered files it in Activity with the right verb', () async { await addCompletedBookings([ {'orderid': '101', 'pickupcustomer': 'Receiver 101'}, ]); final done = await getCompletedBookings(); expect(done.single['orderstatus'], 'delivered'); expect(stopStatusOf(done.single).isWorkComplete, isTrue); }); test('a delivered order keys on the consignment, not the booking', () { // The same trap `bookingassignmentid` sets on the accept side. final s = sixOrders().first..['consignmentid'] = '900'; expect(MilkRun.consignmentIdOf(s), '900'); expect(MilkRun.consignmentIdOf(sixOrders().first), ''); }); }); group('26 · a milk run never sees the logistics desk', () { test('no payment, pricing, address capture or verification', () { const p = ServiceProfile.milkMan; expect(p.collectsCash, isFalse); expect(p.pricesShipment, isFalse); expect(p.capturesShipmentAddresses, isFalse); expect(p.initiatesShipment, isFalse); expect(p.needsVerification, isFalse); }); test('and nothing is ever owed at a milk-run door', () { ServiceProfile.setActive(ServiceProfile.milkMan); expect(stopCollectionAmount({'collectionamt': 450}), 0); }); }); group('27 · logistics is untouched', () { test('its whole desk is still on', () { const p = ServiceProfile.parcel; expect(p.capturesShipmentAddresses, isTrue); expect(p.pricesShipment, isTrue); expect(p.initiatesShipment, isTrue); expect(p.collectsCash, isTrue); expect(p.needsVerification, isTrue); }); test('accepted logistics work still moves off Home', () { expect(ServiceProfile.parcel.handoffAt, HandoffPoint.accepted); expect(ServiceProfile.milkMan.handoffAt, HandoffPoint.collected); }); test('it has no kitchen and never drives to its customer', () { const p = ServiceProfile.parcel; expect(p.sourceIsKitchen, isFalse); expect(p.deliversToCustomer, isFalse); }); test('and picked still finishes it', () { ServiceProfile.setActive(ServiceProfile.parcel); expect(StopStatus.picked.isWorkComplete, isTrue); }); }); group('the wire carries what the grouping needs', () { test('the adapter passes source, drop and consignment through', () { final s = ApiConfig.pickupFromBooking({ 'bookingid': 42, 'bookingreference': 'DM-42', 'status': 'Miler_Assigned', 'pickupaddress': 'Vidhya Kitchen, Trichy Road', 'deliveryaddress': 'Flat 3, Anna Nagar', 'sourceid': 'K1', 'sourcename': 'Vidhya Kitchen', 'consignmentid': 900, 'customerphone': '9876543210', }); expect(MilkRun.sourceNameOf(s), 'Vidhya Kitchen'); expect(MilkRun.sourceKeyOf(s), 'id:K1'); expect(MilkRun.consignmentIdOf(s), '900'); expect(s['pickupcontactno'], '9876543210'); expect(s['dropaddress'], 'Flat 3, Anna Nagar'); }); test('a booking without source fields is never given a name', () { // BACKEND DEPENDENCY: sourceid/sourcename are not deployed everywhere. // The rule that matters is unchanged and is what this asserts: the app // does not paper over a missing counter with a guessed kitchen name. final s = ApiConfig.pickupFromBooking({ 'bookingid': 43, 'bookingreference': 'DM-43', 'pickupaddress': '9 Mettupalayam Road', }); expect(MilkRun.sourceNameOf(s), ''); }); test('...but it still groups by the address it does carry', () { // ── What changed, and why it is not "inventing" one ── // // This used to demand `'none'`, which put every source-less order in one // bucket regardless of where it was collected. On a real morning that // failed in the other direction: eight orders off one kitchen, seven // carrying its id and one not, and the eighth could not join them — it // fell out of the dropdown into a flat header of its own titled with the // customer's name. // // Keying on the address invents nothing. It is the weakest *true* // statement on the row — these are collected from the same address — and // it is only reached when the id and the name are both absent. The name // is still empty, which is what the test above pins. Map at(String address) => ApiConfig.pickupFromBooking({ 'bookingid': address.hashCode, 'bookingreference': 'DM-$address', 'pickupaddress': address, }); // Same counter, spelled as two different rows would spell it. expect( MilkRun.sourceKeyOf(at('9 Mettupalayam Road')), MilkRun.sourceKeyOf(at('9, METTUPALAYAM ROAD ')), ); // A different place stays a different group. expect( MilkRun.sourceKeyOf(at('9 Mettupalayam Road')), isNot(MilkRun.sourceKeyOf(at('12 SNS Colony'))), ); // Nothing to group on at all is still its own bucket, never a guess. expect( MilkRun.sourceKeyOf( ApiConfig.pickupFromBooking({ 'bookingid': 44, 'bookingreference': 'DM-44', }), ), 'none', ); }); test('an identified counter still outranks the address', () { // Order of trust: the hub's id, then the name it reads by, then where it // is. Two kitchens at one address must not merge because a row lost its // id; two rows with the same id must not split because someone retyped // the address. final byId = ApiConfig.pickupFromBooking({ 'bookingid': 45, 'bookingreference': 'DM-45', 'sourceid': 'K1', 'pickupaddress': '9 Mettupalayam Road', }); expect(MilkRun.sourceKeyOf(byId), 'id:K1'); }); }); }