production
This commit is contained in:
@@ -96,11 +96,62 @@ class UpdatePickupProvider {
|
||||
/// a file this one has no business touching. What matters is that every
|
||||
/// branch now ends in a real request or an honest failure — there are no
|
||||
/// invented successes left.
|
||||
/// The booking this payload is about, or null when it carries none.
|
||||
///
|
||||
/// Treats `0`, `''` and an unparseable value as absent — the three shapes a
|
||||
/// missing id actually arrives in, none of which is the `null` a `??` chain
|
||||
/// is looking for.
|
||||
static Object? _bookingId(Map<String, dynamic> data) {
|
||||
for (final key in const ['pickupid', 'orderheaderid']) {
|
||||
final raw = data[key];
|
||||
if (raw == null) continue;
|
||||
final s = raw.toString().trim();
|
||||
if (s.isEmpty) continue;
|
||||
final n = int.tryParse(s);
|
||||
if (n != null) {
|
||||
if (n > 0) return n;
|
||||
continue;
|
||||
}
|
||||
// A non-numeric reference — a tracking number — is a real id.
|
||||
return s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updatePickup(Map<String, dynamic> data) async {
|
||||
final String status = (data['orderstatus'] ?? '').toString();
|
||||
final id = data['pickupid'] ?? data['orderheaderid'];
|
||||
final String notes = (data['notes'] ?? '').toString();
|
||||
|
||||
// ── `??` was catching null, and a missing id is never null here ──
|
||||
//
|
||||
// This read `data['pickupid'] ?? data['orderheaderid']`. Every caller
|
||||
// builds its payload with `int.tryParse('${'$'}{stop['pickupid'] ?? 0}') ?? 0`,
|
||||
// so a row without a booking id arrives as **`0`** — which is not null, so
|
||||
// the fallback never fired and the app posted to
|
||||
// `/miler/bookings/0/reached`.
|
||||
//
|
||||
// Nothing complained. The rider's rung advanced anyway (arrival is his own
|
||||
// report and is deliberately not gated on the write landing), so from the
|
||||
// phone it looked identical to a healthy arrival — while the hub was told
|
||||
// about booking zero, and the console showed no arrival at all.
|
||||
//
|
||||
// Zero and empty are *absent* now, and an absent id refuses the call
|
||||
// outright rather than aiming it at a booking that does not exist. See
|
||||
// [_bookingId].
|
||||
final id = _bookingId(data);
|
||||
if (id == null) {
|
||||
ApiConfig.logGap(
|
||||
'update:$status',
|
||||
'no usable booking id on the payload — pickupid='
|
||||
'${data['pickupid']}, orderheaderid=${data['orderheaderid']}. '
|
||||
'Refusing to post; the hub would have been told about booking 0.',
|
||||
);
|
||||
return {
|
||||
'status': false,
|
||||
'message': 'This stop is missing its booking reference.',
|
||||
};
|
||||
}
|
||||
|
||||
final double? lat = double.tryParse(
|
||||
'${data['riderslat'] ?? data['pickuplat'] ?? ''}',
|
||||
);
|
||||
@@ -199,6 +250,26 @@ class UpdatePickupProvider {
|
||||
// mirror that flag — it reads which one happened.
|
||||
final pivot = MilerLifecycle.pickupComplete(picked);
|
||||
MilerLifecycle.report('pickup-complete', pivot);
|
||||
// ── The Picked half of the lifecycle trace ──
|
||||
//
|
||||
// Unconditional, and it prints the response body. `_send` logs only
|
||||
// failures, so a pivot that succeeded and wrote the *wrong* state —
|
||||
// which is the entire production question — left no record at all, and
|
||||
// every investigation of it started by adding this line by hand.
|
||||
//
|
||||
// Read with the `[TRACE][START-RIDE]` line in `releaseForDelivery`:
|
||||
// together they answer "did the app release at Picked?" from one log,
|
||||
// with no console access and no packet capture. See MilerLifecycle.
|
||||
debugPrint(
|
||||
'[TRACE][PICKED] booking=$id '
|
||||
'POST /miler/bookings/$id/pickup-complete '
|
||||
'-> ${picked.status} booking="${pivot.bookingStatus.name}" '
|
||||
'consignment="${pivot.consignmentState.name}" '
|
||||
'id="${pivot.consignmentId}" next="${pivot.nextAction}" '
|
||||
'compatibilityMode=${pivot.isCompatibilityMode} '
|
||||
'startDeliveryCalledHere=false '
|
||||
'raw=${picked.raw}',
|
||||
);
|
||||
if (pivot.isCompatibilityMode) {
|
||||
ApiConfig.logGap(
|
||||
'pickup-complete',
|
||||
@@ -301,11 +372,23 @@ class UpdatePickupProvider {
|
||||
);
|
||||
|
||||
case 'skipped':
|
||||
// Was a fabricated success — `okEnvelope('skip not supported by
|
||||
// backend')` — so the rider watched a stop move to "skipped" while
|
||||
// nothing left the phone and the hub never learned.
|
||||
// ── The booking skip, not the consignment skip ──
|
||||
//
|
||||
// It was a fabricated success once — `okEnvelope('skip not supported
|
||||
// by backend')` — so the rider watched a stop move to "skipped" while
|
||||
// nothing left the phone. The fix that replaced it posted to
|
||||
// `POST /miler/consignments/:id/skip` with a **booking** id, because
|
||||
// that was the only skip route on the contract. Two different
|
||||
// sequences: the call either 404'd or, worse, skipped whichever
|
||||
// consignment happened to carry that number.
|
||||
//
|
||||
// Every caller of this branch is pre-collection — the skip sheet opens
|
||||
// from a pickup card — and the pre-pickup skip now has its own route,
|
||||
// which keeps the booking assigned and resumable. A skip *after*
|
||||
// collection never comes through here: it goes through
|
||||
// `closeDelivery`, which resolves the real consignment id first.
|
||||
return envelope(
|
||||
await MilerApi.skipConsignment(
|
||||
await MilerApi.skipBooking(
|
||||
id,
|
||||
reason: notes.isEmpty ? 'Skipped by rider' : notes,
|
||||
lat: lat,
|
||||
|
||||
Reference in New Issue
Block a user