/// ───────────────────────────────────────────────────────────────────────── /// THE CONTRACT'S OWN VOCABULARY /// /// Assignment, booking and consignment statuses arrive as free-form strings and /// were compared as string literals wherever a screen needed one. That is the /// same mistake [StopStatus] was written to fix on the read side of the parcel /// flow, one layer further out: case-sensitivity landmines, variant spellings, /// and — the expensive one — **an unrecognised value silently taking the /// success branch** because the check was `!= 'Cancelled'`. /// /// ── Unknown is a value, not a crash and not a success ── /// /// The backend will add statuses this build has never heard of. Every enum here /// therefore carries an [unknown] member and parses by *exact match on a /// normalised string*, so a new server value lands on `unknown` and the screens /// treat it as "not something I can act on" rather than as delivered, accepted /// or complete. `values.byName`-style lookups and `firstWhere` without an /// `orElse` both throw; neither is used. /// /// The raw string is kept alongside, because a status this build cannot model /// is still something the rider and the hub can read. /// ───────────────────────────────────────────────────────────────────────── library; /// `Assigned_To_Miler` / `assigned to miler` / `ASSIGNED-TO-MILER` all reduce /// to one key, so a spelling drift on the wire is not a behaviour change here. String _key(Object? raw) => (raw?.toString() ?? '') .trim() .toLowerCase() .replaceAll(RegExp(r'[\s\-]+'), '_'); /// What the hub has done with an offer of work. enum AssignmentStatus { assigned, accepted, rejected, reassigned, completed, cancelled, unknown; static const _byKey = { 'assigned': AssignmentStatus.assigned, 'accepted': AssignmentStatus.accepted, 'rejected': AssignmentStatus.rejected, 'reassigned': AssignmentStatus.reassigned, 'completed': AssignmentStatus.completed, 'cancelled': AssignmentStatus.cancelled, }; static AssignmentStatus parse(Object? raw) => _byKey[_key(raw)] ?? AssignmentStatus.unknown; /// The rider still owes this one a decision. bool get needsDecision => this == AssignmentStatus.assigned; /// He has taken it on and it is not finished. bool get isLive => this == AssignmentStatus.accepted; /// Nothing further will happen here. **`unknown` is deliberately not /// settled** — a status this build cannot read must not be filed as done. bool get isSettled => this == AssignmentStatus.rejected || this == AssignmentStatus.reassigned || this == AssignmentStatus.completed || this == AssignmentStatus.cancelled; } /// A pickup, from the moment it exists to the moment it becomes a consignment. enum BookingStatus { pendingPickup, created, milerAssigned, pickupScheduled, /// The rider is standing at the pickup address. /// /// `POST /miler/bookings/:id/reached` writes this — but until 21 Aug 2026 it /// wrote nothing the app could observe, so **I've arrived** appeared to do /// nothing and the console never showed the rung. The backend now persists /// it under this name. `At_Customer` is the older spelling seen in the wild /// and means the same thing; both parse here. arrivedAtPickup, pickedUp, convertedToConsignment, /// ── The hyperlocal short-circuit ── /// /// `pickup-complete` decides routing from the two pincodes: matching 3-digit /// prefixes are hyperlocal and the parcel goes **straight to /// `Out_for_Delivery`** instead of routing via a hub. Every DailyGrubs run is /// hyperlocal, so this is not an edge case on that line — it is the status a /// collected meal actually carries. /// /// It was missing from this enum, and the cost was precise: [parse] answered /// [unknown], which the legacy translation maps to the empty string, which /// reads as *undecided* — so a bag already in the rider's box came back from /// the queue looking like work he had not accepted yet. The local collected /// record hid it on the device that did the pickup and nowhere else: a /// restart, a reinstall or a second device showed collected orders sitting on /// Home as pending. outForDelivery, /// Handed over. Terminal. delivered, cancelled, unknown; static const _byKey = { 'pending_pickup': BookingStatus.pendingPickup, 'created': BookingStatus.created, 'miler_assigned': BookingStatus.milerAssigned, 'pickup_scheduled': BookingStatus.pickupScheduled, 'arrived_at_pickup': BookingStatus.arrivedAtPickup, 'at_customer': BookingStatus.arrivedAtPickup, 'picked_up': BookingStatus.pickedUp, 'converted_to_consignment': BookingStatus.convertedToConsignment, // Spelled `Out_for_Delivery` on bookings — lower-case `f`, unlike the // consignment enum's `Out_For_Delivery`. `_key` lower-cases before lookup // so both land here, which is deliberate: the difference is a backend // inconsistency, not a distinction, and no caller should have to know it. 'out_for_delivery': BookingStatus.outForDelivery, 'delivered': BookingStatus.delivered, 'cancelled': BookingStatus.cancelled, }; static BookingStatus parse(Object? raw) => _byKey[_key(raw)] ?? BookingStatus.unknown; /// Collection has happened — the pickup-to-delivery boundary has been /// crossed, server-side. /// /// `pickup-complete` is the pivot: it converts the booking into a consignment /// and, on a hyperlocal run, releases it for delivery in the same call. Every /// rung from there on counts, including [delivered] — a delivered order was /// certainly collected, and a predicate that said otherwise would put a /// finished stop back in the pickup domain. /// /// This is what [WorkBoundary] reads. It must never include a rung before the /// hand-over: an acceptance is a decision about work still to be done. bool get isCollected => this == BookingStatus.pickedUp || this == BookingStatus.convertedToConsignment || this == BookingStatus.outForDelivery || this == BookingStatus.delivered; /// The rider still has work to do at this address. bool get isOpen => this == BookingStatus.pendingPickup || this == BookingStatus.created || this == BookingStatus.milerAssigned || this == BookingStatus.pickupScheduled || this == BookingStatus.arrivedAtPickup; /// ── Cancellation is refused once picked up ── /// /// The server enforces it; this is the client half, so the control is not /// offered in a state where pressing it can only fail. bool get canCancel => isOpen; } /// A consignment, from the hub's point of view. enum ConsignmentStatus { created, inwardedAtHub, tripsheetLoaded, inTransit, outForDelivery, delivered, rtoInitiated, returnedToSender, missing, damaged, unknown; static const _byKey = { 'created': ConsignmentStatus.created, 'inwarded_at_hub': ConsignmentStatus.inwardedAtHub, 'tripsheet_loaded': ConsignmentStatus.tripsheetLoaded, 'in_transit': ConsignmentStatus.inTransit, 'out_for_delivery': ConsignmentStatus.outForDelivery, 'delivered': ConsignmentStatus.delivered, 'rto_initiated': ConsignmentStatus.rtoInitiated, 'returned_to_sender': ConsignmentStatus.returnedToSender, 'missing': ConsignmentStatus.missing, 'damaged': ConsignmentStatus.damaged, }; static ConsignmentStatus parse(Object? raw) => _byKey[_key(raw)] ?? ConsignmentStatus.unknown; /// The one state `deliver` and `skip` are legal from — anything else is a /// 400. Offering the control elsewhere is offering a guaranteed failure. bool get isDeliverable => this == ConsignmentStatus.outForDelivery; /// Handed over. Only this one. bool get isDelivered => this == ConsignmentStatus.delivered; /// Going back, or gone. Not failures the rider caused, and not states he can /// work out of on this screen. bool get isReturning => this == ConsignmentStatus.rtoInitiated || this == ConsignmentStatus.returnedToSender; /// Something is wrong with the parcel itself and the hub owns it now. bool get isException => this == ConsignmentStatus.missing || this == ConsignmentStatus.damaged; /// Nothing further happens on the rider's phone. **`unknown` is excluded** — /// see the note at the top of this file. bool get isClosed => isDelivered || isReturning || isException; } /// What the rider is doing, as the availability endpoint understands it. /// /// `Break`, not `On_Break`: the obvious guess is the wrong one, and it is the /// value the server validates against. enum RiderAvailability { offline, available, assigned, onPickup, atCustomer, pickedUp, onDelivery, onBreak, blocked, unknown; static const _byKey = { 'offline': RiderAvailability.offline, 'available': RiderAvailability.available, 'assigned': RiderAvailability.assigned, 'on_pickup': RiderAvailability.onPickup, 'at_customer': RiderAvailability.atCustomer, 'picked_up': RiderAvailability.pickedUp, 'on_delivery': RiderAvailability.onDelivery, 'break': RiderAvailability.onBreak, 'blocked': RiderAvailability.blocked, }; static RiderAvailability parse(Object? raw) => _byKey[_key(raw)] ?? RiderAvailability.unknown; /// The exact string this value is sent back as. Named separately from the /// Dart member so `onBreak` can carry the wire's `Break` without the enum /// having a member called `break`, which is a keyword. String get wire => switch (this) { RiderAvailability.offline => 'Offline', RiderAvailability.available => 'Available', RiderAvailability.assigned => 'Assigned', RiderAvailability.onPickup => 'On_Pickup', RiderAvailability.atCustomer => 'At_Customer', RiderAvailability.pickedUp => 'Picked_Up', RiderAvailability.onDelivery => 'On_Delivery', RiderAvailability.onBreak => 'Break', RiderAvailability.blocked => 'Blocked', // Never sent. A value this build cannot model must not be echoed back to // the server as though it were understood. RiderAvailability.unknown => 'Offline', }; /// On duty in any sense — anything but offline, blocked, or unreadable. bool get isWorking => this != RiderAvailability.offline && this != RiderAvailability.blocked && this != RiderAvailability.unknown; }