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

786 lines
28 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:miler/views/helpers/widgets/slide_action.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/accepted_store.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/Models/stop_status.dart';
import 'package:miler/data/order_manifest.dart';
import 'package:miler/views/Dashboard/home/stop_action_sheet.dart';
import 'package:miler/views/Dashboard/home/trip.dart';
import 'package:miler/views/Dashboard/home/selection_bar.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
import 'package:miler/views/Dashboard/home/trip_card.dart';
/// P3/P4 — the collect-then-deliver run.
///
/// The rule the whole flow hangs on: **Home holds an order until it is in the
/// rider's hands, Bookings holds it after.** Every test here is a way that rule
/// can break.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => SharedPreferences.setMockInitialValues({}));
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
Map<String, dynamic> meal({
required String id,
required int step,
required String kitchen,
String kitchenId = '',
String status = 'ASSIGNED',
}) => {
'orderid': id,
'step': step,
'pickupcustomer': 'Customer $id',
'pickupaddress': '$step Main Road, Anna Nagar',
'orderstatus': status,
'type': 'delivery',
'quantity': 1,
'kitchenname': kitchen,
if (kitchenId.isNotEmpty) 'kitchenid': kitchenId,
'baglabel': 'DG-$id',
'pickuplat': 13.08,
'pickuplon': 80.21,
};
group('the collected state', () {
final stop = meal(id: '1', step: 1, kitchen: 'Kitchen 1');
test('carrying it beats a server view that still says accepted', () {
// The refetch is always a poll or two behind the rider's own hands. If
// the server won here the card would bounce back onto Home after he had
// already left the kitchen with the food.
final state = stopStateOf(
{...stop, 'orderstatus': 'accepted'},
acceptedIds: {'1'},
rejectedIds: const {},
collectedIds: {'1'},
);
expect(state, StopState.collected);
});
test('but a delivered stop is still delivered', () {
// `picked` is the hand-over at the door and it is terminal — nothing
// local may drag it backwards into "still carrying it".
final state = stopStateOf(
{...stop, 'orderstatus': 'picked'},
acceptedIds: const {},
rejectedIds: const {},
collectedIds: {'1'},
);
expect(state, StopState.done);
});
test('a parcel route never sees the state at all', () {
final state = stopStateOf(
stop,
acceptedIds: {'1'},
rejectedIds: const {},
// Default empty — no Doormile call site passes this.
);
expect(state, StopState.accepted);
});
test('it counts as committed but not as resolved', () {
// Committed: he owes somebody a delivery. Not resolved: the trip is not
// finished, and a ring that read 100% with ten meals in the box would be
// telling him he could go home.
expect(StopState.collected.isCommitted, isTrue);
expect(StopState.collected.isResolved, isFalse);
});
});
group('Home holds it until it is in his hands', () {
test('an accepted meal stays on Home; an accepted parcel does not', () {
ServiceProfile.setActive(ServiceProfile.meals);
final trip = Trip.fromStops(
id: 't',
stops: [meal(id: '1', step: 1, kitchen: 'Kitchen 1')],
);
final card = TripCard(trip: trip, acceptedIds: const {'1'});
// Rendering is asserted below; this is the rule underneath it.
expect(ServiceProfile.active.handsOffAtCollection, isTrue);
expect(card.trip.stops.length, 1);
ServiceProfile.setActive(ServiceProfile.parcel);
expect(ServiceProfile.active.handsOffAtCollection, isFalse);
});
testWidgets('the row is on Home when accepted, gone once collected', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.meals);
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final trip = Trip.fromStops(
id: 't',
stops: [meal(id: '1', step: 1, kitchen: 'Kitchen 1')],
);
Future<void> pump(Set<String> collected) async {
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: TripCard(
trip: trip,
acceptedIds: const {'1'},
collectedIds: collected,
).buildStops(),
),
),
),
),
);
await tester.pumpAndSettle();
}
await pump(const {});
expect(find.text('Customer 1'), findsOneWidget);
expect(find.text('Accepted'), findsOneWidget);
// Collected → it belongs to Bookings now.
await pump({'1'});
expect(find.text('Customer 1'), findsNothing);
});
});
group('kitchen grouping', () {
testWidgets('one heading per kitchen, in route order', (tester) async {
ServiceProfile.setActive(ServiceProfile.meals);
tester.view.physicalSize = const Size(1080, 3000);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final trip = Trip.fromStops(
id: 't',
stops: [
meal(id: '1', step: 1, kitchen: 'Kitchen 1', kitchenId: 'K1'),
meal(id: '2', step: 2, kitchen: 'Kitchen 1', kitchenId: 'K1'),
meal(id: '3', step: 3, kitchen: 'Kitchen 2', kitchenId: 'K2'),
],
);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: TripCard(
trip: trip,
acceptedIds: const {'1', '2', '3'},
).buildStops(),
),
),
),
),
);
await tester.pumpAndSettle();
// Two kitchens, named once each however many meals hang off them.
expect(find.text('Kitchen 1'), findsOneWidget);
expect(find.text('Kitchen 2'), findsOneWidget);
// No second bulk control up here: the heading gathers the counter's
// orders for the bar, and the bar is the only thing that commits.
expect(find.textContaining('Collect '), findsNothing);
// One order, one bag — the two numbers the rider checks against the
// shelf, and they are the same number by definition. See [OrderManifest].
expect(find.text('2 orders'), findsOneWidget);
expect(find.text('1 order'), findsOneWidget);
});
testWidgets('a heading gathers one rung, never a mixed batch', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.meals);
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
List<String>? picked;
final trip = Trip.fromStops(
id: 't',
stops: [
meal(id: '1', step: 1, kitchen: 'Kitchen 1'),
meal(id: '2', step: 2, kitchen: 'Kitchen 1'),
],
);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: TripCard(
trip: trip,
// Only one of the two taken on.
acceptedIds: const {'1'},
// The group's select-all is a *selection-mode* control now —
// it appears once the rider has ticked something and leaves
// when he has not, rather than sitting in every kitchen all
// day. Order 2 is ticked here, which puts the run in that
// mode without touching the accepted rung this test is about.
selectedIds: const {'2'},
onToggleSelect: (_) {},
onSelectAll: (ids) => picked = ids,
).buildStops(),
),
),
),
),
);
await tester.pumpAndSettle();
// The header still says how much of the counter is not his yet.
expect(find.text('2 orders · 1 to accept'), findsOneWidget);
// And its tick gathers only the orders on one rung. He cannot be handed
// food for an order he has not taken on, so a heading that swept both
// into one selection would offer a verb that is wrong for half of them —
// see `_selectionAction`, which falls back to Accept on a mixed set.
// Ported: the kitchen heading's tick became the group's one selection
// control. Same job, same rule — it gathers the counter, on one rung.
await tester.tap(find.textContaining('Select all'));
await tester.pumpAndSettle();
expect(picked, ['1'], reason: 'only the accepted one');
});
});
// ── The pickup confirmation is the only place a handover is confirmed ──
//
// There used to be two bulk flows at a counter: this ladder, and a separate
// `Collect N` sheet with its own tick-list. They wrote different state and
// taught two mental models for one physical act. The ladder won; these tests
// are the load sheet's contracts, moved onto the sheet that absorbed them —
// the short pick, the empty crate, and the split between what he took and
// what he did not.
group('the pickup confirmation', () {
Future<RungOutcome?> open(
WidgetTester tester,
int count, {
List<String> markMissing = const [],
}) async {
ServiceProfile.setActive(ServiceProfile.meals);
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
RungOutcome? seen;
final stops = [
for (var i = 1; i <= count; i++)
meal(id: '$i', step: i, kitchen: 'Kitchen 1'),
];
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: StopActionSheet(
stage: StopStatus.arrived,
placeName: 'Kitchen 1',
count: stops.length,
lines: OrderManifest.forGroup(stops),
onConfirm: (outcome) async {
seen = outcome;
return null;
},
),
),
),
),
);
await tester.pumpAndSettle();
if (markMissing.isNotEmpty) {
await tester.tap(find.text('Report a missing order'));
await tester.pumpAndSettle();
for (final name in markMissing) {
await tester.tap(find.text(name));
await tester.pumpAndSettle();
}
}
return seen;
}
testWidgets('nothing is marked by default — the exception is what he marks', (
tester,
) async {
await open(tester, 3);
// A screen that makes him tick three boxes to record "nothing was wrong"
// is a screen he learns to dismiss without reading. So the common case is
// a list he reads, and the shortfall is one tap away.
expect(find.text('3 orders'), findsOneWidget);
expect(find.text('Report a missing order'), findsOneWidget);
expect(find.text('Missing'), findsNothing);
expect(
find.text('Make sure all 3 orders are loaded before continuing.'),
findsOneWidget,
);
});
testWidgets('marking one reports a short pick, and says so', (
tester,
) async {
await open(tester, 3, markMissing: ['Customer 2']);
expect(find.text('2 orders · 1 missing'), findsOneWidget);
expect(find.text('Missing'), findsOneWidget);
expect(
find.text('1 of 3 not supplied — they will leave your route.'),
findsOneWidget,
);
expect(find.text('Slide to confirm pickup'), findsOneWidget);
});
testWidgets('taking nothing at all is not a collection', (tester) async {
await open(tester, 2, markMissing: ['Customer 1', 'Customer 2']);
// An empty crate is a wasted trip and a call to the hub, not a state to
// file silently. The gesture goes dead and the sheet says why.
expect(find.text('Nothing to collect'), findsOneWidget);
expect(
find.text('Nothing was handed over — call your office before you leave.'),
findsOneWidget,
);
// ── Disabled is a different shape, not a dimmed one ──
//
// It used to be the slide control wrapped in an `IgnorePointer`, which
// is a control that still looks draggable and silently is not: the rider
// drags, nothing happens, and he learns the sheet is broken. With
// nothing to collect there is no slide control at all — just an inert
// bar carrying the reason.
expect(
find.byType(MilerSlideAction),
findsNothing,
reason: 'a gesture that cannot fire must not be offered',
);
});
testWidgets('the outcome splits what he took from what he did not', (
tester,
) async {
RungOutcome? captured;
ServiceProfile.setActive(ServiceProfile.meals);
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final stops = [
meal(id: '1', step: 1, kitchen: 'Kitchen 1'),
meal(id: '2', step: 2, kitchen: 'Kitchen 1'),
];
final sheet = StopActionSheet(
stage: StopStatus.arrived,
placeName: 'Kitchen 1',
count: stops.length,
lines: OrderManifest.forGroup(stops),
onConfirm: (outcome) async {
captured = outcome;
return null;
},
);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(home: Scaffold(body: sheet)),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Report a missing order'));
await tester.pumpAndSettle();
await tester.tap(find.text('Customer 2'));
await tester.pumpAndSettle();
// Drive the confirmation the way the slide does, without simulating a
// 300px drag: what is pinned here is the *payload*, not the gesture.
final state = tester.state(find.byType(StopActionSheet));
// ignore: avoid_dynamic_calls
await (state as dynamic).confirmForTest();
await tester.pumpAndSettle();
expect(captured, isNotNull);
expect(captured!.missingOrderIds, ['2']);
expect(captured!.isClean, isFalse);
});
});
group('the collected store', () {
test('records, then clears once delivered', () async {
await addCollectedOrderIds(['1', '2']);
expect(await getCollectedOrderIds(), {'1', '2'});
await removeCollectedOrderIds(['1']);
expect(await getCollectedOrderIds(), {'2'});
});
test('a short pick is its own record, not a skip', () async {
await addNotLoadedOrderIds(['9']);
expect(await getNotLoadedOrderIds(), {'9'});
// Nobody was visited and the customer did nothing wrong, so it must not
// land in the skip store where it would read as a failed doorstep.
expect(await getSkippedOrderIds(), isEmpty);
});
test('a day can be wiped so ids never leak into tomorrow', () async {
await addCollectedOrderIds(['1']);
await addNotLoadedOrderIds(['2']);
await clearServiceRunState();
expect(await getCollectedOrderIds(), isEmpty);
expect(await getNotLoadedOrderIds(), isEmpty);
});
});
group('accept does not hand the meal over to Bookings', () {
/// The gate Bookings applies to decide what the rider is working now.
/// Mirrors the filter in `pickups.dart` so the rule can be asserted without
/// standing up the whole tab.
Future<Set<String>> workableIds() async =>
ServiceProfile.active.handsOffAtCollection
? await getCollectedOrderIds()
: await getAcceptedOrderIds();
test(
'accepted-but-not-collected is not workable on a service run',
() async {
ServiceProfile.setActive(ServiceProfile.meals);
// Accepting writes to the accepted store — that is how the stop knows it
// has been taken on, and Home needs it to render "Accepted".
await addAcceptedBookings([
{'orderid': '1', 'orderstatus': 'accepted'},
]);
// But he is carrying nothing yet, so Bookings must be empty. Letting
// accept fill that tab hands him a delivery list for food still being
// cooked, and empties Home where the collection is organised.
expect(await workableIds(), isEmpty);
await addCollectedOrderIds(['1']);
expect(await workableIds(), {'1'});
},
);
test('a parcel is workable the moment it is accepted', () async {
ServiceProfile.setActive(ServiceProfile.parcel);
await addAcceptedBookings([
{'orderid': '9', 'orderstatus': 'accepted'},
]);
// Unchanged behaviour: for a parcel, accepting IS the hand-off.
expect(await workableIds(), {'9'});
});
test('the two stores stay independent', () async {
ServiceProfile.setActive(ServiceProfile.meals);
await addAcceptedBookings([
{'orderid': '1', 'orderstatus': 'accepted'},
{'orderid': '2', 'orderstatus': 'accepted'},
]);
await addCollectedOrderIds(['1']);
// Both taken on; only one in his hands. Home keeps the second, Bookings
// takes the first.
expect(await getAcceptedOrderIds(), {'1', '2'});
expect(await getCollectedOrderIds(), {'1'});
});
});
group('the ladder, worked from the selection bar', () {
// A rider at a kitchen with five parcels takes one handover, so he ticks
// the five and slides once per rung. The row briefly carried its own
// Arrived / Picked up buttons — five buttons pressed twice for one event —
// and they are gone.
test('each rung names itself in the rider\'s voice', () {
// "Mark as…" rather than "I've…": the button opens a sheet that then
// asks him to confirm the same words, so the two read as one action.
expect(SelectionAction.accept.label, 'Accept');
expect(SelectionAction.arrive.label, 'Mark as Arrived');
expect(SelectionAction.pickUp.label, 'Mark as Picked');
});
test('each rung writes the right status token', () {
expect(SelectionAction.accept.status, 'ACCEPTED');
expect(SelectionAction.arrive.status, 'ARRIVED');
expect(SelectionAction.pickUp.status, 'PICKED');
});
test(
'the two that move parcels are confirmed in a sheet; accepting is not',
() {
// Accepting is reversible and happens in bulk at the start of a shift, so
// it commits from the bar. "Picked" is the rider asserting he is holding
// somebody's lunch — that one gets a sheet with a slide in it, where
// there is room to say which counter and how many orders he is speaking
// for. The slide used to be squeezed inline onto the bar itself.
expect(SelectionAction.accept.confirmsInSheet, isFalse);
expect(SelectionAction.arrive.confirmsInSheet, isTrue);
expect(SelectionAction.pickUp.confirmsInSheet, isTrue);
},
);
Future<void> pumpRow(
WidgetTester tester, {
required String status,
Set<String> accepted = const {'1'},
}) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final trip = Trip.fromStops(
id: 't',
stops: [meal(id: '1', step: 1, kitchen: 'Kitchen 1', status: status)],
);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: TripCard(
trip: trip,
acceptedIds: accepted,
onToggleSelect: (_) {},
).buildStops(),
),
),
),
),
);
await tester.pumpAndSettle();
}
// ── The rung is pressed on the bar and *committed* in a sheet ──
//
// The bar's job at this point in the shift is to be unmissable: he is
// holding a crate at a counter. The commit's job is to be deliberate. Those
// are opposite requirements, which is why they are now two surfaces —
// a full-width button that opens [StopActionSheet], and a slide inside it.
Future<void> pumpBar(
WidgetTester tester, {
required SelectionAction action,
required VoidCallback onAdvance,
}) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.bottomCenter,
child: SelectionBar(
count: 3,
action: action,
onAccept: null,
onClear: () {},
onAdvance: () async => onAdvance(),
),
),
),
),
),
);
await tester.pumpAndSettle();
}
testWidgets('the bar names the rung and how many it covers', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.meals);
await pumpBar(tester, action: SelectionAction.arrive, onAdvance: () {});
expect(find.text('Mark as Arrived (3)'), findsOneWidget);
// Nothing shares the bar with it: a stop already taken on cannot be
// rejected from here, and the count is on the button itself.
expect(find.text('Reject'), findsNothing);
expect(find.textContaining('selected'), findsNothing);
});
testWidgets('the pick-up rung is the same control, renamed', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.meals);
await pumpBar(tester, action: SelectionAction.pickUp, onAdvance: () {});
expect(find.text('Mark as Picked (3)'), findsOneWidget);
});
testWidgets('pressing it hands off rather than writing anything', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.meals);
var opened = 0;
await pumpBar(
tester,
action: SelectionAction.arrive,
onAdvance: () => opened++,
);
await tester.tap(find.text('Mark as Arrived (3)'));
await tester.pumpAndSettle();
// The press opens the confirmation; the slide inside it is what commits.
expect(opened, 1);
});
testWidgets('a kitchen heading ticks every order off that counter', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.meals);
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
List<String>? picked;
final trip = Trip.fromStops(
id: 't',
stops: [
meal(
id: '1',
step: 1,
kitchen: 'Kitchen 1',
kitchenId: 'K1',
status: 'accepted',
),
meal(
id: '2',
step: 2,
kitchen: 'Kitchen 1',
kitchenId: 'K1',
status: 'accepted',
),
meal(
id: '3',
step: 3,
kitchen: 'Kitchen 2',
kitchenId: 'K2',
status: 'accepted',
),
],
);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: TripCard(
trip: trip,
acceptedIds: const {'1', '2', '3'},
// Selection mode, entered on the *other* kitchen — which also
// sharpens the assertion below: Kitchen 2 has a live tick of
// its own, and gathering Kitchen 1 must not disturb it.
selectedIds: const {'3'},
onToggleSelect: (_) {},
onSelectAll: (ids) => picked = ids,
).buildStops(),
),
),
),
),
);
await tester.pumpAndSettle();
// He arrives at a kitchen, not at an order — so the group's own control
// is where the whole counter is gathered for the bar underneath.
await tester.tap(find.textContaining('Select all'));
await tester.pumpAndSettle();
expect(picked, ['1', '2'], reason: 'the other kitchen is left alone');
});
testWidgets('the row states where the stop is and offers no control', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.meals);
await pumpRow(tester, status: 'accepted');
expect(find.text('Customer 1'), findsOneWidget);
expect(find.text('Mark as Arrived'), findsNothing);
expect(find.text('Arrived'), findsNothing);
expect(find.text('Picked up'), findsNothing);
});
testWidgets('the tick survives the accept, so the set can be slid', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.meals);
await pumpRow(tester, status: 'accepted');
// Without this the bar disappears the instant he accepts the very stops
// he is about to walk into a kitchen with.
expect(find.byType(Checkbox), findsNothing); // it is a custom box
// Ported: the card's tick box became the timeline row itself, which
// announces as a selection control while one is meaningful. Same
// contract — the affordance outlives the accept on a meal run.
expect(find.bySemanticsLabel(RegExp('^Select ')), findsOneWidget);
});
testWidgets('a parcel row loses its tick once accepted', (tester) async {
ServiceProfile.setActive(ServiceProfile.parcel);
await pumpRow(tester, status: 'accepted');
// Ticking exists only to choose what to take on.
expect(find.bySemanticsLabel(RegExp('^Select ')), findsNothing);
});
testWidgets('picked up leaves Home', (tester) async {
ServiceProfile.setActive(ServiceProfile.meals);
await pumpRow(tester, status: 'picked');
expect(find.text('Customer 1'), findsNothing);
});
});
group('the doorstep chain', () {
// "I've handed over" → next order. Everything that used to sit between
// those two is gated off, and each gate is a different mechanism, so they
// are asserted together rather than trusted individually.
test('nothing stands between the hand-over and the next order', () {
ServiceProfile.setActive(ServiceProfile.meals);
final p = ServiceProfile.active;
// The proof-of-work page — parcel ticks, weight, condition, OTP, review.
expect(p.needsVerification, isFalse);
// The camera on the final rung.
expect(p.needsProofPhoto, isFalse);
// The payment page, and the amount that would open it.
expect(p.collectsCash, isFalse);
expect(stopCollectionAmount({'collectionamt': 450}), 0);
expect(stopCollectionAmount({'codamount': '₹1,200'}), 0);
});
test('a parcel stop keeps every one of them', () {
ServiceProfile.setActive(ServiceProfile.parcel);
final p = ServiceProfile.active;
expect(p.needsVerification, isTrue);
expect(p.needsProofPhoto, isTrue);
expect(p.collectsCash, isTrue);
expect(stopCollectionAmount({'collectionamt': 450}), 450);
});
});
}