first commit

This commit is contained in:
2026-08-11 13:16:33 +05:30
parent 18531bf46b
commit c8350563d9
388 changed files with 69656 additions and 0 deletions

View 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};
}
}