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 _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?> _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?> _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?> _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']), 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?> createRiderLog( Map 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?> updateRiderLog( Map 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?> 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?> getRiderCount() async { ApiConfig.logGap('getRiderCount', 'No count endpoint in v1.'); return {'status': true, 'count': 0}; } } class BreakRiderLogProvider { Future?> createBreakRiderLog( Map 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?> 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 future) { future.catchError((Object e) { debugPrint('[RIDERLOG] background call failed: $e'); }); }