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>
This commit is contained in:
2026-08-22 05:40:35 +05:30
parent c8350563d9
commit d7348e253f
387 changed files with 80693 additions and 12272 deletions

View File

@@ -0,0 +1,209 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/accepted_store.dart';
import 'package:miler/data/mutation_guard.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/Models/stop_status.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// THE DELIVERY WRITES AGAINST THE CONSIGNMENT, ONCE
///
/// Three ids exist for one job and they come from three different sequences:
///
/// `bookingid` the customer's booking
/// `bookingassignmentid` the row that gave it to this rider
/// `consignmentid` what the booking BECAME at pickup-complete
///
/// `accept`/`reject` key on the assignment, `reached`/`pickup-complete` on the
/// booking, and `deliver`/`skip` on the **consignment**. Passing the wrong one
/// does not fail loudly — the backend answers 404, or worse finds a row that
/// happens to share the number — so these pin the mapping the delivery path
/// depends on.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
setUp(() {
ServiceProfile.setActive(ServiceProfile.milkMan);
SharedPreferences.setMockInitialValues({'userid': 38});
});
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
group('pickup-complete preserves the real consignment id', () {
test('the id minted at the pivot survives to the door', () async {
// `pickup-complete` is the only moment the consignment id is guaranteed
// to be in hand: `GET /miler/bookings` does not return it at all
// (verified against the live API), so if the pivot does not record it,
// the delivery leg has nothing to write against.
await rememberConsignmentId('ORDER-1', '46');
final ids = await getConsignmentIds();
expect(ids['ORDER-1'], '46');
});
test('a booking id is never mistaken for a consignment id', () async {
// Booking 75 became consignment 46 — different sequences, and the
// mapping is keyed by the ORDER, never by the number itself.
await rememberConsignmentId('ORDER-1', '46');
final ids = await getConsignmentIds();
expect(
ids['ORDER-1'],
isNot('75'),
reason: 'the booking id must never end up in the consignment slot',
);
expect(
ids.containsKey('75'),
isFalse,
reason: 'the map is keyed by order, not by booking number',
);
});
test('the mapping is per rider and per line', () async {
await rememberConsignmentId('ORDER-1', '46');
ServiceProfile.setActive(ServiceProfile.parcel);
expect(
(await getConsignmentIds())['ORDER-1'],
isNull,
reason: "one line's consignment mapping is not another's",
);
SharedPreferences.setMockInitialValues({'userid': 99});
ServiceProfile.setActive(ServiceProfile.milkMan);
expect(
(await getConsignmentIds())['ORDER-1'],
isNull,
reason: "another rider's handset drawer is not this one",
);
});
test('a delivered order forgets its mapping', () async {
await rememberConsignmentId('ORDER-1', '46');
await forgetConsignmentIds(['ORDER-1']);
expect(
(await getConsignmentIds())['ORDER-1'],
isNull,
reason: 'nothing else may be posted against a closed consignment',
);
});
});
group('a missing consignment id is reported honestly', () {
// Verified against production 21 Aug 2026: booking 59 is
// `Converted_To_Consignment` and on the Deliveries tab, yet no route
// exposes its consignment id — `/miler/bookings` carries none on any of
// its 29 rows and `/miler/assignments` lists only 12 of those bookings.
//
// The app cannot deliver it. What it CAN do is tell the rider the truth:
// a collected order with an unreachable reference is a hub problem, not
// "you forgot to pick it up" — advice that sends him to look for a stop
// that is not on Home, to redo work he has already done.
test('a collected stop and an uncollected one are different states', () {
// `Converted_To_Consignment` maps to `picked` — collected.
expect(stopStatusFromRaw('picked').isPicked, isTrue);
expect(stopStatusFromRaw('outfordelivery').isDeliveryLeg, isTrue);
// Still awaiting collection — the "pick it up from Home" advice is
// correct only for these.
expect(stopStatusFromRaw('assigned').isPicked, isFalse);
expect(stopStatusFromRaw('assigned').isDeliveryLeg, isFalse);
expect(stopStatusFromRaw('accepted').isPicked, isFalse);
expect(stopStatusFromRaw('accepted').isDeliveryLeg, isFalse);
});
});
group('one press, one write', () {
test('a duplicate Delivered tap produces one mutation', () async {
var calls = 0;
Future<String> deliver() async {
calls++;
await Future<void>.delayed(const Duration(milliseconds: 30));
return 'done';
}
// Both taps land while the first request is still in flight — which is
// what a gloved thumb on a moving bike actually produces.
final first = MutationGuard.run('delivered:ORDER-1', deliver);
final second = MutationGuard.run('delivered:ORDER-1', deliver);
final results = await Future.wait([first, second]);
expect(calls, 1, reason: 'the second press must be dropped, not queued');
expect(results.where((r) => r != null).length, 1);
expect(
results.contains(null),
isTrue,
reason: 'the dropped press returns null so the caller keeps the stop',
);
});
test('the guard frees the key so a genuine retry still works', () async {
var calls = 0;
Future<String> deliver() async {
calls++;
return 'done';
}
await MutationGuard.run('delivered:ORDER-1', deliver);
await MutationGuard.run('delivered:ORDER-1', deliver);
expect(
calls,
2,
reason: 'a retry after the first completed is a real second attempt',
);
});
test('two different stops do not block each other', () async {
var a = 0;
var b = 0;
await Future.wait([
MutationGuard.run('delivered:ORDER-1', () async {
await Future<void>.delayed(const Duration(milliseconds: 20));
a++;
return 'a';
}),
MutationGuard.run('delivered:ORDER-2', () async {
await Future<void>.delayed(const Duration(milliseconds: 20));
b++;
return 'b';
}),
]);
expect(a, 1);
expect(b, 1);
});
test('the outcome is part of the key', () async {
// Delivered and Skipped are different writes against the same stop; one
// must not silently swallow the other.
var delivered = 0;
var skipped = 0;
await Future.wait([
MutationGuard.run('delivered:ORDER-1', () async {
await Future<void>.delayed(const Duration(milliseconds: 20));
delivered++;
return 'd';
}),
MutationGuard.run('skipped:ORDER-1', () async {
await Future<void>.delayed(const Duration(milliseconds: 20));
skipped++;
return 's';
}),
]);
expect(delivered, 1);
expect(skipped, 1);
});
});
group('completing a delivery clears the rider’s hands', () {
test('a delivered order stops being carried', () async {
await addCollectedOrderIds(['ORDER-1', 'ORDER-2']);
expect(await getCollectedOrderIds(), containsAll(['ORDER-1', 'ORDER-2']));
await removeCollectedOrderIds(['ORDER-1']);
final carried = await getCollectedOrderIds();
expect(carried, isNot(contains('ORDER-1')));
expect(
carried,
contains('ORDER-2'),
reason: 'closing one stop must not drop the rest of the load',
);
});
});
}