import 'package:flutter/foundation.dart'; /// ───────────────────────────────────────────────────────────────────────── /// THE APP WITH NO BACKEND BEHIND IT /// /// One switch that takes the whole app off the network: sign-in, the day's /// work, and every status the rider writes. It exists so the flow can be built, /// demonstrated and judged without `api.doormile.com` answering — on a bench, on /// a plane, or against a backend that does not have the meal contract yet. /// /// ── This app has been burned by a mock layer before ── /// /// One was ripped out for a good reason: it wrote rows into the same /// SharedPreferences stores the real work uses, so demo stops surfaced on live /// riders' tabs weeks later with nothing left in the repo to explain them. /// `purgeDemoRecords()` still runs at every launch to clean up after it. /// /// Four rules keep this one from becoming that: /// /// 1. **It cannot ship.** [enabled] is false in a release build unless someone /// types `--dart-define=MOCK_BACKEND=true` on the build command, which is /// not something that happens by accident. Debug and profile builds — the /// ones a developer actually runs — get it by default. /// 2. **It intercepts the transport, not the stores.** Everything is served as /// an *API response*; nothing writes to a store that real work shares. The /// app cannot tell the difference and neither can its data layer. /// 3. **Its ids are self-identifying.** Every record is `MOCK-…`, which is /// exactly what `purgeDemoRecords()` looks for, so anything that does leak /// into a store is removed at the next launch by code that already ships. /// 4. **It says so, loudly.** Every intercepted call is logged with a `[MOCK]` /// prefix, so a confusing session is one glance at the console away from /// being explained. /// /// ── What is given up while it is on ── /// /// The hub is not told anything. A rider can accept, arrive, pick up and /// deliver, and the console will show none of it — every write is answered with /// a success that was manufactured on the phone. That is the correct trade for /// design and demo work and the wrong one for a real shift, which is what rule 1 /// is for. /// ───────────────────────────────────────────────────────────────────────── /// The one switch. /// /// Default: **on** in debug and profile, **off** in release. Override either /// way from the build command: /// /// flutter run # mock /// flutter run --dart-define=MOCK_BACKEND=false # real API /// flutter build apk --dart-define=MOCK_BACKEND=true # deliberate demo /// ── Opt in, not opt out ── /// /// This defaulted to **on** in debug and profile, so every `flutter run` served /// a canned rider, a canned day and a canned login — and the backend was only /// ever exercised by someone who remembered to turn the mock off. Three /// consequences, all of which actually happened: /// /// • A rider signing in with his real number became the fixture's rider. /// • A whole class of contract bugs — a field renamed, a number sent where a /// string was required — could not be seen, because nothing left the device. /// • The default development experience diverged from production silently, /// which is the one kind of divergence nobody notices until release. /// /// The canned day is still one flag away, and is genuinely useful on a bench or /// a plane. It is just no longer what you get by not deciding: /// /// flutter run # the real API /// flutter run --dart-define=MOCK_BACKEND=true # the canned day const bool kMockBackend = bool.fromEnvironment('MOCK_BACKEND'); /// One customer pickup with three doors, for looking at. /// /// **Off unless asked for**, and separate from [kMockBackend] on purpose. The /// bookings route above is deliberately empty — "inventing one here would put /// fictional consignments in front of a parcel rider" — and that stays exactly /// true by default. This is the one shape that cannot be reached any other way: /// `GET /miler/bookings` only returns a row per destination once a /// multi-destination customer-app booking has been collected, so seeing the /// three-door card on a bench needs either a seeded backend or this. /// /// flutter run --dart-define=MOCK_BACKEND=true --dart-define=MOCK_MULTIDROP=true const bool kMockMultiDrop = bool.fromEnvironment('MOCK_MULTIDROP'); /// Canned answers for the routes the rider's day passes through. /// /// Everything else gets a generic success, on purpose: the alternative is a /// mock that fails on an endpoint nobody thought about and reads to the person /// using it as a broken app rather than an unmapped route. class MockBackend { MockBackend._(); /// Whether the canned answers are being served. /// /// Settable so a test can exercise the transport itself — the interception /// happens *above* the HTTP client, so with this on there is no request to /// assert against. Nothing in the app assigns it; it follows [kMockBackend]. static bool enabled = kMockBackend; /// The signed-in rider, when there is no server to ask. /// /// `tenantname` is what puts the build on the meal line — see /// [ServiceProfile] — so the mock day and the mock login agree about which /// application the rider is in. static const Map rider = { 'userid': 90001, 'id': 90001, 'name': 'Suresh Kumar', 'phone': '9876543210', 'mobile': '9876543210', 'email': 'suresh@doormile.test', 'roleid': 5, 'configid': 1001, 'tenantid': 916, 'tenantname': 'DailyGrubs', 'status': 'Active', 'vehicletype': 'Bike', 'vehicleno': 'TN 37 CV 4412', }; /// A token that is obviously not a JWT, so nothing tries to read claims out /// of it and no log line makes it look like a real session. static const String token = 'MOCK-TOKEN-not-a-real-session'; /// One collected customer pickup, as three rows — the shape /// `milerStopsForBooking` returns once `pickup-complete` has fanned a /// multi-destination booking out into a consignment per door. /// /// Written in the **raw wire shape**, not the app's stop shape, because that /// is what the mock intercepts: these go through `ApiConfig.pickupFromBooking` /// exactly as a real response would, so the adapter is being exercised rather /// than bypassed. Same `bookingid` and `bookingreference` on all three, which /// is the whole point — they are told apart only by `destinationseq`. /// /// Ids are `MOCK-` per rule 3 so `purgeDemoRecords()` can find anything that /// leaks into a store. static const List> multiDropVisit = [ { 'bookingid': 900482913, 'bookingreference': 'MOCK-482913', 'status': 'Converted_To_Consignment', 'consignmentid': 900500, 'consignmentstatus': 'Out_for_Delivery', 'trackingno': 'MOCK-TRK-8841020', 'destinationseq': 0, 'destinationcount': 3, 'next_action': 'deliver', 'step': 1, 'pickup_source_type': 'customer', 'pickup_source_name': 'Ravi Kumar', 'pickupaddress': '14, Cross Cut Road, Gandhipuram, Coimbatore 641012', 'pickuplatitude': 11.0183, 'pickuplongitude': 76.9725, 'deliveryaddress': '22, Anna Nagar West, Chennai, Tamil Nadu', 'deliverylatitude': 11.0210, 'deliverylongitude': 76.9810, 'recipientname': 'Anitha Raghavan', 'recipientphone': '9840012201', 'customername': 'Ravi Kumar', 'customerphone': '9843311220', 'parcels': [ {'bookingparcelid': 900001, 'itemcategory': 'General'}, {'bookingparcelid': 900002, 'itemcategory': 'General'}, ], }, { 'bookingid': 900482913, 'bookingreference': 'MOCK-482913', 'status': 'Converted_To_Consignment', 'consignmentid': 900501, 'consignmentstatus': 'Out_for_Delivery', 'trackingno': 'MOCK-TRK-8841021', 'destinationseq': 1, 'destinationcount': 3, 'next_action': 'deliver', 'step': 2, 'pickup_source_type': 'customer', 'pickup_source_name': 'Ravi Kumar', 'pickupaddress': '14, Cross Cut Road, Gandhipuram, Coimbatore 641012', 'pickuplatitude': 11.0183, 'pickuplongitude': 76.9725, 'deliveryaddress': '7, Panampilly Nagar, Ernakulam, Kerala', 'deliverylatitude': 11.0301, 'deliverylongitude': 76.9902, 'recipientname': 'Suresh Menon', 'recipientphone': '9847712345', 'customername': 'Ravi Kumar', 'customerphone': '9843311220', 'parcels': [ {'bookingparcelid': 900003, 'itemcategory': 'General'}, ], }, { 'bookingid': 900482913, 'bookingreference': 'MOCK-482913', 'status': 'Converted_To_Consignment', 'consignmentid': 900502, 'consignmentstatus': 'Out_for_Delivery', 'trackingno': 'MOCK-TRK-8841022', 'destinationseq': 2, 'destinationcount': 3, 'next_action': 'deliver', 'step': 3, 'pickup_source_type': 'customer', 'pickup_source_name': 'Ravi Kumar', 'pickupaddress': '14, Cross Cut Road, Gandhipuram, Coimbatore 641012', 'pickuplatitude': 11.0183, 'pickuplongitude': 76.9725, 'deliveryaddress': '90, Indiranagar 2nd Stage, Bengaluru, Karnataka', 'deliverylatitude': 11.0405, 'deliverylongitude': 77.0011, 'recipientname': 'Meena Prakash', 'recipientphone': '9880045512', 'customername': 'Ravi Kumar', 'customerphone': '9843311220', 'parcels': [ {'bookingparcelid': 900004, 'itemcategory': 'General'}, ], }, ]; /// Answers [method] [path], or null when the caller should fall through to /// the network. Null is never returned while [enabled] — see the class note. static Map? respond( String method, String path, { Object? body, Map? query, }) { if (!enabled) return null; final route = path.split('?').first; debugPrint('[MOCK] $method $route'); // ── Auth ── if (route.endsWith('/login')) { return _ok({'otpsent': true, 'phone': _field(body, 'phone')}); } if (route.endsWith('/verify-pin')) { // The shape the real handler returns, verified against a live 200: the // profile lives under `user` / `user.profile`, and there is no `data` // key. Getting this wrong cost a debugging session once already — see the // note in [MilerApi]. // // The phone is the one the rider actually typed. It used to be the // fixture's, so signing in with your own number made you Suresh Kumar and // the screen you landed on disagreed with the number on the screen you // came from — which reads as a broken login rather than as a mock. final phone = _field(body, 'phone'); final signedIn = { ...rider, if (phone.isNotEmpty) ...{ 'phone': phone, 'mobile': phone, 'contactno': phone, }, }; return { 'success': true, 'message': 'ok', 'token': token, 'user': {...signedIn, 'profile': signedIn}, }; } if (route.endsWith('/profile')) return _ok(rider); // ── The day's work ── // // Empty rather than invented: the meal line's stops come from // [MealRunMock], which the provider reads *before* it reaches the network // at all. A parcel booking list has no fixture and inventing one here would // put fictional consignments in front of a parcel rider. if (route.endsWith('/bookings') || route.endsWith('/assignments')) { return _ok(kMockMultiDrop ? multiDropVisit : const []); } // ── Everything the rider writes ── // // Accept, reject, reached, parcels, payment, pickup-complete, deliver, // skip, duty, breaks, telemetry, device tokens. All answered yes, and all // going nowhere. return _ok(const {}); } static Map _ok(dynamic data) => { 'success': true, 'message': 'ok', 'data': data, }; static String _field(Object? body, String key) => (body is Map && body[key] != null) ? body[key].toString() : ''; }