first commit
This commit is contained in:
299
lib/providers/pickuplog/pickuplog_provider.dart
Normal file
299
lib/providers/pickuplog/pickuplog_provider.dart
Normal file
@@ -0,0 +1,299 @@
|
||||
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};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user