192 lines
8.1 KiB
Dart
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');
|
|
});
|
|
}
|