production

This commit is contained in:
2026-08-28 11:13:15 +05:30
parent d7348e253f
commit 5723d373b2
162 changed files with 17924 additions and 7026 deletions

View File

@@ -0,0 +1,357 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:miler/Models/stop_status.dart';
import 'package:miler/data/milk_run.dart';
import 'package:miler/data/rider_stage.dart';
import 'package:miler/data/service_profile.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// THE RIDER-FACING LIFECYCLE, END TO END
///
/// ```
/// Pending → Accepted → Arrived → Picked → Active → Delivered
/// ```
///
/// Six stages against a backend lifecycle that has five and no arrival in it.
/// The gap is not an oversight on their side: arrival is an **event**, written
/// by `/reached` as a timestamp beside a status that does not move, and asking
/// them to add a rung would be asking them to model an event as a state.
///
/// So the app owes the richer workflow, and these hold it to it. Two resolvers
/// are exercised, deliberately, because they answer different questions about
/// the same order:
///
/// • [riderStageOf] — where is this order in the rider's day?
/// • [MilkRun.stageOf] — how far did the *pickup* get?
///
/// A stop whose delivery is under way is `Active` on the first and `Picked` on
/// the second, and both are correct. That separation is the point of the whole
/// exercise, so it is asserted rather than assumed.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
const id = 'B1';
Map<String, dynamic> row(String status) => {
'orderid': id,
'pickupid': id,
'sourcename': 'Vidhya Kitchen',
'orderstatus': status,
};
RiderStage stage(
String status, {
Set<String> arrived = const {},
Set<String> collected = const {},
Set<String> released = const {},
Set<String> delivered = const {},
}) => riderStageOf(
row(status),
arrivedIds: arrived,
collectedIds: collected,
outForDeliveryIds: released,
deliveredIds: delivered,
);
group('1 · the six stages, walked once in order', () {
test('Pending — the offer', () {
expect(stage('miler_assigned'), RiderStage.pending);
expect(stage('pending'), RiderStage.pending);
});
test('Accepted — pickup_scheduled, and nothing else', () {
// The backend's own word for "the rider took it".
expect(stage('pickup_scheduled'), RiderStage.accepted);
});
test(
'Arrived — the arrival record, because no server field carries it',
() {
// `/reached` writes a timestamp; `GET /miler/bookings` does not return
// it. Until it does, the rider's own record is the only evidence there
// is — see `getArrivedOrderIds`.
expect(stage('pickup_scheduled', arrived: {id}), RiderStage.arrived);
},
);
test('Picked — either backend word, or the collected record', () {
expect(stage('converted_to_consignment'), RiderStage.picked);
expect(stage('picked_up'), RiderStage.picked);
expect(stage('pickup_scheduled', collected: {id}), RiderStage.picked);
});
test('Active — the delivery is under way', () {
expect(stage('out_for_delivery'), RiderStage.active);
expect(stage('active'), RiderStage.active);
expect(
stage('converted_to_consignment', released: {id}),
RiderStage.active,
reason: 'the rider pressed Start delivery; the round has begun',
);
});
test('Delivered — terminal', () {
expect(stage('delivered'), RiderStage.delivered);
});
test('Cancelled sits outside the run', () {
expect(stage('cancelled'), RiderStage.cancelled);
expect(stage('rejected'), RiderStage.cancelled);
});
test('the stages are ordered, and each is its own', () {
final walked = [
stage('miler_assigned'),
stage('pickup_scheduled'),
stage('pickup_scheduled', arrived: {id}),
stage('converted_to_consignment'),
stage('out_for_delivery'),
stage('delivered'),
];
expect(walked.toSet(), hasLength(6), reason: 'six distinct stages');
for (var i = 1; i < walked.length; i++) {
expect(
walked[i].rank,
greaterThan(walked[i - 1].rank),
reason: '${walked[i]} must rank above ${walked[i - 1]}',
);
}
});
});
group('2 · progress never runs backwards', () {
test('a stale arrival cannot un-pick a collected stop', () {
expect(
stage('converted_to_consignment', arrived: {id}),
RiderStage.picked,
);
});
test('a stale arrival cannot un-start a round', () {
expect(stage('out_for_delivery', arrived: {id}), RiderStage.active);
});
test('a stale collection cannot un-deliver an order', () {
expect(
stage('delivered', arrived: {id}, collected: {id}),
RiderStage.delivered,
);
});
test('no local record can revive a cancelled stop', () {
expect(
stage('cancelled', arrived: {id}, collected: {id}, released: {id}),
RiderStage.cancelled,
);
});
});
group('3 · the two resolvers answer different questions', () {
setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan));
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
StopStatus pickupRung(String status, {Set<String> collected = const {}}) =>
MilkRun.stageOf(
row(status),
acceptedIds: const {},
collectedIds: collected,
);
test('delivery under way: Active overall, Picked on the pickup card', () {
// The case the separation exists for, stated in one test.
expect(stage('active', collected: {id}), RiderStage.active);
expect(pickupRung('active', collected: {id}), StopStatus.picked);
});
test('and the pickup milestone is readable from the stage too', () {
expect(stage('out_for_delivery', collected: {id}).pickupComplete, isTrue);
expect(stage('delivered').pickupComplete, isTrue);
expect(stage('pickup_scheduled', arrived: {id}).pickupComplete, isFalse);
expect(
stage('cancelled').pickupComplete,
isFalse,
reason: 'a cancelled stop was never picked',
);
});
test('availability is not a booking status and cannot reach either', () {
// `updateActiveStatus` posts `setAvailability('On_Pickup')` — a fact about
// the **rider**. It has no booking status to write, and the regression it
// caused was the app inventing one. Neither resolver takes availability,
// and there is no spelling of it that parses as a rung.
expect(stopStatusFromRaw('on_pickup'), StopStatus.unknown);
expect(stopStatusFromRaw('On_Pickup'), StopStatus.unknown);
});
});
group('3b · the authoritative contract, field by field', () {
Map<String, dynamic> booking(
String status, {
Object? reachedAt,
String? consignment,
}) => {
'orderid': id,
'pickupid': id,
'orderstatus': status,
if (reachedAt != null) 'reachedat': reachedAt,
if (consignment != null) 'consignmentstatus': consignment,
};
test('Pickup_Scheduled + no reachedat → Accepted', () {
expect(riderStageOf(booking('Pickup_Scheduled')), RiderStage.accepted);
expect(
riderStageOf(booking('Pickup_Scheduled', reachedAt: '')),
RiderStage.accepted,
reason: 'an empty stamp is not an arrival',
);
expect(
riderStageOf(booking('Pickup_Scheduled', reachedAt: 'null')),
RiderStage.accepted,
reason: 'a stringified JSON null is not an arrival either',
);
});
test('Pickup_Scheduled + reachedat → Arrived, from the server alone', () {
// No local record passed in: this is the rung rebuilt from server data,
// which is what retires the compatibility store.
expect(
riderStageOf(
booking('Pickup_Scheduled', reachedAt: '2026-08-25T12:33:02+05:30'),
),
RiderStage.arrived,
);
});
test('a malformed stamp is still an arrival, and does not throw', () {
// Read for presence, not for value: the backend does not write the field
// for a booking nobody arrived at, so an unparseable one is a bad clock
// rather than a bad claim. Parsing it and discarding it would lose the
// rung over a formatting detail.
for (final junk in const ['not-a-date', 0, '2026-13-45', true]) {
expect(
riderStageOf(booking('Pickup_Scheduled', reachedAt: junk)),
RiderStage.arrived,
reason: 'reachedat="$junk" threw or was dropped',
);
}
});
test('Collected_By_Miler → Picked', () {
expect(
riderStageOf(
booking(
'Converted_To_Consignment',
consignment: 'Collected_By_Miler',
),
),
RiderStage.picked,
);
});
test('Out_for_Delivery → Active', () {
expect(
riderStageOf(
booking('Converted_To_Consignment', consignment: 'Out_for_Delivery'),
),
RiderStage.active,
);
});
test('Delivered → Delivered', () {
expect(
riderStageOf(
booking('Converted_To_Consignment', consignment: 'Delivered'),
),
RiderStage.delivered,
);
});
test('the consignment outranks a stale stamp at every rung', () {
const stamp = '2026-08-25T12:33:02+05:30';
for (final (state, expected) in const [
('Collected_By_Miler', RiderStage.picked),
('Out_for_Delivery', RiderStage.active),
('Delivered', RiderStage.delivered),
]) {
expect(
riderStageOf(
booking('Pickup_Scheduled', reachedAt: stamp, consignment: state),
arrivedIds: {id},
collectedIds: {id},
),
expected,
reason: '$state was dragged back by older evidence',
);
}
});
test('CROSS-CITY · the hub\'s rungs are not this rider being Active', () {
// Confirmed by the backend: a cross-city parcel goes
// Collected_By_Miler → Inwarded_at_Hub → Tripsheet_Loaded → In_Transit
// and is delivered by somebody else. The rider's involvement ended at
// the collection, so Picked is where he stays — and no delivery control
// may be offered for a stop he cannot act on.
for (final hub in const [
'Inwarded_at_Hub',
'Tripsheet_Loaded',
'In_Transit',
'Created',
]) {
final at = riderStageOf(
booking('Converted_To_Consignment', consignment: hub),
);
expect(
at,
RiderStage.picked,
reason: '"$hub" resolved to $at — the hub half is not his round',
);
expect(at, isNot(RiderStage.active));
}
});
});
group('4 · restored from persisted state after a restart', () {
// Reconstruction reads the same evidence the live flow writes, so a
// relaunch lands a stop exactly where the rider left it — the one thing a
// derived lifecycle has to get right, because nothing is stored.
test('pickup_scheduled with no local record → Accepted', () {
expect(stage('pickup_scheduled'), RiderStage.accepted);
});
test('pickup_scheduled with a surviving arrival record → Arrived', () {
expect(stage('pickup_scheduled', arrived: {id}), RiderStage.arrived);
});
test('converted_to_consignment → Picked, with no local record at all', () {
expect(stage('converted_to_consignment'), RiderStage.picked);
});
test('a released round comes back Active', () {
expect(
stage('converted_to_consignment', released: {id}),
RiderStage.active,
);
});
test('a finished stop comes back Delivered', () {
expect(stage('delivered'), RiderStage.delivered);
});
test('nothing restores to an earlier rung than it reached', () {
// Every server word, with a full set of stale local records attached.
// None may pull the stop back.
final stale = {id};
for (final (said, expected) in const [
('converted_to_consignment', RiderStage.picked),
('picked_up', RiderStage.picked),
('out_for_delivery', RiderStage.active),
('active', RiderStage.active),
('delivered', RiderStage.delivered),
]) {
expect(
riderStageOf(row(said), arrivedIds: stale),
expected,
reason: '"$said" was dragged back by a stale arrival record',
);
}
});
});
}