Files
doormile_milderapp/test/delivery_leg_test.dart
Thiru-tenext d612916fe4 Session expiry, arrival geofence guard, multi-destination stops
Three fixes found by running the app on a real handset against production.

1. An expired token left the app looking signed in and unable to work.
   MilerApi.onUnauthorized was declared and called on every 401 but never
   assigned, so the token was dropped and nothing else happened: the profile
   stayed on disk, logged_out stayed false, and the rider saw his own name over
   a dashboard whose every call returned 401. He reads that as "no work today".
   The teardown now lives in endSession() and both ways out of a session — the
   Log out button and the 401 path — use it.

2. Arrived was written locally even when the rider was not there.
   updateArrivedStatus answers false for three different things and the caller
   treated all of them as "the write did not land", which is only true of one.
   A geofence refusal and a server refusal now stop the rung and hand back the
   reason; a dead network still advances, as it should.

3. A multi-destination customer pickup collapsed onto one stop.
   GET /miler/bookings returns a row per destination once collected, all with
   the same bookingid and reference. Every local store keys on that id, so the
   accepted store deduped two of three drops away and their consignment ids
   were unrecoverable. orderid is now the stop key; bookingreference stays the
   booking's name. Cards show "Stop 2 of 3" and the receiver's own name and
   number rather than the sender's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
2026-09-18 11:05:40 +05:30

118 lines
5.3 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:miler/data/milk_run.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/views/Dashboard/pickups/pickups.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// THE DELIVERY LEG HAS NO ARRIVAL STEP
///
/// What the rider reported: pressing **Start delivery** opened the map, and the
/// map asked him to press **I've arrived** — which opened a sheet offering
/// *Picked up · Skipped · Not picked* for a bag already in his box.
///
/// Both halves were wrong. Arrival at a customer's door is not a state anything
/// records — no endpoint exists for it — so the button wrote nothing and existed
/// only to reveal the next screen; and the screen it revealed was the pickup
/// flow's. A stop he is carrying is simply **active**, and the only question
/// left is how it ended.
///
/// These pin the vocabulary and the routing of the three outcomes. The control
/// itself is a widget; what must not drift is which endpoint each outcome means
/// and which of them ends the order.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
group('the four ways a stop in custody can end', () {
test('are delivered, skip, cancelled and handed over', () {
// ── `handedOver` was added 2026-09-16 ──
//
// There used to be three, and the missing fourth is why a base-routed
// parcel could not be closed at all: `MilerApi.inwardAtHub` existed, was
// documented and was covered by `hub_handover_contract_test.dart`, and
// had no call site. A rider carrying a Gandhipuram → Chennai consignment
// was told "hand it over at base" by `releaseForDelivery` and given no
// control that would record him doing so.
//
// It is deliberately NOT a kind of `delivered`: a base handover has no
// receiver, no proof photo, no OTP and no COD, and it must never reach
// `deliver` — that route moves the consignment to `Out_for_Delivery`
// against a receiver in another district, which the handset cannot undo.
expect(DeliveryOutcome.values.map((o) => o.label), [
'Delivered',
'Skip',
'Cancelled',
'Handed over',
]);
});
test('delivered is the only one that completes the order', () {
// Skip is a return visit — it increments `attemptcount` rather than
// failing the parcel — and cancelled is a withdrawal. Handed over ends
// this RIDER's custody while the parcel's own journey carries on inside
// the network. None of the three is a delivery, and Activity must not
// file them as one.
expect(DeliveryOutcome.delivered.pastTense, 'Delivered');
expect(DeliveryOutcome.skipped.pastTense, 'Skipped');
expect(DeliveryOutcome.cancelled.pastTense, 'Cancelled');
expect(DeliveryOutcome.handedOver.pastTense, 'Handed over');
});
test('each one is coloured by what it means, not by where it sits', () {
expect(
DeliveryOutcome.delivered.colour,
isNot(DeliveryOutcome.cancelled.colour),
);
expect(
DeliveryOutcome.skipped.colour,
isNot(DeliveryOutcome.cancelled.colour),
);
});
});
group('which leg a stop is on decides which control it gets', () {
setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan));
Map<String, dynamic> stop(String status) => {
'orderid': 'A1',
// The adapter stamps this on every row whatever leg it is on, which is
// why the leg is asked of [MilkRun.workingKind] and never of the payload.
'type': 'pickup',
'orderstatus': status,
'kitchenname': 'Vidhya Kitchen',
};
test('a collected order is on the delivery leg', () {
expect(MilkRun.workingKind(stop('picked')).isDelivery, isTrue);
expect(
MilkRun.workingKind(stop('accepted'), collectedIds: {'A1'}).isDelivery,
isTrue,
);
});
test('an uncollected one is not — it keeps the pickup flow', () {
// The arrival step and the confirmation sheet still belong to the pickup
// leg on logistics, where the collection *is* the job at the door.
expect(MilkRun.workingKind(stop('accepted')).isDelivery, isFalse);
expect(MilkRun.workingKind(stop('arrived')).isDelivery, isFalse);
});
test('and on logistics nothing is ever the delivery leg', () {
// That line's parcel goes to a hub and is delivered by somebody else, so
// the rider never drives to its customer and must keep I'VE ARRIVED.
ServiceProfile.setActive(ServiceProfile.parcel);
for (final status in ['accepted', 'arrived', 'picked']) {
expect(
MilkRun.workingKind(stop(status)).isDelivery,
isFalse,
reason: status,
);
}
});
});
}