Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
236 lines
8.6 KiB
Dart
236 lines
8.6 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']);
|
|
});
|
|
});
|
|
|
|
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 ends the job on a line that ends at the hub', () {
|
|
// Terminal here, unlike on the milk run: the parcel becomes a consignment
|
|
// and the hub carries it from there.
|
|
expect(WorkBoundary.isClosed(order('P1', status: 'picked')), isTrue);
|
|
expect(
|
|
WorkBoundary.domainOf(order('P1', status: 'picked')),
|
|
WorkDomain.closed,
|
|
);
|
|
});
|
|
});
|
|
}
|