Files
doormile_milderapp/test/delivery_modes_test.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

240 lines
8.3 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/consignment_state.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/data/mock_backend.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/views/Dashboard/pickups/pickups.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// TWO BACKENDS, ONE BUILD
///
/// `Collected_By_Miler` and `start-delivery` ship behind a server-side flag
/// that is **off in production today**, so both lifecycles are live at once:
///
/// old `pickup-complete` releases hyperlocal work itself. The consignment
/// is already `Out_for_Delivery` when it reaches the Deliveries tab and
/// there is nothing for **Start delivery** to call.
/// new the pivot stops at `Collected_By_Miler` and the press is what puts
/// the load on the road.
///
/// The app must be right in both **without mirroring the flag**, because a
/// mirrored flag is a second source of truth that can be wrong — and would be
/// wrong for every rider for however long it took to ship an app release after
/// the server toggled. So the behaviour is inferred from what the data says:
/// `consignmentstatus` on the row, then the consignment itself, then the
/// `can_start_delivery` / `can_deliver` permissions.
///
/// These pin both modes, and pin that the old one costs no request.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late List<http.Request> sent;
http.Response jsonRes(Object body, [int code = 200]) => http.Response(
json.encode(body),
code,
headers: const {'content-type': 'application/json'},
);
void stubRoutes(Map<String, http.Response> routes) {
sent = <http.Request>[];
MilerApi.client = MockClient((req) async {
sent.add(req);
for (final e in routes.entries) {
if (req.url.path.endsWith(e.key)) return e.value;
}
return jsonRes({'success': true});
});
}
setUp(() async {
MockBackend.enabled = false;
SharedPreferences.setMockInitialValues({
'userid': 38,
'authtoken': 'test-token',
});
await ApiConfig.setToken('test-token');
ServiceProfile.setActive(ServiceProfile.milkMan);
stubRoutes(const {});
});
tearDown(() {
MilerApi.client = http.Client();
MockBackend.enabled = kMockBackend;
ServiceProfile.setActive(ServiceProfile.parcel);
});
Map<String, dynamic> stop({String? consignmentStatus}) => {
'orderid': 'ORDER-1',
'pickupid': 75,
'consignmentid': 46,
if (consignmentStatus != null) 'consignmentstatus': consignmentStatus,
'orderstatus': 'picked',
};
group('old backend — the pivot already released the load', () {
test('the press costs no request at all', () async {
stubRoutes(const {});
final released = await releaseForDelivery(
stop(consignmentStatus: 'Out_for_Delivery'),
);
expect(released, isTrue);
expect(
sent,
isEmpty,
reason:
'a start-delivery the server can only refuse is not a harmless '
'no-op — it is a 4xx per rider press in the backend\'s log',
);
});
test('a stop already delivered elsewhere is not re-released', () async {
stubRoutes(const {});
expect(
await releaseForDelivery(stop(consignmentStatus: 'Delivered')),
isTrue,
);
expect(sent, isEmpty);
});
});
group('new backend — the rider makes the release', () {
test('a collected consignment is started on the server', () async {
stubRoutes({
'/start-delivery': jsonRes({
'success': true,
'data': {'status': 'Out_for_Delivery'},
}),
});
expect(
await releaseForDelivery(stop(consignmentStatus: 'Collected_By_Miler')),
isTrue,
);
expect(sent.single.method, 'POST');
expect(
sent.single.url.path.endsWith('/miler/consignments/46/start-delivery'),
isTrue,
);
expect(sent.single.headers['Idempotency-Key'], isNotNull);
});
test('a refusal is answered by the consignment, not assumed', () async {
// `INVALID_STATE` means "not Collected_By_Miler" — which covers both a
// real refusal and a consignment another device already released.
stubRoutes({
'/start-delivery': jsonRes({
'success': false,
'code': 'INVALID_STATE',
'message': 'consignment is not collected',
}, 400),
'/miler/consignments/46': jsonRes({
'success': true,
'data': {'status': 'Out_for_Delivery'},
}),
});
expect(
await releaseForDelivery(stop(consignmentStatus: 'Collected_By_Miler')),
isTrue,
reason: 'already on the road is not a reason to block the rider',
);
});
test('a genuine refusal does not fake a release', () async {
stubRoutes({
'/start-delivery': jsonRes({
'success': false,
'code': 'INVALID_STATE',
'message': 'consignment is at the hub',
}, 400),
'/miler/consignments/46': jsonRes({
'success': true,
'data': {'status': 'Inwarded_at_Hub'},
}),
});
expect(
await releaseForDelivery(stop(consignmentStatus: 'Collected_By_Miler')),
isFalse,
reason:
'showing ACTIVE for a consignment the hub still holds is the '
'fiction that made deliver fail at the door with nothing on '
'screen to explain it',
);
});
});
group('a row that says nothing falls back to asking', () {
test('the consignment is read when the row carries no status', () async {
stubRoutes({
'/miler/consignments/46': jsonRes({
'success': true,
'data': {'status': 'Out_for_Delivery'},
}),
});
expect(await releaseForDelivery(stop()), isTrue);
expect(sent.single.method, 'GET');
expect(sent.single.url.path.endsWith('/miler/consignments/46'), isTrue);
});
test('the permission flags answer when the word does not', () {
// A status this build has never heard of, plus the server's own verdict
// on what may be done with it.
expect(
ConsignmentGate.stateFromDetailForTest({
'status': 'Handed_To_Locker_Bank',
'can_deliver': true,
}),
ConsignmentState.outForDelivery,
);
expect(
ConsignmentGate.stateFromDetailForTest({
'status': 'Awaiting_Rider_Departure',
'can_start_delivery': true,
}),
ConsignmentState.collectedByMiler,
);
});
test('no consignment id is not a release', () async {
expect(await releaseForDelivery({'orderid': 'ORDER-9'}), isFalse);
});
});
group('a skipped stop and the server agree about whether it is open', () {
test('a consignment the hub still calls out-for-delivery stays open', () {
expect(ConsignmentState.outForDelivery.isOpenAfterSkip, isTrue);
expect(ConsignmentState.collectedByMiler.isOpenAfterSkip, isTrue);
});
test('a consignment the hub closed is closed', () {
expect(ConsignmentState.delivered.isOpenAfterSkip, isFalse);
expect(ConsignmentState.rtoInitiated.isOpenAfterSkip, isFalse);
expect(ConsignmentState.returnedToSender.isOpenAfterSkip, isFalse);
expect(ConsignmentState.cancelled.isOpenAfterSkip, isFalse);
});
test('a read that failed is not permission to close a stop', () {
expect(
ConsignmentState.unknown.isOpenAfterSkip,
isTrue,
reason:
'a terminal local record over a consignment the hub still '
'calls open is two systems disagreeing about whose problem a '
'parcel is, with only the rider\'s screen saying "nobody\'s"',
);
});
});
}