Files
doormile_milderapp/test/logistics_flow_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

299 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/views/Dashboard/pickups/shipment_capture.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// THE LOGISTICS SHIPMENT DESK
///
/// ```
/// arrived ─▶ verify ─▶ FROM ─▶ TO ─▶ weight ─▶ distance ─▶ price
/// ─▶ payment ─▶ INITIATE ORDER ─▶ picked up ─▶ hub
/// ```
///
/// ── The rule this file is really about ──
///
/// **No number is invented.** The fare comes from the pricing table against the
/// real pincodes and the real weight; when the table has no rule for a
/// combination the screen must say so and refuse, because a locally computed
/// figure that looks official on a doorstep is how a rider ends up collecting
/// the wrong money in the company's name.
///
/// And the mirror, which is the half that breaks quietly: none of this may ever
/// appear on a milk run, where the food was booked and paid for before the
/// rider's shift started.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
ServiceProfile.setActive(ServiceProfile.parcel);
});
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
group('the quote never invents a fare', () {
test('a priced combination reads the band and charges the lower end', () {
final q = ShipmentQuote.fromData({
'found': true,
'zone': 'Local',
'service_type': 'Normal',
'weight': 2.5,
'currency': 'INR',
'results': [
{
'category': 'General',
'category_label': 'General goods',
'min_price': 60,
'max_price': 90,
},
],
}, preferredCategory: 'General');
expect(q.found, isTrue);
expect(q.zone, 'Local');
expect(q.minPrice, 60);
expect(q.maxPrice, 90);
expect(q.isRange, isTrue);
expect(
q.payable,
60,
reason:
'the lower end is what the customer was quoted at booking; '
'arguing about the difference on a doorstep is the worse trade',
);
});
test('an unpriced combination is not a free shipment', () {
final q = ShipmentQuote.fromData({
'found': false,
'zone': 'National',
'service_type': 'Express',
'weight': 400,
'message': 'no pricing configured for this combination',
'results': <dynamic>[],
});
expect(q.found, isFalse);
expect(q.payable, 0);
expect(
q.message,
isNotEmpty,
reason: 'the rider must be told why, not shown a zero he might collect',
);
});
test('found:true with an empty result set is still unpriced', () {
// A malformed answer must fail the same way a clear "no" does, rather
// than falling through to a zero that reads as free.
final q = ShipmentQuote.fromData({
'found': true,
'zone': 'Local',
'results': <dynamic>[],
});
expect(q.found, isFalse);
expect(q.payable, 0);
});
test('the rider-named category wins over the cheapest row', () {
final q = ShipmentQuote.fromData({
'found': true,
'zone': 'Local',
'currency': 'INR',
'results': [
{'category': 'General', 'min_price': 50, 'max_price': 70},
{'category': 'Fragile', 'min_price': 120, 'max_price': 160},
],
}, preferredCategory: 'Fragile');
expect(
q.payable,
120,
reason:
'he said it was fragile; pricing it as general goods '
'undercharges the customer and under-insures the parcel',
);
});
test('with no category named, the cheapest row wins', () {
final q = ShipmentQuote.fromData({
'found': true,
'zone': 'Local',
'results': [
{'category': 'Fragile', 'min_price': 120, 'max_price': 160},
{'category': 'General', 'min_price': 50, 'max_price': 70},
],
});
expect(q.payable, 50);
});
test('string prices off the wire still parse', () {
final q = ShipmentQuote.fromData({
'found': true,
'zone': 'Regional',
'results': [
{'category': 'General', 'min_price': '80.50', 'max_price': '110'},
],
});
expect(q.minPrice, 80.5);
expect(q.maxPrice, 110);
});
});
group('the pricing contract', () {
test('the service tiers are the pricing table\'s, not the booking\'s', () {
// `bookingserviceoptions` uses Normal/Fast/Superfast; the pricing table
// uses Normal/Express, and sending the wrong vocabulary is a 400.
expect(MilerApi.serviceTypes, ['Normal', 'Express']);
});
test('the categories match what the handler will accept', () {
expect(MilerApi.parcelCategories, contains('General'));
expect(MilerApi.parcelCategories, contains('Fragile'));
expect(MilerApi.parcelCategories, contains('Documents'));
});
});
group('the shipment desk on screen', () {
Future<void> pumpDesk(
WidgetTester tester, {
Map<String, dynamic>? booking,
}) 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: ShipmentCapturePage(
pickup:
booking ??
{
'orderid': '2001',
'pickupid': 2001,
'pickupaddress': '9 Mettupalayam Road',
'pickuppincode': '641002',
'dropaddress': '12 Cross Cut Road',
'droppincode': '641012',
},
),
),
),
);
await tester.pump();
// The desk takes a GPS fix on open so the FROM pin is where the rider is
// standing. There is no platform behind the channel here, so that call
// never answers and its timeout is left holding a Timer the binding
// rightly complains about at teardown. Running the clock past the limit
// lets the guard fire and the widget fall back to the booking's own
// coordinates — which is exactly the path a rider with no signal takes.
await tester.pump(const Duration(seconds: 10));
}
testWidgets('asks for FROM, TO, weight, distance and price', (
tester,
) async {
await pumpDesk(tester);
// The two ends of the journey, now named on one card with a rail
// between them rather than as two headed groups of loose fields.
expect(find.text('FROM'), findsOneWidget);
expect(find.text('TO'), findsOneWidget);
expect(find.text('THE PARCEL'), findsOneWidget);
expect(find.text('WHAT THE CUSTOMER PAYS'), findsOneWidget);
expect(find.text('Pickup address'), findsOneWidget);
expect(find.text('Delivery address'), findsOneWidget);
expect(find.text('Weight (kg)'), findsOneWidget);
});
testWidgets('seeds both addresses so the rider confirms, not transcribes', (
tester,
) async {
await pumpDesk(tester);
expect(find.text('9 Mettupalayam Road'), findsOneWidget);
expect(find.text('12 Cross Cut Road'), findsOneWidget);
expect(find.text('641002'), findsOneWidget);
expect(find.text('641012'), findsOneWidget);
});
testWidgets('will not continue without a price', (tester) async {
await pumpDesk(tester);
// No weight typed, so no quote has been asked for and none can exist.
expect(find.text('Weigh the parcel'), findsOneWidget);
final button = tester.widget<ElevatedButton>(
find.descendant(
of: find.byType(ShipmentCapturePage),
matching: find.byType(ElevatedButton),
),
);
expect(
button.onPressed,
isNull,
reason: 'the desk must not hand back a shipment it could not price',
);
});
testWidgets('names what is missing rather than just greying out', (
tester,
) async {
await pumpDesk(tester, booking: {'orderid': '2002', 'pickupid': 2002});
// A disabled button with no reason is the thing riders phone the hub
// about.
expect(find.text('Confirm the pickup address'), findsOneWidget);
});
});
group('none of this reaches a milk run', () {
test('the capabilities are all off', () {
const p = ServiceProfile.milkMan;
expect(p.capturesShipmentAddresses, isFalse);
expect(p.pricesShipment, isFalse);
expect(p.initiatesShipment, isFalse);
expect(p.collectsCash, isFalse);
expect(p.needsVerification, isFalse);
});
test('so nothing is ever owed at a milk-run door', () {
ServiceProfile.setActive(ServiceProfile.milkMan);
// Even a booking that arrives carrying an amount reads as zero: the food
// was paid for by subscription and the rider must not ask for money.
expect(stopCollectionAmount({'collectionamt': 450}), 0);
ServiceProfile.setActive(ServiceProfile.parcel);
expect(stopCollectionAmount({'collectionamt': 450}), 450);
});
test('and the milk run has no kitchen collection on logistics', () {
ServiceProfile.setActive(ServiceProfile.parcel);
expect(ServiceProfile.active.sourceIsKitchen, isFalse);
expect(ServiceProfile.active.bulkLoadProof, isFalse);
});
});
group('the backend dependencies are declared, not hidden', () {
test('the quoted price has nowhere to be recorded, and says so', () {
expect(
MilerApi.missingFromBackend.any((g) => g.contains('quoted price')),
isTrue,
);
});
test('the delivery OTP cannot be verified, and says so', () {
expect(
MilerApi.missingFromBackend.any((g) => g.contains('delivery OTP')),
isTrue,
);
});
});
}