production
This commit is contained in:
@@ -25,42 +25,62 @@ import 'package:miler/views/helpers/widgets/app_widgets.dart';
|
||||
|
||||
/// ── PROXIMITY ENFORCEMENT IS OFF ──
|
||||
///
|
||||
/// Turned off deliberately on 2026-08-19, at the founder's call, because it was
|
||||
/// refusing real work: riders pressing **Picked up** at a counter were told
|
||||
/// "You're 4.2 km from this stop", and a rider who cannot record what he has
|
||||
/// physically done has no way round it. Correctness of the fence matters less
|
||||
/// than a rider being able to work.
|
||||
/// Off again on 2026-08-25, the same day it was turned on, at the founder's
|
||||
/// call. Nothing was found wrong with it — this is a decision about when to
|
||||
/// switch it on, not a retraction of the work.
|
||||
///
|
||||
/// It is off for **every** status — accepted, arrived, picked up, out for
|
||||
/// delivery, delivered — and on both gates, this one and the bulk check on
|
||||
/// Home. Half a fence is worse than none: it would block one route into a rung
|
||||
/// and wave through another, with two different messages and no explanation of
|
||||
/// why one worked.
|
||||
///
|
||||
/// ── Turning it back on ──
|
||||
///
|
||||
/// flutter run --dart-define=ENFORCE_GEOFENCE=true
|
||||
/// flutter build apk --dart-define=ENFORCE_GEOFENCE=true
|
||||
///
|
||||
/// Or flip [kGeofenceEnforced]'s default back to `true` when the underlying
|
||||
/// problem is fixed. That problem is worth naming, because it is the reason
|
||||
/// this is off rather than merely loosened: the fence compares the rider's GPS
|
||||
/// against `pickuplat`/`pickuplon` **on the booking**, and those come from
|
||||
/// wherever the customer dropped a pin — so the distance it measures is as
|
||||
/// often the data being wrong as the rider being absent.
|
||||
/// It is off for **every** status — arrived, picked, picked up, delivery
|
||||
/// arrived, delivered, cancelled — and on both gates, this one and the bulk
|
||||
/// check on Home. Half a fence is worse than none: it would block one route
|
||||
/// into a rung and wave through another, with two different messages and no
|
||||
/// explanation of why one worked.
|
||||
///
|
||||
/// ── What it costs while it is off ──
|
||||
///
|
||||
/// This is the one control that decided whether the app was telling the truth
|
||||
/// This is the one control that decides whether the app is telling the truth
|
||||
/// about where a rider was standing when he said a stop was done. With it off,
|
||||
/// "Picked up" means he pressed a button, not that he was there — the
|
||||
/// timestamps and the GPS still ride along on every status write, so the office
|
||||
/// can audit after the fact, but nothing is refused at the moment of the press.
|
||||
///
|
||||
/// ── What is ready for the day it goes back on ──
|
||||
///
|
||||
/// Recorded here because the work is done and the flag is the only thing
|
||||
/// holding it, so nobody has to rediscover any of it. The fence was off from
|
||||
/// 2026-08-19 because it refused real work — riders pressing **Picked up** at a
|
||||
/// counter were told "You're 4.2 km from this stop" — and that was a
|
||||
/// measurement problem rather than a strictness one. Three causes, all fixed:
|
||||
///
|
||||
/// • **The fix was not worth measuring with.** Home handed the fence a
|
||||
/// `LocationAccuracy.low` position — a ~1 km hint on Android — or a cached
|
||||
/// one of any age. See [_freshFix] and [kGeofenceFixMaxAge].
|
||||
/// • **The phone's own error was charged to the rider.** The comparison is
|
||||
/// `distance - accuracy > radius`, so a rider 12 m out on a ±20 m fix is not
|
||||
/// refused for a precision the hardware never provided.
|
||||
/// • **There were three radii.** A configured `pickupradius` defaulting to
|
||||
/// 100 here, a hardcoded 500 in Home's bulk gate, and no agreement between
|
||||
/// them. There is one now: [kGeofenceRadiusMeters], set to 10.
|
||||
///
|
||||
/// All three are live in the code below and simply do not run while this is
|
||||
/// false. The fourth cause is real and none of this fixes it: **the booking's
|
||||
/// own coordinates are often wrong**, because they come from wherever the
|
||||
/// customer dropped a pin. A stop carrying *no* coordinates is allowed through
|
||||
/// — that is the hub's data, not something a rider can resolve from a doorstep
|
||||
/// — but a stop carrying wrong ones will still refuse him.
|
||||
///
|
||||
/// ── Turning it on ──
|
||||
///
|
||||
/// flutter run --dart-define=ENFORCE_GEOFENCE=true
|
||||
/// flutter build apk --dart-define=ENFORCE_GEOFENCE=true
|
||||
///
|
||||
/// Or flip the default. Ten metres is tight — at or inside consumer GPS
|
||||
/// accuracy — so if the first reports are riders blocked at doors, raise
|
||||
/// [kGeofenceRadiusMeters] before reaching for this switch again.
|
||||
///
|
||||
/// Every bypass is logged in every build mode (see `_checkGeofence`), so a
|
||||
/// build's own log says which way it was compiled.
|
||||
///
|
||||
/// ── The history, so neither old mistake comes back ──
|
||||
/// ── The history, so none of the old mistakes come back ──
|
||||
///
|
||||
/// This was once `kDebugMode`, which meant the fence was off in exactly the
|
||||
/// builds anyone tested with — so it was never really tested. It was then
|
||||
@@ -76,6 +96,56 @@ const bool kGeofenceEnforced = bool.fromEnvironment(
|
||||
/// The name every call site reads. Derived, so there is one switch and not two.
|
||||
const bool kBypassGeofenceForTesting = !kGeofenceEnforced;
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// HOW CLOSE IS "AT THE STOP" — 10 metres
|
||||
///
|
||||
/// A product decision, taken deliberately and tight: the rider must be *at the
|
||||
/// door*, not on the street outside it, before he can mark a stop arrived,
|
||||
/// picked or delivered.
|
||||
///
|
||||
/// ── Why this is a constant and no longer the server's `pickupradius` ──
|
||||
///
|
||||
/// The fence used to read `pickupradius` out of prefs, which login writes from
|
||||
/// the profile and defaults to 100. That made the strictness of the app's one
|
||||
/// honesty control a per-tenant configuration value nobody on this side could
|
||||
/// see, and it silently disagreed with the second gate on Home, which was
|
||||
/// hardcoded to 500. Three numbers for one rule. This is the rule.
|
||||
///
|
||||
/// ── What 10 metres actually demands, stated plainly ──
|
||||
///
|
||||
/// This is at or inside the accuracy of consumer GPS. A phone reports a fix
|
||||
/// with an error radius, and 5–15 m in the open is normal while 30–50 m
|
||||
/// between buildings is ordinary rather than exceptional. A fence smaller than
|
||||
/// the error it is measured with will refuse a rider who is genuinely standing
|
||||
/// at the door — which is the exact failure that got this whole control turned
|
||||
/// off once before, and the reason two things below are not optional:
|
||||
///
|
||||
/// • **The fix must be worth 10 m.** [LocationAccuracy.best], not the `low`
|
||||
/// the callers were passing — `low` is a ~1 km hint on Android and against
|
||||
/// a 10 m fence it is not a measurement, it is a coin toss. See
|
||||
/// [_freshFix].
|
||||
///
|
||||
/// • **The phone's own error is credited to the rider.** The check is
|
||||
/// `distance - accuracy > radius`, not `distance > radius`: a rider 12 m
|
||||
/// away on a fix that says ±20 m has not been shown to be outside the
|
||||
/// fence, and refusing him is asserting a precision the hardware did not
|
||||
/// provide. He is refused when the *phone* says he is outside, not when the
|
||||
/// arithmetic does.
|
||||
///
|
||||
/// Together those keep 10 m meaning "at the door" without it meaning "when the
|
||||
/// satellites are kind". If riders still report being blocked at a door, this
|
||||
/// number is the knob — raise it here, in one place, rather than turning the
|
||||
/// fence off again.
|
||||
const double kGeofenceRadiusMeters = 10;
|
||||
|
||||
/// How stale a cached fix may be before the fence refuses to measure with it.
|
||||
///
|
||||
/// A last-known position is instant and free and can be an hour old. Against a
|
||||
/// 100 m fence that was survivable; against 10 m it is how a rider marks a
|
||||
/// delivery arrived from the previous street because that is where the phone
|
||||
/// last looked.
|
||||
const Duration kGeofenceFixMaxAge = Duration(seconds: 30);
|
||||
|
||||
class PickupsController extends GetxController
|
||||
with ConnectivityControllerMixin {
|
||||
MilerKalmanFilter? _kf;
|
||||
@@ -260,6 +330,66 @@ class PickupsController extends GetxController
|
||||
}
|
||||
|
||||
// ---------------- Location helpers ----------------
|
||||
/// The error radius, in metres, of the fix [_ensureLatLng] last obtained.
|
||||
///
|
||||
/// Read by [_checkGeofence], which credits it to the rider — see
|
||||
/// [kGeofenceRadiusMeters]. Starts at zero so a fence measured before any fix
|
||||
/// has been taken is strict rather than accidentally generous.
|
||||
double _lastFixAccuracy = 0;
|
||||
|
||||
/// A position good enough to measure a [kGeofenceRadiusMeters] fence with.
|
||||
///
|
||||
/// ── What this replaced, and why it had to go ──
|
||||
///
|
||||
/// The old ladder was: last-known first, then `high` for 4s, then `low` for
|
||||
/// 2s. Every rung of it is wrong against a 10 m fence.
|
||||
///
|
||||
/// • **Last-known first** returns instantly and can be an hour old. The
|
||||
/// fence would then be measured from wherever the phone last happened to
|
||||
/// look — reliably the previous stop, on a round.
|
||||
/// • **`low` as a fallback** is a ~1 km hint on Android. Against 10 m that
|
||||
/// is not a degraded measurement, it is noise being treated as evidence,
|
||||
/// and it fails in both directions: it blocks a rider at the door and
|
||||
/// waves through one two streets away.
|
||||
///
|
||||
/// So: ask for the best fix the hardware will give, wait long enough for the
|
||||
/// GPS to actually settle, and fall back to a cached one **only** if it is
|
||||
/// fresher than [kGeofenceFixMaxAge]. Whatever comes back carries its own
|
||||
/// accuracy into [_lastFixAccuracy], so the fence knows how much to trust it.
|
||||
Future<Position?> _freshFix() async {
|
||||
Position? pos;
|
||||
try {
|
||||
pos = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
// 8s, not 4. A cold GPS under a roofline needs the time, and the
|
||||
// rider is standing still at a door — this is the one moment in his
|
||||
// round where a few seconds buys something.
|
||||
timeLimit: Duration(seconds: 8),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
// No live fix. A recent cached one is a measurement; a stale one is not.
|
||||
try {
|
||||
final cached = await Geolocator.getLastKnownPosition();
|
||||
final age = cached == null
|
||||
? null
|
||||
: DateTime.now().difference(cached.timestamp);
|
||||
if (cached != null && age != null && age <= kGeofenceFixMaxAge) {
|
||||
pos = cached;
|
||||
} else if (cached != null) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] cached fix is ${age?.inSeconds}s old — too stale to '
|
||||
'measure a ${kGeofenceRadiusMeters.toStringAsFixed(0)}m fence',
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
_lastFixAccuracy = pos?.accuracy ?? 0;
|
||||
return pos;
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _ensureLatLng(String lat, String lng) async {
|
||||
String outLat = lat;
|
||||
String outLng = lng;
|
||||
@@ -268,16 +398,34 @@ class PickupsController extends GetxController
|
||||
final needsFetch =
|
||||
(lat == '0' || lat.isEmpty || lng == '0' || lng.isEmpty);
|
||||
|
||||
// Fast path: if we have valid coordinates, use them immediately
|
||||
if (!needsFetch) return {'lat': outLat, 'lng': outLng};
|
||||
// ── The two shortcuts below are disabled while the fence is on ──
|
||||
//
|
||||
// Every caller of this method feeds its answer to [_checkGeofence] and
|
||||
// then puts the same pair on the payload. Both shortcuts hand back a
|
||||
// position of unknown provenance: the first trusts whatever the screen
|
||||
// passed in — Home passes a `LocationAccuracy.low` fix, which is a ~1 km
|
||||
// hint — and the second reuses a cached value with no age on it at all.
|
||||
// Neither carries an accuracy, so [_lastFixAccuracy] would be stale too
|
||||
// and the fence would measure a 10 m rule with a number it cannot
|
||||
// characterise.
|
||||
//
|
||||
// With the fence enforced this takes one real fix per status write. That
|
||||
// is a few seconds, once, at a door the rider is standing still at — and
|
||||
// it is the whole basis on which the app is about to refuse or allow his
|
||||
// press. With the fence off the shortcuts stand: nothing is being decided
|
||||
// on the answer, it is telemetry.
|
||||
if (kBypassGeofenceForTesting) {
|
||||
// Fast path: if we have valid coordinates, use them immediately
|
||||
if (!needsFetch) return {'lat': outLat, 'lng': outLng};
|
||||
|
||||
// Reuse recently cached coordinates first if fresh (e.g. within 30s)
|
||||
// For now just check if they exist to save time
|
||||
if (currentLat.value.isNotEmpty &&
|
||||
currentLat.value != '0' &&
|
||||
currentLng.value.isNotEmpty &&
|
||||
currentLng.value != '0') {
|
||||
return {'lat': currentLat.value, 'lng': currentLng.value};
|
||||
// Reuse recently cached coordinates first if fresh (e.g. within 30s)
|
||||
// For now just check if they exist to save time
|
||||
if (currentLat.value.isNotEmpty &&
|
||||
currentLat.value != '0' &&
|
||||
currentLng.value.isNotEmpty &&
|
||||
currentLng.value != '0') {
|
||||
return {'lat': currentLat.value, 'lng': currentLng.value};
|
||||
}
|
||||
}
|
||||
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
@@ -292,32 +440,7 @@ class PickupsController extends GetxController
|
||||
return {'lat': outLat, 'lng': outLng};
|
||||
}
|
||||
|
||||
Position? pos;
|
||||
|
||||
// 1. Try Last Known Position (Instant)
|
||||
try {
|
||||
pos = await Geolocator.getLastKnownPosition();
|
||||
} catch (_) {}
|
||||
|
||||
// 2. If no last known, try current with a single balanced timeout
|
||||
// Reduced complicated retry logic to one solid attempt
|
||||
if (pos == null) {
|
||||
try {
|
||||
pos = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy:
|
||||
LocationAccuracy.high, // Better accuracy for pickup
|
||||
timeLimit: const Duration(seconds: 4),
|
||||
);
|
||||
} catch (_) {
|
||||
// Fallback to low accuracy if high fails quickly
|
||||
try {
|
||||
pos = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.low,
|
||||
timeLimit: const Duration(seconds: 2),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
final pos = await _freshFix();
|
||||
|
||||
if (pos != null) {
|
||||
final now = DateTime.now();
|
||||
@@ -484,7 +607,27 @@ class PickupsController extends GetxController
|
||||
|
||||
if (!ok) {
|
||||
debugPrint('[UPDATE][PICKED][FAILED] resp=${jsonEncode(resp)}');
|
||||
// ── The reason, kept, because the rider was told nothing ──
|
||||
//
|
||||
// Verified on production 27 Aug 2026, booking 207:
|
||||
// `POST /miler/bookings/207/pickup-complete` → **500**
|
||||
// `{"message":"failed to convert booking to consignment"}`.
|
||||
//
|
||||
// The app was right to refuse the move — `_advanceStop` leaves an
|
||||
// unconfirmed pivot on Home rather than pushing it across the boundary
|
||||
// — but it did that *silently*. The rider slid, the sheet closed, and
|
||||
// the stop simply stayed where it was with nothing on screen. He slid
|
||||
// again, and again, because nothing told him the hub had refused.
|
||||
//
|
||||
// The server's own sentence is the honest thing to show him, so it is
|
||||
// kept here for the caller to read straight after its `await`, on the
|
||||
// same terms as [lastBlockedReason].
|
||||
final why = (resp?['message'] ?? '').toString().trim();
|
||||
lastPickupRefusal = why.isEmpty
|
||||
? 'Your office could not record this pickup. Try again in a moment.'
|
||||
: why;
|
||||
} else {
|
||||
lastPickupRefusal = null;
|
||||
// ── What the stop actually became, in the server's words ──
|
||||
//
|
||||
// The pivot has two legitimate outcomes and the caller must stamp the
|
||||
@@ -1051,14 +1194,13 @@ class PickupsController extends GetxController
|
||||
}
|
||||
|
||||
// ---------------- Geofencing helpers ----------------
|
||||
Future<int> _getPickupRadius() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt('pickupradius') ?? 100;
|
||||
} catch (_) {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
//
|
||||
// The radius is [kGeofenceRadiusMeters] and nothing else. It read the
|
||||
// server's `pickupradius` out of prefs, which meant the app's one honesty
|
||||
// control was a per-tenant number nobody here could see — and it disagreed
|
||||
// with Home's own hardcoded 500. `pickupradius` is still stored at login;
|
||||
// it simply no longer decides this.
|
||||
double get _geofenceRadius => kGeofenceRadiusMeters;
|
||||
|
||||
// Helper method to show snackbar reliably in both debug and release builds
|
||||
//
|
||||
@@ -1138,6 +1280,13 @@ class PickupsController extends GetxController
|
||||
/// a caller can read it straight after its `await`.
|
||||
String? lastBlockedReason;
|
||||
|
||||
/// Why the last `pickup-complete` was refused, in the server's own words.
|
||||
///
|
||||
/// Null after a successful pivot. Set only when the call came back not-OK,
|
||||
/// so a caller can say what happened instead of leaving the rider sliding at
|
||||
/// a stop the hub keeps rejecting. See [updatePickedStatus].
|
||||
String? lastPickupRefusal;
|
||||
|
||||
Future<bool> _checkGeofence(
|
||||
double targetLat,
|
||||
double targetLng,
|
||||
@@ -1175,26 +1324,44 @@ class PickupsController extends GetxController
|
||||
currentLat.abs() <= 90 &&
|
||||
currentLng.abs() <= 180;
|
||||
|
||||
if (!hasValidTarget || !hasValidCurrent) {
|
||||
// If coordinates are missing or invalid, show error and block
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] Missing or invalid coordinates for $action. Target: ($targetLat, $targetLng), Current: ($currentLat, $currentLng)',
|
||||
);
|
||||
}
|
||||
|
||||
// Show warning snackbar using helper method
|
||||
_showErrorSnackbar(
|
||||
'Location Warning',
|
||||
'Missing coordinates. Proceeding with update.',
|
||||
bgColor: ColorConstants.warning,
|
||||
seconds: 3,
|
||||
// ── Two ways to have no coordinates, and only one of them is the rider's ──
|
||||
//
|
||||
// This used to treat both the same and wave both through: "Missing
|
||||
// coordinates. Proceeding with update." That is the bypass that makes a
|
||||
// fence decorative — turn location off and every rung opens — and it was
|
||||
// survivable only because the fence itself was off.
|
||||
//
|
||||
// **No target.** The booking carries no pin. That is the hub's data, the
|
||||
// rider cannot fix it from a doorstep, and blocking him leaves the stop
|
||||
// unworkable by anyone. Allowed, and logged, exactly as before.
|
||||
if (!hasValidTarget) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] $action allowed: the stop carries no coordinates '
|
||||
'($targetLat, $targetLng), so proximity cannot be checked. This is a '
|
||||
'data gap on the booking, not a rider who is somewhere else.',
|
||||
);
|
||||
return true; // Allow update to proceed despite missing coords
|
||||
return true;
|
||||
}
|
||||
|
||||
// **No fix.** Location is off, permission is denied, or the GPS did not
|
||||
// settle in time. This one the rider *can* fix, and it is the difference
|
||||
// between a fence and a suggestion — so it is refused, and the message
|
||||
// says which of the three to go and change.
|
||||
if (!hasValidCurrent) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] $action refused: no usable fix '
|
||||
'($currentLat, $currentLng)',
|
||||
);
|
||||
lastBlockedReason =
|
||||
'Your phone could not find your location, so this stop cannot be '
|
||||
'marked ${action.toLowerCase()}. Turn location on, allow it for '
|
||||
'Miler, and step outside if you can.';
|
||||
_showErrorSnackbar('Location Error', lastBlockedReason!, seconds: 5);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final radius = await _getPickupRadius();
|
||||
final radius = _geofenceRadius;
|
||||
final distance = Geolocator.distanceBetween(
|
||||
targetLat,
|
||||
targetLng,
|
||||
@@ -1202,16 +1369,30 @@ class PickupsController extends GetxController
|
||||
currentLng,
|
||||
);
|
||||
final distanceKm = distance / 1000.0;
|
||||
final radiusKm = radius / 1000.0;
|
||||
final distanceMeters = distance;
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] Action: $action | Target: ($targetLat, $targetLng) | Current: ($currentLat, $currentLng) | Distance: ${distanceMeters.toStringAsFixed(1)}m (${distanceKm.toStringAsFixed(3)}km) | Radius: ${radius}m (${radiusKm.toStringAsFixed(3)}km)',
|
||||
);
|
||||
}
|
||||
// ── The phone's own error is credited to the rider ──
|
||||
//
|
||||
// A fix carries an accuracy in metres, and at a 10 m fence that number
|
||||
// is the same size as the thing being measured. Comparing a raw distance
|
||||
// against 10 m asserts a precision the hardware did not provide, and the
|
||||
// rider standing at the door on a ±25 m fix is the one it refuses.
|
||||
//
|
||||
// So the fence is measured against the *nearest point the phone allows*:
|
||||
// 12 m away on a ±20 m fix has not been shown to be outside it. He is
|
||||
// blocked when the phone says he is outside, not when the arithmetic
|
||||
// does. See [kGeofenceRadiusMeters].
|
||||
final slack = _lastFixAccuracy;
|
||||
final effective = (distance - slack).clamp(0.0, double.infinity);
|
||||
|
||||
if (distance > radius) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] $action | target ($targetLat, $targetLng) '
|
||||
'| rider ($currentLat, $currentLng) '
|
||||
'| ${distanceMeters.toStringAsFixed(1)}m ±${slack.toStringAsFixed(0)}m '
|
||||
'→ ${effective.toStringAsFixed(1)}m vs ${radius.toStringAsFixed(0)}m',
|
||||
);
|
||||
|
||||
if (effective > radius) {
|
||||
// ── One sentence, in metres he can act on ──
|
||||
//
|
||||
// Was three lines of "Distance: 4213 m (4.21 km) / Required: Within
|
||||
@@ -1221,7 +1402,8 @@ class PickupsController extends GetxController
|
||||
? '${distanceKm.toStringAsFixed(1)} km'
|
||||
: '${distanceMeters.toStringAsFixed(0)} m';
|
||||
lastBlockedReason =
|
||||
"You're $away from this stop — get within $radius m to mark it "
|
||||
"You're $away from this stop — get within "
|
||||
'${radius.toStringAsFixed(0)} m to mark it '
|
||||
'${action.toLowerCase()}';
|
||||
_showErrorSnackbar('Location Error', lastBlockedReason!, seconds: 5);
|
||||
return false;
|
||||
@@ -1601,6 +1783,18 @@ class PickupsController extends GetxController
|
||||
/// `reached` that writes `Arrived_At_Pickup`.
|
||||
String? lastArrivalNotice;
|
||||
|
||||
/// Set when the server **refused** the arrival, as opposed to never hearing
|
||||
/// it. Null on success and on a network failure.
|
||||
///
|
||||
/// ── The distinction the caller needs ──
|
||||
///
|
||||
/// `updateArrivedStatus` answers false for both a 4xx and a dead network, and
|
||||
/// those want opposite handling: a rider with no signal at a kitchen door must
|
||||
/// carry on, and a rider whose arrival the server rejected on a business rule
|
||||
/// must not be shown as arrived. `ApiResult.status` is 0 when nothing came
|
||||
/// back and the HTTP code when something did, which is the whole test.
|
||||
String? lastArrivalRefusal;
|
||||
|
||||
Future<bool> updateArrivedStatus({
|
||||
required int pickupId,
|
||||
required int orderHeaderId,
|
||||
@@ -1659,9 +1853,18 @@ class PickupsController extends GetxController
|
||||
arrivedShimmer.value = false;
|
||||
|
||||
if (!ok) {
|
||||
// A code at or above 400 is the server answering. Anything else — 0
|
||||
// most often — is the request never getting there.
|
||||
final code = int.tryParse('${resp?['code'] ?? 0}') ?? 0;
|
||||
lastArrivalRefusal = code >= 400
|
||||
? (resp?['message']?.toString().trim().isNotEmpty == true
|
||||
? resp!['message'].toString()
|
||||
: 'Your office would not accept this arrival.')
|
||||
: null;
|
||||
debugPrint('[UPDATE][ARRIVED][FAILED] resp=${jsonEncode(resp)}');
|
||||
return false;
|
||||
}
|
||||
lastArrivalRefusal = null;
|
||||
|
||||
// ── Succeeded, but did the hub record it? ──
|
||||
//
|
||||
@@ -1678,7 +1881,7 @@ class PickupsController extends GetxController
|
||||
arrivalConfirmedByServer.value = resp?['confirmed'] == true;
|
||||
if (!arrivalConfirmedByServer.value) {
|
||||
lastArrivalNotice =
|
||||
'Marked arrived on your phone. Your hub has not recorded it — '
|
||||
'Marked arrived on your phone. Your office has not recorded it — '
|
||||
'their system is not accepting arrivals yet.';
|
||||
debugPrint(
|
||||
'[UPDATE][ARRIVED][UNCONFIRMED] server said '
|
||||
|
||||
Reference in New Issue
Block a user