production

This commit is contained in:
2026-08-28 11:13:15 +05:30
parent d7348e253f
commit 5723d373b2
162 changed files with 17924 additions and 7026 deletions

View File

@@ -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.',
}),

View File

@@ -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,

View File

@@ -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,
);
}
}