import 'package:flutter_test/flutter_test.dart'; import 'package:miler/Models/stop_status.dart'; import 'package:miler/data/milk_run.dart'; import 'package:miler/data/rider_stage.dart'; import 'package:miler/data/service_profile.dart'; /// ───────────────────────────────────────────────────────────────────────── /// THE RIDER-FACING LIFECYCLE, END TO END /// /// ``` /// Pending → Accepted → Arrived → Picked → Active → Delivered /// ``` /// /// Six stages against a backend lifecycle that has five and no arrival in it. /// The gap is not an oversight on their side: arrival is an **event**, written /// by `/reached` as a timestamp beside a status that does not move, and asking /// them to add a rung would be asking them to model an event as a state. /// /// So the app owes the richer workflow, and these hold it to it. Two resolvers /// are exercised, deliberately, because they answer different questions about /// the same order: /// /// • [riderStageOf] — where is this order in the rider's day? /// • [MilkRun.stageOf] — how far did the *pickup* get? /// /// A stop whose delivery is under way is `Active` on the first and `Picked` on /// the second, and both are correct. That separation is the point of the whole /// exercise, so it is asserted rather than assumed. /// ───────────────────────────────────────────────────────────────────────── void main() { const id = 'B1'; Map row(String status) => { 'orderid': id, 'pickupid': id, 'sourcename': 'Vidhya Kitchen', 'orderstatus': status, }; RiderStage stage( String status, { Set arrived = const {}, Set collected = const {}, Set released = const {}, Set delivered = const {}, }) => riderStageOf( row(status), arrivedIds: arrived, collectedIds: collected, outForDeliveryIds: released, deliveredIds: delivered, ); group('1 · the six stages, walked once in order', () { test('Pending — the offer', () { expect(stage('miler_assigned'), RiderStage.pending); expect(stage('pending'), RiderStage.pending); }); test('Accepted — pickup_scheduled, and nothing else', () { // The backend's own word for "the rider took it". expect(stage('pickup_scheduled'), RiderStage.accepted); }); test( 'Arrived — the arrival record, because no server field carries it', () { // `/reached` writes a timestamp; `GET /miler/bookings` does not return // it. Until it does, the rider's own record is the only evidence there // is — see `getArrivedOrderIds`. expect(stage('pickup_scheduled', arrived: {id}), RiderStage.arrived); }, ); test('Picked — either backend word, or the collected record', () { expect(stage('converted_to_consignment'), RiderStage.picked); expect(stage('picked_up'), RiderStage.picked); expect(stage('pickup_scheduled', collected: {id}), RiderStage.picked); }); test('Active — the delivery is under way', () { expect(stage('out_for_delivery'), RiderStage.active); expect(stage('active'), RiderStage.active); expect( stage('converted_to_consignment', released: {id}), RiderStage.active, reason: 'the rider pressed Start delivery; the round has begun', ); }); test('Delivered — terminal', () { expect(stage('delivered'), RiderStage.delivered); }); test('Cancelled sits outside the run', () { expect(stage('cancelled'), RiderStage.cancelled); expect(stage('rejected'), RiderStage.cancelled); }); test('the stages are ordered, and each is its own', () { final walked = [ stage('miler_assigned'), stage('pickup_scheduled'), stage('pickup_scheduled', arrived: {id}), stage('converted_to_consignment'), stage('out_for_delivery'), stage('delivered'), ]; expect(walked.toSet(), hasLength(6), reason: 'six distinct stages'); for (var i = 1; i < walked.length; i++) { expect( walked[i].rank, greaterThan(walked[i - 1].rank), reason: '${walked[i]} must rank above ${walked[i - 1]}', ); } }); }); group('2 · progress never runs backwards', () { test('a stale arrival cannot un-pick a collected stop', () { expect( stage('converted_to_consignment', arrived: {id}), RiderStage.picked, ); }); test('a stale arrival cannot un-start a round', () { expect(stage('out_for_delivery', arrived: {id}), RiderStage.active); }); test('a stale collection cannot un-deliver an order', () { expect( stage('delivered', arrived: {id}, collected: {id}), RiderStage.delivered, ); }); test('no local record can revive a cancelled stop', () { expect( stage('cancelled', arrived: {id}, collected: {id}, released: {id}), RiderStage.cancelled, ); }); }); group('3 · the two resolvers answer different questions', () { setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan)); tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); StopStatus pickupRung(String status, {Set collected = const {}}) => MilkRun.stageOf( row(status), acceptedIds: const {}, collectedIds: collected, ); test('delivery under way: Active overall, Picked on the pickup card', () { // The case the separation exists for, stated in one test. expect(stage('active', collected: {id}), RiderStage.active); expect(pickupRung('active', collected: {id}), StopStatus.picked); }); test('and the pickup milestone is readable from the stage too', () { expect(stage('out_for_delivery', collected: {id}).pickupComplete, isTrue); expect(stage('delivered').pickupComplete, isTrue); expect(stage('pickup_scheduled', arrived: {id}).pickupComplete, isFalse); expect( stage('cancelled').pickupComplete, isFalse, reason: 'a cancelled stop was never picked', ); }); test('availability is not a booking status and cannot reach either', () { // `updateActiveStatus` posts `setAvailability('On_Pickup')` — a fact about // the **rider**. It has no booking status to write, and the regression it // caused was the app inventing one. Neither resolver takes availability, // and there is no spelling of it that parses as a rung. expect(stopStatusFromRaw('on_pickup'), StopStatus.unknown); expect(stopStatusFromRaw('On_Pickup'), StopStatus.unknown); }); }); group('3b · the authoritative contract, field by field', () { Map booking( String status, { Object? reachedAt, String? consignment, }) => { 'orderid': id, 'pickupid': id, 'orderstatus': status, if (reachedAt != null) 'reachedat': reachedAt, if (consignment != null) 'consignmentstatus': consignment, }; test('Pickup_Scheduled + no reachedat → Accepted', () { expect(riderStageOf(booking('Pickup_Scheduled')), RiderStage.accepted); expect( riderStageOf(booking('Pickup_Scheduled', reachedAt: '')), RiderStage.accepted, reason: 'an empty stamp is not an arrival', ); expect( riderStageOf(booking('Pickup_Scheduled', reachedAt: 'null')), RiderStage.accepted, reason: 'a stringified JSON null is not an arrival either', ); }); test('Pickup_Scheduled + reachedat → Arrived, from the server alone', () { // No local record passed in: this is the rung rebuilt from server data, // which is what retires the compatibility store. expect( riderStageOf( booking('Pickup_Scheduled', reachedAt: '2026-08-25T12:33:02+05:30'), ), RiderStage.arrived, ); }); test('a malformed stamp is still an arrival, and does not throw', () { // Read for presence, not for value: the backend does not write the field // for a booking nobody arrived at, so an unparseable one is a bad clock // rather than a bad claim. Parsing it and discarding it would lose the // rung over a formatting detail. for (final junk in const ['not-a-date', 0, '2026-13-45', true]) { expect( riderStageOf(booking('Pickup_Scheduled', reachedAt: junk)), RiderStage.arrived, reason: 'reachedat="$junk" threw or was dropped', ); } }); test('Collected_By_Miler → Picked', () { expect( riderStageOf( booking( 'Converted_To_Consignment', consignment: 'Collected_By_Miler', ), ), RiderStage.picked, ); }); test('Out_for_Delivery → Active', () { expect( riderStageOf( booking('Converted_To_Consignment', consignment: 'Out_for_Delivery'), ), RiderStage.active, ); }); test('Delivered → Delivered', () { expect( riderStageOf( booking('Converted_To_Consignment', consignment: 'Delivered'), ), RiderStage.delivered, ); }); test('the consignment outranks a stale stamp at every rung', () { const stamp = '2026-08-25T12:33:02+05:30'; for (final (state, expected) in const [ ('Collected_By_Miler', RiderStage.picked), ('Out_for_Delivery', RiderStage.active), ('Delivered', RiderStage.delivered), ]) { expect( riderStageOf( booking('Pickup_Scheduled', reachedAt: stamp, consignment: state), arrivedIds: {id}, collectedIds: {id}, ), expected, reason: '$state was dragged back by older evidence', ); } }); test('CROSS-CITY · the hub\'s rungs are not this rider being Active', () { // Confirmed by the backend: a cross-city parcel goes // Collected_By_Miler → Inwarded_at_Hub → Tripsheet_Loaded → In_Transit // and is delivered by somebody else. The rider's involvement ended at // the collection, so Picked is where he stays — and no delivery control // may be offered for a stop he cannot act on. for (final hub in const [ 'Inwarded_at_Hub', 'Tripsheet_Loaded', 'In_Transit', 'Created', ]) { final at = riderStageOf( booking('Converted_To_Consignment', consignment: hub), ); expect( at, RiderStage.picked, reason: '"$hub" resolved to $at — the hub half is not his round', ); expect(at, isNot(RiderStage.active)); } }); }); group('4 · restored from persisted state after a restart', () { // Reconstruction reads the same evidence the live flow writes, so a // relaunch lands a stop exactly where the rider left it — the one thing a // derived lifecycle has to get right, because nothing is stored. test('pickup_scheduled with no local record → Accepted', () { expect(stage('pickup_scheduled'), RiderStage.accepted); }); test('pickup_scheduled with a surviving arrival record → Arrived', () { expect(stage('pickup_scheduled', arrived: {id}), RiderStage.arrived); }); test('converted_to_consignment → Picked, with no local record at all', () { expect(stage('converted_to_consignment'), RiderStage.picked); }); test('a released round comes back Active', () { expect( stage('converted_to_consignment', released: {id}), RiderStage.active, ); }); test('a finished stop comes back Delivered', () { expect(stage('delivered'), RiderStage.delivered); }); test('nothing restores to an earlier rung than it reached', () { // Every server word, with a full set of stale local records attached. // None may pull the stop back. final stale = {id}; for (final (said, expected) in const [ ('converted_to_consignment', RiderStage.picked), ('picked_up', RiderStage.picked), ('out_for_delivery', RiderStage.active), ('active', RiderStage.active), ('delivered', RiderStage.delivered), ]) { expect( riderStageOf(row(said), arrivedIds: stale), expected, reason: '"$said" was dragged back by a stale arrival record', ); } }); }); }