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?> createPickupLog( Map 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?> updatePickup(Map 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 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?> submitParcels( Object bookingId, Map 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?> submitPayment( Object bookingId, Map 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?> 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}; } }