Files
doormile_milderapp/test/miler_contract_regression_test.dart
2026-08-28 11:13:15 +05:30

866 lines
34 KiB
Dart

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<String, dynamic> booking(int id, Map<String, dynamic> 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<String, dynamic> 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<Uri> asked;
/// Answers `weekly` with [weekly] and every `daily` with its own total.
setUp(() => SharedPreferences.setMockInitialValues({}));
void stub({
Map<String, dynamic> weekly = const {'total_kms': 40},
double dailyKm = 5,
}) {
asked = <Uri>[];
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', <dynamic>[]),
('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<http.Request> sent;
void stub() {
sent = <http.Request>[];
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 geofence bypass does not stop the write leaving', () async {
// The suspicion this rules out. `_checkGeofence` returns true early when
// enforcement is off and its success branch contains nothing else — so
// the bypass skips the distance *rejection* and nothing more.
//
// Asserted through the provider rather than by reading the flag, because
// "the request went out with the fence off" is the claim, and the flag is
// only evidence for it.
expect(kBypassGeofenceForTesting, isTrue);
await UpdatePickupProvider().updatePickup({
'pickupid': 4211,
'orderstatus': 'arrived',
'riderslat': '11.016800',
'riderslon': '76.955800',
});
expect(sent, isNotEmpty, reason: 'the bypass 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<String, dynamic> row(String status) => {
'orderid': 'B1',
'pickupid': 'B1',
'sourcename': 'Vidhya Kitchen',
'orderstatus': status,
};
StopStatus rung(
String status, {
Set<String> collected = const {},
Set<String> arrived = const {},
Set<String> released = const {},
Set<String> 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<http.Request> sent;
setUp(() {
SharedPreferences.setMockInitialValues({});
sent = <http.Request>[];
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);
});
});
}