Files
doormile_milderapp/lib/data/mock_backend.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

183 lines
8.0 KiB
Dart

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');
/// 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<String, dynamic> 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';
/// 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<String, dynamic>? respond(
String method,
String path, {
Object? body,
Map<String, String>? 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 = <String, dynamic>{
...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(const <dynamic>[]);
}
// ── 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 <String, dynamic>{});
}
static Map<String, dynamic> _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() : '';
}