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
254 lines
9.2 KiB
Dart
254 lines
9.2 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:miler/data/task_profile.dart';
|
|
|
|
/// One rider, mixed work, and every row deciding its own workflow.
|
|
///
|
|
/// These tests exist because the app used to answer "what kind of work is
|
|
/// this?" by looking at the **rider** — his tenant, resolved once at login into
|
|
/// a single app-wide service line. That answer is the same for every row on the
|
|
/// screen, so a customer pickup sitting beside a kitchen collection either took
|
|
/// the kitchen's rules or disappeared. The rider is one person with a mixed
|
|
/// queue; the queue decides.
|
|
///
|
|
/// Every input below is what `GET /miler/bookings` actually sends.
|
|
|
|
Map<String, dynamic> _row({
|
|
String? nextAction,
|
|
String? pickupSourceType,
|
|
String? stopType,
|
|
}) => <String, dynamic>{
|
|
if (nextAction != null) 'next_action': nextAction,
|
|
if (pickupSourceType != null) 'pickup_source_type': pickupSourceType,
|
|
if (stopType != null) 'stoptype': stopType,
|
|
};
|
|
|
|
void main() {
|
|
group('a row is classified from the server\'s own words', () {
|
|
test('a customer door pickup is a customer pickup', () {
|
|
final t = TaskProfile.of(
|
|
_row(nextAction: 'pickup', pickupSourceType: 'customer'),
|
|
);
|
|
expect(t.kind, WorkKind.customerPickup);
|
|
expect(t.capturesShipmentDetails, isTrue);
|
|
expect(t.groupsBySource, isFalse);
|
|
});
|
|
|
|
test('a kitchen load is a source pickup, and groups', () {
|
|
final t = TaskProfile.of(
|
|
_row(nextAction: 'pickup', pickupSourceType: 'kitchen'),
|
|
);
|
|
expect(t.kind, WorkKind.sourcePickup);
|
|
expect(t.groupsBySource, isTrue);
|
|
expect(t.capturesShipmentDetails, isFalse);
|
|
});
|
|
|
|
test('a base collection is a source pickup too', () {
|
|
expect(
|
|
TaskProfile.of(_row(nextAction: 'pickup', pickupSourceType: 'hub')).kind,
|
|
WorkKind.sourcePickup,
|
|
);
|
|
});
|
|
|
|
test('carrying it to a receiver is a delivery', () {
|
|
expect(
|
|
TaskProfile.of(_row(nextAction: 'deliver')).kind,
|
|
WorkKind.customerDelivery,
|
|
);
|
|
expect(
|
|
TaskProfile.of(_row(nextAction: 'start_delivery')).kind,
|
|
WorkKind.customerDelivery,
|
|
);
|
|
});
|
|
|
|
test('carrying it to a base is a handover', () {
|
|
final t = TaskProfile.of(_row(nextAction: 'inward_at_hub'));
|
|
expect(t.kind, WorkKind.hubHandover);
|
|
expect(t.endsAtHub, isTrue);
|
|
expect(t.deliversToCustomer, isFalse);
|
|
});
|
|
|
|
test('terminal rows are done', () {
|
|
expect(TaskProfile.of(_row(nextAction: 'none')).kind, WorkKind.done);
|
|
expect(
|
|
TaskProfile.of(_row(nextAction: 'handed_to_hub')).kind,
|
|
WorkKind.done,
|
|
);
|
|
});
|
|
|
|
test('collected with no stated leg is custody-without-destination', () {
|
|
// NextLeg.unknown's case: it is in his bag and nobody has said where it
|
|
// goes. Not a delivery (no destination to offer) and not done (he is
|
|
// still holding it).
|
|
final t = TaskProfile.of(_row(stopType: 'delivery'));
|
|
expect(t.kind, WorkKind.unknown);
|
|
expect(t.deliversToCustomer, isFalse);
|
|
expect(t.endsAtHub, isFalse);
|
|
});
|
|
|
|
test('an unheard-of source takes the safe shape, never a guess', () {
|
|
final t = TaskProfile.of(
|
|
_row(nextAction: 'pickup', pickupSourceType: 'something_new'),
|
|
);
|
|
expect(t.kind, WorkKind.unknown);
|
|
expect(
|
|
t.capturesShipmentDetails,
|
|
isFalse,
|
|
reason: 'the door-capture flow is a claim this row has not earned',
|
|
);
|
|
expect(
|
|
t.groupsBySource,
|
|
isFalse,
|
|
reason: 'nor is a heading it can be filed under',
|
|
);
|
|
});
|
|
|
|
test('a row with no type fields at all does not group', () {
|
|
// Every fixture written before pickup_source_type shipped, and every
|
|
// booking still in flight from then. This defaulted to sourcePickup once
|
|
// and split a one-base logistics day into a group per customer --
|
|
// hub_pickup_group_test.dart is the test that caught it.
|
|
final legacy = <String, dynamic>{
|
|
'orderid': '9',
|
|
'pickupid': '9',
|
|
'pickupaddress': 'RS Puram, Coimbatore',
|
|
'type': 'pickup',
|
|
};
|
|
final t = TaskProfile.of(legacy);
|
|
expect(t.kind, WorkKind.unknown);
|
|
expect(t.groupsBySource, isFalse);
|
|
expect(t.capturesShipmentDetails, isFalse);
|
|
});
|
|
|
|
test('the leg outranks where it was collected from', () {
|
|
// Picked up at a customer's door, now in the bag on its way to a base.
|
|
// It is a handover, not a pickup — it stopped being a pickup the moment
|
|
// it was collected, and only the leg knows that.
|
|
final t = TaskProfile.of(
|
|
_row(nextAction: 'inward_at_hub', pickupSourceType: 'customer'),
|
|
);
|
|
expect(t.kind, WorkKind.hubHandover);
|
|
});
|
|
});
|
|
|
|
group('surfaces', () {
|
|
test('an unaccepted customer pickup waits on Home', () {
|
|
final t = TaskProfile.of(
|
|
_row(nextAction: 'pickup', pickupSourceType: 'customer'),
|
|
);
|
|
expect(t.surfaceWhen(accepted: false), WorkSurface.home);
|
|
});
|
|
|
|
test('accepting a customer pickup moves it to Bookings', () {
|
|
final t = TaskProfile.of(
|
|
_row(nextAction: 'pickup', pickupSourceType: 'customer'),
|
|
);
|
|
expect(t.surfaceWhen(accepted: true), WorkSurface.bookings);
|
|
});
|
|
|
|
test('a kitchen load stays on Home even once accepted', () {
|
|
// Accepting a manifest puts nothing in his hands. It leaves Home when he
|
|
// is actually carrying it, not when he agrees to fetch it.
|
|
final t = TaskProfile.of(
|
|
_row(nextAction: 'pickup', pickupSourceType: 'kitchen'),
|
|
);
|
|
expect(t.surfaceWhen(accepted: true), WorkSurface.home);
|
|
});
|
|
|
|
test('anything in custody is on Deliveries', () {
|
|
for (final action in ['deliver', 'start_delivery', 'inward_at_hub']) {
|
|
expect(
|
|
TaskProfile.of(_row(nextAction: action)).surfaceWhen(accepted: true),
|
|
WorkSurface.deliveries,
|
|
reason: action,
|
|
);
|
|
}
|
|
expect(
|
|
TaskProfile.of(_row(stopType: 'delivery')).surfaceWhen(accepted: true),
|
|
WorkSurface.deliveries,
|
|
);
|
|
});
|
|
|
|
test('finished work is on Activity', () {
|
|
expect(
|
|
TaskProfile.of(_row(nextAction: 'none')).surfaceWhen(accepted: true),
|
|
WorkSurface.activity,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('the mixed shift', () {
|
|
// The case the architecture exists for: one rider around RS Puram holding
|
|
// ten meal parcels to collect at a Gandhipuram kitchen and five customer
|
|
// pickups at five different doors, all at once.
|
|
final queue = <Map<String, dynamic>>[
|
|
for (var i = 0; i < 10; i++)
|
|
_row(nextAction: 'pickup', pickupSourceType: 'kitchen'),
|
|
for (var i = 0; i < 5; i++)
|
|
_row(nextAction: 'pickup', pickupSourceType: 'customer'),
|
|
];
|
|
|
|
test('all fifteen survive classification', () {
|
|
final kinds = queue.map((r) => TaskProfile.of(r).kind).toList();
|
|
expect(kinds.length, 15);
|
|
expect(kinds.where((k) => k == WorkKind.sourcePickup).length, 10);
|
|
expect(kinds.where((k) => k == WorkKind.customerPickup).length, 5);
|
|
expect(
|
|
kinds.contains(WorkKind.unknown),
|
|
isFalse,
|
|
reason: 'nothing falls through to the safe shape when the server spoke',
|
|
);
|
|
});
|
|
|
|
test('accepting the five does not move the ten', () {
|
|
final surfaces = queue
|
|
.map((r) => TaskProfile.of(r).surfaceWhen(accepted: true))
|
|
.toList();
|
|
expect(surfaces.where((s) => s == WorkSurface.home).length, 10);
|
|
expect(surfaces.where((s) => s == WorkSurface.bookings).length, 5);
|
|
});
|
|
|
|
test('collected meal parcels coexist with a CX handover on Deliveries', () {
|
|
final afterCollection = <Map<String, dynamic>>[
|
|
for (var i = 0; i < 10; i++) _row(nextAction: 'deliver'),
|
|
// The CX pickup, now completed, has become a base handover.
|
|
_row(nextAction: 'inward_at_hub', pickupSourceType: 'customer'),
|
|
];
|
|
final surfaces = afterCollection
|
|
.map((r) => TaskProfile.of(r).surfaceWhen(accepted: true))
|
|
.toSet();
|
|
expect(surfaces, {WorkSurface.deliveries});
|
|
});
|
|
});
|
|
|
|
group('a trip is described by what is actually in it', () {
|
|
Map<String, dynamic> row({String? a, String? src}) => <String, dynamic>{
|
|
if (a != null) 'next_action': a,
|
|
if (src != null) 'pickup_source_type': src,
|
|
};
|
|
|
|
test('a mixed trip both groups and ends at a base', () {
|
|
final stops = [
|
|
row(a: 'pickup', src: 'kitchen'),
|
|
row(a: 'pickup', src: 'customer'),
|
|
row(a: 'inward_at_hub'),
|
|
];
|
|
expect(TripShape.groupsBySource(stops), isTrue);
|
|
expect(TripShape.endsAtHub(stops), isTrue);
|
|
expect(TripShape.hasCustomerPickup(stops), isTrue);
|
|
});
|
|
|
|
test('one parcel bound for a base ends the whole day at that base', () {
|
|
// `any`, not `every`. Telling a rider "END · HOME" while he is still
|
|
// carrying something the network needs is the failure worth avoiding.
|
|
final stops = [row(a: 'deliver'), row(a: 'inward_at_hub')];
|
|
expect(TripShape.endsAtHub(stops), isTrue);
|
|
});
|
|
|
|
test('a pure meal round neither groups to a base nor returns to one', () {
|
|
final stops = [row(a: 'deliver'), row(a: 'deliver')];
|
|
expect(TripShape.endsAtHub(stops), isFalse);
|
|
expect(TripShape.hasCustomerPickup(stops), isFalse);
|
|
});
|
|
});
|
|
}
|