@fleetbase/fleetops-engine 0.6.54 → 0.6.56

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 (220) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/admin/routing-settings.hbs +2 -1
  3. package/addon/components/cell/attached-vehicle.hbs +10 -0
  4. package/addon/components/cell/attached-vehicle.js +58 -0
  5. package/addon/components/cell/device-identity.hbs +24 -0
  6. package/addon/components/cell/device-identity.js +129 -0
  7. package/addon/components/cell/driver-identity.hbs +33 -0
  8. package/addon/components/cell/driver-identity.js +143 -0
  9. package/addon/components/cell/equipment-identity.hbs +5 -0
  10. package/addon/components/cell/equipment-identity.js +48 -0
  11. package/addon/components/cell/part-identity.hbs +5 -0
  12. package/addon/components/cell/part-identity.js +63 -0
  13. package/addon/components/cell/telematic-device.hbs +38 -0
  14. package/addon/components/cell/telematic-device.js +44 -0
  15. package/addon/components/cell/telematic-provider.hbs +33 -16
  16. package/addon/components/cell/telematic-provider.js +50 -8
  17. package/addon/components/cell/vehicle-identity.hbs +33 -0
  18. package/addon/components/cell/vehicle-identity.js +138 -0
  19. package/addon/components/device/details.js +1 -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 +45 -20
  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/device-event/details.hbs +192 -0
  31. package/addon/components/device-event/details.js +151 -0
  32. package/addon/components/driver/schedule.js +1 -1
  33. package/addon/components/equipment/form.js +8 -1
  34. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  35. package/addon/components/layout/fleet-ops-sidebar.js +20 -6
  36. package/addon/components/map/drawer/device-event-listing.js +2 -0
  37. package/addon/components/map/drawer/driver-listing.js +2 -0
  38. package/addon/components/map/drawer/place-listing.js +2 -0
  39. package/addon/components/map/drawer/position-listing.hbs +11 -6
  40. package/addon/components/map/drawer/position-listing.js +17 -4
  41. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  42. package/addon/components/sensor/panel-header.hbs +47 -15
  43. package/addon/components/sensor/panel-header.js +59 -1
  44. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  45. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  46. package/addon/components/table/cell/dropdown.hbs +28 -0
  47. package/addon/components/table/cell/dropdown.js +91 -0
  48. package/addon/components/telematic/details.hbs +1 -1
  49. package/addon/components/telematic/form.hbs +2 -2
  50. package/addon/components/telematic/settings.hbs +1 -1
  51. package/addon/components/vendor/panel-header.hbs +82 -0
  52. package/addon/components/vendor/panel-header.js +48 -0
  53. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  54. package/addon/controllers/connectivity/devices/index/details/vehicle.js +50 -2
  55. package/addon/controllers/connectivity/devices/index.js +144 -129
  56. package/addon/controllers/connectivity/events/details.js +70 -0
  57. package/addon/controllers/connectivity/events/index.js +187 -11
  58. package/addon/controllers/connectivity/sensors/index.js +160 -14
  59. package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
  60. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  61. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  62. package/addon/controllers/maintenance/equipment/index.js +1 -2
  63. package/addon/controllers/maintenance/parts/index.js +1 -2
  64. package/addon/controllers/management/drivers/index.js +82 -42
  65. package/addon/controllers/management/fuel-reports/index.js +63 -4
  66. package/addon/controllers/management/issues/index.js +63 -4
  67. package/addon/controllers/management/vehicles/index.js +33 -6
  68. package/addon/controllers/settings/map.js +15 -0
  69. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  70. package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
  71. package/addon/routes/connectivity/devices/index.js +7 -0
  72. package/addon/routes/connectivity/events/details.js +28 -0
  73. package/addon/routes/connectivity/events/index.js +6 -2
  74. package/addon/routes/connectivity/sensors/index.js +7 -2
  75. package/addon/routes/connectivity/telematics/details/devices.js +2 -0
  76. package/addon/routes/management/vehicles/index/details.js +3 -1
  77. package/addon/services/device-actions.js +30 -0
  78. package/addon/services/device-event-actions.js +1 -1
  79. package/addon/services/equipment-actions.js +1 -0
  80. package/addon/services/map-adapter/google.js +25 -0
  81. package/addon/services/map-adapter/leaflet.js +1 -1
  82. package/addon/services/map-adapter-interface.js +1 -1
  83. package/addon/services/map-manager.js +4 -1
  84. package/addon/services/map-settings.js +15 -0
  85. package/addon/services/sensor-actions.js +3 -0
  86. package/addon/services/service-rate-actions.js +39 -1
  87. package/addon/services/vehicle-actions.js +24 -12
  88. package/addon/services/vendor-actions.js +3 -0
  89. package/addon/styles/fleetops-engine.css +9 -1
  90. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +66 -2
  91. package/addon/templates/connectivity/events/details.hbs +40 -0
  92. package/addon/templates/management/vendors/index/details.hbs +2 -2
  93. package/addon/templates/settings/map.hbs +38 -9
  94. package/addon/templates/settings/notifications.hbs +2 -1
  95. package/addon/templates/settings/routing.hbs +1 -0
  96. package/addon/utils/device-table-columns.js +288 -0
  97. package/addon/utils/identity-cell-resource.js +20 -0
  98. package/addon/utils/leaflet-plugin-loader.js +5 -5
  99. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  100. package/addon/utils/route-colors.js +1 -1
  101. package/addon/utils/trackable-option.js +93 -0
  102. package/app/components/cell/attached-vehicle.js +1 -0
  103. package/app/components/cell/device-identity.js +1 -0
  104. package/app/components/cell/driver-identity.js +1 -0
  105. package/app/components/cell/equipment-identity.js +1 -0
  106. package/app/components/cell/part-identity.js +1 -0
  107. package/app/components/cell/telematic-device.js +1 -0
  108. package/app/components/cell/vehicle-identity.js +1 -0
  109. package/app/components/device/panel-tabs/events.hbs +30 -0
  110. package/app/components/device/panel-tabs/events.js +128 -0
  111. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  112. package/app/components/device/panel-tabs/sensors.js +93 -0
  113. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  114. package/app/components/device/panel-tabs/vehicle.js +90 -0
  115. package/app/components/device-event/details.hbs +192 -0
  116. package/app/components/device-event/details.js +1 -0
  117. package/app/components/table/cell/dropdown/action-item.js +1 -0
  118. package/app/components/table/cell/dropdown.js +1 -0
  119. package/app/components/vendor/panel-header.js +1 -0
  120. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  121. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  122. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  123. package/app/utils/map-drawer-dropdown-position.js +1 -0
  124. package/app/utils/trackable-option.js +1 -0
  125. package/composer.json +1 -1
  126. package/extension.json +1 -1
  127. package/package.json +4 -4
  128. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  129. package/server/src/Console/Commands/SyncTelematics.php +4 -4
  130. package/server/src/Exports/ContactExport.php +12 -5
  131. package/server/src/Exports/DeviceExport.php +92 -0
  132. package/server/src/Exports/DriverExport.php +28 -3
  133. package/server/src/Exports/EquipmentExport.php +94 -0
  134. package/server/src/Exports/FleetExport.php +22 -7
  135. package/server/src/Exports/FuelReportExport.php +16 -1
  136. package/server/src/Exports/IssueExport.php +25 -3
  137. package/server/src/Exports/MaintenanceExport.php +106 -0
  138. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  139. package/server/src/Exports/OrderExport.php +24 -5
  140. package/server/src/Exports/PartExport.php +99 -0
  141. package/server/src/Exports/PlaceExport.php +33 -7
  142. package/server/src/Exports/SensorExport.php +94 -0
  143. package/server/src/Exports/ServiceAreaExport.php +31 -7
  144. package/server/src/Exports/ServiceRateExport.php +41 -1
  145. package/server/src/Exports/TelematicExport.php +90 -0
  146. package/server/src/Exports/VehicleExport.php +274 -8
  147. package/server/src/Exports/VendorExport.php +15 -2
  148. package/server/src/Exports/WorkOrderExport.php +92 -0
  149. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  150. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  151. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  152. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  153. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  154. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  155. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  156. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +36 -1
  157. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
  158. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  159. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  160. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  161. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  162. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  163. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  164. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  165. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  166. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  167. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  168. package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
  169. package/server/src/Http/Filter/DeviceFilter.php +50 -4
  170. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  171. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  172. package/server/src/Http/Filter/PartFilter.php +47 -0
  173. package/server/src/Http/Filter/SensorFilter.php +34 -6
  174. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  175. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  176. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  177. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  178. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  179. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  180. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  181. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  182. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  183. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  184. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  185. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  186. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  187. package/server/src/Http/Resources/v1/Device.php +72 -0
  188. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  189. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  190. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  191. package/server/src/Http/Resources/v1/Part.php +64 -0
  192. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  193. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  194. package/server/src/Models/Device.php +2 -0
  195. package/server/src/Models/DeviceEvent.php +81 -0
  196. package/server/src/Models/Equipment.php +55 -0
  197. package/server/src/Models/Order.php +2 -1
  198. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  199. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  200. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  201. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  202. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  203. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +105 -5
  204. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  205. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  206. package/server/src/Support/Telematics/Providers/SafeeProvider.php +567 -47
  207. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  208. package/server/src/Support/Telematics/TelematicService.php +28 -9
  209. package/server/src/routes.php +67 -15
  210. package/server/tests/DeviceFilterTest.php +9 -0
  211. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  212. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  213. package/server/tests/ExportCoverageTest.php +79 -0
  214. package/server/tests/MetricsRegistryTest.php +45 -5
  215. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  216. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  217. package/server/tests/TelematicsHardeningTest.php +873 -7
  218. package/addon/controllers/connectivity/events/index/details.js +0 -3
  219. package/addon/routes/connectivity/events/index/details.js +0 -3
  220. package/addon/templates/connectivity/events/index/details.hbs +0 -1
@@ -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
  }
@@ -194,19 +194,30 @@ class TelematicService
194
194
  {
195
195
  $device = $this->linkDevice($telematic, $provider->normalizeDevice($payload));
196
196
 
197
- $event = null;
197
+ $event = null;
198
+ $events = [];
198
199
  try {
199
- $eventData = $provider->normalizeEvent($payload);
200
- if ($this->hasEventSignal($eventData)) {
201
- $event = $this->storeDeviceEvent($telematic, $eventData, $device);
200
+ $eventPayloads = method_exists($provider, 'normalizeEvents') ? $provider->normalizeEvents($payload) : [];
201
+ if (empty($eventPayloads)) {
202
+ $eventPayloads = [$provider->normalizeEvent($payload)];
203
+ }
204
+
205
+ foreach ($eventPayloads as $eventData) {
206
+ if ($this->hasEventSignal($eventData)) {
207
+ $storedEvent = $this->storeDeviceEvent($telematic, $eventData, $device);
208
+ $events[] = $storedEvent;
209
+ $event ??= $storedEvent;
210
+ }
202
211
  }
203
212
  } catch (\Throwable) {
204
- $event = null;
213
+ $event = null;
214
+ $events = [];
205
215
  }
206
216
 
207
217
  return [
208
218
  'device' => $device,
209
219
  'event' => $event,
220
+ 'events' => $events,
210
221
  'sensors' => $this->storeSnapshotSensors($telematic, $provider, $payload, $device),
211
222
  ];
212
223
  }
@@ -663,6 +674,10 @@ class TelematicService
663
674
  $vehicle->altitude = $eventData['altitude'];
664
675
  }
665
676
 
677
+ if (array_key_exists('odometer', $eventData) && $eventData['odometer'] !== null) {
678
+ $vehicle->odometer = $eventData['odometer'];
679
+ }
680
+
666
681
  $vehicle->telematics = array_merge($vehicle->telematics ?? [], [
667
682
  'last_event_uuid' => $event->uuid,
668
683
  'last_event_id' => $event->public_id,
@@ -757,13 +772,17 @@ class TelematicService
757
772
  return collect($rawSensors)
758
773
  ->map(function ($sensor, $name) {
759
774
  if (is_array($sensor)) {
760
- return array_merge(['name' => $name], $sensor);
775
+ return array_merge([
776
+ 'sensor_key' => $name,
777
+ 'name' => $name,
778
+ ], $sensor);
761
779
  }
762
780
 
763
781
  return [
764
- 'name' => $name,
765
- 'type' => $name,
766
- 'value' => $sensor,
782
+ 'sensor_key' => $name,
783
+ 'name' => $name,
784
+ 'type' => $name,
785
+ 'value' => $sensor,
767
786
  ];
768
787
  })
769
788
  ->values()
@@ -105,6 +105,61 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
105
105
  $router->put('{id}', 'FuelReportController@update');
106
106
  $router->delete('{id}', 'FuelReportController@delete');
107
107
  });
108
+ // fuel-transactions routes
109
+ $router->group(['prefix' => 'fuel-transactions'], function () use ($router) {
110
+ $router->post('/', 'FuelTransactionController@create');
111
+ $router->get('/', 'FuelTransactionController@query');
112
+ $router->get('{id}', 'FuelTransactionController@find');
113
+ $router->put('{id}', 'FuelTransactionController@update');
114
+ $router->delete('{id}', 'FuelTransactionController@delete');
115
+ $router->post('{id}/match-vehicle', 'FuelTransactionController@matchVehicle');
116
+ $router->post('{id}/match-order', 'FuelTransactionController@matchOrder');
117
+ $router->post('{id}/reprocess', 'FuelTransactionController@reprocess');
118
+ $router->post('{id}/review', 'FuelTransactionController@review');
119
+ });
120
+ // equipment routes
121
+ $router->group(['prefix' => 'equipment'], function () use ($router) {
122
+ $router->post('/', 'EquipmentController@create');
123
+ $router->get('/', 'EquipmentController@query');
124
+ $router->get('{id}', 'EquipmentController@find');
125
+ $router->put('{id}', 'EquipmentController@update');
126
+ $router->delete('{id}', 'EquipmentController@delete');
127
+ });
128
+ // parts routes
129
+ $router->group(['prefix' => 'parts'], function () use ($router) {
130
+ $router->post('/', 'PartController@create');
131
+ $router->get('/', 'PartController@query');
132
+ $router->get('{id}', 'PartController@find');
133
+ $router->put('{id}', 'PartController@update');
134
+ $router->delete('{id}', 'PartController@delete');
135
+ });
136
+ // work-orders routes
137
+ $router->group(['prefix' => 'work-orders'], function () use ($router) {
138
+ $router->post('/', 'WorkOrderController@create');
139
+ $router->get('/', 'WorkOrderController@query');
140
+ $router->get('{id}', 'WorkOrderController@find');
141
+ $router->put('{id}', 'WorkOrderController@update');
142
+ $router->delete('{id}', 'WorkOrderController@delete');
143
+ $router->post('{id}/send', 'WorkOrderController@send');
144
+ });
145
+ // devices routes
146
+ $router->group(['prefix' => 'devices'], function () use ($router) {
147
+ $router->post('/', 'DeviceController@create');
148
+ $router->get('/', 'DeviceController@query');
149
+ $router->get('{id}', 'DeviceController@find');
150
+ $router->put('{id}', 'DeviceController@update');
151
+ $router->delete('{id}', 'DeviceController@delete');
152
+ $router->post('{id}/attach', 'DeviceController@attach');
153
+ $router->post('{id}/detach', 'DeviceController@detach');
154
+ });
155
+ // sensors routes
156
+ $router->group(['prefix' => 'sensors'], function () use ($router) {
157
+ $router->post('/', 'SensorController@create');
158
+ $router->get('/', 'SensorController@query');
159
+ $router->get('{id}', 'SensorController@find');
160
+ $router->put('{id}', 'SensorController@update');
161
+ $router->delete('{id}', 'SensorController@delete');
162
+ });
108
163
  // orders routes
109
164
  $router->group(['prefix' => 'orders', 'middleware' => []], function () use ($router) {
110
165
  $router->post('/', 'OrderController@create');
@@ -315,7 +370,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
315
370
  $router->get('facilitators/{id}', $controller('getAsFacilitator'));
316
371
  $router->get('customers/{id}', $controller('getAsCustomer'));
317
372
  $router->post('{id}/convert-to-vendor', $controller('convertToVendor'));
318
- $router->delete('bulk-delete', $controller('bulkDelete'));
319
373
  }
320
374
  );
321
375
  $router->fleetbaseRoutes(
@@ -330,7 +384,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
330
384
  $router->post('{id}/assign-vehicle', $controller('assignVehicle'));
331
385
  $router->post('{id}/unassign-vehicle', $controller('unassignVehicle'));
332
386
  $router->match(['get', 'post'], 'export', $controller('export'));
333
- $router->delete('bulk-delete', $controller('bulkDelete'));
334
387
  $router->post('import', $controller('import'));
335
388
  // Driver scheduling endpoints
336
389
  $router->get('{id}/schedule-items', $controller('scheduleItems'));
@@ -349,7 +402,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
349
402
  $router->post('remove-vehicle', $controller('removeVehicle'));
350
403
  $router->match(['get', 'post'], 'export', $controller('export'));
351
404
  $router->post('import', $controller('import'));
352
- $router->delete('bulk-delete', $controller('bulkDelete'));
353
405
  }
354
406
  );
355
407
  $router->fleetbaseRoutes(
@@ -357,7 +409,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
357
409
  function ($router, $controller) {
358
410
  $router->match(['get', 'post'], 'export', $controller('export'));
359
411
  $router->post('import', $controller('import'));
360
- $router->delete('bulk-delete', $controller('bulkDelete'));
361
412
  }
362
413
  );
363
414
  $router->fleetbaseRoutes(
@@ -385,14 +436,12 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
385
436
  function ($router, $controller) {
386
437
  $router->match(['get', 'post'], 'export', $controller('export'));
387
438
  $router->post('import', $controller('import'));
388
- $router->delete('bulk-delete', $controller('bulkDelete'));
389
439
  }
390
440
  );
391
441
  $router->fleetbaseRoutes(
392
442
  'integrated-vendors',
393
443
  function ($router, $controller) {
394
444
  $router->get('supported', $controller('getSupported'));
395
- $router->delete('bulk-delete', $controller('bulkDelete'));
396
445
  }
397
446
  );
398
447
  $router->fleetbaseRoutes(
@@ -421,7 +470,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
421
470
  $router->patch('start', $controller('start'));
422
471
  // Scheduler: set scheduled_at + driver without triggering dispatch
423
472
  $router->patch('schedule', $controller('scheduleOrder'));
424
- $router->delete('bulk-delete', $controller('bulkDelete'));
425
473
  $router->match(['get', 'post'], 'export', $controller('export'));
426
474
  }
427
475
  );
@@ -434,7 +482,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
434
482
  $router->get('lookup', $controller('geocode'))->middleware(['cache.headers:private;max_age=3600']);
435
483
  $router->get('avatars', $controller('avatars'));
436
484
  $router->match(['get', 'post'], 'export', $controller('export'));
437
- $router->delete('bulk-delete', $controller('bulkDelete'));
438
485
  $router->post('import', $controller('import'));
439
486
  }
440
487
  );
@@ -449,7 +496,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
449
496
  'service-areas',
450
497
  function ($router, $controller) {
451
498
  $router->match(['get', 'post'], 'export', $controller('export'));
452
- $router->delete('bulk-delete', $controller('bulkDelete'));
453
499
  }
454
500
  );
455
501
  $router->group(
@@ -473,10 +519,8 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
473
519
  $router->fleetbaseRoutes(
474
520
  'service-rates',
475
521
  function ($router, $controller) {
476
- $router->delete('bulk-delete', $controller('bulkDelete'));
477
522
  $router->get('for-route', $controller('getServicesForRoute'));
478
523
  $router->match(['get', 'post'], 'export', $controller('export'));
479
- $router->get('for-route', $controller('getServicesForRoute'));
480
524
  }
481
525
  );
482
526
  $router->fleetbaseRoutes('tracking-numbers');
@@ -494,7 +538,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
494
538
  $router->post('{id}/detach-device', $controller('detachDevice'));
495
539
  $router->match(['get', 'post'], 'export', $controller('export'));
496
540
  $router->post('import', $controller('import'));
497
- $router->delete('bulk-delete', $controller('bulkDelete'));
498
541
  }
499
542
  );
500
543
  $router->fleetbaseRoutes('vehicle-devices');
@@ -510,18 +553,22 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
510
553
  $router->delete('{id}/personnels/{contact}', $controller('removeVendorPersonnel'));
511
554
  $router->post('{id}/assign-driver', $controller('assignDriver'));
512
555
  $router->post('{id}/remove-driver', $controller('removeDriver'));
513
- $router->delete('bulk-delete', $controller('bulkDelete'));
514
556
  $router->post('import', $controller('import'));
515
557
  }
516
558
  );
517
- $router->fleetbaseRoutes('devices');
559
+ $router->fleetbaseRoutes('devices', function ($router, $controller) {
560
+ $router->match(['get', 'post'], 'export', $controller('export'));
561
+ });
518
562
  $router->post('devices/{id}/attach', 'DeviceController@attach');
519
563
  $router->post('devices/{id}/detach', 'DeviceController@detach');
520
564
  $router->fleetbaseRoutes('device-events', function ($router, $controller) {
521
565
  $router->post('{id}/mark-processed', $controller('markProcessed'));
522
566
  });
523
- $router->fleetbaseRoutes('sensors');
567
+ $router->fleetbaseRoutes('sensors', function ($router, $controller) {
568
+ $router->match(['get', 'post'], 'export', $controller('export'));
569
+ });
524
570
  $router->fleetbaseRoutes('telematics', function ($router, $controller) {
571
+ $router->match(['get', 'post'], 'export', $controller('export'));
525
572
  $router->get('providers', $controller('providers'));
526
573
  $router->get('{id}/logs', $controller('logs'));
527
574
  $router->get('{id}/devices', $controller('devices'));
@@ -531,6 +578,7 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
531
578
  $router->post('{key}/test-credentials', $controller('testCredentials'));
532
579
  });
533
580
  $router->fleetbaseRoutes('maintenance-schedules', function ($router, $controller) {
581
+ $router->match(['get', 'post'], 'export', $controller('export'));
534
582
  $router->post('import', $controller('import'));
535
583
  $router->post('{id}/pause', $controller('pause'));
536
584
  $router->post('{id}/resume', $controller('resume'));
@@ -539,19 +587,23 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
539
587
  $router->get('{id}/ical', $controller('ical'));
540
588
  });
541
589
  $router->fleetbaseRoutes('work-orders', function ($router, $controller) {
590
+ $router->match(['get', 'post'], 'export', $controller('export'));
542
591
  $router->post('import', $controller('import'));
543
592
  $router->post('{id}/send', $controller('sendEmail'));
544
593
  });
545
594
  $router->fleetbaseRoutes('maintenances', function ($router, $controller) {
595
+ $router->match(['get', 'post'], 'export', $controller('export'));
546
596
  $router->post('import', $controller('import'));
547
597
  $router->post('{id}/line-items', $controller('addLineItem'));
548
598
  $router->put('{id}/line-items/{index}', $controller('updateLineItem'));
549
599
  $router->delete('{id}/line-items/{index}', $controller('removeLineItem'));
550
600
  });
551
601
  $router->fleetbaseRoutes('equipment', function ($router, $controller) {
602
+ $router->match(['get', 'post'], 'export', $controller('export'));
552
603
  $router->post('import', $controller('import'));
553
604
  });
554
605
  $router->fleetbaseRoutes('parts', function ($router, $controller) {
606
+ $router->match(['get', 'post'], 'export', $controller('export'));
555
607
  $router->post('import', $controller('import'));
556
608
  });
557
609
  $router->fleetbaseRoutes('warranties');
@@ -9,6 +9,8 @@ test('device filter exposes telematics detail filter contract', function () {
9
9
  ->toContain("'vehicle'")
10
10
  ->toContain("'connection_status'")
11
11
  ->toContain("'device_id'")
12
+ ->toContain("'type'")
13
+ ->toContain("'serial_number'")
12
14
  ->toContain("'last_online_at'")
13
15
  ->toContain("'updated_at'");
14
16
 
@@ -16,8 +18,13 @@ test('device filter exposes telematics detail filter contract', function () {
16
18
  ->toContain('public function query(?string $searchQuery)')
17
19
  ->toContain('public function deviceId(?string $deviceId)')
18
20
  ->toContain("where('device_id', 'like'")
21
+ ->toContain('public function type(string|array|null $type)')
22
+ ->toContain("whereIn('type', \$type)")
23
+ ->toContain('public function serialNumber(?string $serialNumber)')
24
+ ->toContain("where('serial_number', 'like'")
19
25
  ->toContain('public function vehicle(?string $vehicle)')
20
26
  ->toContain("where('attachable_uuid', \$vehicle)")
27
+ ->toContain("where('public_id', \$vehicle)")
21
28
  ->toContain('public function connectionStatus')
22
29
  ->toContain("'online'")
23
30
  ->toContain("'recently_offline'")
@@ -30,7 +37,9 @@ test('device filter exposes telematics detail filter contract', function () {
30
37
  ->toContain('protected function filterDate');
31
38
 
32
39
  expect($controller)
40
+ ->toContain("withCount('sensors')")
33
41
  ->toContain("filled('connection_status')")
42
+ ->toContain("filled('serial_number')")
34
43
  ->toContain("filled('last_online_at')")
35
44
  ->toContain("filled('updated_at')");
36
45
  });
@@ -1,16 +1,19 @@
1
1
  <?php
2
2
 
3
3
  use Fleetbase\FleetOps\Models\Driver;
4
+ use Fleetbase\FleetOps\Models\Equipment;
4
5
  use Fleetbase\FleetOps\Models\Vehicle;
5
6
 
6
- test('driver and vehicle statuses default to available', function () {
7
+ test('driver vehicle and equipment statuses default to available', function () {
7
8
  expect((new Driver())->status)
8
9
  ->toBe('available')
9
10
  ->and((new Vehicle())->status)
11
+ ->toBe('available')
12
+ ->and((new Equipment())->status)
10
13
  ->toBe('available');
11
14
  });
12
15
 
13
- test('legacy active and null driver and vehicle statuses normalize to available', function () {
16
+ test('legacy active and null driver vehicle and equipment statuses normalize to available', function () {
14
17
  $driver = new Driver();
15
18
  $driver->status = 'active';
16
19
  expect($driver->status)->toBe('available');
@@ -24,6 +27,20 @@ test('legacy active and null driver and vehicle statuses normalize to available'
24
27
 
25
28
  $vehicle->status = null;
26
29
  expect($vehicle->status)->toBe('available');
30
+
31
+ $equipment = new Equipment();
32
+ $equipment->status = 'active';
33
+ expect($equipment->status)->toBe('available');
34
+
35
+ $equipment->status = null;
36
+ expect($equipment->status)->toBe('available');
37
+ });
38
+
39
+ test('equipment preserves explicit non default status values', function () {
40
+ $equipment = new Equipment();
41
+ $equipment->status = 'maintenance';
42
+
43
+ expect($equipment->status)->toBe('maintenance');
27
44
  });
28
45
 
29
46
  test('dispatch driver availability queries use available status', function () {
@@ -0,0 +1,35 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Driver;
4
+ use Fleetbase\FleetOps\Models\Equipment;
5
+ use Fleetbase\FleetOps\Models\Vehicle;
6
+ use Illuminate\Database\Eloquent\Relations\Relation;
7
+
8
+ test('equipment assignment type aliases normalize to model classes', function () {
9
+ $equipment = new Equipment();
10
+
11
+ $equipment->equipable_type = 'fleet-ops:vehicle';
12
+ expect($equipment->equipable_type)->toBe(Vehicle::class);
13
+
14
+ $equipment->equipable_type = 'fleet-ops:driver';
15
+ expect($equipment->equipable_type)->toBe(Driver::class);
16
+ });
17
+
18
+ test('equipment assignment type preserves null and existing model classes', function () {
19
+ $equipment = new Equipment();
20
+
21
+ $equipment->equipable_type = null;
22
+ expect($equipment->equipable_type)->toBeNull();
23
+
24
+ $equipment->equipable_type = Vehicle::class;
25
+ expect($equipment->equipable_type)->toBe(Vehicle::class);
26
+ });
27
+
28
+ test('legacy equipment assignment aliases are registered for morph reads', function () {
29
+ new Equipment();
30
+
31
+ expect(Relation::getMorphedModel('fleet-ops:vehicle'))
32
+ ->toBe(Vehicle::class)
33
+ ->and(Relation::getMorphedModel('fleet-ops:driver'))
34
+ ->toBe(Driver::class);
35
+ });
@@ -0,0 +1,79 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Exports\DeviceExport;
4
+ use Fleetbase\FleetOps\Exports\MaintenanceExport;
5
+ use Fleetbase\FleetOps\Exports\MaintenanceScheduleExport;
6
+ use Fleetbase\FleetOps\Exports\VehicleExport;
7
+ use Fleetbase\FleetOps\Exports\WorkOrderExport;
8
+
9
+ test('expanded export headings include important operational fields', function () {
10
+ expect((new VehicleExport())->headings())
11
+ ->toContain('Plate Number')
12
+ ->toContain('VIN')
13
+ ->toContain('Fuel Card Number')
14
+ ->toContain('Status')
15
+ ->toContain('Body Type')
16
+ ->toContain('Usage Type')
17
+ ->toContain('Transmission')
18
+ ->toContain('Measurement System')
19
+ ->toContain('Engine Number')
20
+ ->toContain('Engine Size (L)')
21
+ ->toContain('Payload Capacity (kg)')
22
+ ->toContain('Payload Volume (m3)')
23
+ ->toContain('DPF Equipped')
24
+ ->toContain('Insurance Value')
25
+ ->toContain('Loan Amount')
26
+ ->toContain('Vehicle Skills')
27
+ ->toContain('Updated At');
28
+
29
+ expect((new WorkOrderExport())->headings())
30
+ ->toContain('Assignee')
31
+ ->toContain('Target')
32
+ ->toContain('Due At')
33
+ ->toContain('Completion Percentage');
34
+
35
+ expect((new DeviceExport())->headings())
36
+ ->toContain('Connection Status')
37
+ ->toContain('Attached To')
38
+ ->toContain('Telematic Provider')
39
+ ->toContain('Last Seen');
40
+ });
41
+
42
+ test('relationship exports use readable name columns', function () {
43
+ expect((new VehicleExport())->headings())
44
+ ->toContain('Driver')
45
+ ->toContain('Vendor');
46
+
47
+ expect((new WorkOrderExport())->headings())
48
+ ->toContain('Assignee')
49
+ ->toContain('Target');
50
+
51
+ expect((new MaintenanceExport())->headings())
52
+ ->toContain('Asset')
53
+ ->toContain('Performed By');
54
+
55
+ expect((new MaintenanceScheduleExport())->headings())
56
+ ->toContain('Subject')
57
+ ->toContain('Default Assignee');
58
+ });
59
+
60
+ test('vehicle export helpers format spreadsheet friendly values', function () {
61
+ $export = new VehicleExport();
62
+ $helper = fn (string $method, ...$arguments) => (new ReflectionMethod($export, $method))->invoke($export, ...$arguments);
63
+
64
+ expect($helper('yesNo', true))->toBe('Yes')
65
+ ->and($helper('yesNo', false))->toBe('No')
66
+ ->and($helper('yesNo', null))->toBeNull()
67
+ ->and($helper('joinSkills', ['hazmat', 'refrigerated']))->toBe('hazmat, refrigerated')
68
+ ->and($helper('joinSkills', []))->toBeNull();
69
+ });
70
+
71
+ test('resources with visible export actions have backend export routes', function () {
72
+ $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
73
+
74
+ foreach (['devices', 'sensors', 'telematics', 'maintenance-schedules', 'work-orders', 'maintenances', 'equipment', 'parts'] as $resource) {
75
+ expect($routes)->toContain("\$router->fleetbaseRoutes('{$resource}'");
76
+ }
77
+
78
+ expect(substr_count($routes, "\$router->match(['get', 'post'], 'export', \$controller('export'));"))->toBeGreaterThanOrEqual(19);
79
+ });