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

246
test/work_scope_test.dart Normal file
View File

@@ -0,0 +1,246 @@
import 'dart:convert';
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/service_profile.dart';
import 'package:miler/data/work_scope.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// TWO LINES, ONE HANDSET, NO MIXING
///
/// Miler runs a milk-man round and a logistics day off one app and one login,
/// and it kept finished work under *global* SharedPreferences keys —
/// `completed_bookings` and friends. One phone, one drawer, whoever wrote
/// last: log out and back in as another rider, or move tenant, and the
/// previous scope's history was sitting in the new session's Activity.
///
/// These pin the ownership rule at the data boundary. Nothing here filters on
/// a display string — no kitchen names, no "Milk", no screen titles — only on
/// identity the session can prove: rider, tenant, line.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
const milkA = WorkScope(userId: 38, tenantId: 13, line: ServiceLine.milkMan);
const logisticsA = WorkScope(
userId: 38,
tenantId: 13,
line: ServiceLine.parcel,
);
const milkB = WorkScope(userId: 99, tenantId: 13, line: ServiceLine.milkMan);
const otherTenant = WorkScope(
userId: 38,
tenantId: 77,
line: ServiceLine.milkMan,
);
group('the key', () {
test('rider, tenant and line each change the drawer', () {
final keys = {
milkA.scoped('completed_bookings'),
logisticsA.scoped('completed_bookings'),
milkB.scoped('completed_bookings'),
otherTenant.scoped('completed_bookings'),
};
expect(
keys.length,
4,
reason:
'all three identity parts must be in the key — two scopes '
'sharing a key is the leak itself',
);
expect(
milkA.scoped('completed_bookings'),
contains('completed_bookings'),
);
});
test('the same session resolves to the same drawer', () {
expect(
milkA.scoped('skipped_bookings'),
const WorkScope(
userId: 38,
tenantId: 13,
line: ServiceLine.milkMan,
).scoped('skipped_bookings'),
);
});
});
group('ownership', () {
test('a milk-man record is not a logistics record', () {
final row = milkA.stamp({'orderid': 'A1'});
expect(milkA.owns(row), isTrue);
expect(
logisticsA.owns(row),
isFalse,
reason: 'same rider, same tenant, other LINE — must not cross',
);
expect(logisticsA.excludes(row), isTrue);
});
test('another rider cannot claim it', () {
final row = milkA.stamp({'orderid': 'A1'});
expect(milkB.owns(row), isFalse);
expect(milkB.excludes(row), isTrue);
});
test('another tenant cannot claim it', () {
final row = milkA.stamp({'orderid': 'A1'});
expect(otherTenant.owns(row), isFalse);
expect(otherTenant.excludes(row), isTrue);
});
test('the API stamps its own identity and it is honoured', () {
// Rows straight off `/miler/assignments` carry the rider; no scope stamp
// is involved and it must still be respected.
expect(milkA.excludes({'orderid': 'A1', 'mileruserid': 99}), isTrue);
expect(milkA.excludes({'orderid': 'A1', 'mileruserid': 38}), isFalse);
});
test('silence is read differently by the two questions', () {
// A row with no identity: `owns` refuses to adopt it (used on legacy
// rows, where inventing ownership IS the leak), `excludes` keeps it
// (used on rows the API just returned for this session).
final bare = {'orderid': 'A1'};
expect(milkA.owns(bare), isFalse);
expect(milkA.excludes(bare), isFalse);
});
});
group('the store, scoped', () {
setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan));
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
String today() {
final n = DateTime.now();
return '${n.year}-${n.month.toString().padLeft(2, '0')}-'
'${n.day.toString().padLeft(2, '0')}';
}
test(
'completed work written as one rider is invisible to the next',
() async {
// Rider 38 finishes a stop.
SharedPreferences.setMockInitialValues({'userid': 38});
await addCompletedBookings([
{'orderid': 'MILK-1', 'pickupcustomer': 'Joe'},
], terminalStatus: 'delivered');
final mine = await getCompletedBookings();
expect(mine.map((r) => r['orderid']), contains('MILK-1'));
// Rider 99 signs in on the same handset. Same day, same store, same
// process — a different drawer.
SharedPreferences.setMockInitialValues({'userid': 99});
final theirs = await getCompletedBookings();
expect(
theirs.where((r) => r['orderid'] == 'MILK-1'),
isEmpty,
reason: "logging in must never expose the previous rider's history",
);
},
);
test('switching line cannot expose the other line’s history', () async {
SharedPreferences.setMockInitialValues({'userid': 38});
ServiceProfile.setActive(ServiceProfile.milkMan);
await addCompletedBookings([
{'orderid': 'ROUND-1'},
], terminalStatus: 'delivered');
expect(
(await getCompletedBookings()).map((r) => r['orderid']),
contains('ROUND-1'),
);
// Same rider, same tenant, logistics day.
ServiceProfile.setActive(ServiceProfile.parcel);
expect(
(await getCompletedBookings()).where((r) => r['orderid'] == 'ROUND-1'),
isEmpty,
reason: 'a milk-man delivery belongs only to milk-man Activity',
);
// And a logistics collection stays out of the round's history.
await addCompletedBookings([
{'orderid': 'PARCEL-1'},
], terminalStatus: 'picked');
ServiceProfile.setActive(ServiceProfile.milkMan);
expect(
(await getCompletedBookings()).where((r) => r['orderid'] == 'PARCEL-1'),
isEmpty,
reason: 'a logistics pickup belongs only to logistics Activity',
);
});
test('a stored record carries the identity that produced it', () async {
SharedPreferences.setMockInitialValues({'userid': 38});
await addCompletedBookings([
{'orderid': 'STAMP-1'},
], terminalStatus: 'delivered');
final row = (await getCompletedBookings()).single;
expect(row['scopeuserid'], 38);
expect(row['scopeline'], ServiceLine.milkMan.name);
});
test('logout empties this scope', () async {
SharedPreferences.setMockInitialValues({'userid': 38});
await addCompletedBookings([
{'orderid': 'BYE-1'},
], terminalStatus: 'delivered');
expect(await getCompletedBookings(), isNotEmpty);
await clearScopedStores();
expect(
await getCompletedBookings(),
isEmpty,
reason: 'signing out must not leave records for the next rider',
);
});
test('unattributable legacy rows are dropped, not adopted', () async {
// The pre-scoping global key, holding somebody's rows. Nothing on them
// says whose, so adopting them would be inventing ownership.
SharedPreferences.setMockInitialValues({
'userid': 38,
'completed_bookings': jsonEncode([
{'orderid': 'LEGACY-1', 'completedday': today()},
]),
});
await migrateLegacyStores();
expect(
(await getCompletedBookings()).where((r) => r['orderid'] == 'LEGACY-1'),
isEmpty,
reason: 'a row that cannot prove ownership must not be adopted',
);
final prefs = await SharedPreferences.getInstance();
expect(
prefs.containsKey('completed_bookings'),
isFalse,
reason: 'the global key is drained so it can never be read again',
);
});
test('a legacy row that proves ownership is carried across', () async {
SharedPreferences.setMockInitialValues({
'userid': 38,
'completed_bookings': jsonEncode([
{
'orderid': 'LEGACY-MINE',
'completedday': today(),
'mileruserid': 38,
'scopeline': 'milkMan',
},
]),
});
await migrateLegacyStores();
expect(
(await getCompletedBookings()).map((r) => r['orderid']),
contains('LEGACY-MINE'),
);
});
});
}