@fleetbase/fleetops-engine 0.6.53 → 0.6.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/addon/components/admin/routing-settings.hbs +2 -1
  2. package/addon/components/cell/attached-vehicle.hbs +10 -0
  3. package/addon/components/cell/attached-vehicle.js +58 -0
  4. package/addon/components/cell/device-identity.hbs +24 -0
  5. package/addon/components/cell/device-identity.js +129 -0
  6. package/addon/components/cell/driver-identity.hbs +33 -0
  7. package/addon/components/cell/driver-identity.js +143 -0
  8. package/addon/components/cell/equipment-identity.hbs +5 -0
  9. package/addon/components/cell/equipment-identity.js +48 -0
  10. package/addon/components/cell/part-identity.hbs +5 -0
  11. package/addon/components/cell/part-identity.js +63 -0
  12. package/addon/components/cell/telematic-device.hbs +38 -0
  13. package/addon/components/cell/telematic-device.js +44 -0
  14. package/addon/components/cell/telematic-provider.hbs +33 -16
  15. package/addon/components/cell/telematic-provider.js +50 -8
  16. package/addon/components/cell/vehicle-identity.hbs +33 -0
  17. package/addon/components/cell/vehicle-identity.js +138 -0
  18. package/addon/components/device/details.hbs +106 -135
  19. package/addon/components/device/details.js +174 -1
  20. package/addon/components/device/form.hbs +2 -2
  21. package/addon/components/device/form.js +7 -0
  22. package/addon/components/device/panel-header.hbs +46 -21
  23. package/addon/components/device/panel-header.js +49 -1
  24. package/addon/components/device/panel-tabs/events.hbs +30 -0
  25. package/addon/components/device/panel-tabs/events.js +128 -0
  26. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  27. package/addon/components/device/panel-tabs/sensors.js +93 -0
  28. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  29. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  30. package/addon/components/equipment/form.js +8 -1
  31. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +76 -11
  32. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  33. package/addon/components/layout/fleet-ops-sidebar.js +101 -17
  34. package/addon/components/map/drawer/device-event-listing.js +2 -0
  35. package/addon/components/map/drawer/driver-listing.js +2 -0
  36. package/addon/components/map/drawer/place-listing.js +2 -0
  37. package/addon/components/map/drawer/position-listing.hbs +11 -6
  38. package/addon/components/map/drawer/position-listing.js +17 -4
  39. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  40. package/addon/components/order/form/details.hbs +1 -43
  41. package/addon/components/order/form/details.js +18 -53
  42. package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
  43. package/addon/components/order/form/orchestrator-constraints.js +61 -0
  44. package/addon/components/order/form/payload.js +48 -2
  45. package/addon/components/order/form/route.js +15 -0
  46. package/addon/components/order/form/service-rate.hbs +62 -11
  47. package/addon/components/order/form/service-rate.js +124 -2
  48. package/addon/components/order/form.hbs +2 -0
  49. package/addon/components/sensor/panel-header.hbs +47 -15
  50. package/addon/components/sensor/panel-header.js +59 -1
  51. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  52. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  53. package/addon/components/table/cell/dropdown.hbs +28 -0
  54. package/addon/components/table/cell/dropdown.js +91 -0
  55. package/addon/components/telematic/details.hbs +7 -1
  56. package/addon/components/telematic/form.js +16 -8
  57. package/addon/components/vendor/panel-header.hbs +82 -0
  58. package/addon/components/vendor/panel-header.js +48 -0
  59. package/addon/components/work-order/details.hbs +5 -1
  60. package/addon/components/work-order/form.hbs +26 -4
  61. package/addon/components/work-order/form.js +5 -1
  62. package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
  63. package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
  64. package/addon/controllers/connectivity/devices/index/details/vehicle.js +122 -0
  65. package/addon/controllers/connectivity/devices/index/details.js +12 -0
  66. package/addon/controllers/connectivity/devices/index.js +144 -129
  67. package/addon/controllers/connectivity/events/details.js +3 -0
  68. package/addon/controllers/connectivity/events/index.js +187 -11
  69. package/addon/controllers/connectivity/sensors/index.js +156 -14
  70. package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
  71. package/addon/controllers/connectivity/telematics/details/devices.js +170 -129
  72. package/addon/controllers/connectivity/telematics/details/events.js +39 -11
  73. package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
  74. package/addon/controllers/maintenance/equipment/index.js +1 -2
  75. package/addon/controllers/maintenance/parts/index.js +1 -2
  76. package/addon/controllers/maintenance/work-orders/index.js +18 -1
  77. package/addon/controllers/management/drivers/index.js +82 -42
  78. package/addon/controllers/management/fuel-reports/index.js +63 -4
  79. package/addon/controllers/management/issues/index.js +63 -4
  80. package/addon/controllers/management/vehicles/index.js +33 -6
  81. package/addon/routes/connectivity/devices/index/details/events.js +37 -1
  82. package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
  83. package/addon/routes/connectivity/devices/index/details/vehicle.js +29 -0
  84. package/addon/routes/connectivity/devices/index.js +7 -0
  85. package/addon/routes/connectivity/events/details.js +3 -0
  86. package/addon/routes/connectivity/events/index.js +6 -2
  87. package/addon/routes/connectivity/sensors/index.js +7 -2
  88. package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
  89. package/addon/routes/connectivity/telematics/details/devices.js +6 -0
  90. package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
  91. package/addon/routes/connectivity/telematics/details.js +1 -1
  92. package/addon/routes/connectivity/telematics/edit.js +1 -1
  93. package/addon/routes/maintenance/work-orders/index.js +1 -0
  94. package/addon/routes.js +2 -0
  95. package/addon/services/device-actions.js +30 -0
  96. package/addon/services/device-event-actions.js +1 -1
  97. package/addon/services/equipment-actions.js +1 -0
  98. package/addon/services/order-creation.js +54 -1
  99. package/addon/services/order-validation.js +3 -2
  100. package/addon/services/sensor-actions.js +3 -0
  101. package/addon/services/service-rate-actions.js +40 -1
  102. package/addon/services/vehicle-actions.js +24 -12
  103. package/addon/services/vendor-actions.js +3 -0
  104. package/addon/styles/fleetops-engine.css +28 -4
  105. package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
  106. package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
  107. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +147 -0
  108. package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
  109. package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
  110. package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
  111. package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
  112. package/addon/templates/management/vehicles/index/edit.hbs +1 -1
  113. package/addon/templates/management/vendors/index/details.hbs +2 -2
  114. package/addon/templates/settings/notifications.hbs +2 -1
  115. package/addon/templates/settings/routing.hbs +1 -0
  116. package/addon/utils/device-table-columns.js +288 -0
  117. package/addon/utils/fleet-ops-options.js +38 -0
  118. package/addon/utils/identity-cell-resource.js +20 -0
  119. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  120. package/addon/utils/trackable-option.js +93 -0
  121. package/app/components/cell/attached-vehicle.js +1 -0
  122. package/app/components/cell/device-identity.js +1 -0
  123. package/app/components/cell/driver-identity.js +1 -0
  124. package/app/components/cell/equipment-identity.js +1 -0
  125. package/app/components/cell/part-identity.js +1 -0
  126. package/app/components/cell/telematic-device.js +1 -0
  127. package/app/components/cell/vehicle-identity.js +1 -0
  128. package/app/components/device/panel-tabs/events.hbs +30 -0
  129. package/app/components/device/panel-tabs/events.js +128 -0
  130. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  131. package/app/components/device/panel-tabs/sensors.js +93 -0
  132. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  133. package/app/components/device/panel-tabs/vehicle.js +90 -0
  134. package/app/components/order/form/orchestrator-constraints.js +1 -0
  135. package/app/components/table/cell/dropdown/action-item.js +1 -0
  136. package/app/components/table/cell/dropdown.js +1 -0
  137. package/app/components/vendor/panel-header.js +1 -0
  138. package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
  139. package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
  140. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  141. package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
  142. package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
  143. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  144. package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
  145. package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
  146. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  147. package/app/utils/map-drawer-dropdown-position.js +1 -0
  148. package/app/utils/trackable-option.js +1 -0
  149. package/composer.json +1 -1
  150. package/extension.json +1 -1
  151. package/package.json +4 -4
  152. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
  153. package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
  154. package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
  155. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
  156. package/server/src/Console/Commands/SyncTelematics.php +89 -0
  157. package/server/src/Exceptions/TelematicProviderException.php +13 -2
  158. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
  159. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +55 -1
  160. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +17 -6
  161. package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
  162. package/server/src/Http/Controllers/Internal/v1/LiveController.php +178 -1
  163. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
  164. package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
  165. package/server/src/Http/Controllers/Internal/v1/SearchController.php +2 -2
  166. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
  167. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  168. package/server/src/Http/Filter/DeviceEventFilter.php +65 -0
  169. package/server/src/Http/Filter/DeviceFilter.php +80 -0
  170. package/server/src/Http/Filter/SensorFilter.php +125 -0
  171. package/server/src/Http/Filter/VehicleFilter.php +13 -0
  172. package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
  173. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
  174. package/server/src/Http/Resources/v1/Driver.php +11 -2
  175. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  176. package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
  177. package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
  178. package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
  179. package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
  180. package/server/src/Jobs/SyncTelematicDevicesJob.php +180 -123
  181. package/server/src/Models/Device.php +17 -1
  182. package/server/src/Models/DeviceEvent.php +82 -1
  183. package/server/src/Models/Driver.php +16 -0
  184. package/server/src/Models/Equipment.php +55 -0
  185. package/server/src/Models/Sensor.php +15 -2
  186. package/server/src/Models/Telematic.php +1 -1
  187. package/server/src/Models/Vehicle.php +5 -1
  188. package/server/src/Models/WorkOrder.php +5 -2
  189. package/server/src/Observers/DriverObserver.php +3 -3
  190. package/server/src/Observers/FleetObserver.php +23 -0
  191. package/server/src/Observers/VehicleObserver.php +3 -3
  192. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  193. package/server/src/Support/LiveCacheService.php +1 -1
  194. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +143 -20
  195. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  196. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  197. package/server/src/Support/Telematics/Providers/SafeeProvider.php +70 -5
  198. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  199. package/server/src/Support/Telematics/TelematicService.php +330 -30
  200. package/server/src/routes.php +1 -1
  201. package/server/tests/DeviceFilterTest.php +45 -0
  202. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  203. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  204. package/server/tests/OperationsMonitorEndpointTest.php +38 -0
  205. package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
  206. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  207. package/server/tests/TelematicsHardeningTest.php +598 -6
  208. package/server/tests/VehicleDriverAssignmentTest.php +27 -0
  209. package/translations/en-us.yaml +1 -0
  210. package/addon/controllers/connectivity/events/index/details.js +0 -3
  211. package/addon/routes/connectivity/events/index/details.js +0 -3
  212. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -2,6 +2,8 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
4
 
5
+ use Illuminate\Support\Carbon;
6
+
5
7
  /**
6
8
  * Class FlespiProvider.
7
9
  *
@@ -77,34 +79,71 @@ class FlespiProvider extends AbstractProvider
77
79
 
78
80
  public function normalizeDevice(array $payload): array
79
81
  {
82
+ $telemetry = $payload['telemetry'] ?? $payload;
83
+ $externalId = $payload['id'] ?? $payload['device.id'] ?? null;
84
+ $occurredAt = $this->parseTimestamp($telemetry['timestamp'] ?? $payload['last_active'] ?? null);
85
+
80
86
  return [
81
- 'external_id' => $payload['id'],
82
- 'device_name' => $payload['name'] ?? 'Unknown Device',
83
- 'device_provider' => 'flespi',
84
- 'device_model' => $payload['device_type_id'] ?? null,
85
- 'imei' => $payload['configuration']['ident'] ?? null,
86
- 'phone' => $payload['configuration']['phone'] ?? null,
87
- 'status' => isset($payload['telemetry']) ? 'active' : 'inactive',
88
- 'location' => [
89
- 'lat' => $payload['telemetry']['position.latitude'] ?? null,
90
- 'lng' => $payload['telemetry']['position.longitude'] ?? null,
87
+ 'device_id' => $externalId,
88
+ 'external_id' => $externalId,
89
+ 'name' => $payload['name'] ?? $telemetry['device.name'] ?? 'Unknown Device',
90
+ 'provider' => 'flespi',
91
+ 'model' => $payload['device_type_id'] ?? $payload['device_type_name'] ?? null,
92
+ 'imei' => $payload['configuration']['ident'] ?? $payload['ident'] ?? $telemetry['ident'] ?? null,
93
+ 'serial_number' => $payload['configuration']['serial'] ?? $payload['serial'] ?? null,
94
+ 'phone' => $payload['configuration']['phone'] ?? null,
95
+ 'status' => isset($payload['telemetry']) ? 'active' : 'inactive',
96
+ 'online' => $this->resolveOnline($telemetry),
97
+ 'last_seen_at' => $occurredAt,
98
+ 'location' => [
99
+ 'lat' => $telemetry['position.latitude'] ?? null,
100
+ 'lng' => $telemetry['position.longitude'] ?? null,
101
+ ],
102
+ 'speed' => $telemetry['position.speed'] ?? $telemetry['vehicle.speed'] ?? null,
103
+ 'heading' => $telemetry['position.direction'] ?? $telemetry['position.heading'] ?? null,
104
+ 'altitude' => $telemetry['position.altitude'] ?? null,
105
+ 'odometer' => $telemetry['vehicle.mileage'] ?? $telemetry['vehicle.odometer'] ?? null,
106
+ 'ignition' => $this->extractIgnition($telemetry),
107
+ 'fuel_level' => $telemetry['fuel.level'] ?? $telemetry['can.fuel.level'] ?? null,
108
+ 'meta' => [
109
+ 'raw' => $payload,
110
+ 'provider_status' => array_filter([
111
+ 'status' => $payload['status'] ?? $telemetry['status'] ?? null,
112
+ 'online' => $telemetry['online'] ?? null,
113
+ 'connected' => $telemetry['device.connected'] ?? $telemetry['connected'] ?? null,
114
+ ], fn ($value) => $value !== null),
91
115
  ],
92
- 'meta' => $payload,
93
116
  ];
94
117
  }
95
118
 
96
119
  public function normalizeEvent(array $payload): array
97
120
  {
121
+ $deviceId = $payload['device.id'] ?? $payload['id'] ?? null;
122
+
98
123
  return [
99
- 'external_id' => $payload['id'] ?? null,
100
- 'device_id' => $payload['device.id'] ?? null,
124
+ 'external_id' => $payload['id'] ?? $deviceId,
125
+ 'device_id' => $deviceId,
101
126
  'event_type' => $payload['event.enum'] ?? 'telemetry_update',
102
- 'occurred_at' => isset($payload['timestamp']) ? date('Y-m-d H:i:s', $payload['timestamp']) : now(),
127
+ 'occurred_at' => $this->parseTimestamp($payload['timestamp'] ?? null) ?? now(),
128
+ 'online' => $this->resolveOnline($payload),
103
129
  'location' => [
104
130
  'lat' => $payload['position.latitude'] ?? null,
105
131
  'lng' => $payload['position.longitude'] ?? null,
106
132
  ],
107
- 'meta' => $payload,
133
+ 'speed' => $payload['position.speed'] ?? $payload['vehicle.speed'] ?? null,
134
+ 'heading' => $payload['position.direction'] ?? $payload['position.heading'] ?? null,
135
+ 'altitude' => $payload['position.altitude'] ?? null,
136
+ 'odometer' => $payload['vehicle.mileage'] ?? $payload['vehicle.odometer'] ?? null,
137
+ 'ignition' => $this->extractIgnition($payload),
138
+ 'fuel_level' => $payload['fuel.level'] ?? $payload['can.fuel.level'] ?? null,
139
+ 'meta' => [
140
+ 'raw' => $payload,
141
+ 'provider_status' => array_filter([
142
+ 'status' => $payload['status'] ?? null,
143
+ 'online' => $payload['online'] ?? null,
144
+ 'connected' => $payload['device.connected'] ?? $payload['connected'] ?? null,
145
+ ], fn ($value) => $value !== null),
146
+ ],
108
147
  ];
109
148
  }
110
149
 
@@ -180,4 +219,39 @@ class FlespiProvider extends AbstractProvider
180
219
  {
181
220
  return true;
182
221
  }
222
+
223
+ protected function parseTimestamp($value): ?string
224
+ {
225
+ if (!$value) {
226
+ return null;
227
+ }
228
+
229
+ if (is_numeric($value)) {
230
+ return Carbon::createFromTimestamp((float) $value)->toDateTimeString();
231
+ }
232
+
233
+ return Carbon::parse($value)->toDateTimeString();
234
+ }
235
+
236
+ protected function resolveOnline(array $payload): ?bool
237
+ {
238
+ $value = $payload['online'] ?? $payload['device.connected'] ?? $payload['connected'] ?? null;
239
+
240
+ if ($value === null) {
241
+ return isset($payload['timestamp']);
242
+ }
243
+
244
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $value;
245
+ }
246
+
247
+ protected function extractIgnition(array $payload): ?bool
248
+ {
249
+ $value = $payload['engine.ignition.status'] ?? $payload['ignition.status'] ?? null;
250
+
251
+ if ($value === null) {
252
+ return null;
253
+ }
254
+
255
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $value;
256
+ }
183
257
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
4
 
5
+ use Illuminate\Support\Carbon;
5
6
  use Illuminate\Support\Facades\Http;
6
7
 
7
8
  /**
@@ -75,19 +76,21 @@ class GeotabProvider extends AbstractProvider
75
76
  {
76
77
  $limit = $options['limit'] ?? 100;
77
78
 
78
- $response = Http::post($this->baseUrl, [
79
- 'method' => 'Get',
80
- 'params' => [
81
- 'credentials' => [
82
- 'database' => $this->credentials['database'],
83
- 'sessionId' => $this->sessionId,
84
- ],
85
- 'typeName' => 'Device',
86
- 'resultsLimit' => $limit,
87
- ],
88
- ])->json();
79
+ $response = $this->apiCall('Get', [
80
+ 'typeName' => 'Device',
81
+ 'resultsLimit' => $limit,
82
+ ]);
89
83
 
90
- $devices = $response['result'] ?? [];
84
+ $devices = $response['result'] ?? [];
85
+ $logsByDevice = $this->fetchLatestLogRecords($devices, $options);
86
+ $devices = array_map(function ($device) use ($logsByDevice) {
87
+ $deviceId = $device['id'] ?? null;
88
+ if ($deviceId && isset($logsByDevice[$deviceId])) {
89
+ $device['latest_log_record'] = $logsByDevice[$deviceId];
90
+ }
91
+
92
+ return $device;
93
+ }, $devices);
91
94
 
92
95
  return [
93
96
  'devices' => $devices,
@@ -98,43 +101,76 @@ class GeotabProvider extends AbstractProvider
98
101
 
99
102
  public function fetchDeviceDetails(string $externalId): array
100
103
  {
101
- $response = Http::post($this->baseUrl, [
102
- 'method' => 'Get',
103
- 'params' => [
104
- 'credentials' => [
105
- 'database' => $this->credentials['database'],
106
- 'sessionId' => $this->sessionId,
107
- ],
108
- 'typeName' => 'Device',
109
- 'search' => ['id' => $externalId],
110
- ],
111
- ])->json();
104
+ $response = $this->apiCall('Get', [
105
+ 'typeName' => 'Device',
106
+ 'search' => ['id' => $externalId],
107
+ ]);
112
108
 
113
109
  return $response['result'][0] ?? [];
114
110
  }
115
111
 
116
112
  public function normalizeDevice(array $payload): array
117
113
  {
114
+ $latestLog = $payload['latest_log_record'] ?? [];
115
+ $hasTelemetry = isset($latestLog['dateTime']) || isset($latestLog['latitude'], $latestLog['longitude']);
116
+
118
117
  return [
119
- 'external_id' => $payload['id'],
120
- 'device_name' => $payload['name'] ?? 'Unknown Device',
121
- 'device_provider' => 'geotab',
122
- 'device_model' => $payload['deviceType'] ?? null,
123
- 'imei' => $payload['serialNumber'] ?? null,
124
- 'vin' => $payload['vehicleIdentificationNumber'] ?? null,
125
- 'status' => 'active',
126
- 'meta' => $payload,
118
+ 'device_id' => $payload['id'] ?? null,
119
+ 'external_id' => $payload['id'] ?? null,
120
+ 'name' => $payload['name'] ?? 'Unknown Device',
121
+ 'provider' => 'geotab',
122
+ 'model' => $payload['deviceType'] ?? null,
123
+ 'imei' => $payload['serialNumber'] ?? null,
124
+ 'vin' => $payload['vehicleIdentificationNumber'] ?? null,
125
+ 'serial_number' => $payload['serialNumber'] ?? null,
126
+ 'status' => 'active',
127
+ 'online' => $hasTelemetry ? true : null,
128
+ 'last_seen_at' => $latestLog ? $this->parseTimestamp($latestLog['dateTime'] ?? null) : null,
129
+ 'location' => [
130
+ 'lat' => $latestLog['latitude'] ?? null,
131
+ 'lng' => $latestLog['longitude'] ?? null,
132
+ ],
133
+ 'speed' => $latestLog['speed'] ?? null,
134
+ 'heading' => $latestLog['bearing'] ?? $latestLog['heading'] ?? null,
135
+ 'altitude' => $latestLog['altitude'] ?? null,
136
+ 'meta' => [
137
+ 'raw' => $payload,
138
+ 'provider_status' => array_filter([
139
+ 'active_from' => $payload['activeFrom'] ?? null,
140
+ 'active_to' => $payload['activeTo'] ?? null,
141
+ 'groups' => $payload['groups'] ?? null,
142
+ 'has_log' => $hasTelemetry,
143
+ ], fn ($value) => $value !== null),
144
+ ],
127
145
  ];
128
146
  }
129
147
 
130
148
  public function normalizeEvent(array $payload): array
131
149
  {
150
+ $latestLog = $payload['latest_log_record'] ?? $payload;
151
+ $deviceId = data_get($latestLog, 'device.id') ?? $payload['deviceId'] ?? $payload['id'] ?? null;
152
+ $hasTelemetry = isset($latestLog['dateTime']) || isset($latestLog['latitude'], $latestLog['longitude']);
153
+
132
154
  return [
133
- 'external_id' => $payload['id'] ?? null,
134
- 'device_id' => $payload['device']['id'] ?? $payload['deviceId'] ?? null,
155
+ 'external_id' => $latestLog['id'] ?? $payload['id'] ?? null,
156
+ 'device_id' => $deviceId,
135
157
  'event_type' => $payload['type'] ?? 'status_data',
136
- 'occurred_at' => $payload['dateTime'] ?? now(),
137
- 'meta' => $payload,
158
+ 'occurred_at' => $this->parseTimestamp($latestLog['dateTime'] ?? null) ?? now(),
159
+ 'online' => $hasTelemetry ? true : null,
160
+ 'location' => [
161
+ 'lat' => $latestLog['latitude'] ?? null,
162
+ 'lng' => $latestLog['longitude'] ?? null,
163
+ ],
164
+ 'speed' => $latestLog['speed'] ?? null,
165
+ 'heading' => $latestLog['bearing'] ?? $latestLog['heading'] ?? null,
166
+ 'altitude' => $latestLog['altitude'] ?? null,
167
+ 'meta' => [
168
+ 'raw' => $payload,
169
+ 'provider_status' => array_filter([
170
+ 'has_log' => $hasTelemetry,
171
+ 'device' => data_get($latestLog, 'device.id'),
172
+ ], fn ($value) => $value !== null),
173
+ ],
138
174
  ];
139
175
  }
140
176
 
@@ -179,4 +215,56 @@ class GeotabProvider extends AbstractProvider
179
215
  {
180
216
  return false;
181
217
  }
218
+
219
+ protected function apiCall(string $method, array $params): array
220
+ {
221
+ $params['credentials'] = [
222
+ 'database' => $this->credentials['database'],
223
+ 'sessionId' => $this->sessionId,
224
+ ];
225
+
226
+ return Http::post($this->baseUrl, [
227
+ 'method' => $method,
228
+ 'params' => $params,
229
+ ])->json() ?? [];
230
+ }
231
+
232
+ protected function fetchLatestLogRecords(array $devices, array $options = []): array
233
+ {
234
+ $deviceIds = array_values(array_filter(array_map(fn ($device) => $device['id'] ?? null, $devices)));
235
+ if (empty($deviceIds)) {
236
+ return [];
237
+ }
238
+
239
+ $fromDate = $options['from_date'] ?? Carbon::now()->subHours(24)->toIso8601String();
240
+ $response = $this->apiCall('Get', [
241
+ 'typeName' => 'LogRecord',
242
+ 'search' => ['fromDate' => $fromDate],
243
+ 'resultsLimit' => max(count($deviceIds) * 5, 100),
244
+ ]);
245
+
246
+ $logsByDevice = [];
247
+ foreach ($response['result'] ?? [] as $record) {
248
+ $deviceId = data_get($record, 'device.id');
249
+ if (!$deviceId || !in_array($deviceId, $deviceIds, true)) {
250
+ continue;
251
+ }
252
+
253
+ $current = $logsByDevice[$deviceId] ?? null;
254
+ if (!$current || Carbon::parse($record['dateTime'] ?? now())->greaterThan(Carbon::parse($current['dateTime'] ?? now()))) {
255
+ $logsByDevice[$deviceId] = $record;
256
+ }
257
+ }
258
+
259
+ return $logsByDevice;
260
+ }
261
+
262
+ protected function parseTimestamp($value): ?string
263
+ {
264
+ if (!$value) {
265
+ return null;
266
+ }
267
+
268
+ return Carbon::parse($value)->toDateTimeString();
269
+ }
182
270
  }
@@ -16,10 +16,11 @@ class SafeeProvider extends AbstractProvider
16
16
  protected string $baseUrl = 'https://api.safee.com';
17
17
  protected int $requestsPerMinute = 3000;
18
18
  protected ?string $accessToken = null;
19
+ protected array $authContext = [];
19
20
 
20
21
  protected function prepareAuthentication(): void
21
22
  {
22
- $this->baseUrl = rtrim($this->credentials['api_base_url'] ?? $this->credentials['server_uri'] ?? $this->baseUrl, '/');
23
+ $this->baseUrl = $this->resolveBaseUrl();
23
24
  $this->accessToken = $this->credentials['access_token'] ?? $this->authenticate();
24
25
  $scheme = $this->credentials['authorization_scheme'] ?? 'Bearer';
25
26
 
@@ -45,13 +46,14 @@ class SafeeProvider extends AbstractProvider
45
46
  'metadata' => [
46
47
  'status' => $response['status'] ?? null,
47
48
  'time' => $response['time'] ?? null,
49
+ ...$this->safeDiagnosticMetadata(),
48
50
  ],
49
51
  ];
50
52
  } catch (\Throwable $e) {
51
53
  return [
52
54
  'success' => false,
53
55
  'message' => $e->getMessage(),
54
- 'metadata' => [],
56
+ 'metadata' => $this->safeDiagnosticMetadata(),
55
57
  ];
56
58
  }
57
59
  }
@@ -108,8 +110,19 @@ class SafeeProvider extends AbstractProvider
108
110
  'lat' => $position['lat'] ?? null,
109
111
  'lng' => $position['lng'] ?? null,
110
112
  ],
111
- 'meta' => [
112
- 'raw' => $payload,
113
+ 'speed' => $payload['speed'] ?? $payload['lastSpeed'] ?? null,
114
+ 'heading' => $payload['heading'] ?? $payload['angle'] ?? null,
115
+ 'altitude' => $payload['altitude'] ?? $payload['alt'] ?? null,
116
+ 'odometer' => $this->extractOdometer($payload),
117
+ 'ignition' => $this->extractIgnition($payload),
118
+ 'fuel_level' => $this->extractFuelLevel($payload),
119
+ 'meta' => [
120
+ 'raw' => $payload,
121
+ 'provider_status' => array_filter([
122
+ 'status' => $rawStatus,
123
+ 'normalized' => $status,
124
+ 'vehicleStatus' => $payload['vehicleStatus'] ?? null,
125
+ ], fn ($value) => $value !== null),
113
126
  'plate_number' => $payload['plateNumber'] ?? $payload['plate_number'] ?? null,
114
127
  'door_number' => $payload['doorNumber'] ?? null,
115
128
  'driver' => $payload['driver'] ?? null,
@@ -134,12 +147,14 @@ class SafeeProvider extends AbstractProvider
134
147
  'event_type' => data_get($payload, 'event.code') ?? data_get($payload, 'event.name') ?? 'telemetry_update',
135
148
  'message' => data_get($payload, 'event.name') ?? data_get($payload, 'event.message') ?? null,
136
149
  'occurred_at' => $this->parseTimestamp($payload['date'] ?? $payload['deviceTime'] ?? $payload['time'] ?? null),
150
+ 'online' => $this->resolveOnline($payload),
137
151
  'location' => [
138
152
  'lat' => $position['lat'] ?? null,
139
153
  'lng' => $position['lng'] ?? null,
140
154
  ],
141
155
  'speed' => $payload['speed'] ?? $payload['lastSpeed'] ?? null,
142
156
  'heading' => $payload['heading'] ?? $payload['angle'] ?? null,
157
+ 'altitude' => $payload['altitude'] ?? $payload['alt'] ?? null,
143
158
  'odometer' => $this->extractOdometer($payload),
144
159
  'ignition' => $this->extractIgnition($payload),
145
160
  'fuel_level' => $this->extractFuelLevel($payload),
@@ -219,7 +234,8 @@ class SafeeProvider extends AbstractProvider
219
234
  }
220
235
  }
221
236
 
222
- $tokenUrl = $this->baseUrl . '/auth/realms/' . $this->credentials['realm_id'] . '/protocol/openid-connect/token';
237
+ $tokenUrl = $this->baseUrl . '/auth/realms/' . $this->credentials['realm_id'] . '/protocol/openid-connect/token';
238
+ $this->authContext = $this->buildAuthContext($tokenUrl);
223
239
 
224
240
  $response = Http::asForm()
225
241
  ->acceptJson()
@@ -245,6 +261,44 @@ class SafeeProvider extends AbstractProvider
245
261
  return $token;
246
262
  }
247
263
 
264
+ protected function resolveBaseUrl(): string
265
+ {
266
+ $baseUrl = $this->filledCredential('api_base_url') ?? $this->filledCredential('server_uri') ?? $this->baseUrl;
267
+
268
+ return rtrim($baseUrl, '/');
269
+ }
270
+
271
+ protected function filledCredential(string $key): ?string
272
+ {
273
+ $value = $this->credentials[$key] ?? null;
274
+
275
+ if (!is_string($value)) {
276
+ return null;
277
+ }
278
+
279
+ $value = trim($value);
280
+
281
+ return $value === '' ? null : $value;
282
+ }
283
+
284
+ protected function buildAuthContext(string $tokenUrl): array
285
+ {
286
+ $parts = parse_url($tokenUrl) ?: [];
287
+ $scheme = $parts['scheme'] ?? null;
288
+ $host = $parts['host'] ?? null;
289
+
290
+ return [
291
+ 'auth_host' => $host ? trim(($scheme ? $scheme . '://' : '') . $host, '/') : null,
292
+ 'auth_path' => $parts['path'] ?? null,
293
+ 'realm_id' => $this->credentials['realm_id'] ?? null,
294
+ ];
295
+ }
296
+
297
+ protected function safeDiagnosticMetadata(): array
298
+ {
299
+ return array_filter($this->authContext, fn ($value) => $value !== null && $value !== '');
300
+ }
301
+
248
302
  protected function safeeGet(string $endpoint): array
249
303
  {
250
304
  $response = Http::withHeaders($this->headers)
@@ -306,6 +360,17 @@ class SafeeProvider extends AbstractProvider
306
360
  return in_array(strtolower($status), ['offline', 'inactive', 'deleted', 'expired'], true) ? 'inactive' : 'active';
307
361
  }
308
362
 
363
+ protected function resolveOnline(array $payload): ?bool
364
+ {
365
+ if (array_key_exists('online', $payload)) {
366
+ return filter_var($payload['online'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $payload['online'];
367
+ }
368
+
369
+ $status = $payload['status'] ?? $payload['vehicleStatus'] ?? null;
370
+
371
+ return $status ? $this->normalizeVehicleStatus($status) === 'active' : null;
372
+ }
373
+
309
374
  protected function extractOdometer(array $payload): mixed
310
375
  {
311
376
  return data_get($payload, 'canbus.odometer')
@@ -2,6 +2,8 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
4
 
5
+ use Illuminate\Support\Carbon;
6
+
5
7
  /**
6
8
  * Class SamsaraProvider.
7
9
  *
@@ -78,30 +80,68 @@ class SamsaraProvider extends AbstractProvider
78
80
 
79
81
  public function normalizeDevice(array $payload): array
80
82
  {
83
+ $position = $this->extractPosition($payload);
84
+
81
85
  return [
82
- 'external_id' => $payload['id'],
83
- 'device_name' => $payload['name'] ?? 'Unknown Device',
84
- 'device_provider' => 'samsara',
85
- 'device_model' => $payload['make'] ?? null,
86
- 'vin' => $payload['vin'] ?? null,
87
- 'license_plate' => $payload['licensePlate'] ?? null,
88
- 'status' => 'active',
89
- 'meta' => $payload,
86
+ 'device_id' => $payload['id'] ?? null,
87
+ 'external_id' => $payload['id'] ?? null,
88
+ 'name' => $payload['name'] ?? 'Unknown Device',
89
+ 'provider' => 'samsara',
90
+ 'model' => $payload['make'] ?? $payload['model'] ?? null,
91
+ 'vin' => $payload['vin'] ?? null,
92
+ 'serial_number' => $payload['serial'] ?? $payload['serialNumber'] ?? null,
93
+ 'license_plate' => $payload['licensePlate'] ?? null,
94
+ 'status' => $this->normalizeStatus($payload),
95
+ 'online' => $this->resolveOnline($payload),
96
+ 'last_seen_at' => $this->parseTimestamp($payload['time'] ?? data_get($payload, 'location.time') ?? data_get($payload, 'gps.time') ?? null),
97
+ 'location' => [
98
+ 'lat' => $position['lat'] ?? null,
99
+ 'lng' => $position['lng'] ?? null,
100
+ ],
101
+ 'speed' => $this->extractSpeed($payload),
102
+ 'heading' => $this->extractHeading($payload),
103
+ 'altitude' => $this->extractAltitude($payload),
104
+ 'odometer' => data_get($payload, 'odometerMeters') ?? data_get($payload, 'obdOdometerMeters.value'),
105
+ 'fuel_level' => data_get($payload, 'fuelPercent.value') ?? data_get($payload, 'fuelPercent'),
106
+ 'meta' => [
107
+ 'raw' => $payload,
108
+ 'provider_status' => array_filter([
109
+ 'status' => $payload['status'] ?? null,
110
+ 'gateway_status' => data_get($payload, 'gateway.status'),
111
+ 'online' => $payload['online'] ?? $payload['isOnline'] ?? data_get($payload, 'gateway.online'),
112
+ ], fn ($value) => $value !== null),
113
+ ],
90
114
  ];
91
115
  }
92
116
 
93
117
  public function normalizeEvent(array $payload): array
94
118
  {
119
+ $position = $this->extractPosition($payload);
120
+ $deviceId = data_get($payload, 'vehicle.id') ?? $payload['vehicleId'] ?? $payload['id'] ?? null;
121
+
95
122
  return [
96
- 'external_id' => $payload['id'] ?? null,
97
- 'device_id' => $payload['vehicle']['id'] ?? $payload['vehicleId'] ?? null,
123
+ 'external_id' => $payload['id'] ?? $deviceId,
124
+ 'device_id' => $deviceId,
98
125
  'event_type' => $payload['eventType'] ?? 'vehicle_update',
99
- 'occurred_at' => $payload['time'] ?? now(),
126
+ 'occurred_at' => $this->parseTimestamp($payload['time'] ?? data_get($payload, 'location.time') ?? data_get($payload, 'gps.time') ?? null) ?? now(),
127
+ 'online' => $this->resolveOnline($payload),
100
128
  'location' => [
101
- 'lat' => $payload['location']['latitude'] ?? null,
102
- 'lng' => $payload['location']['longitude'] ?? null,
129
+ 'lat' => $position['lat'] ?? null,
130
+ 'lng' => $position['lng'] ?? null,
131
+ ],
132
+ 'speed' => $this->extractSpeed($payload),
133
+ 'heading' => $this->extractHeading($payload),
134
+ 'altitude' => $this->extractAltitude($payload),
135
+ 'odometer' => data_get($payload, 'odometerMeters') ?? data_get($payload, 'obdOdometerMeters.value'),
136
+ 'fuel_level' => data_get($payload, 'fuelPercent.value') ?? data_get($payload, 'fuelPercent'),
137
+ 'meta' => [
138
+ 'raw' => $payload,
139
+ 'provider_status' => array_filter([
140
+ 'status' => $payload['status'] ?? null,
141
+ 'gateway_status' => data_get($payload, 'gateway.status'),
142
+ 'online' => $payload['online'] ?? $payload['isOnline'] ?? data_get($payload, 'gateway.online'),
143
+ ], fn ($value) => $value !== null),
103
144
  ],
104
- 'meta' => $payload,
105
145
  ];
106
146
  }
107
147
 
@@ -176,4 +216,72 @@ class SamsaraProvider extends AbstractProvider
176
216
  {
177
217
  return true;
178
218
  }
219
+
220
+ protected function extractPosition(array $payload): array
221
+ {
222
+ $position = $payload['location']
223
+ ?? $payload['gps']
224
+ ?? $payload['currentLocation']
225
+ ?? $payload['lastKnownLocation']
226
+ ?? data_get($payload, 'vehicle.location')
227
+ ?? [];
228
+
229
+ return [
230
+ 'lat' => $position['latitude'] ?? $position['lat'] ?? null,
231
+ 'lng' => $position['longitude'] ?? $position['lng'] ?? null,
232
+ ];
233
+ }
234
+
235
+ protected function extractSpeed(array $payload): mixed
236
+ {
237
+ return data_get($payload, 'location.speedMilesPerHour')
238
+ ?? data_get($payload, 'location.speed')
239
+ ?? data_get($payload, 'gps.speedMilesPerHour')
240
+ ?? data_get($payload, 'speedMilesPerHour')
241
+ ?? data_get($payload, 'speed');
242
+ }
243
+
244
+ protected function extractHeading(array $payload): mixed
245
+ {
246
+ return data_get($payload, 'location.headingDegrees')
247
+ ?? data_get($payload, 'location.heading')
248
+ ?? data_get($payload, 'gps.headingDegrees')
249
+ ?? data_get($payload, 'headingDegrees')
250
+ ?? data_get($payload, 'heading');
251
+ }
252
+
253
+ protected function extractAltitude(array $payload): mixed
254
+ {
255
+ return data_get($payload, 'location.altitudeMeters')
256
+ ?? data_get($payload, 'gps.altitudeMeters')
257
+ ?? data_get($payload, 'altitudeMeters')
258
+ ?? data_get($payload, 'altitude');
259
+ }
260
+
261
+ protected function parseTimestamp($value): ?string
262
+ {
263
+ if (!$value) {
264
+ return null;
265
+ }
266
+
267
+ return Carbon::parse($value)->toDateTimeString();
268
+ }
269
+
270
+ protected function resolveOnline(array $payload): ?bool
271
+ {
272
+ $value = $payload['online'] ?? $payload['isOnline'] ?? data_get($payload, 'gateway.online') ?? null;
273
+
274
+ if ($value === null) {
275
+ return $this->extractPosition($payload)['lat'] !== null;
276
+ }
277
+
278
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $value;
279
+ }
280
+
281
+ protected function normalizeStatus(array $payload): string
282
+ {
283
+ $status = strtolower((string) ($payload['status'] ?? data_get($payload, 'gateway.status') ?? 'active'));
284
+
285
+ return in_array($status, ['inactive', 'offline', 'deactivated'], true) ? 'inactive' : 'active';
286
+ }
179
287
  }