Files
doormile_milderapp/test/delivery_queue_test.dart
2026-08-28 15:07:30 +05:30

192 lines
8.1 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/order_manifest.dart';
import 'package:miler/data/service_profile.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// THE DELIVERY QUEUE, AND THE LABEL 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. **A label recorded at the counter is the label at the door** — even
/// after the first order is delivered and the list underneath it shrinks,
/// and even after a second counter loads on top of it.
///
/// The second is the subtle one, and it is why the label is *stored* at pickup
/// rather than recomputed from whatever is on screen. Anything derived from a
/// position silently renames every remaining order the moment one leaves the
/// list, which sends the rider looking for something already in somebody's
/// hallway.
///
/// ── The derived numbering is gone, and these tests changed with it ──
///
/// They used to assert `Bag 1 … Bag 5`, manufactured from each order's position
/// when the payload printed nothing — the app numbering its own list and
/// presenting the result as a physical fact. Only a label a counter actually
/// printed is stored now, so the fixtures carry real codes and a counter that
/// prints nothing stores nothing. The store's contract is unchanged and is
/// still what is pinned here.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => SharedPreferences.setMockInitialValues({}));
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
/// [printed] is what the counter stuck on the order. Omitted, the counter
/// printed nothing — which is the common route, and stores nothing.
Map<String, dynamic> order(String id, String customer, {String? printed}) => {
'orderid': id,
'pickupcustomer': customer,
'kitchenname': 'Vidhya Kitchen',
'kitchenid': 'K1',
'quantity': 3,
if (printed != null) 'baglabel': printed,
};
final five = [
order('1', 'Joe', printed: 'DG-1001'),
order('2', 'Arun', printed: 'DG-1002'),
order('3', 'Priya', printed: 'DG-1003'),
order('4', 'Kumar', printed: 'DG-1004'),
order('5', 'Ravi', printed: 'DG-1005'),
];
group('the hand-off', () {
test(
'five picked orders become five deliveries, each with its label',
() async {
ServiceProfile.setActive(ServiceProfile.meals);
// What the pickup confirmation writes when the slide lands.
final manifest = OrderManifest.forGroup(five);
await addCollectedOrderIds([for (final l in manifest) l.orderId]);
await saveOrderLabels({for (final l in manifest) l.orderId: l.label});
final carrying = await getCollectedOrderIds();
final labels = await getOrderLabels();
expect(carrying.length, 5, reason: 'five picked is five to deliver');
expect(labels.length, 5, reason: 'and five labels, one each');
expect(labels['1'], 'DG-1001');
expect(labels['5'], 'DG-1005');
// No aggregate anywhere: the count of deliveries and the count of
// labels are the same set, not two numbers that can drift.
expect(labels.keys.toSet(), carrying);
},
);
test('an order 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 saveOrderLabels({
for (final l in OrderManifest.forGroup(loaded)) l.orderId: l.label,
});
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 carries no label, because it was never handed over.
expect((await getOrderLabels()).containsKey('3'), isFalse);
});
});
group('label identity outlives the queue', () {
test('delivering the first does not disturb the rest', () async {
ServiceProfile.setActive(ServiceProfile.meals);
await saveOrderLabels({
for (final l in OrderManifest.forGroup(five)) l.orderId: l.label,
});
// Joe is delivered and leaves the carrying set.
await addCollectedOrderIds([
for (final o in five) o['orderid'] as String,
]);
await removeCollectedOrderIds(['1']);
final labels = await getOrderLabels();
expect(await getCollectedOrderIds(), {'2', '3', '4', '5'});
// Arun keeps the code the counter stuck on his order. Nothing is read
// off a position, so a list that shrinks under him changes nothing.
expect(labels['2'], 'DG-1002');
expect(labels['3'], 'DG-1003');
expect(labels['5'], 'DG-1005');
});
test('a second kitchen adds to the map rather than replacing it', () async {
ServiceProfile.setActive(ServiceProfile.meals);
await saveOrderLabels({
for (final l in OrderManifest.forGroup(five.take(2).toList()))
l.orderId: l.label,
});
// A different counter, printing its own codes.
final second = [
order('9', 'Meena', printed: 'AM-2001'),
order('10', 'Sai', printed: 'AM-2002'),
];
await saveOrderLabels({
for (final l in OrderManifest.forGroup(second)) l.orderId: l.label,
});
final labels = await getOrderLabels();
expect(labels.length, 4, reason: 'the first load must survive the second');
expect(labels['1'], 'DG-1001');
expect(labels['9'], 'AM-2001');
expect(labels['10'], 'AM-2002');
});
test('a skipped delivery keeps its label so it can be resumed', () async {
ServiceProfile.setActive(ServiceProfile.meals);
await saveOrderLabels({
for (final l in OrderManifest.forGroup(five)) l.orderId: l.label,
});
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 getOrderLabels())['3'], 'DG-1003');
});
test("a day's run is wiped so bags never leak into tomorrow", () async {
await saveOrderLabels({'1': 'Bag 1'});
await clearServiceRunState();
expect(await getOrderLabels(), 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 saveOrderLabels({'MOCK-M-1001': 'Bag 1', '77': 'Bag 2'});
await purgeDemoRecords();
final bags = await getOrderLabels();
expect(bags.containsKey('MOCK-M-1001'), isFalse);
expect(bags['77'], 'Bag 2', reason: 'real work is untouched');
});
}