import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/Models/stop_status.dart'; import 'package:miler/controllers/riderkm.dart'; import 'package:miler/data/api_config.dart'; import 'package:miler/data/miler_api.dart'; import 'package:miler/data/milk_run.dart'; import 'package:miler/data/route_order.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/home/homepage.dart'; import 'package:miler/data/stop_compliance.dart'; import 'package:miler/controllers/pickups_controller.dart'; import 'package:miler/providers/pickuplog/pickuplog_provider.dart'; /// ───────────────────────────────────────────────────────────────────────── /// FOUR CONTRACTS THE APP MEETS THE BACKEND ON /// /// Each of these has already been wrong once, and each was wrong in the same /// way: the app read a field the server does not send, or ignored one it does. /// None of them raised anything — an absent key and an empty answer are the /// same value, so every failure was silent and every symptom was cosmetic /// ("the chart is empty", "the route is in the wrong order"). That is what /// makes them worth pinning rather than trusting. /// ───────────────────────────────────────────────────────────────────────── void main() { TestWidgetsFlutterBinding.ensureInitialized(); // ══════════════════════════════════════════════════════════════════════ // 1 · ROUTE SEQUENCING // // The backend's rule, given 25 Aug: `sequencedat` is the authority, not // `step`. Non-null → a route was assigned, follow `step` exactly. Null → no // route, and nearest-first is correct rather than a fallback for a bug. // // `RouteOrder` implemented that correctly and it never ran, because // `pickupFromBooking` was dropping the field. Every adapted row looked // unsequenced and the app re-sorted routes the hub had solved. // ══════════════════════════════════════════════════════════════════════ group('1 · sequencedat survives the adapter and decides the order', () { Map booking(int id, Map extra) => { 'bookingid': id, 'bookingno': 'B$id', 'pickupaddress': '$id Thadagam Road, Coimbatore', 'pickupcustomer': 'Sri Balaji Stores', ...extra, }; /// The rider is beside stop 3, so nearest-first would put it first. Any /// test that expects the hub's order has to be able to lose to proximity /// if the gate fails — otherwise it passes for the wrong reason. double distanceTo(Map s) => switch ('${s['orderid']}') { '3' => 10, '1' => 4000, '2' => 9000, _ => 99999, }; test('a sequenced booking keeps its stamp and its step', () { final row = ApiConfig.pickupFromBooking( booking(1, {'step': 2, 'sequencedat': '2026-08-25T09:00:00+05:30'}), ); expect( RouteOrder.isSequenced(row), isTrue, reason: 'the stamp did not survive pickupFromBooking', ); expect(RouteOrder.sequenceOf(row), 2); }); test('the hub order is followed, and proximity does not override it', () { final stops = [ for (final (id, step) in const [(3, 3), (1, 1), (2, 2)]) ApiConfig.pickupFromBooking( booking(id, { 'step': step, 'sequencedat': '2026-08-25T09:00:00+05:30', }), ), ]; final (sorted, source) = RouteOrder.sort(stops, distanceTo: distanceTo); expect(source, RouteOrderSource.adminSequence); expect( sorted.map((s) => '${s['orderid']}').toList(), ['1', '2', '3'], reason: 'the rider is standing on B3 — if this comes back B3-first the ' 'stamp was ignored and nearest-first won', ); }); test('no stamp means no route, and the fallback is correct', () { // The backend's own words: `step: 0` with a null stamp is a rider holding // fewer than two active stops, or a stop with no coordinates. Neither is // a route, and re-sorting is the right answer rather than a workaround. final stops = [ for (final id in const [1, 2, 3]) ApiConfig.pickupFromBooking( booking(id, {'step': 0, 'sequencedat': null}), ), ]; expect(RouteOrder.hasAdminSequence(stops), isFalse); final (sorted, source) = RouteOrder.sort(stops, distanceTo: distanceTo); expect(source, isNot(RouteOrderSource.adminSequence)); expect( '${sorted.first['orderid']}', '3', reason: 'with no route assigned, the nearest stop leads', ); }); test('a blank stamp is no stamp', () { final row = ApiConfig.pickupFromBooking( booking(1, {'step': 0, 'sequencedat': ' '}), ); expect(RouteOrder.isSequenced(row), isFalse); }); }); // ══════════════════════════════════════════════════════════════════════ // 2 · PER-STOP DISTANCE // // Three spellings, because the app writes two of them itself: the contract's // `compliance.actualkm`, the backend's `riderkms` on a raw booking row, and // `actualkms` — what `PickupsController` posts on the arrival and pickup // writes, so it is the key on any record stamped locally before the queue // caught up. Reading only the first two made a stop the rider had just // finished report no distance at all. // ══════════════════════════════════════════════════════════════════════ group('2 · every distance spelling resolves', () { test('compliance.actualkm — the contract\'s own', () { final c = StopCompliance.of({ 'compliance': {'actualkm': 4.2}, }); expect(c.actualKm, 4.2); }); test('riderkms — a row straight off the API', () { expect(StopCompliance.of({'riderkms': 3.1}).actualKm, 3.1); }); test('actualkms — a row this app stamped itself', () { expect(StopCompliance.of({'actualkms': '2.75'}).actualKm, 2.75); }); test('the contract wins when more than one is present', () { final c = StopCompliance.of({ 'compliance': {'actualkm': 9.9}, 'riderkms': 3.1, 'actualkms': '2.75', }); expect( c.actualKm, 9.9, reason: 'the authoritative figure must outrank the locally stamped one', ); }); test('riderkms outranks the local stamp', () { final c = StopCompliance.of({'riderkms': 3.1, 'actualkms': '2.75'}); expect(c.actualKm, 3.1); }); test('no distance at all is null, not zero', () { // Null is what the UI turns into the em dash. A zero would render as a // real measurement of nothing, which is a different claim. expect(StopCompliance.of(const {}).actualKm, isNull); expect(StopCompliance.of({'riderkms': 0}).actualKm, isNull); expect(StopCompliance.of({'actualkms': ''}).actualKm, isNull); }); }); // ══════════════════════════════════════════════════════════════════════ // 3 · THE WEEK'S DISTANCE // // `GET /miler/earnings` answers with six totals for the period asked for and // no series. The app read a `breakdown` array that is not on the contract, so // it was always null and the chart drew an empty week while the rider had // ridden all of it. // // Seven daily calls build the week now. The fast path stays, so the day the // field ships the seven calls stop — and that switch has to be automatic and // has to refuse anything it cannot draw. // ══════════════════════════════════════════════════════════════════════ group('3 · weekly distance: fast path and fallback', () { late List asked; /// Answers `weekly` with [weekly] and every `daily` with its own total. setUp(() => SharedPreferences.setMockInitialValues({})); void stub({ Map weekly = const {'total_kms': 40}, double dailyKm = 5, }) { asked = []; MilerApi.client = MockClient((req) async { asked.add(req.url); final period = req.url.queryParameters['period']; final body = period == 'weekly' ? weekly : {'total_kms': dailyKm}; return http.Response( jsonEncode({'success': true, 'data': body}), 200, headers: {'content-type': 'application/json'}, ); }); } int dailyCalls() => asked.where((u) => u.queryParameters['period'] == 'daily').length; test( 'FAST PATH · a valid breakdown is used, and no daily call is made', () async { stub( weekly: { 'total_kms': 40.5, // The backend's own example shape: ISO dates, not weekday names. 'breakdown': [ {'day': '2026-08-19', 'kms': 18.4}, {'day': '2026-08-20', 'kms': 22.1}, ], }, ); final res = await RiderWeeklyKmController().getRiderWeeklyKms(1); final details = res['details'] as List; expect(details, hasLength(2)); expect(res['total_kms'], 40.5); expect( dailyCalls(), 0, reason: 'the seven daily calls must stop the day breakdown ships', ); }, ); test('FAST PATH · an ISO date is labelled with the day it names', () async { stub( weekly: { 'total_kms': 18.4, 'breakdown': [ {'day': '2026-08-19', 'kms': 18.4}, ], }, ); final res = await RiderWeeklyKmController().getRiderWeeklyKms(1); final first = (res['details'] as List).first; // 2026-08-19 is a Wednesday. The chart labels a bar with the first three // characters of `day`, so an un-normalised ISO date renders as `202`. expect(first.day, 'Wed'); expect(first.kms, 18.4); }); test('FAST PATH · a weekday name is passed through untouched', () async { stub( weekly: { 'breakdown': [ {'day': 'Mon', 'kms': 3}, ], }, ); final res = await RiderWeeklyKmController().getRiderWeeklyKms(1); expect((res['details'] as List).first.day, 'Mon'); expect(dailyCalls(), 0); }); for (final (name, bad) in <(String, Object?)>[ ('absent', null), ('empty', []), ('not a list', 'nope'), ('entries that are not maps', ['Mon', 'Tue']), ( 'an entry with no day', [ {'kms': 4}, ], ), ( 'an entry whose distance will not parse', [ {'day': 'Mon', 'kms': 'lots'}, ], ), ]) { test('FALLBACK · $name breakdown falls through to seven calls', () async { stub(weekly: {'total_kms': 35, if (bad != null) 'breakdown': bad}); final res = await RiderWeeklyKmController().getRiderWeeklyKms(1); final details = res['details'] as List; expect( dailyCalls(), 7, reason: 'a series the chart cannot draw is not a series', ); expect(details, hasLength(7)); expect( details.every((d) => d.kms == 5), isTrue, reason: 'each bar is its own day\'s total_kms', ); }); } test( 'FALLBACK · the daily series is labelled and ordered oldest first', () async { stub(); final res = await RiderWeeklyKmController().getRiderWeeklyKms(1); final days = (res['details'] as List).map((d) => d.day).toList(); expect(days, hasLength(7)); expect(days.toSet(), hasLength(7), reason: 'seven distinct weekdays'); expect( days.last, const [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', ][DateTime.now().weekday - 1], reason: 'the last bar is today', ); }, ); test('FALLBACK · the server\'s weekly total is not overruled', () async { // Seven daily figures are a reconstruction. The weekly number the backend // computed is the one that stands. stub(weekly: {'total_kms': 99}, dailyKm: 5); final res = await RiderWeeklyKmController().getRiderWeeklyKms(1); expect(res['total_kms'], 99); }); test( 'FALLBACK · with no weekly total, the reconstruction stands in', () async { stub(weekly: {'total_kms': 0}, dailyKm: 5); final res = await RiderWeeklyKmController().getRiderWeeklyKms(1); expect(res['total_kms'], 35); }, ); }); // ══════════════════════════════════════════════════════════════════════ // 3b · THE TWO LIFECYCLE WRITES ACTUALLY LEAVE THE PHONE // // The defect these pin: the map sheet's **I've arrived** control never called // the arrival API. `updateArrivedStatus` was reachable from `homepage.dart` // and nowhere else, so on the logistics line — where the map sheet is the // only arrival control — `reached` never fired, the local record was never // written, and the rung went from ACCEPTED straight to whatever came next. // // Nothing raised. A transition that is never attempted looks exactly like one // that succeeded and was not reflected, which is why this was read as a // status-mapping bug for as long as it was. // // These assert the wire, not the widget: the endpoint, the identifier in the // path, and the body — with the geofence bypass ON, which is the state the // build ships in and the thing that was suspected of eating the call. // ══════════════════════════════════════════════════════════════════════ group('3b · the arrival and pickup writes reach the network', () { late List sent; void stub() { sent = []; MilerApi.client = MockClient((req) async { sent.add(req); return http.Response( jsonEncode({'success': true, 'data': {}}), 200, headers: {'content-type': 'application/json'}, ); }); } setUp(() { SharedPreferences.setMockInitialValues({}); stub(); }); tearDown(() => MilerApi.client = http.Client()); http.Request only(String fragment) { final hits = sent.where((r) => r.url.path.contains(fragment)).toList(); expect( hits, hasLength(1), reason: 'expected exactly one request to …/$fragment, sent: ' '${sent.map((r) => r.url.path).toList()}', ); return hits.single; } test('ARRIVED · endpoint, path id and body', () async { await MilerApi.reached(4211, lat: 11.0168, lon: 76.9558); final req = only('reached'); expect(req.method, 'POST'); // The **booking** id, in the path. Not the assignment id — that one keys // accept/reject only — and not a consignment, which does not exist yet. expect(req.url.path, endsWith('/miler/bookings/4211/reached')); expect(jsonDecode(req.body), {'latitude': 11.0168, 'longitude': 76.9558}); }); test( 'ARRIVED · a fix-less arrival still posts, with no coordinates', () async { // The body omits what it does not have rather than sending zeros, which // would be a claim about where the rider was standing. await MilerApi.reached(4211); expect(jsonDecode(only('reached').body), isEmpty); }, ); test('PICKED · endpoint, path id and body', () async { await MilerApi.pickupComplete(4211, lat: 11.0168, lon: 76.9558); final req = only('pickup-complete'); expect(req.method, 'POST'); expect(req.url.path, endsWith('/miler/bookings/4211/pickup-complete')); expect(jsonDecode(req.body), {'latitude': 11.0168, 'longitude': 76.9558}); expect( req.headers['Idempotency-Key'], isNotNull, reason: 'the pivot is the one write a dropped acknowledgement must not ' 'be able to run twice', ); }); test( 'the provider routes ARRIVED to reached, not to anything else', () async { await UpdatePickupProvider().updatePickup({ 'pickupid': 4211, 'orderheaderid': 77, 'orderstatus': 'arrived', 'riderslat': '11.016800', 'riderslon': '76.955800', }); final req = only('reached'); // `pickupid` IS the booking id — see `ApiConfig.pickupFromBooking`, which // maps `bookingid` onto it. This pins that, because the provider picks // the path id off the payload and a change there would be silent. expect(req.url.path, endsWith('/miler/bookings/4211/reached')); expect(jsonDecode(req.body), { 'latitude': 11.0168, 'longitude': 76.9558, }); }, ); test('the provider routes PICKED to pickup-complete', () async { await UpdatePickupProvider().updatePickup({ 'pickupid': 4211, 'orderheaderid': 77, 'orderstatus': 'picked', 'riderslat': '11.016800', 'riderslon': '76.955800', }); expect( only('pickup-complete').url.path, endsWith('/miler/bookings/4211/pickup-complete'), ); }); test( 'a missing booking id refuses the call, it does not post to zero', () async { // ── The silent one ── // // The provider resolved its path id with `pickupid ?? orderheaderid`, and // `??` catches only null. Every caller builds the payload with // `int.tryParse(...) ?? 0`, so a row without a booking id arrived as // **0** — not null — the fallback never fired, and the app posted to // `/miler/bookings/0/reached`. // // Nothing complained on the phone: arrival is the rider's own report and // is deliberately not gated on the write landing, so his rung advanced // and the row looked healthy. The hub was told about booking zero, and // the console showed no arrival — which is exactly how this was reported. final res = await UpdatePickupProvider().updatePickup({ 'pickupid': 0, 'orderheaderid': 0, 'orderstatus': 'arrived', 'riderslat': '11.016800', 'riderslon': '76.955800', }); expect(sent, isEmpty, reason: 'booking 0 is not a booking'); expect(res?['status'], isFalse); }, ); test( 'a real id still posts, and zero on the other key is ignored', () async { await UpdatePickupProvider().updatePickup({ 'pickupid': 0, 'orderheaderid': 77, 'orderstatus': 'arrived', 'riderslat': '11.016800', 'riderslon': '76.955800', }); expect( only('reached').url.path, endsWith('/miler/bookings/77/reached'), ); }, ); test('the provider itself holds no proximity gate', () async { // ── What this test used to say ── // // `expect(kBypassGeofenceForTesting, isTrue)` — it asserted the fence was // OFF, and then that the write still went out. The flag is gone: the // fence is enforced by default now and lives in `lib/data/geofence.dart`. // // The claim underneath it survives and is worth keeping: **the fence is // in the controller, not in the transport.** `UpdatePickupProvider` maps // a legacy payload onto a v1 route and posts it, full stop. If a // proximity check ever appears down here there would be two fences again, // reachable by different paths, which is the shape that gave the app a // 500 m radius on Home and a 10 m one on a stop. // // The controller-level rule — blocked means no request — is asserted // end-to-end in `geofence_test.dart`. await UpdatePickupProvider().updatePickup({ 'pickupid': 4211, 'orderstatus': 'arrived', 'riderslat': '11.016800', 'riderslon': '76.955800', }); expect(sent, isNotEmpty, reason: 'the provider swallowed the request'); expect(only('reached').url.path, contains('/4211/reached')); }); }); // ══════════════════════════════════════════════════════════════════════ // 3c · THE PICKUP MILESTONE IS NOT THE DELIVERY STATUS // // The backend's booking lifecycle, confirmed by them and with no Arrived rung // anywhere in it: // // pending → miler_assigned → pickup_scheduled → converted_to_consignment // → out_for_delivery / active → delivered // // Three of those words carry the **pickup** milestone and all three parsed as // `unknown`: `pickup_scheduled`, `converted_to_consignment` and `picked_up`. // So the one field the pickup UI reads had nothing in it, and `active` — a // *delivery* state, which does parse — won by default. That is the second // half of "Picked shows as Active"; the first was the app writing `active` // onto the rung itself. // // Nothing about the backend's meanings is changed here. `active` still means // active, and the delivery surfaces still read it that way. What is fixed is // that the pickup surfaces resolve the *pickup* fact and it outranks a // delivery state that came after it. // ══════════════════════════════════════════════════════════════════════ group('3c · the pickup rung, derived from authoritative facts', () { setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan)); tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); Map row(String status) => { 'orderid': 'B1', 'pickupid': 'B1', 'sourcename': 'Vidhya Kitchen', 'orderstatus': status, }; StopStatus rung( String status, { Set collected = const {}, Set arrived = const {}, Set released = const {}, Set accepted = const {'B1'}, }) => MilkRun.stageOf( row(status), acceptedIds: accepted, collectedIds: collected, arrivedIds: arrived, outForDeliveryIds: released, ); test('the backend\'s pickup words parse at all', () { // Each of these was StopStatus.unknown. expect(stopStatusFromRaw('pickup_scheduled'), StopStatus.accepted); expect(stopStatusFromRaw('converted_to_consignment'), StopStatus.picked); expect(stopStatusFromRaw('picked_up'), StopStatus.picked); }); test('pickup_scheduled, nothing else → Accepted', () { expect(rung('pickup_scheduled'), StopStatus.accepted); }); test('pickup_scheduled + an arrival fact → Arrived', () { expect( rung('pickup_scheduled', arrived: {'B1'}), StopStatus.arrived, reason: 'the arrival is the rider\'s own record; nothing outranks it ' 'here because nothing further along has happened', ); }); test('converted_to_consignment → Picked', () { expect(rung('converted_to_consignment'), StopStatus.picked); }); test('picked_up → Picked', () { expect(rung('picked_up'), StopStatus.picked); }); test('active + pickup evidence → Picked on the pickup UI', () { // The case the whole separation exists for. The delivery lifecycle has // moved on; the pickup milestone did not un-happen. expect(rung('active', collected: {'B1'}), StopStatus.picked); }); test('out_for_delivery + pickup evidence → Picked on the pickup UI', () { expect(rung('out_for_delivery', collected: {'B1'}), StopStatus.picked); }); test('delivered keeps its own rung, not the pickup one', () { // A delivered stop is finished. The pickup milestone is history, and the // rung a rider is shown is the one that is current. expect(rung('delivered', collected: {'B1'}), StopStatus.delivered); }); test('active WITHOUT pickup evidence stays active', () { // The line the brief drew: do not infer Picked merely because a booking // is Active. Without a collection fact there is nothing to infer from. // // `accepted` is emptied here on purpose: the local accept store is // checked below `active` in the ladder, so leaving it in would answer // `accepted` and the test would pass for the wrong reason. expect(rung('active', accepted: const {}), StopStatus.active); }); test('the delivery resolver still reads active as active', () { // Nothing about the backend's meaning is being renamed. expect(stopStatusFromRaw('active'), StopStatus.active); expect(stopStatusFromRaw('out_for_delivery'), StopStatus.outForDelivery); }); test('an arrival record cannot walk a picked stop backwards', () { expect( rung('converted_to_consignment', arrived: {'B1'}, collected: {'B1'}), StopStatus.picked, ); expect( rung('out_for_delivery', arrived: {'B1'}, released: {'B1'}), StopStatus.outForDelivery, ); expect(rung('delivered', arrived: {'B1'}), StopStatus.delivered); }); }); // ══════════════════════════════════════════════════════════════════════ // 3d · START DELIVERY IS THE ONLY THING THAT MAKES A ROUND ACTIVE // // The one rider action that moves `Collected_By_Miler → Out_for_Delivery`. // Not pickup-complete, not opening the Deliveries tab, not navigation, not // availability — all of which have at some point been mistaken for it. // ══════════════════════════════════════════════════════════════════════ group('3d · start-delivery, and what must not call it', () { late List sent; setUp(() { SharedPreferences.setMockInitialValues({}); sent = []; MilerApi.client = MockClient((req) async { sent.add(req); return http.Response( jsonEncode({'success': true, 'data': {}}), 200, headers: {'content-type': 'application/json'}, ); }); }); tearDown(() => MilerApi.client = http.Client()); test('it is keyed on the consignment, with no body', () async { await MilerApi.startDelivery('C-4211'); final req = sent.single; expect(req.method, 'POST'); // The **consignment**, not the booking. They are different sequences and // posting one where the other belongs is a 404 the rider is shown as // success. expect( req.url.path, endsWith('/miler/consignments/C-4211/start-delivery'), ); expect( jsonDecode(req.body), isEmpty, reason: 'the contract takes no parameters; inventing one is how a ' 'handler starts rejecting a call that used to work', ); expect(req.headers['Idempotency-Key'], isNotNull); }); test('pickup-complete does not start a round by itself', () async { await MilerApi.pickupComplete(4211, lat: 11.0, lon: 76.9); expect( sent.where((r) => r.url.path.contains('start-delivery')), isEmpty, reason: 'Picked has to stay visible until the rider sets off; a ' 'pickup that starts its own round erases the rung', ); expect(sent.single.url.path, contains('pickup-complete')); }); test('availability is its own call and touches no lifecycle', () async { await MilerApi.setAvailability('On_Pickup'); final req = sent.single; expect(req.url.path, contains('/miler/availability')); expect( req.url.path, isNot(contains('bookings')), reason: 'availability is a fact about the RIDER', ); expect(req.url.path, isNot(contains('consignments'))); }); }); // ══════════════════════════════════════════════════════════════════════ // 4 · ARRIVED PRECEDENCE // // The local store exists only because `reached` did not persist. It is the // weakest record in the app and its one rule is that it must never compete // with the server: it may speak where the server has said nothing further // along, and nowhere else. // // The rule used to be three exclusions — not complete, not cancelled, not // rejected — and `isWorkComplete` is line-dependent: on a milk run it means // *delivered or cancelled* only. So `picked`, `outForDelivery` and // `deliveryArrived` all passed it, and a stale local record could walk a stop // the hub knew was on the road back to a rung the rider left an hour ago. // ══════════════════════════════════════════════════════════════════════ group('4 · the local ARRIVED record never outranks the server', () { setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan)); tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); /// The shipped rule, reached through the same public surface `_fetchQueues` /// uses — the status the server reported, parsed. bool mayShow(String serverStatus) => HomepageArrivalPrecedence.localArrivalMayShow( stopStatusFromRaw(serverStatus), ); test('it may speak where the server has nothing further along', () { for (final said in const [ 'new', 'assigned', 'accepted', 'arrived', 'this is not a status the app knows', ]) { expect( mayShow(said), isTrue, reason: '"$said" is not a rung past arrival', ); } }); test('it is silent once the server has moved the stop on', () { // Every one of these passed the old three-exclusion test on a milk run. for (final said in const [ 'picked', 'outfordelivery', 'deliveryarrived', 'delivered', 'cancelled', 'rejected', ]) { expect( mayShow(said), isFalse, reason: '"$said" is the server\'s own answer — a local record that ' 'overwrote it would walk the stop backwards', ); } }); test('the rung the map path writes on arrival is ARRIVED, not ACTIVE', () { // ── Where "Picked" became "Active" ── // // `_startPickupNavigation` wrote `orderstatus = 'active'` onto the row. // Two separate facts were going into one field: *which stop is live*, // which `_activePickupOrderId` already carries and is what the LIVE mark // reads, and *how far up the pickup ladder this stop is*. // // `active` was never a server state either. The provider maps it to // `setAvailability('On_Pickup')` — a fact about the **rider** — because // there is no rider endpoint that sets a booking Active; // `Pickup_Scheduled` is system-set. So the word was app-generated and it // outranked the real rung on every screen reading `orderstatus`. // // Pinned on the resolver, not on any widget's text. expect( stopStatusFromRaw('active'), StopStatus.active, reason: 'active parses as its own thing and is not a pickup rung', ); expect(stopStatusFromRaw('active'), isNot(StopStatus.picked)); expect(stopStatusFromRaw('active').isPicked, isFalse); expect( stopStatusFromRaw('active').isFinishedPickup, isFalse, reason: 'an active stop is not a collected one', ); }); test('the server\'s own arrival word resolves to the arrived rung', () { for (final said in const [ 'Arrived_At_Pickup', 'arrived_at_pickup', 'ARRIVEDATPICKUP', 'arrived', ]) { expect( stopStatusFromRaw(said), StopStatus.arrived, reason: '"$said" must land on the rung the rider was shown', ); } }); test('picked and active are distinct rungs, and stay distinct', () { // The explicit regression: `Picked != Active`. expect(stopStatusFromRaw('picked'), StopStatus.picked); expect(stopStatusFromRaw('picked'), isNot(stopStatusFromRaw('active'))); expect(stopStatusFromRaw('picked').isPicked, isTrue); }); test('and it is silent on a parcel route too', () { // `isWorkComplete` differs by line, which is exactly what made the old // blacklist leak. The rule must not depend on which line is running. ServiceProfile.setActive(ServiceProfile.parcel); expect(mayShow('picked'), isFalse); expect(mayShow('accepted'), isTrue); }); }); }