@fleetbase/fleetops-engine 0.6.54 → 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 (127) 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/form.hbs +2 -2
  19. package/addon/components/device/form.js +7 -0
  20. package/addon/components/device/panel-header.hbs +45 -20
  21. package/addon/components/device/panel-header.js +49 -1
  22. package/addon/components/device/panel-tabs/events.hbs +30 -0
  23. package/addon/components/device/panel-tabs/events.js +128 -0
  24. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  25. package/addon/components/device/panel-tabs/sensors.js +93 -0
  26. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  27. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  28. package/addon/components/equipment/form.js +8 -1
  29. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  30. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  31. package/addon/components/map/drawer/device-event-listing.js +2 -0
  32. package/addon/components/map/drawer/driver-listing.js +2 -0
  33. package/addon/components/map/drawer/place-listing.js +2 -0
  34. package/addon/components/map/drawer/position-listing.hbs +11 -6
  35. package/addon/components/map/drawer/position-listing.js +17 -4
  36. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  37. package/addon/components/sensor/panel-header.hbs +47 -15
  38. package/addon/components/sensor/panel-header.js +59 -1
  39. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  40. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  41. package/addon/components/table/cell/dropdown.hbs +28 -0
  42. package/addon/components/table/cell/dropdown.js +91 -0
  43. package/addon/components/vendor/panel-header.hbs +82 -0
  44. package/addon/components/vendor/panel-header.js +48 -0
  45. package/addon/controllers/connectivity/devices/index/details/vehicle.js +49 -1
  46. package/addon/controllers/connectivity/devices/index.js +144 -129
  47. package/addon/controllers/connectivity/events/details.js +3 -0
  48. package/addon/controllers/connectivity/events/index.js +187 -11
  49. package/addon/controllers/connectivity/sensors/index.js +156 -14
  50. package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
  51. package/addon/controllers/maintenance/equipment/index.js +1 -2
  52. package/addon/controllers/maintenance/parts/index.js +1 -2
  53. package/addon/controllers/management/drivers/index.js +82 -42
  54. package/addon/controllers/management/fuel-reports/index.js +63 -4
  55. package/addon/controllers/management/issues/index.js +63 -4
  56. package/addon/controllers/management/vehicles/index.js +33 -6
  57. package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
  58. package/addon/routes/connectivity/devices/index.js +7 -0
  59. package/addon/routes/connectivity/events/details.js +3 -0
  60. package/addon/routes/connectivity/events/index.js +6 -2
  61. package/addon/routes/connectivity/sensors/index.js +7 -2
  62. package/addon/routes/connectivity/telematics/details/devices.js +2 -0
  63. package/addon/services/device-actions.js +30 -0
  64. package/addon/services/device-event-actions.js +1 -1
  65. package/addon/services/equipment-actions.js +1 -0
  66. package/addon/services/sensor-actions.js +3 -0
  67. package/addon/services/service-rate-actions.js +39 -1
  68. package/addon/services/vehicle-actions.js +24 -12
  69. package/addon/services/vendor-actions.js +3 -0
  70. package/addon/styles/fleetops-engine.css +8 -0
  71. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +64 -1
  72. package/addon/templates/management/vendors/index/details.hbs +2 -2
  73. package/addon/templates/settings/notifications.hbs +2 -1
  74. package/addon/templates/settings/routing.hbs +1 -0
  75. package/addon/utils/device-table-columns.js +288 -0
  76. package/addon/utils/identity-cell-resource.js +20 -0
  77. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  78. package/addon/utils/trackable-option.js +93 -0
  79. package/app/components/cell/attached-vehicle.js +1 -0
  80. package/app/components/cell/device-identity.js +1 -0
  81. package/app/components/cell/driver-identity.js +1 -0
  82. package/app/components/cell/equipment-identity.js +1 -0
  83. package/app/components/cell/part-identity.js +1 -0
  84. package/app/components/cell/telematic-device.js +1 -0
  85. package/app/components/cell/vehicle-identity.js +1 -0
  86. package/app/components/device/panel-tabs/events.hbs +30 -0
  87. package/app/components/device/panel-tabs/events.js +128 -0
  88. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  89. package/app/components/device/panel-tabs/sensors.js +93 -0
  90. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  91. package/app/components/device/panel-tabs/vehicle.js +90 -0
  92. package/app/components/table/cell/dropdown/action-item.js +1 -0
  93. package/app/components/table/cell/dropdown.js +1 -0
  94. package/app/components/vendor/panel-header.js +1 -0
  95. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  96. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  97. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  98. package/app/utils/map-drawer-dropdown-position.js +1 -0
  99. package/app/utils/trackable-option.js +1 -0
  100. package/composer.json +1 -1
  101. package/extension.json +1 -1
  102. package/package.json +4 -4
  103. package/server/src/Console/Commands/SyncTelematics.php +4 -4
  104. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -1
  105. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
  106. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  107. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  108. package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
  109. package/server/src/Http/Filter/DeviceFilter.php +24 -1
  110. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  111. package/server/src/Models/Device.php +2 -0
  112. package/server/src/Models/DeviceEvent.php +81 -0
  113. package/server/src/Models/Equipment.php +55 -0
  114. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +1 -0
  115. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  116. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  117. package/server/src/Support/Telematics/Providers/SafeeProvider.php +26 -2
  118. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  119. package/server/src/routes.php +0 -1
  120. package/server/tests/DeviceFilterTest.php +9 -0
  121. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  122. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  123. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  124. package/server/tests/TelematicsHardeningTest.php +238 -7
  125. package/addon/controllers/connectivity/events/index/details.js +0 -3
  126. package/addon/routes/connectivity/events/index/details.js +0 -3
  127. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -2,7 +2,10 @@
2
2
 
3
3
  use Fleetbase\FleetOps\Contracts\TelematicProviderDescriptor;
4
4
  use Fleetbase\FleetOps\Support\Telematics\Providers\AfaqyProvider;
5
+ use Fleetbase\FleetOps\Support\Telematics\Providers\FlespiProvider;
6
+ use Fleetbase\FleetOps\Support\Telematics\Providers\GeotabProvider;
5
7
  use Fleetbase\FleetOps\Support\Telematics\Providers\SafeeProvider;
8
+ use Fleetbase\FleetOps\Support\Telematics\Providers\SamsaraProvider;
6
9
  use Illuminate\Http\Client\ConnectionException;
7
10
  use Illuminate\Support\Facades\Http;
8
11
 
@@ -21,6 +24,11 @@ test('device event model and migration expose lifecycle fields used by telematic
21
24
  ->toContain("'occurred_at'")
22
25
  ->toContain("'processed_at'")
23
26
  ->toContain("'data'")
27
+ ->toContain("'device_imei'")
28
+ ->toContain("'device_connection_status'")
29
+ ->toContain("'provider_descriptor'")
30
+ ->toContain('public function getDeviceImeiAttribute(): ?string')
31
+ ->toContain('public function getProviderDescriptorAttribute(): array')
24
32
  ->toContain("'occurred_at' => 'datetime'")
25
33
  ->toContain("'processed_at' => 'datetime'");
26
34
  });
@@ -79,20 +87,240 @@ test('device details render consistent telematics connection state and timestamp
79
87
  });
80
88
 
81
89
  test('native providers normalize device payloads to canonical FleetOps keys', function () {
82
- $afaqy = file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/AfaqyProvider.php');
83
- $safee = file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/SafeeProvider.php');
90
+ $providers = [
91
+ file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/AfaqyProvider.php'),
92
+ file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/FlespiProvider.php'),
93
+ file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/GeotabProvider.php'),
94
+ file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/SafeeProvider.php'),
95
+ file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/SamsaraProvider.php'),
96
+ ];
84
97
 
85
- foreach ([$afaqy, $safee] as $provider) {
98
+ foreach ($providers as $provider) {
86
99
  expect($provider)
87
100
  ->toContain("'device_id'")
101
+ ->toContain("'external_id'")
88
102
  ->toContain("'name'")
89
103
  ->toContain("'provider'")
90
104
  ->toContain("'model'")
91
105
  ->toContain("'online'")
92
- ->toContain("'last_seen_at'");
106
+ ->toContain("'last_seen_at'")
107
+ ->toContain("'location'")
108
+ ->toContain("'speed'")
109
+ ->toContain("'heading'")
110
+ ->toContain("'altitude'");
93
111
  }
94
112
  });
95
113
 
114
+ test('flespi telemetry normalizes positional event fields', function () {
115
+ $event = (new FlespiProvider())->normalizeEvent([
116
+ 'id' => 'message-1',
117
+ 'device.id' => 'device-1',
118
+ 'timestamp' => 1781769600,
119
+ 'position.latitude' => 25.2048,
120
+ 'position.longitude' => 55.2708,
121
+ 'position.speed' => 42,
122
+ 'position.direction' => 91,
123
+ 'position.altitude' => 12,
124
+ 'vehicle.mileage' => 12345,
125
+ 'engine.ignition.status' => true,
126
+ 'fuel.level' => 67,
127
+ ]);
128
+
129
+ expect($event)->toMatchArray([
130
+ 'external_id' => 'message-1',
131
+ 'device_id' => 'device-1',
132
+ 'event_type' => 'telemetry_update',
133
+ 'online' => true,
134
+ 'location' => ['lat' => 25.2048, 'lng' => 55.2708],
135
+ 'speed' => 42,
136
+ 'heading' => 91,
137
+ 'altitude' => 12,
138
+ 'odometer' => 12345,
139
+ 'ignition' => true,
140
+ 'fuel_level' => 67,
141
+ ]);
142
+ expect($event['meta'])->toHaveKeys(['raw', 'provider_status']);
143
+ });
144
+
145
+ test('samsara telemetry variants normalize positional event fields', function () {
146
+ $event = (new SamsaraProvider())->normalizeEvent([
147
+ 'id' => 'event-1',
148
+ 'vehicle' => ['id' => 'vehicle-1'],
149
+ 'time' => '2026-06-18T08:00:00Z',
150
+ 'location' => [
151
+ 'latitude' => 25.2048,
152
+ 'longitude' => 55.2708,
153
+ 'speedMilesPerHour' => 30,
154
+ 'headingDegrees' => 180,
155
+ 'altitudeMeters' => 16,
156
+ ],
157
+ 'odometerMeters' => 1000,
158
+ 'fuelPercent' => 50,
159
+ 'gateway' => ['status' => 'connected', 'online' => true],
160
+ ]);
161
+
162
+ expect($event)->toMatchArray([
163
+ 'external_id' => 'event-1',
164
+ 'device_id' => 'vehicle-1',
165
+ 'event_type' => 'vehicle_update',
166
+ 'online' => true,
167
+ 'location' => ['lat' => 25.2048, 'lng' => 55.2708],
168
+ 'speed' => 30,
169
+ 'heading' => 180,
170
+ 'altitude' => 16,
171
+ 'odometer' => 1000,
172
+ 'fuel_level' => 50,
173
+ ]);
174
+ expect($event['meta']['provider_status'])->toMatchArray([
175
+ 'gateway_status' => 'connected',
176
+ 'online' => true,
177
+ ]);
178
+ });
179
+
180
+ test('safee telemetry includes online and altitude event fields', function () {
181
+ $event = (new SafeeProvider())->normalizeEvent([
182
+ 'id' => 'event-1',
183
+ 'deviceId' => 'device-1',
184
+ 'status' => 'online',
185
+ 'date' => '2026-06-18T08:00:00Z',
186
+ 'lat' => 25.2048,
187
+ 'lon' => 55.2708,
188
+ 'speed' => 30,
189
+ 'heading' => 100,
190
+ 'altitude' => 15,
191
+ ]);
192
+
193
+ expect($event)->toMatchArray([
194
+ 'external_id' => 'event-1',
195
+ 'device_id' => 'device-1',
196
+ 'online' => true,
197
+ 'location' => ['lat' => 25.2048, 'lng' => 55.2708],
198
+ 'speed' => 30,
199
+ 'heading' => 100,
200
+ 'altitude' => 15,
201
+ ]);
202
+ });
203
+
204
+ test('geotab latest log record drives device and event telemetry', function () {
205
+ $payload = [
206
+ 'id' => 'device-1',
207
+ 'name' => 'Truck 1',
208
+ 'deviceType' => 'GO9',
209
+ 'serialNumber' => 'serial-1',
210
+ 'vehicleIdentificationNumber' => 'VIN123',
211
+ 'latest_log_record' => [
212
+ 'id' => 'log-1',
213
+ 'dateTime' => '2026-06-18T08:00:00Z',
214
+ 'latitude' => 25.2048,
215
+ 'longitude' => 55.2708,
216
+ 'speed' => 55,
217
+ 'bearing' => 90,
218
+ 'altitude' => 20,
219
+ 'device' => ['id' => 'device-1'],
220
+ ],
221
+ ];
222
+
223
+ $provider = new GeotabProvider();
224
+ $device = $provider->normalizeDevice($payload);
225
+ $event = $provider->normalizeEvent($payload);
226
+
227
+ expect($device)->toMatchArray([
228
+ 'device_id' => 'device-1',
229
+ 'external_id' => 'device-1',
230
+ 'name' => 'Truck 1',
231
+ 'provider' => 'geotab',
232
+ 'model' => 'GO9',
233
+ 'imei' => 'serial-1',
234
+ 'vin' => 'VIN123',
235
+ 'serial_number' => 'serial-1',
236
+ 'online' => true,
237
+ 'location' => ['lat' => 25.2048, 'lng' => 55.2708],
238
+ 'speed' => 55,
239
+ 'heading' => 90,
240
+ 'altitude' => 20,
241
+ ]);
242
+
243
+ expect($event)->toMatchArray([
244
+ 'external_id' => 'log-1',
245
+ 'device_id' => 'device-1',
246
+ 'event_type' => 'status_data',
247
+ 'online' => true,
248
+ 'location' => ['lat' => 25.2048, 'lng' => 55.2708],
249
+ 'speed' => 55,
250
+ 'heading' => 90,
251
+ 'altitude' => 20,
252
+ ]);
253
+ });
254
+
255
+ test('geotab polling fetches recent log records and merges latest record into device snapshots', function () {
256
+ $requests = [];
257
+
258
+ Http::fake(function ($request) use (&$requests) {
259
+ $requests[] = $request;
260
+ $body = json_decode($request->body(), true);
261
+ $typeName = data_get($body, 'params.typeName');
262
+
263
+ if ($typeName === 'Device') {
264
+ return Http::response([
265
+ 'result' => [
266
+ ['id' => 'device-1', 'name' => 'Truck 1'],
267
+ ['id' => 'device-2', 'name' => 'Truck 2'],
268
+ ],
269
+ ], 200);
270
+ }
271
+
272
+ if ($typeName === 'LogRecord') {
273
+ return Http::response([
274
+ 'result' => [
275
+ ['id' => 'old-log', 'device' => ['id' => 'device-1'], 'dateTime' => '2026-06-18T07:00:00Z', 'latitude' => 1, 'longitude' => 2],
276
+ ['id' => 'new-log', 'device' => ['id' => 'device-1'], 'dateTime' => '2026-06-18T08:00:00Z', 'latitude' => 3, 'longitude' => 4],
277
+ ['id' => 'other-log', 'device' => ['id' => 'other-device'], 'dateTime' => '2026-06-18T08:00:00Z', 'latitude' => 5, 'longitude' => 6],
278
+ ],
279
+ ], 200);
280
+ }
281
+
282
+ return Http::response(['result' => []], 200);
283
+ });
284
+
285
+ $provider = new class extends GeotabProvider {
286
+ public function fetchDevicesForTest(): array
287
+ {
288
+ $this->credentials = [
289
+ 'database' => 'testing-db',
290
+ ];
291
+ $this->sessionId = 'testing-session';
292
+
293
+ return $this->fetchDevices(['limit' => 2, 'from_date' => '2026-06-18T00:00:00Z']);
294
+ }
295
+ };
296
+
297
+ $result = $provider->fetchDevicesForTest();
298
+
299
+ expect($result['devices'])->toHaveCount(2);
300
+ expect($result['devices'][0]['latest_log_record'])->toMatchArray([
301
+ 'id' => 'new-log',
302
+ 'latitude' => 3,
303
+ 'longitude' => 4,
304
+ ]);
305
+ expect($result['devices'][1])->not->toHaveKey('latest_log_record');
306
+ expect($requests)->toHaveCount(2);
307
+ expect($requests[0]->data())->toMatchArray([
308
+ 'method' => 'Get',
309
+ 'params' => [
310
+ 'typeName' => 'Device',
311
+ 'resultsLimit' => 2,
312
+ ],
313
+ ]);
314
+ expect($requests[1]->data())->toMatchArray([
315
+ 'method' => 'Get',
316
+ 'params' => [
317
+ 'typeName' => 'LogRecord',
318
+ 'search' => ['fromDate' => '2026-06-18T00:00:00Z'],
319
+ 'resultsLimit' => 100,
320
+ ],
321
+ ]);
322
+ });
323
+
96
324
  test('telematics details use public id for consumer webhook URLs and do not read ember uuid', function () {
97
325
  $component = file_get_contents(__DIR__ . '/../../addon/components/telematic/details.js');
98
326
  $template = file_get_contents(__DIR__ . '/../../addon/components/telematic/details.hbs');
@@ -444,17 +672,20 @@ test('telematics device sync records provider pagination and skipped device coun
444
672
  ->toContain("method_exists(\$e, 'context') ? \$e->context() : []");
445
673
  });
446
674
 
447
- test('telematics polling command is registered and scheduled for no webhook providers', function () {
675
+ test('telematics polling command is registered and scheduled for discovery providers by default', function () {
448
676
  $command = file_get_contents(__DIR__ . '/../src/Console/Commands/SyncTelematics.php');
449
677
  $provider = file_get_contents(__DIR__ . '/../src/Providers/FleetOpsServiceProvider.php');
450
678
  $details = file_get_contents(__DIR__ . '/../../addon/components/telematic/details.hbs');
451
679
 
452
680
  expect($command)
453
681
  ->toContain("protected \$signature = 'fleetops:sync-telematics")
682
+ ->toContain('{--exclude-webhook-providers : Skip providers that support webhooks}')
454
683
  ->toContain('SyncTelematicDevicesJob::dispatch($telematic')
455
- ->toContain('!$descriptor->supportsWebhooks')
684
+ ->toContain('$excludeWebhookProviders = (bool) $this->option(\'exclude-webhook-providers\')')
685
+ ->toContain('!$excludeWebhookProviders || !$descriptor->supportsWebhooks')
456
686
  ->toContain('$descriptor->supportsDiscovery')
457
- ->toContain("whereIn('status', ['active', 'connected'])");
687
+ ->toContain("whereIn('status', ['active', 'connected'])")
688
+ ->not->toContain('sync-webhook-providers');
458
689
 
459
690
  expect($provider)
460
691
  ->toContain('Console\\Commands\\SyncTelematics::class')
@@ -1,3 +0,0 @@
1
- import Controller from '@ember/controller';
2
-
3
- export default class ConnectivityEventsIndexDetailsController extends Controller {}
@@ -1,3 +0,0 @@
1
- import Route from '@ember/routing/route';
2
-
3
- export default class ConnectivityEventsIndexDetailsRoute extends Route {}