import 'package:flutter_test/flutter_test.dart'; import 'package:miler/data/api_config.dart'; import 'package:miler/data/api_status.dart'; /// ───────────────────────────────────────────────────────────────────────── /// A STATUS THIS BUILD HAS NEVER HEARD OF MUST NOT BE A SUCCESS /// /// Statuses were compared as string literals wherever a screen needed one, so /// an unrecognised value took whichever branch the condition happened to fall /// through to — and the conditions were written as `!= 'Cancelled'`, which /// means a new backend status read as *fine*. /// /// The backend will add values this build does not know. These pin the two /// rules that make that safe: unknown parses to `unknown`, and `unknown` is /// excluded from every "this is finished / this worked" predicate. /// ───────────────────────────────────────────────────────────────────────── void main() { statusTranslation(); group('assignment', () { test('parses the contract\'s values', () { expect(AssignmentStatus.parse('Assigned'), AssignmentStatus.assigned); expect(AssignmentStatus.parse('Accepted'), AssignmentStatus.accepted); expect(AssignmentStatus.parse('Rejected'), AssignmentStatus.rejected); expect(AssignmentStatus.parse('Reassigned'), AssignmentStatus.reassigned); expect(AssignmentStatus.parse('Completed'), AssignmentStatus.completed); expect(AssignmentStatus.parse('Cancelled'), AssignmentStatus.cancelled); }); test('survives case and separator drift on the wire', () { expect(AssignmentStatus.parse('ACCEPTED'), AssignmentStatus.accepted); expect(AssignmentStatus.parse(' accepted '), AssignmentStatus.accepted); }); test('an unknown value is unknown, and is not settled', () { final s = AssignmentStatus.parse('Escalated_To_Ops'); expect(s, AssignmentStatus.unknown); expect( s.isSettled, isFalse, reason: 'a status this build cannot read must not be filed as done', ); expect(s.isLive, isFalse); expect(s.needsDecision, isFalse); }); test('null and empty do not throw', () { expect(AssignmentStatus.parse(null), AssignmentStatus.unknown); expect(AssignmentStatus.parse(''), AssignmentStatus.unknown); }); }); group('booking', () { test('parses the contract\'s values', () { expect( BookingStatus.parse('Pending_Pickup'), BookingStatus.pendingPickup, ); expect( BookingStatus.parse('Miler_Assigned'), BookingStatus.milerAssigned, ); expect( BookingStatus.parse('Pickup_Scheduled'), BookingStatus.pickupScheduled, ); expect(BookingStatus.parse('Picked_Up'), BookingStatus.pickedUp); expect( BookingStatus.parse('Converted_To_Consignment'), BookingStatus.convertedToConsignment, ); }); // ── pickup-complete is the pivot ── // // It converts the booking into a consignment, so both rungs past it are // "collected". A check that only looked for Picked_Up would treat a // converted booking as still needing collection. test('collection covers both rungs past the pivot', () { expect(BookingStatus.pickedUp.isCollected, isTrue); expect(BookingStatus.convertedToConsignment.isCollected, isTrue); expect(BookingStatus.pickupScheduled.isCollected, isFalse); }); // The server refuses cancellation once picked up; the client must not offer // a control whose only outcome is a 400. test('cancellation is refused once picked up', () { expect(BookingStatus.pendingPickup.canCancel, isTrue); expect(BookingStatus.milerAssigned.canCancel, isTrue); expect(BookingStatus.pickedUp.canCancel, isFalse); expect(BookingStatus.convertedToConsignment.canCancel, isFalse); expect(BookingStatus.unknown.canCancel, isFalse); }); }); group('consignment', () { test('parses every state the contract lists', () { const wire = { '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, }; wire.forEach((raw, expected) { expect(ConsignmentStatus.parse(raw), expected, reason: raw); }); }); // Deliver and skip are legal only from Out_for_Delivery — anything else is // a 400. Offering the control elsewhere offers a guaranteed failure. test('only Out_for_Delivery is deliverable', () { expect(ConsignmentStatus.outForDelivery.isDeliverable, isTrue); for (final s in ConsignmentStatus.values) { if (s == ConsignmentStatus.outForDelivery) continue; expect(s.isDeliverable, isFalse, reason: '$s must not offer deliver'); } }); test('returning and exception states are closed, unknown is not', () { expect(ConsignmentStatus.rtoInitiated.isReturning, isTrue); expect(ConsignmentStatus.returnedToSender.isReturning, isTrue); expect(ConsignmentStatus.missing.isException, isTrue); expect(ConsignmentStatus.damaged.isException, isTrue); expect(ConsignmentStatus.delivered.isClosed, isTrue); expect( ConsignmentStatus.unknown.isClosed, isFalse, reason: 'an unreadable status must not close a consignment', ); }); }); group('rider availability', () { test('the break value is Break, not On_Break', () { expect(RiderAvailability.parse('Break'), RiderAvailability.onBreak); expect(RiderAvailability.onBreak.wire, 'Break'); expect( RiderAvailability.parse('On_Break'), RiderAvailability.unknown, reason: 'On_Break is the obvious guess and the wrong one', ); }); test('every known value round-trips through the wire form', () { for (final s in RiderAvailability.values) { if (s == RiderAvailability.unknown) continue; expect(RiderAvailability.parse(s.wire), s, reason: s.name); } }); test('an unknown status is never echoed back as though understood', () { final s = RiderAvailability.parse('On_Refuelling_Stop'); expect(s, RiderAvailability.unknown); expect(s.wire, 'Offline'); expect(s.isWorking, isFalse); }); }); } /// ───────────────────────────────────────────────────────────────────────── /// THE ONE PLACE THE CONTRACT BECOMES THE UI'S VOCABULARY /// /// `legacyStatusFromNew` was a switch on raw strings whose `default` returned /// the value unchanged. Two things went wrong quietly: `Pending_Pickup` and /// `Created` were not listed and fell through as themselves, and any status the /// backend added tomorrow would arrive in the UI as an unrecognised string the /// row logic then had to guess at. /// ───────────────────────────────────────────────────────────────────────── void statusTranslation() { group('booking status → the row vocabulary', () { test('everything the rider still has to decide lands as assigned', () { for (final raw in const ['Pending_Pickup', 'Created', 'Miler_Assigned']) { expect(ApiConfig.legacyStatusFromNew(raw), 'assigned', reason: raw); } }); test('the working rungs keep their meaning', () { // ── `Pickup_Scheduled` is the ACCEPTED rung ── // // This expected `active`, and that expectation was the bug: `active` // means *the rider is on the stop* to every row in this app, and // `stopStateOf` reads a raw `active` ahead of the local accepted record. // So the moment the queue came back after an accept, the stop reported as // arrived — the selection bar offered **Mark as Picked** for a kitchen // the rider had not reached, and arrival, the one rung with a real // endpoint behind it, could never be recorded or seen by the hub. // // The backend writes this status when the rider accepts; the console has // always mapped it to *accepted*. The two applications were reading one // status two different ways. expect(ApiConfig.legacyStatusFromNew('Pickup_Scheduled'), 'accepted'); expect(ApiConfig.legacyStatusFromNew('At_Customer'), 'arrived'); expect(ApiConfig.legacyStatusFromNew('Picked_Up'), 'Picked up'); expect( ApiConfig.legacyStatusFromNew('Converted_To_Consignment'), 'picked', ); expect(ApiConfig.legacyStatusFromNew('Cancelled'), 'cancelled'); }); // ── Wrong-but-safe beats wrong-and-settled ── // // A status this build cannot classify must never render as picked, // cancelled or otherwise finished. It renders as undecided, which is the // state a rider can still act on. test('an unknown status never becomes a settled one', () { for (final raw in const [ 'In_Transit', 'Escalated_To_Ops', '', 'total nonsense', ]) { final legacy = ApiConfig.legacyStatusFromNew(raw); expect(legacy, '', reason: raw); expect(legacy, isNot('picked')); expect(legacy, isNot('cancelled')); expect(legacy, isNot('Picked up')); } expect(ApiConfig.legacyStatusFromNew(null), ''); }); }); }