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
This commit is contained in:
2026-09-18 11:05:40 +05:30
parent 127fa062ed
commit d612916fe4
53 changed files with 6346 additions and 748 deletions

View File

@@ -295,6 +295,44 @@ class ApiConfig {
'referenceno',
'orderid',
]);
// ── One customer pickup can be several drops ──
//
// A customer-app booking carries N destinations and `GET /miler/bookings`
// returns ONE ROW PER DESTINATION once the pickup has been collected —
// same `bookingid`, same `bookingreference`, different door. The backend
// labels them for us: `destinationseq` is which one this is and
// `destinationcount` how many there are (both absent, or 1, on every
// console and milk-run booking, which is the whole existing world).
//
// Everything the rider's device remembers about a stop is keyed on
// `orderid` — the accepted store dedupes on it, the consignment-id map
// files under it, the collected and out-for-delivery sets hold it, the ETA
// and km keys are built from it. So three drops sharing one `orderid` is
// not a display bug: `addAcceptedBookings` deduped two of the three away
// before any screen saw them, and the two consignment ids that lost the
// race were unrecoverable, which is a bag the rider is holding with no
// door to take it to.
//
// So `orderid` becomes the **stop** key and carries the destination on it.
// The booking's own reference is untouched below in `bookingreference`,
// which is what every screen shows the rider and what he reads out on the
// phone. Single-destination bookings are byte-identical to before — the
// suffix only exists where there is something to tell apart.
final destinationCount =
int.tryParse(
(pick(['destinationcount', 'destinationCount']) ?? '').toString(),
) ??
1;
final destinationSeq =
int.tryParse(
(pick(['destinationseq', 'destinationSeq']) ?? '').toString(),
) ??
0;
final bookingKey = ref ?? id;
final stopKey = destinationCount > 1
? '$bookingKey#$destinationSeq'
: bookingKey;
final status = pick([
'status',
'bookingstatus',
@@ -320,11 +358,37 @@ class ApiConfig {
return <String, dynamic>{
// identity
'pickupid': id,
'orderid': ref ?? id,
// The STOP key — see the note above. `bookingreference` is the booking's
// own name and is what gets shown; this is what gets remembered.
'orderid': stopKey,
'orderheaderid': id,
'bookingid': id, // keep original too
'bookingreference': s(ref),
// ── Which drop of the visit this is ──
//
// Carried so a card can say "Stop 2 of 3" rather than showing three rows
// that are identical down to the reference number. `destinationcount` is
// 1 for every single-destination and console booking, and the UI treats
// 1 as "say nothing".
'destinationseq': destinationSeq,
'destinationcount': destinationCount,
// ── The parcel's own number, and who is waiting for it ──
//
// All three exist per DESTINATION, not per booking: on a three-drop
// pickup each door has its own tracking number and its own receiver, and
// the booking-level customer is the SENDER, who is not at any of them.
// Dropped by this adapter until now, so the rider arrived at a stranger's
// door with the sender's name on his screen and no number to read out.
'trackingno': s(pick(['trackingno', 'trackingNo', 'tracking_no'])),
'recipientname': s(
pick(['recipientname', 'recipientName', 'recipient_name']),
),
'recipientphone': s(
pick(['recipientphone', 'recipientPhone', 'recipient_phone']),
),
// status
'orderstatus': fromConsignment.isNotEmpty
? fromConsignment