Files
doormile_milderapp/test/next_leg_test.dart
2026-09-09 12:55:23 +05:30

291 lines
12 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:miler/data/next_leg.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/data/work_domain.dart';
import 'package:miler/data/milk_run.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// WHERE A COLLECTED PARCEL GOES NEXT
///
/// The logistics line used to answer this with a line-level constant —
/// `ServiceProfile.endsAtHub` — and `work_domain.dart` closed every collected
/// parcel because of it. These pin the per-order replacement against the four
/// operational cases it exists for:
///
/// 1. Hub H1 → customer collected at a hub, delivered by this rider
/// 2. customer → hub collected at a door, handed to the network
/// 3. customer → customer hyperlocal, one rider, no hub
/// 4. customer → another state collected here, handed to the local hub
///
/// The routing decision is the **backend's** in every one of them. No test here
/// asserts anything about a pincode, a district or a city name, because nothing
/// in the app is allowed to read one for this purpose.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
setUp(() => ServiceProfile.setActive(ServiceProfile.parcel));
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
/// A queue row as `GET /miler/bookings` delivers it, after the adapter.
Map<String, dynamic> row(
String id, {
String status = 'picked',
String consignment = '',
String nextAction = '',
String source = '',
String drop = '',
}) => <String, dynamic>{
'orderid': id,
'orderstatus': status,
'consignmentstatus': consignment,
if (nextAction.isNotEmpty) 'next_action': nextAction,
'sourcename': source,
'dropaddress': drop,
};
group('the four operational cases', () {
test('CASE 1 — Hub H1 → customer: the rider delivers it himself', () {
// Collected at the hub; the pivot released it into his hands.
final stop = row(
'H1-001',
consignment: 'Collected_By_Miler',
source: 'Hub H1',
drop: 'Hopes, Coimbatore',
);
expect(NextLegResolver.resolve(stop).leg, NextLeg.customer);
expect(WorkBoundary.isClosed(stop), isFalse,
reason: 'a parcel in his hands is not finished work');
expect(WorkBoundary.domainOf(stop), WorkDomain.delivery);
expect(MilkRun.navigatesToCustomer(stop), isTrue,
reason: 'the receiver is the immediate destination');
});
test('CASE 3 — customer → customer hyperlocal: never closed at pickup', () {
// Gandhipuram → Hopes. The pivot released it in the same call, which is
// what compatibility mode does, so the row comes back Out_for_Delivery.
final stop = row(
'HYP-1',
consignment: 'Out_for_Delivery',
source: 'Rajesh Kumar',
drop: 'Hopes, Coimbatore',
);
expect(NextLegResolver.resolve(stop).leg, NextLeg.customer);
// ── The bug this whole change exists for ──
//
// `isPicked && endsAtHub` closed this row, and endsAtHub is always true
// on logistics. The rider walked away from Rajesh Kumar's door carrying a
// parcel the app had filed as finished.
expect(WorkBoundary.isClosed(stop), isFalse);
expect(WorkBoundary.domainOf(stop), WorkDomain.delivery);
expect(MilkRun.navigatesToCustomer(stop), isTrue);
});
test('CASE 4 — customer → another state: hub leg, no customer delivery', () {
// Gandhipuram → Chennai. The pivot routed it into the network.
final stop = row(
'ICT-1',
consignment: 'Created',
source: 'Rajesh Kumar',
drop: 'T Nagar, Chennai',
);
const pivot = {'ICT-1': NextActionWord.inwardAtHub};
expect(
NextLegResolver.resolve(stop, pivotAction: NextActionWord.inwardAtHub).leg,
NextLeg.hub,
);
// It is in his custody, so it is the work tab's — not closed, not Home's.
expect(
WorkBoundary.isClosed(stop, pivotActions: pivot),
isFalse,
reason: 'a parcel he is carrying must not vanish into Activity',
);
expect(
WorkBoundary.domainOf(stop, pivotActions: pivot),
WorkDomain.delivery,
);
// ── The safety assertion ──
//
// Chennai is 500km away. Nothing may offer it as this rider's next
// navigation target.
expect(
MilkRun.navigatesToCustomer(stop, pivotActions: pivot),
isFalse,
reason: 'a hub-routed parcel must never point at its receiver',
);
});
test('CASE 2 — the hub taking it in is what ends the rider s job', () {
// There is no rider-side handover mutation yet (backend gap). The one way
// a hub-routed parcel legitimately leaves his queue today is hub staff
// inwarding it in their console, which the next poll reports.
final handed = row('ICT-1', consignment: 'Inwarded_at_Hub');
const pivot = {'ICT-1': NextActionWord.inwardAtHub};
expect(NextLegResolver.resolve(handed, pivotAction: NextActionWord.inwardAtHub).leg,
NextLeg.closed);
expect(WorkBoundary.isClosed(handed, pivotActions: pivot), isTrue);
expect(
WorkBoundary.domainOf(handed, pivotActions: pivot),
WorkDomain.closed,
);
});
});
group('precedence — the server outranks the local record', () {
test('a live consignment state beats a stale pivot instruction', () {
// The pivot said hub. The server has since said the parcel is out for
// delivery in this rider's hands. The server wins: a local record is
// continuity, never authority.
final stop = row('X1', consignment: 'Out_for_Delivery');
final decision =
NextLegResolver.resolve(stop, pivotAction: NextActionWord.inwardAtHub);
expect(decision.leg, NextLeg.customer);
expect(decision.source, 'row.consignmentstatus');
});
test('a terminal state beats everything', () {
final stop = row('X2', status: 'delivered', consignment: 'Delivered');
expect(
NextLegResolver.resolve(stop, pivotAction: NextActionWord.startDelivery).leg,
NextLeg.closed,
);
});
test('live next_action on the row outranks the persisted pivot', () {
// Not sent by the backend today — pinned so the resolver is already
// correct on the day `GET /miler/bookings` carries it.
final stop = row('X3', consignment: 'Created', nextAction: 'start_delivery');
final decision =
NextLegResolver.resolve(stop, pivotAction: NextActionWord.inwardAtHub);
expect(decision.leg, NextLeg.customer);
expect(decision.source, 'row.next_action');
});
test('Created alone decides nothing — it is also the transient state', () {
// `Created` is what a hub-routed pivot lands on AND what a consignment
// holds while the pivot is still routing it. Reading it as "hub" would
// strand every freshly-collected hyperlocal parcel.
final stop = row('X4', consignment: 'Created');
final decision = NextLegResolver.resolve(stop);
expect(decision.leg, NextLeg.unknown);
expect(decision.source, 'none');
});
test('unknown keeps custody: not closed, and no customer destination', () {
final stop = row('X5', consignment: 'Created');
// Never written off — he is holding it.
expect(WorkBoundary.isClosed(stop), isFalse);
expect(WorkBoundary.domainOf(stop), WorkDomain.delivery);
// Never pointed anywhere — the server has not said where it goes.
expect(MilkRun.navigatesToCustomer(stop), isFalse);
});
});
group('the backend outranks the ServiceProfile default', () {
test('logistics stays endsAtHub, and it no longer closes an order', () {
// The capability is untouched — it still describes the line.
expect(ServiceProfile.parcel.endsAtHub, isTrue);
expect(ServiceProfile.parcel.deliversToCustomer, isFalse);
// And it no longer decides one order's fate.
final hyperlocal = row('P1', consignment: 'Collected_By_Miler');
expect(WorkBoundary.isClosed(hyperlocal), isFalse);
expect(MilkRun.navigatesToCustomer(hyperlocal), isTrue);
});
test('a milk run is unchanged by any of this', () {
ServiceProfile.setActive(ServiceProfile.milkMan);
// A round's rows often carry no consignment state at all; the line's own
// shape still answers for them.
final collected = row('M1', status: 'picked');
expect(WorkBoundary.isClosed(collected), isFalse);
expect(
MilkRun.navigatesToCustomer(collected, collectedIds: const {'M1'}),
isTrue,
);
});
});
group('the leg decides the words and the controls', () {
test('only a customer leg may be released into a delivery round', () {
// The gate that keeps a Chennai consignment out of `Out_for_Delivery`.
expect(NextLeg.customer.allowsCustomerDelivery, isTrue);
expect(NextLeg.hub.allowsCustomerDelivery, isFalse);
expect(NextLeg.unknown.allowsCustomerDelivery, isFalse);
expect(NextLeg.closed.allowsCustomerDelivery, isFalse);
});
test('a handover and a delivery are never worded the same', () {
// Activity has to be able to tell "handed across a counter" from "given
// to the person it belongs to". One verb for both is a false record.
expect(
NextLeg.hub.completionVerb,
isNot(equals(NextLeg.customer.completionVerb)),
);
expect(NextLeg.customer.completionVerb, contains('customer'));
});
test('no leg vocabulary says "hub" to a rider', () {
// The house rule, enforced for every rider-facing string by
// `no_hub_in_rider_copy_test.dart`. Pinned here too because this is where
// the words are chosen: "base" for the place, "your office" for people.
for (final leg in NextLeg.values) {
expect(leg.eyebrow.toLowerCase(), isNot(contains('hub')));
expect(leg.completionVerb.toLowerCase(), isNot(contains('hub')));
}
});
test('a closed order has no eyebrow to draw', () {
expect(NextLeg.closed.eyebrow, isEmpty);
});
});
group('one owner per order', () {
test('a picked parcel is never on Home and Deliveries at once', () {
final day = [
row('A', consignment: 'Out_for_Delivery'), // hyperlocal, customer leg
row('B', consignment: 'Created'), // hub-routed
row('C', status: 'assigned', consignment: ''), // not collected
];
const pivot = {'B': NextActionWord.inwardAtHub};
final home = WorkBoundary.pickupQueue(day, pivotActions: pivot);
final deliveries = WorkBoundary.deliveryQueue(day, pivotActions: pivot);
final homeIds = home.map(MilkRun.idOf).toSet();
final deliveryIds = deliveries.map(MilkRun.idOf).toSet();
expect(homeIds.intersection(deliveryIds), isEmpty,
reason: 'dual ownership is the failure this boundary exists to stop');
expect(deliveryIds, containsAll(<String>{'A', 'B'}));
expect(homeIds, contains('C'));
});
test('restart and poll preserve the leg', () {
// Same row, read again on a later poll with nothing but the persisted
// pivot to go on — which is exactly the cold-start case.
final stop = row('B', consignment: 'Created');
const pivot = {'B': NextActionWord.inwardAtHub};
for (var i = 0; i < 3; i++) {
expect(
WorkBoundary.domainOf(stop, pivotActions: pivot),
WorkDomain.delivery,
);
expect(MilkRun.navigatesToCustomer(stop, pivotActions: pivot), isFalse);
}
});
});
}