Files
doormile_milderapp/lib/data/meal_run_mock.dart
2026-08-28 15:07:30 +05:30

359 lines
13 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:miler/data/mock_backend.dart';
import 'package:miler/data/service_profile.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// A MEAL DAY, WITH NO BACKEND BEHIND IT
///
/// The meal line has no endpoints. The Doormile backend serves parcel bookings
/// and consignments and knows nothing about kitchens, crates or subscribers, and
/// building against invented URLs would produce a flow that compiles, demos, and
/// is wrong the day a real contract arrives.
///
/// So the meal line runs on this: a complete day, held in memory, walked with
/// the real screens. Every status change the rider makes is recorded here
/// instead of being sent, which means the whole process — accept, arrive, load
/// the crate, deliver, skip, fail — can be designed, used and judged before a
/// single endpoint exists.
///
/// ── Why this is not the demo layer that was deleted ──
///
/// A mock layer was ripped out of this app for a good reason: it wrote rows into
/// the same SharedPreferences stores the real work uses, so demo stops surfaced
/// on a live rider's tabs weeks later, with nothing left in the repo to explain
/// them. Three rules keep this one from becoming that:
///
/// 1. **It cannot reach a parcel rider.** [active] is false unless the signed-in
/// profile is the meal line, and an unrecognised tenant resolves to parcel.
/// There is no path from a normal login to this data.
/// 2. **Its ids are self-identifying.** Every id is `MOCK-…`, which is exactly
/// what `purgeDemoRecords()` already looks for — so anything that does leak
/// into a store is removed at the next launch, by code that already ships.
/// 3. **It has one switch.** [kMealMockEnabled] is the whole feature. When the
/// meal endpoints land, that constant goes false and the provider falls
/// through to the real call; nothing else in the app knows this file exists.
/// ─────────────────────────────────────────────────────────────────────────
/// Whether the day below is served instead of calling the API.
///
/// ── One switch for the whole app, not two ──
///
/// This used to be its own constant, and it was turned off on 2026-08-12 for a
/// real reason: while it was on, a meal rider tapped **Arrived** and **Picked
/// up**, watched the card advance, and the hub console never changed. Every
/// status write on this line was recorded in a `Map` on the phone and reported
/// as a success.
///
/// That failure mode has not gone away — it is simply what running without a
/// backend *means*, and it now applies to the whole app rather than to this
/// file alone. So the two switches became one: [kMockBackend] takes sign-in,
/// the day and every status write off the network together, and this follows
/// it. There is no state where the meal day is fictional but the statuses are
/// real, which is the confusing half of what used to be possible.
///
/// Turn the app back on to the real backend with:
///
/// flutter run --dart-define=MOCK_BACKEND=false
///
/// The meal line then falls through to the three booking routes it actually
/// depends on, all of which exist and are proven by the parcel line:
///
/// list `GET /miler/bookings`
/// arrived `POST /miler/bookings/:id/reached`
/// picked up `POST /miler/bookings/:id/pickup-complete`
/// Follows the one mock switch at runtime rather than at compile time, so a
/// test (or a bench session) that turns the canned backend off turns this off
/// with it. See [MockBackend.enabled].
bool get kMealMockEnabled => MockBackend.enabled;
/// Wall-clock helper so the mock day always looks like *today's* shift rather
/// than a fixed date that reads as stale the moment anybody opens it.
String _slot(int hour, int minute) {
final now = DateTime.now();
final t = DateTime(now.year, now.month, now.day, hour, minute);
return t.toIso8601String();
}
/// A meal run held in memory: two kitchens, seven subscribers, one rider.
///
/// Coordinates are real Coimbatore addresses in route order, because a route
/// that doubles back looks like a bug in the sequencing rather than what it is.
class MealRunMock {
MealRunMock._();
/// True when the app should serve this instead of calling the API.
static bool get active =>
kMealMockEnabled && ServiceProfile.active.sourceIsKitchen;
/// Status overrides the rider has caused this session, keyed by order id.
///
/// The mock's own rows are `assigned`; everything after that is something he
/// did. Held separately rather than mutated into the rows so a reset is one
/// `clear()` and the day itself stays declarative.
static final Map<String, String> _status = <String, String>{};
/// Records what a status call *would* have sent. Always succeeds — there is
/// nothing to fail.
static bool setStatus(String orderId, String status) {
if (orderId.isEmpty) return true;
_status[orderId] = status;
debugPrint('[MEAL_MOCK] $orderId → $status');
return true;
}
/// Same, keyed by the numeric booking id the controllers carry.
static bool setStatusByPickupId(int pickupId, String status) {
if (pickupId <= 0) return true;
final row = _day.firstWhere(
(r) => r['pickupid'] == pickupId,
orElse: () => const <String, dynamic>{},
);
final id = (row['orderid'] ?? '').toString();
return setStatus(id, status);
}
/// Puts the day back to the top. Wired to nothing yet — it exists so a
/// demo can be run twice without reinstalling the app.
static void reset() => _status.clear();
/// The day as the cards read it, with whatever the rider has done applied.
///
/// A fresh copy every call: the screens mutate stop maps in place (compliance
/// stamps, proof blocks), and handing out the master rows would let one run's
/// leftovers show up in the next.
static List<Map<String, dynamic>> stops() => [
for (final row in _day)
{...row, 'orderstatus': _status[row['orderid']] ?? row['orderstatus']},
];
// ── The day itself ──────────────────────────────────────────────────────
//
// Two kitchens is deliberate rather than decorative: a single-kitchen day
// hides every question the grouping exists to answer — which crate is this,
// which bags belong to it, what happens when the second load is still at a
// counter he has not reached.
//
// Five orders on the first counter and four on the second, because the whole
// pickup flow is a claim about a *set*: `5 orders · 5 bags`, five names on the
// confirmation, five deliveries afterwards. A day of ones would let every one
// of those read correctly while being wrong.
static final List<Map<String, dynamic>> _day = [
// ── Vidhya Kitchen · Gandhi Nagar, Peelamedu ──
_drop(
id: 1001,
step: 1,
name: 'Joe Mathew',
phone: '9840012001',
address: '12, SNS Colony, Peelamedu',
lat: 11.0271,
lng: 76.9962,
kitchen: _vidhya,
label: 'DG-1001',
meals: 1,
),
_drop(
id: 1002,
step: 2,
name: 'Arun Prakash',
phone: '9840012002',
address: '21, New Street, Peelamedu',
lat: 11.0248,
lng: 76.9941,
kitchen: _vidhya,
label: 'DG-1002',
meals: 1,
),
_drop(
id: 1003,
step: 3,
name: 'Priya Venkatesh',
phone: '9840012003',
address: '45, West Avenue, RS Puram',
lat: 11.0043,
lng: 76.9518,
kitchen: _vidhya,
label: 'DG-1003',
meals: 1,
),
_drop(
id: 1004,
step: 4,
name: 'Kumar Selvam',
phone: '9840012004',
address: 'Flat 3C, Lakshmi Towers, 100 Feet Road, Gandhipuram',
lat: 11.0168,
lng: 76.9558,
kitchen: _vidhya,
label: 'DG-1004',
meals: 1,
),
_drop(
id: 1005,
step: 5,
name: 'Ravi Shankar',
phone: '9840012005',
address: '8, Krishna Colony, 2nd Street, Singanallur',
lat: 10.9925,
lng: 77.0289,
kitchen: _vidhya,
label: 'DG-1005',
meals: 1,
),
// ── Annapoorna Mess · Thadagam Road ──
_drop(
id: 2001,
step: 6,
name: 'Meena Krishnan',
phone: '9840012006',
address: '31, Sarojini Street, Gandhipuram',
lat: 11.0181,
lng: 76.9603,
kitchen: _annapoorna,
label: 'AM-2001',
meals: 1,
),
_drop(
id: 2002,
step: 7,
name: 'Sai Ganesh',
phone: '9840012007',
address: '14, Trichy Road, above the pharmacy, Ramanathapuram',
lat: 10.9871,
lng: 76.9931,
kitchen: _annapoorna,
label: 'AM-2002',
meals: 1,
),
_drop(
id: 2003,
step: 8,
name: 'Devi Lakshmi',
phone: '9840012008',
address: '22, Kamarajar Road, near the school gate, Uppilipalayam',
lat: 10.9903,
lng: 76.9977,
kitchen: _annapoorna,
label: 'AM-2003',
meals: 1,
),
_drop(
id: 2004,
step: 9,
name: 'Karthik Raja',
phone: '9840012009',
address: '5B, Thadagam Road, opposite the water tank',
lat: 11.0221,
lng: 76.9391,
kitchen: _annapoorna,
label: 'AM-2004',
meals: 1,
),
];
/// A counter the rider collects from: what it is called, where it is, and the
/// id everything groups on.
static const _vidhya = (
id: 'K1',
name: 'Vidhya Kitchen',
address: 'Gandhi Nagar, Peelamedu, Coimbatore - 641004',
lat: 11.0261,
lng: 76.9931,
locationId: 901,
);
static const _annapoorna = (
id: 'K2',
name: 'Annapoorna Mess',
address: '19, Thadagam Road, RS Puram, Coimbatore - 641002',
lat: 11.0195,
lng: 76.9412,
locationId: 902,
);
/// One subscriber drop, in the legacy stop shape every card in the app reads.
///
/// ── Both ends, in the right fields ──
///
/// The `pickup*` fields are the **kitchen** and the `drop*` fields are the
/// **customer**, because that is what the app navigates on: before collection
/// `navigationTarget` reads `pickuplat/pickuplon`, and after it reads
/// `droplat/droplon` — see [MilkRun]. This fixture used to put the customer in
/// both, which sent a rider to a subscriber's flat to collect a lunch that was
/// still at a counter three kilometres away.
///
/// `type: delivery` is stated rather than inferred: the app would reach the
/// same answer from the profile, but a fixture that relies on a fallback is
/// testing the fallback rather than the screen.
static Map<String, dynamic> _drop({
required int id,
required int step,
required String name,
required String phone,
required String address,
required double lat,
required double lng,
required ({
String id,
String name,
String address,
double lat,
double lng,
int locationId,
})
kitchen,
required String label,
required int meals,
}) => <String, dynamic>{
// `MOCK-` so `purgeDemoRecords()` recognises anything that leaks into a
// store. See the note at the top of this file.
'orderid': 'MOCK-M-$id',
'pickupid': id,
'orderheaderid': id,
'pickuplocationid': kitchen.locationId,
'orderstatus': 'assigned',
'step': step,
// Who is being handed food.
'pickupcustomer': name,
'pickupcontactno': phone,
// Where he collects: the counter.
'pickupaddress': kitchen.address,
'pickuplat': kitchen.lat,
'pickuplon': kitchen.lng,
'pickuplong': kitchen.lng,
// Where it goes: the door.
'dropaddress': '$address, Coimbatore, Tamil Nadu 641004',
'droplat': lat,
'droplon': lng,
// The counter itself. The grouping, the headings, the manifest and the
// bag identity all key off these.
'kitchenid': kitchen.id,
'kitchenname': kitchen.name,
'baglabel': label,
// A drop, and how many boxes are in it. One order is one bag — the count
// here is the *meal* count inside that bag, and nothing reads it as a bag
// count. See [OrderManifest].
'type': 'delivery',
'deliveryqty': meals,
'quantity': meals,
// Nothing is owed at any door — a subscriber paid the client by the month.
// Stated as zero rather than omitted so a payload reader cannot mistake a
// missing field for an unknown amount.
'collectionamt': 0,
'pickupamt': 0,
// The lunch slot the whole run belongs to.
'starttime': _slot(11, 30),
'endtime': _slot(13, 30),
'eta': '8',
'kms': '2.4',
};
}