updated the sheet

This commit is contained in:
2026-08-28 18:16:28 +05:30
parent 69f4f3e909
commit 074cc0eccf
43 changed files with 2617 additions and 1201 deletions

View File

@@ -3,7 +3,8 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:miler/data/api_config.dart';
import 'package:miler/data/miler_api.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:audioplayers/audioplayers.dart';
import 'dart:math' as math;
@@ -20,7 +21,6 @@ class BackgroundDeliveryLog {
static final CreateDeliveryLogProvider _logProvider =
CreateDeliveryLogProvider();
static final http.Client _httpClient = http.Client();
static final FlutterTts _tts = FlutterTts();
static final AudioPlayer _proximityPlayer = AudioPlayer();
@@ -60,38 +60,37 @@ class BackgroundDeliveryLog {
final now = DateTime.now();
final today = '${now.year}-${_pad(now.month)}-${_pad(now.day)}';
// Build API URL using v1 endpoint (getdeliveries)
final bool isLive = ApiConstants.mainRoute == 'live';
final baseUrl = isLive
? ApiConstants.currentDeliveryLive
: ApiConstants.currentDeliveryDev;
final uri = Uri.parse(baseUrl).replace(
queryParameters: {
'userid': userId.toString(),
'fromdate': today,
'todate': today,
't': DateTime.now().millisecondsSinceEpoch.toString(),
},
);
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Fetching deliveries: $uri');
// Fetch deliveries from API using v1 endpoint
final deliveries = await _fetchDeliveriesFromApi(uri);
// ── The delivery twin of the pickup fetch, with the same two faults ──
//
// It built a `deliveries/getdeliveries` URL by hand and fetched it with a
// bare `_httpClient.get` — **no bearer token** — and put `userid` in the
// query string. The URL itself already resolved to a v1 route through the
// [DeliveryApi] facade, so the call could only ever be answered 401.
//
// And the filter looked for `orderstatus == 'active'`, which is not a
// value this app's vocabulary contains: [ApiConfig.legacyStatusFromNew]
// maps the v1 statuses to `assigned` / `accepted` / `arrived` / `picked`,
// never to `active`. So it returned nothing on every tick and fell
// through to the shift-end check, which is what the device log shows.
//
// `GET /miler/bookings` through [MilerApi], asked with the session, about
// the rider the session names.
final res = await MilerApi.bookings(date: today);
final deliveries = res.list;
debugPrint(
'[ACTIVE_DELIVERY_LOG][BG] API returned ${deliveries.length} items',
'[ACTIVE_DELIVERY_LOG][BG] /miler/bookings -> ${res.status}, '
'${deliveries.length} items',
);
// Filter for active orders only
const inProgress = {'accepted', 'arrived', 'picked'};
final activeOrders = deliveries.whereType<Map<String, dynamic>>().where((
order,
) {
final status = (order['orderstatus']?.toString().toLowerCase() ?? '')
.trim();
final isActive = status == 'active';
return isActive;
final status = ApiConfig.legacyStatusFromNew(
(order['status'] ?? order['orderstatus'])?.toString(),
);
return inProgress.contains(status);
}).toList();
if (activeOrders.isEmpty) {
@@ -202,11 +201,8 @@ class BackgroundDeliveryLog {
'logstatus': await _resolveLogStatus(orderId, coords),
};
// Determine API endpoint
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createDeliveryLogLive
: ApiConstants.createDeliveryLogDev;
// v1: `/miler/consignments/logs`, via the DeliveryApi facade.
final url = ApiConstants.createDeliveryLogLive;
// Post the log
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Posting log for $orderId');
@@ -434,33 +430,6 @@ class BackgroundDeliveryLog {
}
/// Fetch deliveries from API using v1 endpoint
static Future<List<dynamic>> _fetchDeliveriesFromApi(Uri uri) async {
try {
final res = await _httpClient
.get(uri)
.timeout(const Duration(seconds: 10));
if (res.statusCode >= 200 && res.statusCode < 300) {
final decoded = json.decode(res.body);
final data = decoded is Map<String, dynamic>
? (decoded['details'] ?? decoded['data'] ?? decoded)
: decoded;
if (data is List) {
return data;
}
if (data is Map && data['items'] is List) {
return data['items'] as List;
}
return [];
}
return [];
} catch (e) {
return [];
}
}
/// Helper to pad numbers with leading zero
static String _pad(int n) => n.toString().padLeft(2, '0');
@@ -540,10 +509,6 @@ class BackgroundDeliveryLog {
final List<String> remaining = [];
bool anySuccess = false;
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createDeliveryLogLive
: ApiConstants.createDeliveryLogDev;
for (final itemStr in queue) {
try {
final Map<String, dynamic> item = jsonDecode(itemStr);
@@ -554,6 +519,8 @@ class BackgroundDeliveryLog {
if (payload.isEmpty) continue;
// v1: `/miler/consignments/logs`, via the DeliveryApi facade.
final url = ApiConstants.createDeliveryLogLive;
debugPrint(
'[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Retrying for orderId: $orderId',
);
@@ -770,14 +737,18 @@ class BackgroundDeliveryLog {
'[AUTO_SHIFT_END] 🚀 Initiating auto-break and offline sequence...',
);
// 1. Get Required IDs
// ── The four ids and the fix are no longer this function's business ──
//
// `userid`, `partnerid`, `shiftid`, `logid` and a position were assembled
// here to fill a legacy break-log body. On v1 the break and the duty end
// are both keyed on the **token**: `POST /miler/breaks/start` opens a
// break for whoever the session names, and `PUT /miler/duty/end` closes
// whichever duty log is open for them. Sending an id would not make it
// more correct; it is what let the old call be made unauthenticated.
//
// `userid` is still read, as the one guard worth keeping: no signed-in
// rider, nothing to end.
final int userid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
final int partnerid =
prefs.getInt('partnerid') ?? prefs.getInt('partnerId') ?? 0;
final int shiftid =
prefs.getInt('shiftid') ?? prefs.getInt('shiftId') ?? 0;
final int logid = prefs.getInt('logid') ?? prefs.getInt('logId') ?? 0;
if (userid == 0) {
debugPrint(
'[AUTO_SHIFT_END] ❌ Missing userid, cannot create break log',
@@ -785,61 +756,28 @@ class BackgroundDeliveryLog {
return;
}
// 2. Get Location
final Map<String, String>? coords = await _getCoordinatesWithFallback();
final String lat = coords?['lat'] ?? '0';
final String lng = coords?['lng'] ?? '0';
// 3. Prepare Break Log Payload
final now = DateTime.now();
final int localBreakId =
(DateTime.now().millisecondsSinceEpoch % 900) + 100; // Random-ish ID
final String breakdate =
'${now.year}-${_pad(now.month)}-${_pad(now.day)} ${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
final String breakstart =
'${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
final payload = <String, dynamic>{
"breakid": localBreakId,
"logid": logid,
"breakdate": breakdate,
"userid": userid,
"partnerid": partnerid,
"shiftid": shiftid,
"breakstart": breakstart,
"breakend": "",
"breakhours": 0.0,
"latitude": lat,
"longitude": lng,
};
// 4. Call API to Create Break
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createBreakRiderLogLive
: ApiConstants.createBreakRiderLogDev;
debugPrint('[AUTO_SHIFT_END] Creating break log: $url');
// We use a separate provider instance or http call if needed,
// but _logProvider is for delivery logs. We need a generic post or use http directly.
// Since we don't have BreakRiderLogProvider here, we'll use http directly for simplicity and isolation.
// 4. Close the shift with a break, on v1.
//
// This hand-posted the retired backend's `partners/createbreaklog`
// with a bare `_httpClient.post` and no bearer
// token — an unauthenticated write keyed on a `userid` in the
// body. `POST /miler/breaks/start` is the v1 endpoint, and
// [MilerApi] attaches the session.
debugPrint('[AUTO_SHIFT_END] Creating break log (v1)');
try {
final response = await _httpClient
.post(
Uri.parse(url),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(payload),
)
.timeout(const Duration(seconds: 10));
final res = await MilerApi.startBreak('Shift_End');
if (response.statusCode >= 200 && response.statusCode < 300) {
if (res.ok) {
debugPrint('[AUTO_SHIFT_END] ✅ Break log created successfully');
// Parse response to get server break ID if needed, but mainly we just need to go offline
final body = jsonDecode(response.body);
final det = (body['details'] is Map) ? body['details'] : body;
final serverBreakId = det['breakid'];
final serverBreakId = res.map['breaklogid'] ?? res.map['breakid'];
if (serverBreakId != null) {
await prefs.setInt(
@@ -851,7 +789,7 @@ class BackgroundDeliveryLog {
await prefs.setInt('break_start_epoch', now.millisecondsSinceEpoch);
} else {
debugPrint(
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${response.statusCode}',
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${res.status}',
);
}
} catch (e) {
@@ -863,27 +801,15 @@ class BackgroundDeliveryLog {
await prefs.setInt('onduty', 0);
await prefs.setBool('online', false);
// 6. Update Rider Log (Set Duty = 0)
// We should also update the main rider log to say onduty=0
final updateUrl = ApiConstants.mainRoute == 'live'
? ApiConstants.updateRiderLogLive
: ApiConstants.updateRiderLogDev;
final updatePayload = {
"userid": userid,
"onduty": 0,
"latitude": lat,
"longitude": lng,
};
// 6. End duty, on v1.
//
// Was an unauthenticated POST of `{userid, onduty: 0}` to the
// legacy `updateriderlog`. `PUT /miler/duty/end` closes whichever
// duty log is open for the token's rider, which is the same fact
// without the id or the open door.
try {
await _httpClient
.post(
Uri.parse(updateUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(updatePayload),
)
.timeout(const Duration(seconds: 5));
await MilerApi.endDuty();
debugPrint('[AUTO_SHIFT_END] ✅ Rider status updated to Offline');
} catch (_) {}

View File

@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'dart:math' as math;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/xpress/views/helpers/constants/apiconstants.dart';
import 'package:miler/xpress/providers/Riderlog/riderlog_provider.dart';
import 'package:miler/xpress/background/backgroundservice.dart';
import 'package:geolocator/geolocator.dart';
@@ -354,12 +353,9 @@ class _BackgroundRiderLog {
payload['lastname'] = lastName.trim();
}
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
final provider = CreateRiderLogProvider();
final resp = await provider.createRiderLog(base, payload);
final resp = await provider.createRiderLog(payload);
if (resp == null || resp.isEmpty) return;
final det = (resp['details'] is Map<String, dynamic>)

View File

@@ -1,7 +1,6 @@
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import 'dart:convert';
import 'package:miler/xpress/views/helpers/constants/apiconstants.dart';
import 'package:miler/xpress/Models/riders/riders_models.dart';
import 'package:miler/xpress/providers/Riderlog/riderlog_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -15,6 +14,7 @@ import 'package:miler/xpress/controllers/logcontroller.dart';
import 'package:miler/xpress/background/foreground_service.dart' as fg;
import 'package:miler/xpress/utils/kalman_filter.dart';
import 'package:miler/xpress/utils/mqtt_service.dart';
import 'package:miler/data/heartbeat.dart';
import 'package:miler/xpress/views/helpers/constants/mqtt_constants.dart';
import 'package:battery_plus/battery_plus.dart';
@@ -40,11 +40,9 @@ class RiderLogController extends GetxController {
Future<void> fetchRiderLogs({required String userId}) async {
isLoading.value = true;
try {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.getRiderLogLive
: ApiConstants.getRiderLogDev;
final url = "$base?userid=$userId";
final response = await _getProvider.getRiderLog(url);
// v1 answers duty for the *token's* rider — `userid` is neither sent
// nor needed. See [GetRiderLogProvider].
final response = await _getProvider.getRiderLog();
// Handle either {details: {...}} or {data: [...]} shapes
if (response == null) {
@@ -122,11 +120,7 @@ class RiderLogController extends GetxController {
}
Future<void> fetchRiderCount({required String userId}) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.getRiderCountLive
: ApiConstants.getRiderCountDev;
final url = "$base?userid=$userId";
final response = await _getProvider.getRiderCount(url);
final response = await _getProvider.getRiderCount();
final dynamic cnt = response != null ? response['count'] : null;
riderCount.value = cnt is num
? cnt.toInt()
@@ -134,46 +128,30 @@ class RiderLogController extends GetxController {
}
Future<bool> createLogin(RiderLogin login) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
final ok = await _createProvider.createRiderLog(
base,
_CreateRiderLogRequestCompat.fromRiderLogin(login),
);
return ok != null && ok.isNotEmpty;
}
Future<bool> updateLog(RiderUpdate update) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.updateRiderLogLive
: ApiConstants.updateRiderLogDev;
final ok = await _updateProvider.updateRiderLog(
base,
_UpdateRiderLogRequestCompat.fromRiderUpdate(update),
);
return ok != null && ok.isNotEmpty;
}
Future<bool> createBreak(RiderBreak brk) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createBreakRiderLogLive
: ApiConstants.createBreakRiderLogDev;
final ok = await _breakProvider.createBreakRiderLog(
base,
_BreakLogRequestCompat.fromRiderBreak(brk),
);
return ok != null && ok.isNotEmpty;
}
Future<bool> updateBreak(RiderBreak brk) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.updateBreakRiderLogLive
: ApiConstants.updateBreakRiderLogDev;
final ok = await _breakProvider.updateBreakRiderLog(
base,
_BreakLogRequestCompat.fromRiderBreak(brk),
);
// `PUT /miler/breaks/end` takes no body and closes whichever break is
// open, which the server knows and the app was only guessing at.
final ok = await _breakProvider.updateBreakRiderLog();
return ok != null && ok.isNotEmpty;
}
@@ -190,10 +168,6 @@ class RiderLogController extends GetxController {
required String latitude,
required String longitude,
}) async {
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.updateBreakRiderLogLive
: ApiConstants.updateBreakRiderLogDev;
final payload = {
"breakid": breakid,
"logid": logid,
@@ -209,11 +183,9 @@ class RiderLogController extends GetxController {
};
try {
// Debug: show exact URL and JSON body
debugPrint('[BREAK][UPDATE] URL: $url');
debugPrint('[BREAK][UPDATE] Body: $payload');
final response = await _breakProvider.updateBreakRiderLog(url, payload);
final response = await _breakProvider.updateBreakRiderLog();
// Debug: show raw response
debugPrint(
@@ -375,13 +347,9 @@ class RiderLogController extends GetxController {
payload.remove('lastname');
}
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
debugPrint('[RIDERLOG][CREATE LOGIN NOW] URL: $base');
debugPrint('[RIDERLOG][CREATE LOGIN NOW] Payload: $payload');
final resp = await _createProvider.createRiderLog(base, payload);
final resp = await _createProvider.createRiderLog(payload);
debugPrint(
'[RIDERLOG][CREATE LOGIN NOW] Response: ${resp == null ? 'null' : resp.toString()}',
);
@@ -391,7 +359,7 @@ class RiderLogController extends GetxController {
debugPrint(
'[RIDERLOG][CREATE LOGIN NOW] Failed, saving to offline queue',
);
await _saveToOfflineQueue(base, payload);
await _saveToOfflineQueue('/miler/logs', payload);
return false;
}
@@ -500,11 +468,7 @@ class RiderLogController extends GetxController {
payload.remove('lastname');
}
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
await _saveToOfflineQueue(base, payload);
await _saveToOfflineQueue('/miler/logs', payload);
}
} catch (e) {
debugPrint('[RIDERLOG] Error saving offline log: $e');
@@ -532,10 +496,15 @@ class RiderLogController extends GetxController {
await prefs.setInt('onduty', on ? 1 : 0);
if (on) {
await createLoginNowV2();
final int interval = prefs.getInt('logseconds') ?? 0;
if (interval > 0) {
startAutoCreateLoginLoop(seconds: interval);
}
// ── Going on duty always starts the heartbeat ──
//
// Gated on `interval > 0`, this read a stored zero as a decision not
// to beat. The v1 login has no `logseconds` to store, so the gate was
// shut on every rider: duty went on, one log was posted by the call
// above, and nothing followed it. Mirrors the logistics controller —
// see [resolveLogSeconds].
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
startAutoCreateLoginLoop(seconds: interval);
// Ensure foreground logging notification is started when going on-duty
try {
@@ -604,16 +573,43 @@ class RiderLogController extends GetxController {
}
}
/// Whether the periodic heartbeat is currently running on the stream path.
///
/// ── Why a test needs to be able to see this ──
///
/// The `logseconds` bug was invisible from outside the class. Duty went on,
/// the API accepted it, one log was posted by the immediate call — and the
/// loop that was supposed to follow it simply never started. Nothing threw,
/// nothing logged an error, and the only symptom was a console panel that had
/// been blank for so long it read as a backend problem.
///
/// Android normally runs the heartbeat as a foreground service instead, and
/// leaves this null; the test host is never Android, so the stream path is
/// the one under test. See [startAutoCreateLoginLoop].
@visibleForTesting
bool get autoLoopRunning => _autoLoginSubscription != null;
// Start periodic createRiderLog calls based on seconds (or prefs 'logseconds')
// ✅ CRITICAL: When there are active deliveries, use 30 seconds (same as delivery logs)
// Otherwise, use the configured logseconds interval
void startAutoCreateLoginLoop({int? seconds}) async {
/// `Future<void>`, not `void`. It was fire-and-forget, which meant no caller
/// could wait for the loop to actually be up — and the one place that most
/// wanted to, [setOnDuty], had no way to tell whether the heartbeat it had
/// just asked for existed. Every existing call site ignores the future and is
/// unchanged by this.
Future<void> startAutoCreateLoginLoop({int? seconds}) async {
final prefs = await SharedPreferences.getInstance();
// ✅ Check if there are active deliveries - if yes, use 30 seconds (same as delivery logs)
final bool hasActiveDeliveries =
prefs.getBool('has_live_deliveries') ?? false;
final int baseInterval = seconds ?? (prefs.getInt('logseconds') ?? 0);
// The configured cadence, or the floor under it. `?? 0` here was the
// second half of the same bug as the one in [setOnDuty]: a caller passing
// no `seconds` fell through to a stored zero and returned at the
// `interval <= 0` guard below. See [resolveLogSeconds].
final int baseInterval = resolveLogSeconds(
seconds ?? prefs.getInt('logseconds'),
);
// When there are active deliveries, post rider logs every 30 seconds (matching delivery logs)
// Otherwise, use the configured interval
@@ -625,12 +621,18 @@ class RiderLogController extends GetxController {
);
}
await stopAutoCreateLoginLoop(); // Cancel existing subscription
// Stop before start, which is what keeps this method safe to call from
// every place that does. Whichever call arrives last wins, and there is
// never a second loop: the subscription is cancelled and the foreground
// service stopped before either is created again.
await stopAutoCreateLoginLoop();
// Attempt to flush offline logs on loop start
flushOfflineLogs();
if (interval <= 0) return;
// No `interval <= 0` guard. [resolveLogSeconds] cannot return one, and the
// guard that used to be here is what turned a missing `logseconds` into a
// rider who never reported.
final int onduty = prefs.getInt('onduty') ?? 0;
if (onduty != 1) {
debugPrint('[RIDERLOG][AUTO LOOP] Not starting - onduty=$onduty');
@@ -921,10 +923,6 @@ class RiderLogController extends GetxController {
final breakdate = _formatDateTimeFull(now); // e.g. 2025-10-16 16:36:16
final breakstart = _formatTime(now); // e.g. 16:36:16
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createBreakRiderLogLive
: ApiConstants.createBreakRiderLogDev;
// Build payload with all required fields in the exact format the API expects
final payload = <String, dynamic>{
"breakid": localBreakId,
@@ -941,13 +939,12 @@ class RiderLogController extends GetxController {
};
// Debug logs for terminal visibility
debugPrint('[BREAK][CREATE] URL: $url');
debugPrint('[BREAK][CREATE] Payload: $payload');
// Persist local break id immediately to ensure update can reference it
await prefs.setInt('breakId', localBreakId);
final resp = await _breakProvider.createBreakRiderLog(url, payload);
final resp = await _breakProvider.createBreakRiderLog(payload);
debugPrint(
'[BREAK][CREATE] Response: ${resp == null ? 'null' : resp.toString()}',
@@ -1045,9 +1042,6 @@ class RiderLogController extends GetxController {
final breakhoursDouble = duration.inSeconds / 3600.0;
// Debug logs for terminal visibility
debugPrint(
'[BREAK][UPDATE] URL: ${ApiConstants.mainRoute == 'live' ? ApiConstants.updateBreakRiderLogLive : ApiConstants.updateBreakRiderLogDev}',
);
debugPrint(
'[BREAK][UPDATE] Fields: breakid=$breakid, logid=$logid, userid=$userid, partnerid=$partnerid, shiftid=$shiftid, breakend=$breakend, breakhours=$breakhoursDouble, lat=$latitude, lng=$longitude',
);
@@ -1101,10 +1095,6 @@ class RiderLogController extends GetxController {
final iso = _formatDateTimeFull(now);
final loginTime = _formatTime(now);
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
// ✅ Check if there are active deliveries to set status
final bool hasActiveDeliveries =
prefs.getBool('has_live_deliveries') ?? false;
@@ -1141,10 +1131,9 @@ class RiderLogController extends GetxController {
userfcmtoken: userfcmtoken ?? '',
).toJson();
debugPrint('[RIDERLOG][CREATE LOGIN] URL: $url');
debugPrint('[RIDERLOG][CREATE LOGIN] Payload: $payload');
final resp = await _createProvider.createRiderLog(url, payload);
final resp = await _createProvider.createRiderLog(payload);
debugPrint(
'[RIDERLOG][CREATE LOGIN] Response: ${resp == null ? 'null' : resp.toString()}',
);
@@ -1205,7 +1194,7 @@ class RiderLogController extends GetxController {
// We can use _createProvider generic call
debugPrint('[RIDERLOG][OFFLINE] Retrying: $payload');
final resp = await _createProvider.createRiderLog(url, payload);
final resp = await _createProvider.createRiderLog(payload);
if (resp != null && resp.isNotEmpty) {
debugPrint('[RIDERLOG][OFFLINE] Success!');

View File

@@ -107,7 +107,7 @@ class DeliveryApi {
/// **ABSENT** — the generic status write the ported flow uses for every
/// transition (accepted → active → arrived → delivered → skipped).
///
/// Was `queue.workolik.com/live/api/v1/deliveries/updatedelivery`. There is no
/// Was `deliveries/updatedelivery` on the retired backend. There is no
/// single status-write route on Doormile; the transitions above are separate
/// endpoints. Call sites that still post a bare status change land here and
/// will 404 until either they are split or the route is built.
@@ -117,7 +117,7 @@ class DeliveryApi {
/// **LIVE** — periodic location/telemetry log.
///
/// Was `queue.workolik.com/live/api/v2/partners/createriderlog`.
/// Was `partners/createriderlog` on the retired backend.
static String get createRiderLog => url('/miler/logs');
/// **LIVE** — read them back.
@@ -142,7 +142,7 @@ class DeliveryApi {
/// **ABSENT** — per-delivery event log.
///
/// Was `queue.workolik.com/live/api/v2/deliveries/createdeliverylog`. The
/// Was `deliveries/createdeliverylog` on the retired backend. The
/// closest real thing is consignment logs, which key on a consignmentid the
/// delivery flow does not always hold.
static String get createDeliveryLog => url('/miler/consignments/logs');

View File

@@ -1,260 +1,46 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:http/io_client.dart';
import 'package:flutter/foundation.dart';
// Combined Riderlog providers:
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken nodes.
/// ─────────────────────────────────────────────────────────────────────────
/// THE DELIVERY LINE'S RIDER LOG IS THE PARCEL LINE'S RIDER LOG
///
/// **Empty since the delivery line moved to Doormile.** The one entry here
/// pinned `queue.workolik.com` to 66.116.225.226, because some Indian carriers'
/// DNS resolved that host to a CDN node that answered 404. Nothing calls that
/// host any more — every delivery-line write goes to `api.doormile.com` via
/// [DeliveryApi] — so the pin was routing advice about a server this app no
/// longer talks to.
/// ── What this file used to be ──
///
/// It is deliberately not re-pointed at the Doormile host: that IP belongs to
/// the old backend, and pinning it for `api.doormile.com` would send every
/// request to the wrong server with the right SNI. If Doormile ever needs the
/// same workaround, add it here with its own verified address.
const _knownGoodIPs = <String, String>{};
/// A second, complete implementation of duty, breaks and the location
/// heartbeat, posting to the two retired pre-v1 hosts over raw `http`. It survived the migration only because
/// nothing in `lib/` imported it: the delivery shell reaches it through
/// [riderShell], which is production for any tenant on the delivery line.
///
/// It carried two things that had already been deleted from the parcel line:
///
/// • **`_buildSslBypassClient()`** — an `IOClient` whose
/// `badCertificateCallback` returned `true` for *every* certificate, with a
/// hand-rolled `SecureSocket` upgrade and a hardcoded IP pin. Every request
/// it carried — a rider's GPS track, his duty state, his break times — could
/// be read and rewritten by anything on the path. The parcel line removed
/// this; the delivery line kept it.
/// • **The legacy hosts themselves**, which the v1 migration left behind.
///
/// ── Why this is an export and not a rewrite ──
///
/// `lib/providers/Riderlog/riderlog_provider.dart` already does exactly this
/// job against `api.doormile.com`: same four class names, same legacy
/// `{status, details}` envelopes, mapped onto `POST /miler/duty/start`,
/// `PUT /miler/location`, `POST /miler/logs` and the break endpoints. The
/// delivery line's controller calls the same four classes by the same names.
///
/// So there is nothing to port. Re-exporting means one implementation, one duty
/// gate, one telemetry payload and one place where a contract change lands —
/// which is the whole point of having migrated the parcel line at all. The two
/// lines cannot drift, because there is only one of them.
///
/// The `String urldata` first parameter is gone from every method; callers that
/// were building a legacy URL to pass in have had that construction deleted
/// rather than made to compile.
/// ─────────────────────────────────────────────────────────────────────────
library;
/// Creates an IOClient that:
/// 1. Bypasses SSL certificate errors
/// 2. Forces known-good IPs for hosts where carrier DNS returns broken CDN nodes
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
IOClient _buildSslBypassClient() {
final httpClient = HttpClient()
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
true;
httpClient
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
final host = uri.host;
final port = uri.port;
// Use known-good IP if available, else resolve normally (prefer IPv4)
InternetAddress? target;
final knownIP = _knownGoodIPs[host];
if (knownIP != null) {
target = InternetAddress(knownIP);
debugPrint('[SSL_CLIENT] Using known-good IP: $knownIP for $host');
} else {
try {
final addresses = await InternetAddress.lookup(
host,
type: InternetAddressType.IPv4,
);
if (addresses.isNotEmpty) target = addresses.first;
} catch (_) {}
}
if (uri.scheme == 'https') {
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
.then(
(plain) => SecureSocket.secure(
plain,
host: host, // SNI = original hostname for Nginx routing
onBadCertificate: (_) => true,
supportedProtocols: ['http/1.1'],
),
)
.then((s) => s as Socket);
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
}
return Socket.startConnect(target ?? InternetAddress(host), port);
};
return IOClient(httpClient);
}
class CreateRiderLogProvider {
Future<Map<String, dynamic>?> createRiderLog(
String urldata,
Map<String, dynamic> data,
) async {
const maxAttempts = 3;
try {
debugPrint('createRiderLog payload ${json.encode(data)}');
} catch (_) {}
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
final client = _buildSslBypassClient();
try {
final url = Uri.parse(urldata);
final response = await client.post(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('createRiderLog url $urldata (attempt $attempt)');
debugPrint('createRiderLog status ${response.statusCode}');
debugPrint('createRiderLog response ${response.body}');
if (response.statusCode >= 200 && response.statusCode < 300) {
return json.decode(response.body.toString()) as Map<String, dynamic>;
} else {
debugPrint(
'createRiderLog failed: HTTP ${response.statusCode} (attempt $attempt/$maxAttempts)',
);
// On 404/5xx, wait and retry to potentially hit a different CDN node
if (attempt < maxAttempts) {
await Future.delayed(const Duration(seconds: 1));
}
}
} catch (e) {
debugPrint('createRiderLog exception (attempt $attempt): $e');
if (attempt < maxAttempts) {
await Future.delayed(const Duration(seconds: 1));
}
} finally {
client.close();
}
}
debugPrint('createRiderLog failed after $maxAttempts attempts');
return null;
}
}
class UpdateRiderLogProvider {
Future<Map<String, dynamic>?> updateRiderLog(
String urldata,
Map<String, dynamic> data,
) async {
try {
final url = Uri.parse(urldata);
final response = await put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('updateRiderLog url: $urldata');
debugPrint('updateRiderLog response: ${response.body}');
if (response.statusCode >= 200 && response.statusCode < 300) {
final decoded = json.decode(response.body);
if (decoded is Map<String, dynamic>) {
return decoded;
} else {
debugPrint(
'⚠️ updateRiderLog: Expected Map but got ${decoded.runtimeType}',
);
return {};
}
} else {
debugPrint('❌ updateRiderLog failed with code ${response.statusCode}');
return {};
}
} catch (e) {
debugPrint('❌ Exception in updateRiderLog: $e');
return {};
}
}
}
class GetRiderLogProvider {
Future<Map<String, dynamic>?> getRiderLog(String urldata) async {
Map<String, dynamic>? getRiderLogResponse;
try {
final url = Uri.parse(urldata);
final response = await get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('getRiderLog response ${response.body}');
debugPrint('getRiderLog url ${urldata.toString()}');
getRiderLogResponse =
json.decode(response.body.toString()) as Map<String, dynamic>;
} catch (e) {
debugPrint(e.toString());
}
return getRiderLogResponse;
}
Future<Map<String, dynamic>?> getRiderCount(String urldata) async {
Map<String, dynamic>? getRiderCountResponse;
try {
final url = Uri.parse(urldata);
final response = await get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('getRiderCount response ${response.body}');
debugPrint('getRiderCount url ${urldata.toString()}');
getRiderCountResponse =
json.decode(response.body.toString()) as Map<String, dynamic>;
} catch (e) {
debugPrint(e.toString());
}
return getRiderCountResponse;
}
}
class BreakRiderLogProvider {
Future<Map<String, dynamic>?> createBreakRiderLog(
String urldata,
Map<String, dynamic> data,
) async {
Map<String, dynamic>? breakLogResponse;
try {
final url = Uri.parse(urldata);
final response = await post(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('createBreakRiderLog url $urldata');
debugPrint('createBreakRiderLog response ${response.body}');
breakLogResponse =
json.decode(response.body.toString()) as Map<String, dynamic>;
} catch (e) {
debugPrint(e.toString());
}
return breakLogResponse;
}
Future<Map<String, dynamic>?> updateBreakRiderLog(
String urldata,
Map<String, dynamic> data,
) async {
Map<String, dynamic>? breakLogResponse;
try {
final url = Uri.parse(urldata);
final response = await put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('updateBreakRiderLog url $urldata');
debugPrint('updateBreakRiderLog response ${response.body}');
breakLogResponse =
json.decode(response.body.toString()) as Map<String, dynamic>;
} catch (e) {
debugPrint(e.toString());
}
return breakLogResponse;
}
}
export 'package:miler/providers/Riderlog/riderlog_provider.dart'
show
BreakRiderLogProvider,
CreateRiderLogProvider,
GetRiderLogProvider,
UpdateRiderLogProvider,
unawaited;

View File

@@ -1,67 +1,25 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:http/io_client.dart';
import 'package:http/http.dart' as http;
import 'package:miler/data/api_config.dart';
import 'package:flutter/foundation.dart';
// Combined Deliverylog providers:
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken CDN nodes.
/// Confirmed: 66.116.225.226 = 200 OK, 125.21.240.67 = 404/405.
/// Empty since the delivery line moved to Doormile — see the note on the
/// matching map in `providers/Riderlog/riderlog_provider.dart`. The old entry
/// pinned `queue.workolik.com`, a host nothing calls now, and must not be
/// re-pointed at `api.doormile.com`.
const _knownGoodIPs = <String, String>{};
/// Creates an IOClient that:
/// 1. Bypasses SSL certificate errors
/// 2. Forces known-good IPs to avoid broken CDN nodes from carrier DNS
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
IOClient _buildSslBypassClient() {
final httpClient = HttpClient()
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
true;
httpClient
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
final host = uri.host;
final port = uri.port;
InternetAddress? target;
final knownIP = _knownGoodIPs[host];
if (knownIP != null) {
target = InternetAddress(knownIP);
} else {
try {
final addresses = await InternetAddress.lookup(
host,
type: InternetAddressType.IPv4,
);
if (addresses.isNotEmpty) target = addresses.first;
} catch (_) {}
}
if (uri.scheme == 'https') {
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
.then(
(plain) => SecureSocket.secure(
plain,
host: host,
onBadCertificate: (_) => true,
supportedProtocols: ['http/1.1'],
),
)
.then((s) => s as Socket);
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
}
return Socket.startConnect(target ?? InternetAddress(host), port);
};
return IOClient(httpClient);
}
/// ── The SSL bypass is gone ──
///
/// This file built its own `IOClient` with `onBadCertificate: (_) => true` and
/// a hand-rolled `SecureSocket` upgrade, so **every certificate was accepted**
/// — a delivery rider's position, his order ids and his session were readable
/// and rewritable by anything on the path. The parcel line deleted the same
/// code (see `providers/Riderlog/riderlog_provider.dart`); the delivery line
/// kept it because nothing in `lib/` imported this file directly.
///
/// It also sent **no `Authorization` header**. The URLs it posts to are v1
/// routes on `api.doormile.com` — resolved through the [DeliveryApi] facade —
/// so every one of these calls could only ever have been answered with a 401.
/// A plain client plus [ApiConfig.authHeaders] fixes both at once.
Future<Map<String, String>> _headers() => ApiConfig.authHeaders();
class CreateDeliveryLogProvider {
Future<Map<String, dynamic>?> createDeliveryLog(
@@ -70,19 +28,12 @@ class CreateDeliveryLogProvider {
bool wrapInArray = true,
}) async {
Map<String, dynamic>? result;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final body = json.encode(wrapInArray ? [data] : data);
final response = await client
.post(
url,
body: body,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.post(url, body: body, headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('createDeliveryLog url $urldata');
debugPrint(body);
@@ -110,18 +61,11 @@ class UpdateDeliveryProvider {
String urldata,
) async {
Map<String, dynamic>? updateDeliveryResponse;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client
.put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.put(url, body: json.encode(data), headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('updateDelivery url $urldata');
debugPrint('updateDelivery status ${response.statusCode}');
@@ -148,18 +92,11 @@ class UpdateDeliveryProvider {
String urldata,
) async {
Map<String, dynamic>? updateDeliveryResponse;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client
.put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.put(url, body: json.encode(data), headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('updateArrived url $urldata');
debugPrint('updateArrived status ${response.statusCode}');
@@ -185,18 +122,11 @@ class UpdateDeliveryProvider {
String urldata,
) async {
Map<String, dynamic>? updateDeliveryResponse;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client
.put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.put(url, body: json.encode(data), headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('updatePicked url $urldata');
debugPrint('updatePicked status ${response.statusCode}');
@@ -222,18 +152,11 @@ class UpdateDeliveryProvider {
String urldata,
) async {
Map<String, dynamic>? updateDeliveryResponse;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client
.put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.put(url, body: json.encode(data), headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('updateActive url $urldata');
debugPrint('updateActive status ${response.statusCode}');
@@ -258,7 +181,7 @@ class UpdateDeliveryProvider {
class GetDeliveryLogProvider {
Future<Map<String, dynamic>?> getDeliveryLog(String urldata) async {
Map<String, dynamic>? result;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client.get(

View File

@@ -34,6 +34,7 @@ import 'package:miler/xpress/views/Dashboard/deliveries/deliveries.dart'
as deliveries;
import 'package:miler/xpress/background/live_tracking_service.dart';
import 'package:miler/xpress/data/delivery_api.dart';
import 'package:miler/data/heartbeat.dart';
/// Helper function to launch phone dialer - works in both debug and release builds
/// In release builds, canLaunchUrl may fail due to R8/ProGuard, so we always try to launch
@@ -320,11 +321,13 @@ class _HomepageState extends State<Homepage>
Future<Duration> _getLogInterval() async {
try {
final prefs = await SharedPreferences.getInstance();
final secs = prefs.getInt('logseconds');
final int interval = (secs != null && secs > 0) ? secs : 30;
// Already correct — routed through the shared resolver so the 30 lives in
// one place. See [kDefaultLogSeconds] for what a zero here used to cost
// the rider-log loop, which read the same pref and did not guard it.
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
return Duration(seconds: interval);
} catch (_) {
return const Duration(seconds: 30);
return const Duration(seconds: kDefaultLogSeconds);
}
}