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': [], }); 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': [], }); 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 pumpDesk( WidgetTester tester, { Map? 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( 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, ); }); }); }