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/mutation_guard.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/Models/stop_status.dart'; /// ───────────────────────────────────────────────────────────────────────── /// THE DELIVERY WRITES AGAINST THE CONSIGNMENT, ONCE /// /// Three ids exist for one job and they come from three different sequences: /// /// `bookingid` the customer's booking /// `bookingassignmentid` the row that gave it to this rider /// `consignmentid` what the booking BECAME at pickup-complete /// /// `accept`/`reject` key on the assignment, `reached`/`pickup-complete` on the /// booking, and `deliver`/`skip` on the **consignment**. Passing the wrong one /// does not fail loudly — the backend answers 404, or worse finds a row that /// happens to share the number — so these pin the mapping the delivery path /// depends on. /// ───────────────────────────────────────────────────────────────────────── void main() { setUp(() { ServiceProfile.setActive(ServiceProfile.milkMan); SharedPreferences.setMockInitialValues({'userid': 38}); }); tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); group('pickup-complete preserves the real consignment id', () { test('the id minted at the pivot survives to the door', () async { // `pickup-complete` is the only moment the consignment id is guaranteed // to be in hand: `GET /miler/bookings` does not return it at all // (verified against the live API), so if the pivot does not record it, // the delivery leg has nothing to write against. await rememberConsignmentId('ORDER-1', '46'); final ids = await getConsignmentIds(); expect(ids['ORDER-1'], '46'); }); test('a booking id is never mistaken for a consignment id', () async { // Booking 75 became consignment 46 — different sequences, and the // mapping is keyed by the ORDER, never by the number itself. await rememberConsignmentId('ORDER-1', '46'); final ids = await getConsignmentIds(); expect( ids['ORDER-1'], isNot('75'), reason: 'the booking id must never end up in the consignment slot', ); expect( ids.containsKey('75'), isFalse, reason: 'the map is keyed by order, not by booking number', ); }); test('the mapping is per rider — and follows him across lines', () async { // ── This asserted the opposite until 2026-09-16 ── // // The scope key carried a third part, the rider's service line: // `consignment_ids::u38.t13.milkMan`. That was right while a rider was // one thing for the life of his account. He is not — one Miler carries // meal parcels, logistics collections and customer pickups in the same // shift — and keying his records by a line split one day's work across // two drawers, with whichever drawer the app resolved into being the only // one he could see. // // For a *consignment id* that was not a cosmetic loss: the id is what // `deliver` and `skip` key on, so a stop collected while the app read one // line and delivered after it read the other could not be closed at all. // The rider was told "this order was never picked up on the system". await rememberConsignmentId('ORDER-1', '46'); ServiceProfile.setActive(ServiceProfile.parcel); expect( (await getConsignmentIds())['ORDER-1'], '46', reason: 'one rider, one day, one drawer — whatever he is carrying', ); SharedPreferences.setMockInitialValues({'userid': 99}); expect( (await getConsignmentIds())['ORDER-1'], isNull, reason: "another rider's handset drawer is still not this one", ); }); test('a delivered order forgets its mapping', () async { await rememberConsignmentId('ORDER-1', '46'); await forgetConsignmentIds(['ORDER-1']); expect( (await getConsignmentIds())['ORDER-1'], isNull, reason: 'nothing else may be posted against a closed consignment', ); }); }); group('a missing consignment id is reported honestly', () { // Verified against production 21 Aug 2026: booking 59 is // `Converted_To_Consignment` and on the Deliveries tab, yet no route // exposes its consignment id — `/miler/bookings` carries none on any of // its 29 rows and `/miler/assignments` lists only 12 of those bookings. // // The app cannot deliver it. What it CAN do is tell the rider the truth: // a collected order with an unreachable reference is a hub problem, not // "you forgot to pick it up" — advice that sends him to look for a stop // that is not on Home, to redo work he has already done. test('a collected stop and an uncollected one are different states', () { // `Converted_To_Consignment` maps to `picked` — collected. expect(stopStatusFromRaw('picked').isPicked, isTrue); expect(stopStatusFromRaw('outfordelivery').isDeliveryLeg, isTrue); // Still awaiting collection — the "pick it up from Home" advice is // correct only for these. expect(stopStatusFromRaw('assigned').isPicked, isFalse); expect(stopStatusFromRaw('assigned').isDeliveryLeg, isFalse); expect(stopStatusFromRaw('accepted').isPicked, isFalse); expect(stopStatusFromRaw('accepted').isDeliveryLeg, isFalse); }); }); group('one press, one write', () { test('a duplicate Delivered tap produces one mutation', () async { var calls = 0; Future deliver() async { calls++; await Future.delayed(const Duration(milliseconds: 30)); return 'done'; } // Both taps land while the first request is still in flight — which is // what a gloved thumb on a moving bike actually produces. final first = MutationGuard.run('delivered:ORDER-1', deliver); final second = MutationGuard.run('delivered:ORDER-1', deliver); final results = await Future.wait([first, second]); expect(calls, 1, reason: 'the second press must be dropped, not queued'); expect(results.where((r) => r != null).length, 1); expect( results.contains(null), isTrue, reason: 'the dropped press returns null so the caller keeps the stop', ); }); test('the guard frees the key so a genuine retry still works', () async { var calls = 0; Future deliver() async { calls++; return 'done'; } await MutationGuard.run('delivered:ORDER-1', deliver); await MutationGuard.run('delivered:ORDER-1', deliver); expect( calls, 2, reason: 'a retry after the first completed is a real second attempt', ); }); test('two different stops do not block each other', () async { var a = 0; var b = 0; await Future.wait([ MutationGuard.run('delivered:ORDER-1', () async { await Future.delayed(const Duration(milliseconds: 20)); a++; return 'a'; }), MutationGuard.run('delivered:ORDER-2', () async { await Future.delayed(const Duration(milliseconds: 20)); b++; return 'b'; }), ]); expect(a, 1); expect(b, 1); }); test('the outcome is part of the key', () async { // Delivered and Skipped are different writes against the same stop; one // must not silently swallow the other. var delivered = 0; var skipped = 0; await Future.wait([ MutationGuard.run('delivered:ORDER-1', () async { await Future.delayed(const Duration(milliseconds: 20)); delivered++; return 'd'; }), MutationGuard.run('skipped:ORDER-1', () async { await Future.delayed(const Duration(milliseconds: 20)); skipped++; return 's'; }), ]); expect(delivered, 1); expect(skipped, 1); }); }); group('completing a delivery clears the rider’s hands', () { test('a delivered order stops being carried', () async { await addCollectedOrderIds(['ORDER-1', 'ORDER-2']); expect(await getCollectedOrderIds(), containsAll(['ORDER-1', 'ORDER-2'])); await removeCollectedOrderIds(['ORDER-1']); final carried = await getCollectedOrderIds(); expect(carried, isNot(contains('ORDER-1'))); expect( carried, contains('ORDER-2'), reason: 'closing one stop must not drop the rest of the load', ); }); }); }