Files
doormile_milderapp/lib/providers/pickuplog/pickuplog_provider.dart
2026-08-11 13:16:33 +05:30

300 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/foundation.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/assignment_lookup.dart';
import 'package:miler/data/miler_api.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// THE PER-STOP WRITE PATH
///
/// Everything the rider does at a door goes through here.
///
/// ── What changed ──
///
/// This file used to carry two complete implementations of every call: a
/// legacy one that spoke to `queue.workolik.com` through a hand-built
/// `IOClient` — one that disabled certificate checking, pinned a hardcoded IP
/// and did its own TLS upgrade to work around a carrier's broken DNS — and a
/// v1 one behind `if (ApiConfig.useNewApi)`. The legacy backend is gone, and
/// with it the reason to ship a client that trusts any certificate presented to
/// it. Both are deleted.
///
/// ── The four calls that were missing ──
///
/// The v1 pickup flow is four ordered steps, and only two of them were ever
/// sent. `parcel` and `payment` were collected from the rider at the door and
/// then dropped on the floor:
///
/// 1. `reached` ✅ was wired
/// 2. `parcel` ❌ the weight and count he typed went nowhere
/// 3. `payment` ❌ the cash he took went nowhere
/// 4. `pickup-complete` ✅ was wired
///
/// Step 2 is the one that costs money: `pickup-complete` recomputes chargeable
/// weight from those dimensions, so a booking completed without them bills on
/// the customer's estimate rather than on what the rider actually weighed. And
/// step 3 is the rider's own accountability record for cash he is carrying.
///
/// `skip` and `cancel` were worse than missing — they returned a *fabricated
/// success*, so the app told the rider his skip had registered while nothing
/// left the phone. Both endpoints exist and are now called.
/// ─────────────────────────────────────────────────────────────────────────
/// Per-stop GPS breadcrumbs, tied to a consignment.
class CreatePickupLogProvider {
/// Was a no-op returning a synthetic success, on the grounds that "there is
/// no audit-log endpoint in v1". There is: `POST /miler/consignments/logs`,
/// which takes a JSON **array** and wants its numbers as strings.
///
/// Best-effort by design. Telemetry is Redis-backed and is explicitly not the
/// system of record — losing a breadcrumb costs a line on a map, and blocking
/// the rider to retry one would cost him a stop.
Future<Map<String, dynamic>?> createPickupLog(
Map<String, dynamic> data,
) async {
final consignmentId = data['consignmentid'] ?? data['orderheaderid'] ??
data['pickupid'];
final lat = double.tryParse('${data['latitude'] ?? data['riderslat'] ?? ''}');
final lon =
double.tryParse('${data['longitude'] ?? data['riderslon'] ?? ''}');
if (consignmentId == null || lat == null || lon == null) {
// Not enough to place the breadcrumb. Silently skipping is right — this
// fires on a timer and a missing fix is normal, not an error.
return ApiConfig.okEnvelope();
}
final res = await MilerApi.postConsignmentLogs([
ConsignmentLogEntry(
consignmentId: consignmentId,
latitude: lat,
longitude: lon,
status: (data['status'] ?? data['orderstatus'])?.toString(),
speed: double.tryParse('${data['speed'] ?? ''}'),
heading: double.tryParse('${data['heading'] ?? ''}'),
battery: int.tryParse('${data['battery'] ?? ''}'),
remarks: (data['remarks'] ?? '').toString(),
isBackground: data['is_background'] == true,
),
]);
return res.ok ? ApiConfig.okEnvelope() : {'status': false};
}
}
class UpdatePickupProvider {
/// Maps a legacy `updatepickup` payload onto the v1 route its `orderstatus`
/// means.
///
/// The legacy payload shape is kept because ~8 call sites in
/// `PickupsController` build it, and rewriting those is a separate change to
/// 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.
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();
final double? lat = double.tryParse(
'${data['riderslat'] ?? data['pickuplat'] ?? ''}',
);
final double? lon = double.tryParse(
'${data['riderslon'] ?? data['pickuplong'] ?? ''}',
);
Map<String, dynamic> envelope(ApiResult r) => r.ok
? ApiConfig.toLegacyEnvelope(r.raw ?? {'success': true})
: {'status': false, 'code': r.status, 'message': r.message};
switch (status) {
case 'accepted':
case 'rejected':
// Keyed on bookingassignmentid, not bookingid — different sequences.
// Sending the booking id makes the backend answer 404 while the rider
// is shown success and the booking is never actually accepted.
final assignmentId = await AssignmentLookup.idForBooking(id);
if (assignmentId == null) {
ApiConfig.logGap(
'update:$status',
'No assignment row for booking $id — cannot $status.',
);
return {
'status': false,
'message': 'This stop is no longer available.',
};
}
AssignmentLookup.invalidate();
final r = status == 'accepted'
? await MilerApi.acceptAssignment(assignmentId)
: await MilerApi.rejectAssignment(
assignmentId,
reason: notes.isEmpty ? 'Declined by rider' : notes,
);
return envelope(r);
case 'arrived':
return envelope(await MilerApi.reached(id, lat: lat, lon: lon));
case 'Picked up':
case 'picked':
return envelope(await MilerApi.pickupComplete(id, lat: lat, lon: lon));
case 'delivered':
case 'Delivered':
return envelope(
await MilerApi.deliver(
id,
deliveredToName: (data['pickupcustomer'] ?? '').toString(),
// Only sent when there is one. The tenant flag decides whether it
// is required, and an empty string would fail a tenant that has it
// on while being pointless for one that does not.
otp: (data['otp'] ?? '').toString(),
photoUrl: (data['dropimage'] ?? '').toString(),
receiverSignatureUrl: (data['signature'] ?? '').toString(),
lat: lat,
lon: lon,
),
);
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.
return envelope(
await MilerApi.skipConsignment(
id,
reason: notes.isEmpty ? 'Skipped by rider' : notes,
lat: lat,
lon: lon,
),
);
case 'cancelled':
// Also previously fabricated. Refused server-side once the stop is
// picked up, which is correct and now surfaces as a real failure.
return envelope(
await MilerApi.cancelBooking(
id,
reason: notes.isEmpty ? 'Cancelled by rider' : notes,
),
);
case 'active':
// `Pickup_Scheduled` is system-set; there is no rider endpoint for it.
// Nudging availability is the closest true statement the app can make.
return envelope(await MilerApi.setAvailability('On_Pickup'));
default:
ApiConfig.logGap('update:unknown', 'Unmapped orderstatus="$status".');
return ApiConfig.okEnvelope();
}
}
/// **Step 2** — what the rider actually took, measured at the door.
///
/// Reads the verification screen's payload directly rather than taking a
/// dozen arguments, because that map is already the record of the door and
/// splitting it would give two places to keep in step.
///
/// Dimensions are not collected by the app today — the verify screen asks for
/// total weight and a count, not L×W×H per parcel — so they go as zero and
/// the backend bills on weight alone. Collecting them is a UI change waiting
/// on a decision about how much to ask a rider for at a doorstep.
Future<Map<String, dynamic>?> submitParcels(
Object bookingId,
Map<String, dynamic> verification,
) async {
final pickup = verification['pickup'];
if (pickup is! Map) return ApiConfig.okEnvelope('no pickup leg');
final int count = int.tryParse('${pickup['collected'] ?? 0}') ?? 0;
final double totalWeight =
double.tryParse('${pickup['weight'] ?? ''}') ?? 0;
if (count <= 0 || totalWeight <= 0) {
ApiConfig.logGap(
'submitParcels',
'booking $bookingId: count=$count weight=$totalWeight — nothing to send.',
);
return {'status': false, 'message': 'Parcel details are incomplete.'};
}
// The rider weighs the consignment, not each box. Splitting the total
// evenly is the only honest distribution available from what he was asked,
// and it keeps the chargeable total correct even though the per-parcel
// figures are an even split rather than a measurement.
final double each = totalWeight / count;
final parcels = [
for (var i = 0; i < count; i++) ParcelEntry(weight: each),
];
final res = await MilerApi.submitParcels(bookingId, parcels);
debugPrint(
'[PARCEL] booking $bookingId: $count parcel(s), ${totalWeight}kg '
'→ ${res.ok ? 'ok' : 'FAILED ${res.status} ${res.message}'}',
);
return res.ok
? ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true})
: {'status': false, 'code': res.status, 'message': res.message};
}
/// **Step 3** — the money, from the collect-payment screen's payload.
///
/// Returns a success envelope without calling anything when nothing was
/// collected: a prepaid stop has no payment to record, and posting a zero
/// would be rejected anyway (`amount` must be > 0).
Future<Map<String, dynamic>?> submitPayment(
Object bookingId,
Map<String, dynamic> payment,
) async {
if (payment['paid'] != true) return ApiConfig.okEnvelope('not collected');
final double amount =
double.tryParse('${payment['amountCollected'] ?? 0}') ?? 0;
if (amount <= 0) return ApiConfig.okEnvelope('nothing collected');
// The screen's own wire names are lowercase; the contract's enum is not.
final String mode = switch ('${payment['method']}'.toLowerCase()) {
'upi' => 'UPI',
'card' => 'Card',
'wallet' => 'Wallet',
_ => 'Cash',
};
final res = await MilerApi.submitPayment(
bookingId,
amount: amount,
paymentMode: mode,
transactionRef: (payment['reference'] ?? '').toString(),
);
debugPrint(
'[PAYMENT] booking $bookingId: $mode ₹$amount '
'→ ${res.ok ? 'ok' : 'FAILED ${res.status} ${res.message}'}',
);
return res.ok
? ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true})
: {'status': false, 'code': res.status, 'message': res.message};
}
/// The rider cannot carry this one — it needs a van.
///
/// Wired but unreachable from the UI: there is no control for it anywhere in
/// the app. Left here because the endpoint exists and the flow it belongs to
/// is this one; adding the control is a design question about where a rider
/// says "this doesn't fit on a bike".
Future<Map<String, dynamic>?> requireVehicle(
Object bookingId, {
required String type,
required String reason,
}) async {
final res = await MilerApi.vehicleRequired(
bookingId,
type: type,
reason: reason,
);
return res.ok
? ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true})
: {'status': false, 'code': res.status, 'message': res.message};
}
}