import 'package:flutter_test/flutter_test.dart'; import 'package:miler/data/api_config.dart'; import 'package:miler/data/milk_run.dart'; /// ───────────────────────────────────────────────────────────────────────── /// ONE PICKUP, SEVERAL DOORS /// /// A customer-app booking carries N destinations. `GET /miler/bookings` /// returns ONE ROW PER DESTINATION once the visit has been collected — same /// `bookingid`, same `bookingreference`, different door, distinguished only by /// `destinationseq` / `destinationcount`. /// /// Everything the device remembers about a stop is keyed on `orderid`: the /// accepted store dedupes on it (`byId[id] = copy`), the consignment-id map /// files under it, the collected and out-for-delivery sets hold it. So three /// doors sharing one `orderid` was not a display bug — two of the three rows /// were deduped away before any screen saw them, and the two consignment ids /// that lost the race were unrecoverable. A rider holding three bags could /// deliver one. /// /// These tests pin the two halves of the fix that can silently rot: /// /// 1. The **suffix exists only where there is something to tell apart**, so /// every single-destination booking — which is all of logistics and all of /// the milk run — keys exactly as it did before. /// 2. **Collection is per visit, delivery is per door.** The collected set is /// written once, under the booking key, while the pickup is still a single /// pre-pickup row; the rows that replace it each carry their own key and /// must still read as collected. /// ───────────────────────────────────────────────────────────────────────── void main() { /// One row of `GET /miler/bookings` for a three-destination customer pickup, /// after `pickup-complete` has fanned it out. Map destinationRow(int seq, {int count = 3}) => { 'bookingid': 482913, 'bookingreference': 'DM-482913', 'status': 'Converted_To_Consignment', 'destinationseq': seq, 'destinationcount': count, 'consignmentid': 500 + seq, 'trackingno': 'DM-TRK-884102$seq', 'recipientname': ['Anitha R', 'Suresh K', 'Meena P'][seq], 'recipientphone': '987654321$seq', 'deliveryaddress': ['Chennai', 'Ernakulam', 'Bengaluru'][seq], 'customername': 'Ravi Kumar', // the SENDER — not at any of the three doors 'pickupaddress': 'Gandhipuram, Coimbatore', }; group('the three doors are three stops', () { test('each destination gets its own key', () { final keys = [ for (var seq = 0; seq < 3; seq++) MilkRun.idOf(ApiConfig.pickupFromBooking(destinationRow(seq))), ]; expect(keys.toSet(), hasLength(3), reason: 'three doors, three keys'); expect(keys, ['DM-482913#0', 'DM-482913#1', 'DM-482913#2']); }); test('a store keyed by orderid keeps all three', () { // Exactly what addAcceptedBookings does: `byId[orderid] = row`. final byId = >{}; for (var seq = 0; seq < 3; seq++) { final stop = ApiConfig.pickupFromBooking(destinationRow(seq)); byId[MilkRun.idOf(stop)] = stop; } expect(byId, hasLength(3), reason: 'none of the three deduped away'); }); test('each keeps its own consignment id', () { // The map rememberConsignmentId writes into. One slot per door, so no // door can lose its parcel to whichever wrote last. final consignments = {}; for (var seq = 0; seq < 3; seq++) { final stop = ApiConfig.pickupFromBooking(destinationRow(seq)); consignments[MilkRun.idOf(stop)] = MilkRun.consignmentIdOf(stop); } expect(consignments.values.toSet(), {'500', '501', '502'}); }); test('the booking key is recoverable from any of them', () { for (var seq = 0; seq < 3; seq++) { final stop = ApiConfig.pickupFromBooking(destinationRow(seq)); expect(MilkRun.bookingKeyOf(stop), 'DM-482913'); expect(stop['bookingreference'], 'DM-482913'); } }); }); group('the suffix appears only where it is needed', () { test('a single-destination booking keys exactly as before', () { final stop = ApiConfig.pickupFromBooking({ 'bookingid': 77, 'bookingreference': 'DM-000077', 'status': 'Pending_Pickup', 'destinationseq': 0, 'destinationcount': 1, }); expect(MilkRun.idOf(stop), 'DM-000077'); expect(MilkRun.bookingKeyOf(stop), 'DM-000077'); }); test('a booking with no destination fields at all is untouched', () { // Every console booking, and any backend that has not shipped the // destination fields yet. The absence must cost nothing. final stop = ApiConfig.pickupFromBooking({ 'bookingid': 42, 'bookingreference': 'DM-000042', 'status': 'Pending_Pickup', }); expect(MilkRun.idOf(stop), 'DM-000042'); expect(MilkRun.destinationCountOf(stop), 1); expect(MilkRun.stopLabel(stop), isEmpty); }); }); group('the rider is told which door this is', () { test('Stop N of M, counting from one', () { final stop = ApiConfig.pickupFromBooking(destinationRow(1)); expect(MilkRun.stopLabel(stop), 'Stop 2 of 3'); }); test('says nothing when there is only one door', () { final stop = ApiConfig.pickupFromBooking(destinationRow(0, count: 1)); expect( MilkRun.stopLabel(stop), isEmpty, reason: 'a label on every card in the app stops being read', ); }); test('the receiver has a number of their own to ring', () { // The card dials `recipientphone` on a delivery leg, not the booking's // `pickupcontactno` — that one is the SENDER, and ringing him from the // receiver's gate is what it used to do at all three gates. final stop = ApiConfig.pickupFromBooking(destinationRow(0)); expect(stop['recipientphone'], '9876543210'); expect( stop['pickupcontactno'], isNot(stop['recipientphone']), reason: 'sender and receiver are different people', ); }); test('the receiver and the tracking number survive the adapter', () { // The adapter builds a fixed map, so a field it does not name is a field // the UI can never see. These three were unnamed until now, which put the // SENDER's name over the receiver's door. final stop = ApiConfig.pickupFromBooking(destinationRow(2)); expect(stop['recipientname'], 'Meena P'); expect(stop['recipientphone'], '987654321 2'.replaceAll(' ', '')); expect(stop['trackingno'], 'DM-TRK-8841022'); expect( stop['pickupcustomer'], 'Ravi Kumar', reason: 'the sender is still carried, just no longer as the drop', ); }); }); group('collection is per visit, delivery is per door', () { test('a collected visit keeps its doors collected after the split', () { // The rider collected the pickup while it was still ONE row, so this is // what the collected set holds. Nothing in it mentions a destination. final collected = {'DM-482913'}; for (var seq = 0; seq < 3; seq++) { final stop = ApiConfig.pickupFromBooking(destinationRow(seq)); expect( MilkRun.wasCollected(stop, collected), isTrue, reason: 'door $seq must not drop back to "not collected" at the moment ' 'the rider actually has the bag in his hands', ); } }); test('a door the rider has not collected still reads as uncollected', () { final stop = ApiConfig.pickupFromBooking(destinationRow(0)); expect(MilkRun.wasCollected(stop, {'DM-999999'}), isFalse); expect(MilkRun.wasCollected(stop, const {}), isFalse); }); test('a per-door key in the set also counts', () { // Once the delivery half starts writing per-door keys, both spellings // have to answer — the set can hold either during a single shift. final stop = ApiConfig.pickupFromBooking(destinationRow(1)); expect(MilkRun.wasCollected(stop, {'DM-482913#1'}), isTrue); }); }); }