production
This commit is contained in:
@@ -649,7 +649,7 @@ class AuthProvider {
|
||||
'status': false,
|
||||
'code': 403,
|
||||
'message':
|
||||
'Your MPIN is issued by your hub and cannot be changed from the '
|
||||
'Your MPIN is issued by your office and cannot be changed from the '
|
||||
'app. Ask your supervisor to reset it, then sign in with the MPIN '
|
||||
'they give you.',
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -10,11 +10,22 @@ class SummaryProvider {
|
||||
|
||||
Future<PickupStats?> fetchSummaryStats(int userId) => _fetchSummaryStatsNew();
|
||||
|
||||
/// NEW API: GET /miler/earnings?period=daily|weekly|monthly
|
||||
/// -> data:{ completed_stops, total_kms, total_earnings, total_bonus }.
|
||||
/// PickupStats is stop COUNTS per period; map completed_stops for each.
|
||||
/// `GET /miler/earnings?period=daily|weekly|monthly`
|
||||
///
|
||||
/// → `data: { completed_stops, cancelled_stops, total_stops, total_kms,
|
||||
/// total_earnings, total_bonus }`.
|
||||
///
|
||||
/// ── The success rate is read, not derived ──
|
||||
///
|
||||
/// `cancelled_stops` and `total_stops` did not exist, so the page computed a
|
||||
/// success rate from `completed_stops` alone — which is a rate with no
|
||||
/// denominator, and it read 100% on a day with three cancellations. Both
|
||||
/// counts ship now and the figures come off the response.
|
||||
///
|
||||
/// The three period calls stay three calls: the endpoint answers for one
|
||||
/// period at a time, and the page shows all three.
|
||||
Future<PickupStats?> _fetchSummaryStatsNew() async {
|
||||
Future<int> stops(String period) async {
|
||||
Future<({int done, int cancelled, int total})> stops(String period) async {
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
ApiConfig.url('/miler/earnings'),
|
||||
@@ -25,25 +36,34 @@ class SummaryProvider {
|
||||
final d = (decoded is Map && decoded['data'] is Map)
|
||||
? decoded['data'] as Map
|
||||
: {};
|
||||
return int.tryParse('${d['completed_stops'] ?? 0}') ?? 0;
|
||||
int n(String key) => int.tryParse('${d[key] ?? 0}') ?? 0;
|
||||
final done = n('completed_stops');
|
||||
final cancelled = n('cancelled_stops');
|
||||
// A server that has not shipped `total_stops` yet answers 0, and a
|
||||
// total below the completed count is not a total — fall back to what
|
||||
// is certainly true rather than printing a rate over nothing.
|
||||
final reported = n('total_stops');
|
||||
final total = reported >= done + cancelled
|
||||
? reported
|
||||
: done + cancelled;
|
||||
return (done: done, cancelled: cancelled, total: total);
|
||||
}
|
||||
} catch (_) {}
|
||||
return 0;
|
||||
return (done: 0, cancelled: 0, total: 0);
|
||||
}
|
||||
|
||||
final today = await stops('daily');
|
||||
final week = await stops('weekly');
|
||||
final month = await stops('monthly');
|
||||
ApiConfig.logGap(
|
||||
'summaryStats',
|
||||
'v1 earnings has no cancelled/total count; derived from completed_stops.',
|
||||
);
|
||||
return PickupStats(
|
||||
today: today,
|
||||
week: week,
|
||||
month: month,
|
||||
total: month,
|
||||
cancelled: 0,
|
||||
today: today.done,
|
||||
week: week.done,
|
||||
month: month.done,
|
||||
total: month.total,
|
||||
// Was hardcoded to zero because nothing reported it. It is the month's
|
||||
// figure, to match `total` — the two are one fraction and must come from
|
||||
// one period or the rate is nonsense.
|
||||
cancelled: month.cancelled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user