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
234 lines
8.1 KiB
Dart
234 lines
8.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:miler/data/api_config.dart';
|
|
import 'package:miler/data/service_profile.dart';
|
|
import 'package:miler/views/Dashboard/pickups/pickups.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// A HANDOVER CARD NAMES THE BASE IT IS SENDING HIM TO
|
|
///
|
|
/// The card already refused to lead with the receiver, and was right to — on a
|
|
/// base leg he is in another district, and heading the card with him is the
|
|
/// navigation mistake that rule exists to prevent.
|
|
///
|
|
/// But refusing the receiver only left the *pickup*, so a handover card read:
|
|
///
|
|
/// BASE HANDOVER
|
|
/// Anitha R
|
|
/// Gandhipuram
|
|
///
|
|
/// …the customer he had collected from an hour earlier, under a heading telling
|
|
/// him to go to a base. The place he was actually going appeared nowhere on the
|
|
/// card — only on the sheet's slider, after he had already set off. On a round
|
|
/// with two bases that is a parcel left in the wrong building.
|
|
///
|
|
/// `next_hub` rides on the row (the backend sends all six fields: id, name,
|
|
/// address, pincode, latitude, longitude), so the destination is knowable at
|
|
/// card-build time and there is no excuse for the card not to say it.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
void main() {
|
|
setUp(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
|
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
|
|
|
Future<void> pump(WidgetTester tester, Widget child) async {
|
|
tester.view.physicalSize = const Size(1170, 2532);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(body: SingleChildScrollView(child: child)),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
/// A collected parcel routed to a base — the row as the queue returns it with
|
|
/// `MILER_HUB_HANDOVER_ENABLED` on.
|
|
Map<String, dynamic> hubStop({Map<String, dynamic>? hub}) =>
|
|
ApiConfig.pickupFromBooking({
|
|
'bookingid': 4821,
|
|
'orderid': 'ORDER-HUB-1',
|
|
'status': 'Converted_To_Consignment',
|
|
'consignmentstatus': 'Created',
|
|
'consignmentid': 77,
|
|
'next_action': 'inward_at_hub',
|
|
'sourcename': 'Anitha R',
|
|
'pickupcustomer': 'Anitha R',
|
|
'pickupaddress': '12 Gandhipuram, Coimbatore',
|
|
'recipientname': 'Ravi Kumar',
|
|
'dropaddress': '88 Anna Nagar, Chennai 600040',
|
|
'next_hub':
|
|
hub ??
|
|
const {
|
|
'id': 1,
|
|
'name': 'Coimbatore Base',
|
|
'address': '14 Avinashi Road, Peelamedu',
|
|
'pincode': '641004',
|
|
'latitude': 11.0272,
|
|
'longitude': 76.9905,
|
|
},
|
|
});
|
|
|
|
/// The same parcel, going to a customer instead.
|
|
Map<String, dynamic> customerStop() => ApiConfig.pickupFromBooking(const {
|
|
'bookingid': 4822,
|
|
'orderid': 'ORDER-CX-1',
|
|
'consignmentstatus': 'Out_for_Delivery',
|
|
'consignmentid': 78,
|
|
'next_action': 'deliver',
|
|
'pickupcustomer': 'Anitha R',
|
|
'pickupaddress': '12 Gandhipuram, Coimbatore',
|
|
'recipientname': 'Ravi Kumar',
|
|
'dropaddress': '4 Race Course, Coimbatore 641018',
|
|
});
|
|
|
|
testWidgets('the BASE leads, not the sender he collected from', (
|
|
tester,
|
|
) async {
|
|
await pump(
|
|
tester,
|
|
PickupCard(
|
|
item: hubStop(),
|
|
displayStep: 1,
|
|
distanceMeters: 4200,
|
|
collectedIds: const {'ORDER-HUB-1'},
|
|
),
|
|
);
|
|
|
|
expect(find.text('BASE HANDOVER'), findsOneWidget);
|
|
expect(
|
|
find.text('Coimbatore Base'),
|
|
findsOneWidget,
|
|
reason: 'the rider must know WHICH base without opening a sheet',
|
|
);
|
|
expect(
|
|
find.text('Anitha R'),
|
|
findsNothing,
|
|
reason:
|
|
'the sender he collected from an hour ago is not the destination',
|
|
);
|
|
});
|
|
|
|
testWidgets('the base address is readable at a gate', (tester) async {
|
|
await pump(
|
|
tester,
|
|
PickupCard(
|
|
item: hubStop(),
|
|
displayStep: 1,
|
|
distanceMeters: 4200,
|
|
collectedIds: const {'ORDER-HUB-1'},
|
|
),
|
|
);
|
|
|
|
// Street and pincode, not the locality summary the other legs use —
|
|
// "Peelamedu" alone does not find a loading bay.
|
|
expect(
|
|
find.textContaining('14 Avinashi Road'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.textContaining('641004'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the distant receiver never takes the headline', (tester) async {
|
|
await pump(
|
|
tester,
|
|
PickupCard(
|
|
item: hubStop(),
|
|
displayStep: 1,
|
|
distanceMeters: 4200,
|
|
collectedIds: const {'ORDER-HUB-1'},
|
|
),
|
|
);
|
|
|
|
// The rule that predates this change and must survive it: a Chennai
|
|
// receiver on a parcel going to a Coimbatore base must not be drawn as the
|
|
// place the rider is heading.
|
|
expect(find.text('Ravi Kumar'), findsNothing);
|
|
});
|
|
|
|
testWidgets('a base with no coordinates still names itself', (tester) async {
|
|
// It is a caption rather than a destination — navigating to 0,0 lands in
|
|
// the Gulf of Guinea — but the rider still needs to read which base it is.
|
|
// The geofence refuses the handover separately, with a sentence pointing at
|
|
// the office.
|
|
await pump(
|
|
tester,
|
|
PickupCard(
|
|
item: hubStop(hub: const {'id': 2, 'name': 'Salem Base'}),
|
|
displayStep: 1,
|
|
distanceMeters: 4200,
|
|
collectedIds: const {'ORDER-HUB-1'},
|
|
),
|
|
);
|
|
|
|
expect(find.text('BASE HANDOVER'), findsOneWidget);
|
|
// Once — the name is the headline, and the address line stays empty rather
|
|
// than repeating it.
|
|
expect(find.text('Salem Base'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a customer delivery still leads with the RECIPIENT', (
|
|
tester,
|
|
) async {
|
|
// The other half of the contract. This change must not push the base
|
|
// treatment onto a leg that has a real receiver — that fix (recipient, not
|
|
// sender) was itself a production bug once.
|
|
await pump(
|
|
tester,
|
|
PickupCard(
|
|
item: customerStop(),
|
|
displayStep: 1,
|
|
distanceMeters: 1400,
|
|
collectedIds: const {'ORDER-CX-1'},
|
|
),
|
|
);
|
|
|
|
expect(find.text('CUSTOMER DELIVERY'), findsOneWidget);
|
|
expect(find.text('Ravi Kumar'), findsOneWidget);
|
|
|
|
// The sender IS still on the card, and should be: once the parcel is in the
|
|
// box the pickup is history, but it is what establishes custody and on a
|
|
// run with two counters it is how he knows which shelf the label came off.
|
|
// It sits under a hairline at half the weight — so "leads" is a claim about
|
|
// TYPE SIZE, and that is what gets asserted rather than mere presence.
|
|
expect(find.text('Anitha R'), findsOneWidget);
|
|
|
|
double sizeOf(String text) =>
|
|
tester.widget<Text>(find.text(text)).style?.fontSize ?? 0;
|
|
|
|
expect(
|
|
sizeOf('Ravi Kumar'),
|
|
greaterThan(sizeOf('Anitha R')),
|
|
reason: 'the receiver leads; the sender is context beneath him',
|
|
);
|
|
});
|
|
|
|
testWidgets('on a base leg the sender is not drawn as context either', (
|
|
tester,
|
|
) async {
|
|
// The context block is gated on `dropLeads`, which is false for a base leg
|
|
// — so a handover card carries the base and nothing that could be mistaken
|
|
// for a destination.
|
|
await pump(
|
|
tester,
|
|
PickupCard(
|
|
item: hubStop(),
|
|
displayStep: 1,
|
|
distanceMeters: 4200,
|
|
collectedIds: const {'ORDER-HUB-1'},
|
|
),
|
|
);
|
|
|
|
expect(find.text('Anitha R'), findsNothing);
|
|
expect(find.text('Ravi Kumar'), findsNothing);
|
|
expect(find.text('Coimbatore Base'), findsOneWidget);
|
|
});
|
|
}
|