Files
doormile_milderapp/lib/providers/Riderlog/riderlog_provider.dart
2026-08-28 15:07:30 +05:30

242 lines
9.0 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/miler_api.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// DUTY, BREAKS AND THE LOCATION HEARTBEAT
///
/// ── What was deleted here ──
///
/// A `_buildSslBypassClient()` that returned an `IOClient` whose
/// `badCertificateCallback` returned `true` for every certificate, pinned a
/// hardcoded IP for `queue.workolik.com`, and hand-rolled its own
/// `SecureSocket` upgrade. It existed to work around a carrier's broken DNS on
/// a backend that is no longer deployed — and it meant every request it carried
/// could be read and rewritten by anything on the path, including a rider's GPS
/// track and his duty state.
///
/// It is gone along with the rest of the legacy path. `api.doormile.com` is
/// reached over ordinary, verified TLS.
///
/// ── The class names survive their implementations ──
///
/// `RiderLogController` drives duty through create/update/get "rider log"
/// calls, which is the legacy shape. Rather than rewrite that controller, these
/// four classes keep their names and map the intent onto the real endpoints:
/// the first log of a session starts duty, later ones are location heartbeats,
/// and an off-duty payload ends it.
/// ─────────────────────────────────────────────────────────────────────────
String _s(dynamic v) => v == null ? '' : v.toString();
double? _d(dynamic v) => double.tryParse(_s(v));
/// Legacy `{status, details}` for the duty-state readers.
Map<String, dynamic> _dutyEnvelope(Map data) {
final bool on = data['onduty'] == true;
final dutylogid = data['dutylogid'] ?? 0;
return {
'status': true,
'code': 200,
'details': {
'logid': dutylogid,
'onduty': on ? 1 : 0,
'login': data['loginat'],
},
};
}
Future<Map<String, dynamic>?> _startDuty(Map data) async {
final res = await MilerApi.startDuty(
lat: _d(data['latitude']),
lon: _d(data['longitude']),
);
if (res.ok) {
final id = int.tryParse('${res.map['dutylogid'] ?? 0}') ?? 0;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('logid', id);
await prefs.setInt('logId', id);
await prefs.setInt('dutylogid', id);
return {
'status': true,
'code': 200,
'details': {'logid': id, 'login': res.map['loginat'], 'onduty': 1},
};
}
// "Already on duty" is a 400 and is not a failure — it is the app and the
// server disagreeing about state, which the current duty answers.
if (res.status == 400) return _currentDuty();
return {'status': false, 'code': res.status};
}
Future<Map<String, dynamic>?> _currentDuty() async {
final res = await MilerApi.dutyCurrent();
if (!res.ok) return {'status': false, 'code': res.status};
final on = res.map['onduty'] == true;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('onduty', on ? 1 : 0);
if (on) {
final id = int.tryParse('${res.map['dutylogid'] ?? 0}') ?? 0;
await prefs.setInt('logid', id);
await prefs.setInt('logId', id);
}
return _dutyEnvelope(res.map);
}
/// The GPS heartbeat, which is two writes with different lifetimes:
///
/// • `PUT /miler/location` — Redis, and the index dispatch searches. This is
/// the one that decides whether the rider is findable.
/// • `POST /miler/logs` — the telemetry trail, for reconstructing a shift
/// afterwards.
///
/// Both are best-effort and neither blocks the rider. The location write is
/// reported back because duty state depends on it; the log is fire-and-forget.
Future<Map<String, dynamic>?> _heartbeat(Map data) async {
final lat = _d(data['latitude']);
final lon = _d(data['longitude']);
if (lat == null || lon == null) return ApiConfig.okEnvelope('no fix');
final res = await MilerApi.pushLocation(
latitude: lat,
longitude: lon,
pincode: _s(data['pincode']).isEmpty ? null : _s(data['pincode']),
speed: _d(data['speed']),
heading: _d(data['heading']),
);
// Deliberately not awaited into the result: a telemetry failure must never
// look like a location failure, because the caller treats the latter as duty
// state going wrong.
unawaited(
MilerApi.postLog(
latitude: lat,
longitude: lon,
speed: _d(data['speed']),
heading: _d(data['heading']),
accuracy: _d(data['accuracy']),
status: _s(data['status']).isEmpty ? null : _s(data['status']),
orderId: data['orderid'],
battery: int.tryParse(_s(data['battery'])),
isCharging: data['is_charging'] == true,
connection: _s(data['connection']).isEmpty
? null
: _s(data['connection']),
// ── This was being dropped on the floor ──
//
// Every other device field on the heartbeat was forwarded and this one
// was not, so `location_service` could never reach `/miler/logs` however
// faithfully the caller supplied it — and the console's Location Service
// column had no field to read. It is the single most useful thing on
// that panel: a rider who has turned location off explains his own
// silence, and nothing else on the row can.
locationService: _s(data['location_service']).isEmpty
? null
: _s(data['location_service']),
isBackground: data['is_background'] == true,
),
);
return res.ok
? ApiConfig.okEnvelope()
: {'status': false, 'code': res.status};
}
/// True when the payload means "go off duty".
bool _isOffDutyIntent(Map data) {
final status = _s(data['status']).toLowerCase();
final onduty = data['onduty'];
final bool ondutyZero = onduty == 0 || _s(onduty) == '0';
return status.contains('offline') ||
status.contains('logout') ||
data.containsKey('logout') ||
(data.containsKey('onduty') && ondutyZero);
}
class CreateRiderLogProvider {
/// The first call of a session starts duty; every later one is a heartbeat.
Future<Map<String, dynamic>?> createRiderLog(
Map<String, dynamic> data,
) async {
final prefs = await SharedPreferences.getInstance();
final int existing = prefs.getInt('dutylogid') ?? 0;
if (existing <= 0) return _startDuty(data);
return _heartbeat(data);
}
}
class UpdateRiderLogProvider {
Future<Map<String, dynamic>?> updateRiderLog(
Map<String, dynamic> data,
) async {
if (_isOffDutyIntent(data)) {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('dutylogid', 0);
final res = await MilerApi.endDuty();
if (res.ok) await prefs.setInt('onduty', 0);
return res.ok
? ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true})
: {'status': false, 'code': res.status};
}
final onduty = data['onduty'];
if (onduty == 1 || _s(onduty) == '1') {
final prefs = await SharedPreferences.getInstance();
if ((prefs.getInt('dutylogid') ?? 0) <= 0) return _startDuty(data);
}
return _heartbeat(data);
}
}
class GetRiderLogProvider {
Future<Map<String, dynamic>?> getRiderLog() => _currentDuty();
/// There is no rider-count endpoint in the contract, and no screen that needs
/// one — the dashboard figures come from bookings and earnings. Kept as a
/// zero so the one legacy caller does not have to branch.
Future<Map<String, dynamic>?> getRiderCount() async {
ApiConfig.logGap('getRiderCount', 'No count endpoint in v1.');
return {'status': true, 'count': 0};
}
}
class BreakRiderLogProvider {
Future<Map<String, dynamic>?> createBreakRiderLog(
Map<String, dynamic> data,
) async {
final bt = _s(data['breaktype'] ?? data['reason'] ?? 'Personal');
final res = await MilerApi.startBreak(bt);
if (res.ok) {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(
'breaklogid',
int.tryParse('${res.map['breaklogid'] ?? 0}') ?? 0,
);
// The availability enum's break value is `Break`, not `On_Break` — the
// obvious guess is the wrong one and is silently rejected.
unawaited(MilerApi.setAvailability('Break'));
return ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true});
}
return {'status': false, 'code': res.status};
}
Future<Map<String, dynamic>?> updateBreakRiderLog() async {
final res = await MilerApi.endBreak();
if (res.ok) unawaited(MilerApi.setAvailability('Available'));
return res.ok
? ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true})
: {'status': false, 'code': res.status};
}
}
/// Local `unawaited`, so a fire-and-forget call reads as deliberate rather than
/// as a missing `await`.
void unawaited(Future<void> future) {
future.catchError((Object e) {
debugPrint('[RIDERLOG] background call failed: $e');
});
}