288 lines
11 KiB
Dart
288 lines
11 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:miler/data/milk_run.dart';
|
|
import 'package:miler/data/service_profile.dart';
|
|
import 'package:miler/data/work_domain.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// THE BOUNDARY BETWEEN THE TWO WORK SCREENS
|
|
///
|
|
/// ```
|
|
/// PENDING ─ accept ─▶ ACCEPTED ─ navigate ─▶ ARRIVED ─ pick up ─▶ PICKED
|
|
/// └────────────────── HOME owns all of this ───────────────┘ │
|
|
/// ▼
|
|
/// DELIVERIES owns from here on
|
|
/// ```
|
|
///
|
|
/// The defect these pin: an **accepted** booking was claimed by both screens.
|
|
/// It stayed on Home to be collected and it also appeared on Deliveries as live
|
|
/// work, with a strip offering to continue it — which opened a map, an I'VE
|
|
/// ARRIVED and a confirmation sheet for a collection the rider had not made.
|
|
/// Two screens, two filters, one fact, and they disagreed.
|
|
///
|
|
/// So the invariants are asserted on [WorkBoundary] itself rather than by
|
|
/// pumping two widgets and comparing what they drew: it is the single rule both
|
|
/// screens now read, and a rule that cannot be re-expressed per screen is the
|
|
/// actual fix.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
|
|
|
/// One order in the shape the booking adapter produces: the backend has no
|
|
/// per-stop type, so every row calls itself a pickup whatever leg it is on.
|
|
Map<String, dynamic> order(
|
|
String id, {
|
|
required String status,
|
|
String kitchen = 'Vidhya Kitchen',
|
|
}) => {
|
|
'orderid': id,
|
|
'pickupid': id,
|
|
'type': 'pickup',
|
|
'orderstatus': status,
|
|
'kitchenname': kitchen,
|
|
'pickupcustomer': kitchen,
|
|
'dropcustomer': 'Customer $id',
|
|
};
|
|
|
|
group('a kitchen line — the boundary is pickup-complete', () {
|
|
setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan));
|
|
|
|
test('accepted is not picked up, and never leaves Home', () {
|
|
// The whole bug in one assertion. Accepting is a decision about work
|
|
// still to be done; nothing has changed hands.
|
|
final accepted = order('A1', status: 'accepted');
|
|
|
|
expect(
|
|
WorkBoundary.pickupComplete(accepted, collectedIds: const {}),
|
|
isFalse,
|
|
);
|
|
expect(
|
|
WorkBoundary.domainOf(
|
|
accepted,
|
|
acceptedIds: const {'A1'},
|
|
collectedIds: const {},
|
|
),
|
|
WorkDomain.pickup,
|
|
reason: 'an acceptance must never be read as a completed pickup',
|
|
);
|
|
});
|
|
|
|
test('every rung before pickup-complete belongs to Home', () {
|
|
for (final status in ['assigned', 'accepted', 'active', 'arrived']) {
|
|
expect(
|
|
WorkBoundary.domainOf(
|
|
order('A1', status: status),
|
|
acceptedIds: const {'A1'},
|
|
),
|
|
WorkDomain.pickup,
|
|
reason: status,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('the backend saying picked is what moves it', () {
|
|
// Both spellings the contract produces: `Picked_Up` and the conversion
|
|
// to a consignment.
|
|
for (final status in ['picked', 'Picked up']) {
|
|
expect(
|
|
WorkBoundary.domainOf(order('A1', status: status)),
|
|
WorkDomain.delivery,
|
|
reason: status,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('a confirmed pickup keeps it on Deliveries through a stale poll', () {
|
|
// The queue is up to one poll behind, and it answers with what it knew
|
|
// before the hand-over. Without the collected record the order would
|
|
// bounce back to Home for a few seconds, which reads as the confirm
|
|
// having failed — so the record wins, and it is written only after the
|
|
// backend confirms. See `_advanceStop` on Home.
|
|
final stale = order('A1', status: 'accepted');
|
|
|
|
expect(
|
|
WorkBoundary.domainOf(stale, collectedIds: const {'A1'}),
|
|
WorkDomain.delivery,
|
|
);
|
|
});
|
|
|
|
test('the delivery rungs stay on Deliveries', () {
|
|
for (final status in ['out_for_delivery', 'delivery_arrived']) {
|
|
expect(
|
|
WorkBoundary.domainOf(order('A1', status: status)),
|
|
WorkDomain.delivery,
|
|
reason: status,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('delivered and cancelled leave both work screens', () {
|
|
expect(
|
|
WorkBoundary.domainOf(order('A1', status: 'delivered')),
|
|
WorkDomain.closed,
|
|
);
|
|
expect(
|
|
WorkBoundary.domainOf(order('A1', status: 'cancelled')),
|
|
WorkDomain.closed,
|
|
);
|
|
});
|
|
|
|
test('a declined stop stays on Home — it is not delivery work', () {
|
|
// It stays visible there precisely so the rider can change his mind.
|
|
expect(
|
|
WorkBoundary.domainOf(order('A1', status: 'rejected')),
|
|
WorkDomain.pickup,
|
|
);
|
|
});
|
|
|
|
test('picked is the middle of the morning, not the end of it', () {
|
|
// The line does not end at a hub: he is holding the food and still owes
|
|
// every door. Closing it here is what once sent a collected crate to
|
|
// Activity as fifteen finished deliveries.
|
|
expect(WorkBoundary.isClosed(order('A1', status: 'picked')), isFalse);
|
|
});
|
|
});
|
|
|
|
group('one kitchen, partially accepted, partially collected', () {
|
|
setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan));
|
|
|
|
final day = [
|
|
order('A1', status: 'picked'), // collected at the counter
|
|
order('A2', status: 'accepted'), // taken, still on the counter
|
|
order('A3', status: 'assigned'), // not decided yet
|
|
order('A4', status: 'delivered'), // done
|
|
];
|
|
|
|
test('the two screens split the day, with nothing shared', () {
|
|
final home = WorkBoundary.pickupQueue(day, acceptedIds: const {'A2'});
|
|
final deliveries = WorkBoundary.deliveryQueue(
|
|
day,
|
|
acceptedIds: const {'A2'},
|
|
);
|
|
|
|
expect(home.map(MilkRun.idOf), ['A2', 'A3']);
|
|
expect(deliveries.map(MilkRun.idOf), ['A1']);
|
|
|
|
// The invariant that makes the pill trustworthy: no order can be counted
|
|
// by both screens, and none can fall between them.
|
|
final overlap = home
|
|
.map(MilkRun.idOf)
|
|
.toSet()
|
|
.intersection(deliveries.map(MilkRun.idOf).toSet());
|
|
expect(overlap, isEmpty);
|
|
expect(
|
|
home.length + deliveries.length,
|
|
day.length - 1,
|
|
reason: 'A4 closed',
|
|
);
|
|
});
|
|
|
|
test("Home's pill counts the other tab's rows, not its own", () {
|
|
// The pill's tap goes to Deliveries, so its number has to be that tab's
|
|
// — asked of the same rule, from the same day. It used to count accepted
|
|
// work, so it read 3 while the tab it opened listed 1.
|
|
expect(
|
|
WorkBoundary.deliveryQueue(day, acceptedIds: const {'A2'}).length,
|
|
1,
|
|
);
|
|
});
|
|
|
|
test('accepting the rest moves nothing across', () {
|
|
final deliveries = WorkBoundary.deliveryQueue(
|
|
day,
|
|
acceptedIds: const {'A2', 'A3'},
|
|
);
|
|
expect(deliveries.map(MilkRun.idOf), ['A1']);
|
|
});
|
|
|
|
test('a delivered stop leaves the count, not just the list', () {
|
|
// ── The two screens disagreed because they asked different questions ──
|
|
//
|
|
// Both call `deliveryQueue`, which is supposed to be what stops that
|
|
// happening. It cannot, when they pass different arguments: Deliveries
|
|
// passed `closedIds`, Home's pill did not. The rider finished a drop,
|
|
// watched the tab go 22 → 21, and came back to Home still holding 22.
|
|
//
|
|
// Worse on Home than a plain lag, because Home *stamps* a closed stop
|
|
// `picked` so its row cannot flicker back to LIVE while the queue
|
|
// catches up — and `picked` is squarely inside the delivery domain. The
|
|
// pill was counting the stamp that existed to hide the stop.
|
|
final day = [
|
|
order('B1', status: 'picked'),
|
|
order('B2', status: 'picked'),
|
|
];
|
|
|
|
expect(WorkBoundary.deliveryQueue(day).length, 2);
|
|
expect(
|
|
WorkBoundary.deliveryQueue(day, closedIds: const {'B1'}).length,
|
|
1,
|
|
reason: 'the finished drop is off both the list and the count',
|
|
);
|
|
|
|
// And it does not reappear on Home instead — closed is closed, not
|
|
// handed back across the boundary.
|
|
expect(
|
|
WorkBoundary.pickupQueue(day, closedIds: const {'B1'}).length,
|
|
0,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('logistics — the boundary is acceptance, and that is unchanged', () {
|
|
setUp(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
|
|
|
test("an accepted booking is the work tab's, as it always was", () {
|
|
// There is no counter and nothing to batch on this line: the rider drives
|
|
// to one customer and raises the shipment at the door, so the collection
|
|
// *is* the job and it is worked from that tab. The rule is the same one,
|
|
// read through [ServiceProfile.handoffAt].
|
|
expect(
|
|
WorkBoundary.domainOf(
|
|
order('P1', status: 'accepted'),
|
|
acceptedIds: const {'P1'},
|
|
),
|
|
WorkDomain.delivery,
|
|
);
|
|
});
|
|
|
|
test('an undecided booking is still Home\'s', () {
|
|
expect(
|
|
WorkBoundary.domainOf(order('P1', status: 'assigned')),
|
|
WorkDomain.pickup,
|
|
);
|
|
});
|
|
|
|
test('collected no longer ends the job by line default', () {
|
|
// ── This assertion used to be the opposite, and it was the bug ──
|
|
//
|
|
// `isClosed` read `isPicked && ServiceProfile.active.endsAtHub`, which is
|
|
// always true on logistics, so **every** collected parcel was written off
|
|
// at the pickup — including a hyperlocal one the pivot had just released
|
|
// into this rider's own hands. He left the sender's door carrying a
|
|
// parcel his app had already filed as finished.
|
|
//
|
|
// Where a collected parcel goes next is the backend's decision, per
|
|
// order, and this row carries no answer yet: no consignment state and no
|
|
// recorded pivot instruction. So it resolves to [NextLeg.unknown], which
|
|
// keeps custody rather than guessing — the parcel is in his hands, and
|
|
// the honest screen is the work tab, not Activity. See [NextLegResolver].
|
|
expect(WorkBoundary.isClosed(order('P1', status: 'picked')), isFalse);
|
|
expect(
|
|
WorkBoundary.domainOf(order('P1', status: 'picked')),
|
|
WorkDomain.delivery,
|
|
);
|
|
});
|
|
|
|
test('the backend closing it is what closes it', () {
|
|
// `Inwarded_at_Hub` is the hub taking the parcel into its own custody.
|
|
// That is a real ending, and it comes from the server.
|
|
final handed = order('P1', status: 'picked')
|
|
..['consignmentstatus'] = 'Inwarded_at_Hub';
|
|
expect(WorkBoundary.isClosed(handed), isTrue);
|
|
expect(WorkBoundary.domainOf(handed), WorkDomain.closed);
|
|
});
|
|
});
|
|
}
|