Files
doormile_milderapp/test/delivery_queue_test.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
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>
2026-08-22 05:40:35 +05:30

175 lines
7.0 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/accepted_store.dart';
import 'package:miler/data/bag_manifest.dart';
import 'package:miler/data/service_profile.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// THE DELIVERY QUEUE, AND THE BAG THAT SURVIVES IT
///
/// Two promises are made at a kitchen counter and both have to hold all the way
/// to a doorstep:
///
/// 1. **Five orders picked is five deliveries** — no order goes missing in the
/// hand-off, and nothing appears that was not collected.
/// 2. **Joe is Bag 1 and stays Bag 1** — even after Joe is delivered and the
/// list underneath him shrinks.
///
/// The second one is the subtle one, and it is why bag identity is *stored* at
/// pickup rather than recomputed from whatever is on screen. A position-derived
/// bag number silently renames every remaining order the moment one leaves the
/// list, which sends the rider looking for a bag that is already in somebody's
/// hallway.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => SharedPreferences.setMockInitialValues({}));
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
Map<String, dynamic> order(String id, String customer) => {
'orderid': id,
'pickupcustomer': customer,
'kitchenname': 'Vidhya Kitchen',
'kitchenid': 'K1',
'quantity': 3,
};
final five = [
order('1', 'Joe'),
order('2', 'Arun'),
order('3', 'Priya'),
order('4', 'Kumar'),
order('5', 'Ravi'),
];
group('the hand-off', () {
test(
'five picked orders become five deliveries, each with its bag',
() async {
ServiceProfile.setActive(ServiceProfile.meals);
// What the pickup confirmation writes when the slide lands.
final manifest = BagManifest.forGroup(five);
await addCollectedOrderIds([for (final l in manifest) l.orderId]);
await saveBagLabels({for (final l in manifest) l.orderId: l.bag});
final carrying = await getCollectedOrderIds();
final bags = await getBagLabels();
expect(carrying.length, 5, reason: 'five picked is five to deliver');
expect(bags.length, 5, reason: 'and five bags, one each');
expect(bags['1'], 'Bag 1');
expect(bags['5'], 'Bag 5');
// No aggregate anywhere: the count of deliveries and the count of bags
// are the same set, not two numbers that can drift.
expect(bags.keys.toSet(), carrying);
},
);
test('a bag the counter could not supply never becomes a delivery', () async {
ServiceProfile.setActive(ServiceProfile.meals);
// Four handed over, one short — the shortfall path the pickup sheet owns.
final loaded = five.where((o) => o['orderid'] != '3').toList();
await addCollectedOrderIds([
for (final o in loaded) o['orderid'] as String,
]);
await saveBagLabels({
for (final l in BagManifest.forGroup(loaded)) l.orderId: l.bag,
});
await addNotLoadedOrderIds(['3']);
expect(await getCollectedOrderIds(), {'1', '2', '4', '5'});
expect(await getNotLoadedOrderIds(), {'3'});
// A short pick is its own record. It is not a skip: nobody was visited
// and the customer did nothing.
expect(await getSkippedOrderIds(), isEmpty);
// And it is not carrying a bag, because it was never in the box.
expect((await getBagLabels()).containsKey('3'), isFalse);
});
});
group('bag identity outlives the queue', () {
test('delivering the first does not renumber the rest', () async {
ServiceProfile.setActive(ServiceProfile.meals);
await saveBagLabels({
for (final l in BagManifest.forGroup(five)) l.orderId: l.bag,
});
// Joe is delivered and leaves the carrying set.
await addCollectedOrderIds([
for (final o in five) o['orderid'] as String,
]);
await removeCollectedOrderIds(['1']);
final bags = await getBagLabels();
expect(await getCollectedOrderIds(), {'2', '3', '4', '5'});
// Arun does *not* become Bag 1 because the list in front of him shrank.
expect(bags['2'], 'Bag 2');
expect(bags['3'], 'Bag 3');
expect(bags['5'], 'Bag 5');
});
test('a second kitchen adds to the map rather than replacing it', () async {
ServiceProfile.setActive(ServiceProfile.meals);
await saveBagLabels({
for (final l in BagManifest.forGroup(five.take(2).toList()))
l.orderId: l.bag,
});
// A different counter, its own Bag 1 and Bag 2.
final second = [order('9', 'Meena'), order('10', 'Sai')];
await saveBagLabels({
for (final l in BagManifest.forGroup(second)) l.orderId: l.bag,
});
final bags = await getBagLabels();
expect(bags.length, 4, reason: 'the first load must survive the second');
expect(bags['1'], 'Bag 1');
expect(bags['9'], 'Bag 1', reason: 'each counter counts its own shelf');
expect(bags['10'], 'Bag 2');
});
test('a skipped delivery keeps its bag so it can be resumed', () async {
ServiceProfile.setActive(ServiceProfile.meals);
await saveBagLabels({
for (final l in BagManifest.forGroup(five)) l.orderId: l.bag,
});
await addSkippedBooking(five[2], reason: 'Customer unavailable');
expect(await getSkippedOrderIds(), contains('3'));
// And the reason survives with it — the hub needs to know why, and
// "Failed" is not a reason.
final skipped = await getSkippedBookings();
expect(
skipped.firstWhere((b) => b['orderid'] == '3')['skipreason'],
'Customer unavailable',
);
// He is still carrying Priya's lunch — the bag identity is exactly what
// he needs when he comes back to it.
expect((await getBagLabels())['3'], 'Bag 3');
});
test("a day's run is wiped so bags never leak into tomorrow", () async {
await saveBagLabels({'1': 'Bag 1'});
await clearServiceRunState();
expect(await getBagLabels(), isEmpty);
});
});
test('a demo bag never outlives the build that wrote it', () async {
// The mock day's ids are `MOCK-…` so the launch purge can find them. The
// bag map is keyed by order id, so it is swept by the same rule — a stored
// pairing is trusted later *because* it is stored, and a stale one is worse
// than none.
await saveBagLabels({'MOCK-M-1001': 'Bag 1', '77': 'Bag 2'});
await purgeDemoRecords();
final bags = await getBagLabels();
expect(bags.containsKey('MOCK-M-1001'), isFalse);
expect(bags['77'], 'Bag 2', reason: 'real work is untouched');
});
}