@fleetbase/fleetops-engine 0.6.52 → 0.6.54
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.
- package/addon/components/device/details.hbs +106 -135
- package/addon/components/device/details.js +174 -1
- package/addon/components/device/panel-header.hbs +2 -2
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.hbs +201 -0
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +654 -0
- package/addon/components/layout/fleet-ops-sidebar.hbs +19 -233
- package/addon/components/layout/fleet-ops-sidebar.js +364 -395
- package/addon/components/maintenance-schedule/form.js +1 -0
- package/addon/components/order/form/details.hbs +1 -43
- package/addon/components/order/form/details.js +18 -53
- package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
- package/addon/components/order/form/orchestrator-constraints.js +61 -0
- package/addon/components/order/form/payload.js +50 -2
- package/addon/components/order/form/route.js +15 -0
- package/addon/components/order/form/service-rate.hbs +62 -11
- package/addon/components/order/form/service-rate.js +124 -2
- package/addon/components/order/form.hbs +2 -0
- package/addon/components/telematic/details.hbs +7 -1
- package/addon/components/telematic/form.js +16 -8
- package/addon/components/work-order/details.hbs +5 -1
- package/addon/components/work-order/form.hbs +26 -4
- package/addon/components/work-order/form.js +5 -1
- package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
- package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +74 -0
- package/addon/controllers/connectivity/devices/index/details.js +12 -0
- package/addon/controllers/connectivity/devices/index.js +4 -1
- package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
- package/addon/controllers/connectivity/telematics/details/devices.js +90 -41
- package/addon/controllers/connectivity/telematics/details/events.js +39 -11
- package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
- package/addon/controllers/maintenance/index.js +100 -0
- package/addon/controllers/maintenance/work-orders/index.js +18 -1
- package/addon/controllers/management/fuel-transactions/index.js +1 -1
- package/addon/controllers/management/index.js +88 -0
- package/addon/controllers/settings/index.js +142 -0
- package/addon/extension.js +21 -0
- package/addon/routes/analytics/index.js +3 -0
- package/addon/routes/connectivity/devices/index/details/events.js +37 -1
- package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
- package/addon/routes/connectivity/devices/index/details/vehicle.js +7 -0
- package/addon/routes/connectivity/devices/index.js +3 -0
- package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
- package/addon/routes/connectivity/telematics/details/devices.js +4 -0
- package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
- package/addon/routes/connectivity/telematics/details.js +1 -1
- package/addon/routes/connectivity/telematics/edit.js +1 -1
- package/addon/routes/maintenance/index.js +3 -0
- package/addon/routes/maintenance/work-orders/index.js +3 -0
- package/addon/routes/settings/index.js +3 -0
- package/addon/routes.js +2 -0
- package/addon/services/order-creation.js +54 -1
- package/addon/services/order-validation.js +3 -2
- package/addon/services/service-rate-actions.js +1 -0
- package/addon/styles/fleetops-engine.css +1821 -6
- package/addon/templates/analytics/index.hbs +14 -0
- package/addon/templates/analytics/reports/index.hbs +4 -4
- package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
- package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +84 -0
- package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
- package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
- package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
- package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
- package/addon/templates/maintenance/index.hbs +113 -0
- package/addon/templates/management/fuel-transactions/index/details.hbs +1 -1
- package/addon/templates/management/index.hbs +109 -73
- package/addon/templates/management/vehicles/index/edit.hbs +1 -1
- package/addon/templates/operations/orders/index.hbs +1 -1
- package/addon/templates/operations/service-rates/index.hbs +1 -1
- package/addon/templates/settings/index.hbs +83 -0
- package/addon/templates/settings/routing.hbs +2 -2
- package/addon/utils/fleet-ops-options.js +38 -0
- package/app/components/order/form/orchestrator-constraints.js +1 -0
- package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
- package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
- package/app/controllers/maintenance/index.js +1 -0
- package/app/controllers/management/index.js +1 -0
- package/app/controllers/settings/index.js +1 -0
- package/app/routes/analytics/index.js +1 -0
- package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
- package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
- package/app/routes/maintenance/index.js +1 -0
- package/app/routes/settings/index.js +1 -0
- package/app/templates/analytics/index.js +1 -0
- package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
- package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
- package/app/templates/maintenance/index.js +1 -0
- package/app/templates/management/index.js +1 -0
- package/app/templates/settings/index.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
- package/server/migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php +30 -0
- package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
- package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
- package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -0
- package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
- package/server/src/Console/Commands/SyncTelematics.php +89 -0
- package/server/src/Exceptions/TelematicProviderException.php +13 -2
- package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +112 -11
- package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +16 -5
- package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
- package/server/src/Http/Controllers/Internal/v1/HubController.php +300 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +177 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
- package/server/src/Http/Controllers/Internal/v1/SearchController.php +506 -0
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +94 -10
- package/server/src/Http/Filter/DeviceEventFilter.php +44 -0
- package/server/src/Http/Filter/DeviceFilter.php +131 -0
- package/server/src/Http/Filter/DriverFilter.php +6 -0
- package/server/src/Http/Filter/SensorFilter.php +125 -0
- package/server/src/Http/Filter/VehicleFilter.php +19 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
- package/server/src/Http/Resources/v1/Driver.php +11 -2
- package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
- package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
- package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
- package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +181 -66
- package/server/src/Models/Device.php +15 -1
- package/server/src/Models/DeviceEvent.php +1 -1
- package/server/src/Models/Driver.php +17 -1
- package/server/src/Models/Sensor.php +15 -2
- package/server/src/Models/Telematic.php +1 -1
- package/server/src/Models/Vehicle.php +5 -1
- package/server/src/Models/WorkOrder.php +5 -2
- package/server/src/Observers/DriverObserver.php +3 -3
- package/server/src/Observers/FleetObserver.php +23 -0
- package/server/src/Observers/VehicleObserver.php +3 -3
- package/server/src/Providers/FleetOpsServiceProvider.php +12 -0
- package/server/src/Support/LiveCacheService.php +1 -1
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +188 -28
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +44 -3
- package/server/src/Support/Telematics/TelematicService.php +330 -30
- package/server/src/routes.php +10 -0
- package/server/tests/DeviceFilterTest.php +36 -0
- package/server/tests/HubRoutesTest.php +62 -0
- package/server/tests/OperationsMonitorEndpointTest.php +38 -0
- package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
- package/server/tests/TelematicsHardeningTest.php +455 -3
- package/server/tests/VehicleDriverAssignmentTest.php +27 -0
- package/translations/ar-ae.yml +1 -1
- package/translations/en-us.yaml +1 -0
- package/translations/vi-vn.yaml +1 -1
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Support\Telematics;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Contracts\TelematicProviderInterface;
|
|
6
|
+
use Fleetbase\FleetOps\Events\VehicleLocationChanged;
|
|
5
7
|
use Fleetbase\FleetOps\Jobs\SyncTelematicDevicesJob;
|
|
6
8
|
use Fleetbase\FleetOps\Jobs\TestTelematicConnectionJob;
|
|
7
9
|
use Fleetbase\FleetOps\Models\Device;
|
|
8
10
|
use Fleetbase\FleetOps\Models\DeviceEvent;
|
|
9
11
|
use Fleetbase\FleetOps\Models\Sensor;
|
|
10
12
|
use Fleetbase\FleetOps\Models\Telematic;
|
|
13
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
14
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
|
|
15
|
+
use Illuminate\Support\Carbon;
|
|
11
16
|
use Illuminate\Support\Facades\Crypt;
|
|
12
17
|
use Illuminate\Support\Facades\Validator;
|
|
13
18
|
use Illuminate\Support\Str;
|
|
@@ -21,6 +26,14 @@ use Illuminate\Validation\ValidationException;
|
|
|
21
26
|
*/
|
|
22
27
|
class TelematicService
|
|
23
28
|
{
|
|
29
|
+
protected const PROTECTED_DEVICE_STATUSES = [
|
|
30
|
+
'disabled',
|
|
31
|
+
'decommissioned',
|
|
32
|
+
'maintenance',
|
|
33
|
+
'provisioning',
|
|
34
|
+
'pending_activation',
|
|
35
|
+
];
|
|
36
|
+
|
|
24
37
|
protected TelematicProviderRegistry $registry;
|
|
25
38
|
|
|
26
39
|
public function __construct(TelematicProviderRegistry $registry)
|
|
@@ -168,42 +181,34 @@ class TelematicService
|
|
|
168
181
|
'device_id' => $externalId,
|
|
169
182
|
]);
|
|
170
183
|
|
|
171
|
-
$device
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
$device->provider = $deviceData['provider'] ?? $deviceData['device_provider'] ?? $telematic->provider;
|
|
175
|
-
$device->type = $deviceData['type'] ?? null;
|
|
176
|
-
$device->internal_id = $deviceData['internal_id'] ?? $externalId;
|
|
177
|
-
$device->imei = $deviceData['imei'] ?? null;
|
|
178
|
-
$device->imsi = $deviceData['imsi'] ?? null;
|
|
179
|
-
$device->serial_number = $deviceData['serial_number'] ?? null;
|
|
180
|
-
$device->firmware_version = $deviceData['firmware_version'] ?? null;
|
|
181
|
-
$device->status = $deviceData['status'] ?? $device->status ?? 'active';
|
|
184
|
+
$this->reconcileDeviceTelemetry($device, $telematic, array_merge($deviceData, [
|
|
185
|
+
'external_id' => $externalId,
|
|
186
|
+
]));
|
|
182
187
|
|
|
183
|
-
|
|
184
|
-
$device->online = (bool) $deviceData['online'];
|
|
185
|
-
} elseif (!$device->exists) {
|
|
186
|
-
$device->online = $device->status === 'active';
|
|
187
|
-
}
|
|
188
|
+
$device->save();
|
|
188
189
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
190
|
+
return $device;
|
|
191
|
+
}
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
public function ingestDeviceSnapshot(Telematic $telematic, TelematicProviderInterface $provider, array $payload): array
|
|
194
|
+
{
|
|
195
|
+
$device = $this->linkDevice($telematic, $provider->normalizeDevice($payload));
|
|
196
196
|
|
|
197
|
-
$
|
|
198
|
-
|
|
199
|
-
$
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
$event = null;
|
|
198
|
+
try {
|
|
199
|
+
$eventData = $provider->normalizeEvent($payload);
|
|
200
|
+
if ($this->hasEventSignal($eventData)) {
|
|
201
|
+
$event = $this->storeDeviceEvent($telematic, $eventData, $device);
|
|
202
|
+
}
|
|
203
|
+
} catch (\Throwable) {
|
|
204
|
+
$event = null;
|
|
202
205
|
}
|
|
203
206
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
return [
|
|
208
|
+
'device' => $device,
|
|
209
|
+
'event' => $event,
|
|
210
|
+
'sensors' => $this->storeSnapshotSensors($telematic, $provider, $payload, $device),
|
|
211
|
+
];
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
public function storeDeviceEvent(Telematic $telematic, array $eventData, ?Device $device = null): DeviceEvent
|
|
@@ -214,6 +219,7 @@ class TelematicService
|
|
|
214
219
|
|
|
215
220
|
$eventKey = $this->makeEventKey($telematic, $eventData, $device);
|
|
216
221
|
$event = $eventKey ? DeviceEvent::firstOrNew(['_key' => $eventKey]) : new DeviceEvent();
|
|
222
|
+
$wasRecentlyCreated = !$event->exists;
|
|
217
223
|
$event->company_uuid = $telematic->company_uuid;
|
|
218
224
|
$event->device_uuid = $device?->uuid;
|
|
219
225
|
$event->event_type = $eventData['event_type'] ?? $eventData['type'] ?? 'telemetry_update';
|
|
@@ -228,6 +234,7 @@ class TelematicService
|
|
|
228
234
|
$event->data = $eventData['data'] ?? array_filter([
|
|
229
235
|
'speed' => $eventData['speed'] ?? null,
|
|
230
236
|
'heading' => $eventData['heading'] ?? null,
|
|
237
|
+
'altitude' => $eventData['altitude'] ?? null,
|
|
231
238
|
'odometer' => $eventData['odometer'] ?? null,
|
|
232
239
|
'ignition' => $eventData['ignition'] ?? null,
|
|
233
240
|
'fuel_level' => $eventData['fuel_level'] ?? null,
|
|
@@ -241,6 +248,7 @@ class TelematicService
|
|
|
241
248
|
'occurred_at' => $eventData['occurred_at'] ?? null,
|
|
242
249
|
'speed' => $eventData['speed'] ?? null,
|
|
243
250
|
'heading' => $eventData['heading'] ?? null,
|
|
251
|
+
'altitude' => $eventData['altitude'] ?? null,
|
|
244
252
|
'odometer' => $eventData['odometer'] ?? null,
|
|
245
253
|
'ignition' => $eventData['ignition'] ?? null,
|
|
246
254
|
'fuel_level' => $eventData['fuel_level'] ?? null,
|
|
@@ -252,6 +260,7 @@ class TelematicService
|
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
$event->save();
|
|
263
|
+
$this->applyDeviceEventTelemetry($event, $eventData, $device, $wasRecentlyCreated, $telematic);
|
|
255
264
|
|
|
256
265
|
return $event;
|
|
257
266
|
}
|
|
@@ -470,6 +479,297 @@ class TelematicService
|
|
|
470
479
|
return ['latitude' => (float) $lat, 'longitude' => (float) $lng];
|
|
471
480
|
}
|
|
472
481
|
|
|
482
|
+
protected function reconcileDeviceTelemetry(Device $device, ?Telematic $telematic, array $payload): void
|
|
483
|
+
{
|
|
484
|
+
$externalId = $this->resolveExternalId($payload);
|
|
485
|
+
|
|
486
|
+
if ($telematic) {
|
|
487
|
+
$device->company_uuid = $telematic->company_uuid;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
$this->setDeviceAttributeIfPresent($device, 'name', $payload['name'] ?? $payload['device_name'] ?? (!$device->exists ? 'Unknown Device' : null));
|
|
491
|
+
$this->setDeviceAttributeIfPresent($device, 'model', $payload['model'] ?? $payload['device_model'] ?? null);
|
|
492
|
+
$this->setDeviceAttributeIfPresent($device, 'provider', $payload['provider'] ?? $payload['device_provider'] ?? $telematic?->provider);
|
|
493
|
+
$this->setDeviceAttributeIfPresent($device, 'type', $payload['type'] ?? null);
|
|
494
|
+
$this->setDeviceAttributeIfPresent($device, 'internal_id', $payload['internal_id'] ?? $externalId);
|
|
495
|
+
$this->setDeviceAttributeIfPresent($device, 'imei', $payload['imei'] ?? null);
|
|
496
|
+
$this->setDeviceAttributeIfPresent($device, 'imsi', $payload['imsi'] ?? null);
|
|
497
|
+
$this->setDeviceAttributeIfPresent($device, 'serial_number', $payload['serial_number'] ?? null);
|
|
498
|
+
$this->setDeviceAttributeIfPresent($device, 'firmware_version', $payload['firmware_version'] ?? null);
|
|
499
|
+
|
|
500
|
+
$location = $this->normalizeLocation($payload['location'] ?? null);
|
|
501
|
+
if ($location) {
|
|
502
|
+
$device->last_position = $location;
|
|
503
|
+
} elseif (!$device->exists || !$device->last_position) {
|
|
504
|
+
$device->last_position = $this->defaultLocation();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
$lastSeen = $this->resolveTelemetryTimestamp($payload);
|
|
508
|
+
$reportedOnline = $this->resolveReportedOnline($payload);
|
|
509
|
+
|
|
510
|
+
if (!$lastSeen && $reportedOnline === true) {
|
|
511
|
+
$lastSeen = now();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if ($lastSeen) {
|
|
515
|
+
$device->last_online_at = $lastSeen;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
$connectionStatus = $this->connectionStatusForDevice($device, $lastSeen, $reportedOnline);
|
|
519
|
+
$device->online = $connectionStatus === 'online';
|
|
520
|
+
|
|
521
|
+
if (!$this->isProtectedDeviceStatus($device->status)) {
|
|
522
|
+
$device->status = $connectionStatus;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
$device->meta = array_merge($device->meta ?? [], [
|
|
526
|
+
'external_id' => $externalId,
|
|
527
|
+
'provider_status' => array_filter([
|
|
528
|
+
'status' => $payload['status'] ?? null,
|
|
529
|
+
'state' => $payload['state'] ?? null,
|
|
530
|
+
'active' => $payload['active'] ?? data_get($payload, 'meta.active'),
|
|
531
|
+
'online' => $payload['online'] ?? null,
|
|
532
|
+
'is_online' => $payload['is_online'] ?? null,
|
|
533
|
+
], fn ($value) => $value !== null),
|
|
534
|
+
'telemetry_summary' => array_filter([
|
|
535
|
+
'last_seen_at' => $lastSeen?->toDateTimeString(),
|
|
536
|
+
'status' => $connectionStatus,
|
|
537
|
+
'speed' => $payload['speed'] ?? null,
|
|
538
|
+
'heading' => $payload['heading'] ?? null,
|
|
539
|
+
'altitude' => $payload['altitude'] ?? null,
|
|
540
|
+
'odometer' => $payload['odometer'] ?? null,
|
|
541
|
+
'ignition' => $payload['ignition'] ?? null,
|
|
542
|
+
'fuel_level' => $payload['fuel_level'] ?? null,
|
|
543
|
+
], fn ($value) => $value !== null),
|
|
544
|
+
], $payload['meta'] ?? []);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
protected function setDeviceAttributeIfPresent(Device $device, string $key, mixed $value): void
|
|
548
|
+
{
|
|
549
|
+
if ($value === null || $value === '') {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
$device->{$key} = $value;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
protected function resolveTelemetryTimestamp(array $payload): ?Carbon
|
|
557
|
+
{
|
|
558
|
+
$value = $payload['last_online_at']
|
|
559
|
+
?? $payload['last_seen_at']
|
|
560
|
+
?? $payload['occurred_at']
|
|
561
|
+
?? $payload['recorded_at']
|
|
562
|
+
?? $payload['timestamp']
|
|
563
|
+
?? data_get($payload, 'meta.last_update.occurred_at')
|
|
564
|
+
?? data_get($payload, 'meta.occurred_at')
|
|
565
|
+
?? null;
|
|
566
|
+
|
|
567
|
+
if (!$value) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
return $value instanceof Carbon ? $value : Carbon::parse($value);
|
|
573
|
+
} catch (\Throwable) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
protected function resolveReportedOnline(array $payload): ?bool
|
|
579
|
+
{
|
|
580
|
+
$value = $payload['online'] ?? $payload['is_online'] ?? null;
|
|
581
|
+
|
|
582
|
+
if ($value === null) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $value;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
protected function connectionStatusForDevice(Device $device, ?Carbon $lastSeen = null, ?bool $reportedOnline = null): string
|
|
590
|
+
{
|
|
591
|
+
if ($reportedOnline === true) {
|
|
592
|
+
return 'online';
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
$lastSeen ??= $device->last_online_at;
|
|
596
|
+
|
|
597
|
+
if (!$lastSeen) {
|
|
598
|
+
return 'never_connected';
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
$minutesOffline = Carbon::parse($lastSeen)->diffInMinutes(now());
|
|
602
|
+
|
|
603
|
+
if ($minutesOffline <= 10) {
|
|
604
|
+
return 'online';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if ($minutesOffline <= 60) {
|
|
608
|
+
return 'recently_offline';
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if ($minutesOffline <= 1440) {
|
|
612
|
+
return 'offline';
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return 'long_offline';
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
protected function isProtectedDeviceStatus(?string $status): bool
|
|
619
|
+
{
|
|
620
|
+
return in_array($status, self::PROTECTED_DEVICE_STATUSES, true);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
protected function applyDeviceEventTelemetry(DeviceEvent $event, array $eventData, ?Device $device = null, bool $wasRecentlyCreated = true, ?Telematic $telematic = null): void
|
|
624
|
+
{
|
|
625
|
+
$location = $this->normalizeLocation($eventData['location'] ?? null);
|
|
626
|
+
|
|
627
|
+
$device ??= $event->device;
|
|
628
|
+
if ($device) {
|
|
629
|
+
$this->reconcileDeviceTelemetry($device, $telematic, $eventData);
|
|
630
|
+
$device->save();
|
|
631
|
+
$device->loadMissing('attachable');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (!$location) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
$positionData = $this->makePositionData($location, $eventData);
|
|
639
|
+
if ($wasRecentlyCreated && $positionData) {
|
|
640
|
+
$event->createPosition($positionData);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
$attachable = $device?->attachable;
|
|
644
|
+
if ($attachable instanceof Vehicle) {
|
|
645
|
+
$this->updateVehicleTelemetry($attachable, $location, $eventData, $event);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
protected function updateVehicleTelemetry(Vehicle $vehicle, array $location, array $eventData, DeviceEvent $event): void
|
|
650
|
+
{
|
|
651
|
+
$vehicle->location = new SpatialPoint($location['latitude'], $location['longitude']);
|
|
652
|
+
$vehicle->online = array_key_exists('online', $eventData) && $eventData['online'] !== null ? (bool) $eventData['online'] : true;
|
|
653
|
+
|
|
654
|
+
if (array_key_exists('speed', $eventData) && $eventData['speed'] !== null) {
|
|
655
|
+
$vehicle->speed = $eventData['speed'];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (array_key_exists('heading', $eventData) && $eventData['heading'] !== null) {
|
|
659
|
+
$vehicle->heading = $eventData['heading'];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (array_key_exists('altitude', $eventData) && $eventData['altitude'] !== null) {
|
|
663
|
+
$vehicle->altitude = $eventData['altitude'];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
$vehicle->telematics = array_merge($vehicle->telematics ?? [], [
|
|
667
|
+
'last_event_uuid' => $event->uuid,
|
|
668
|
+
'last_event_id' => $event->public_id,
|
|
669
|
+
'last_event_type' => $event->event_type,
|
|
670
|
+
'last_event_at' => optional($event->occurred_at)->toDateTimeString() ?? now()->toDateTimeString(),
|
|
671
|
+
'last_device_uuid' => $event->device_uuid,
|
|
672
|
+
'last_provider' => $event->provider,
|
|
673
|
+
'last_telemetry_data' => array_filter([
|
|
674
|
+
'speed' => $eventData['speed'] ?? null,
|
|
675
|
+
'heading' => $eventData['heading'] ?? null,
|
|
676
|
+
'altitude' => $eventData['altitude'] ?? null,
|
|
677
|
+
'odometer' => $eventData['odometer'] ?? null,
|
|
678
|
+
'ignition' => $eventData['ignition'] ?? null,
|
|
679
|
+
'fuel_level' => $eventData['fuel_level'] ?? null,
|
|
680
|
+
], fn ($value) => $value !== null),
|
|
681
|
+
]);
|
|
682
|
+
$vehicle->save();
|
|
683
|
+
|
|
684
|
+
broadcast(new VehicleLocationChanged($vehicle, [
|
|
685
|
+
'source' => 'telematics',
|
|
686
|
+
'device_event_uuid' => $event->uuid,
|
|
687
|
+
'provider' => $event->provider,
|
|
688
|
+
]));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
protected function makePositionData(array $location, array $eventData): array
|
|
692
|
+
{
|
|
693
|
+
return array_filter([
|
|
694
|
+
'latitude' => $location['latitude'],
|
|
695
|
+
'longitude' => $location['longitude'],
|
|
696
|
+
'heading' => $eventData['heading'] ?? null,
|
|
697
|
+
'bearing' => $eventData['heading'] ?? null,
|
|
698
|
+
'speed' => $eventData['speed'] ?? null,
|
|
699
|
+
'altitude' => $eventData['altitude'] ?? null,
|
|
700
|
+
], fn ($value) => $value !== null);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
protected function hasEventSignal(array $eventData): bool
|
|
704
|
+
{
|
|
705
|
+
return (bool) array_filter([
|
|
706
|
+
$eventData['event_id'] ?? null,
|
|
707
|
+
$eventData['external_event_id'] ?? null,
|
|
708
|
+
$eventData['ident'] ?? null,
|
|
709
|
+
$eventData['event_type'] ?? null,
|
|
710
|
+
$eventData['occurred_at'] ?? null,
|
|
711
|
+
$eventData['recorded_at'] ?? null,
|
|
712
|
+
$eventData['timestamp'] ?? null,
|
|
713
|
+
$eventData['location']['lat'] ?? null,
|
|
714
|
+
$eventData['location']['latitude'] ?? null,
|
|
715
|
+
$eventData['speed'] ?? null,
|
|
716
|
+
$eventData['heading'] ?? null,
|
|
717
|
+
$eventData['altitude'] ?? null,
|
|
718
|
+
$eventData['odometer'] ?? null,
|
|
719
|
+
$eventData['ignition'] ?? null,
|
|
720
|
+
$eventData['fuel_level'] ?? null,
|
|
721
|
+
], fn ($value) => $value !== null);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
protected function storeSnapshotSensors(Telematic $telematic, TelematicProviderInterface $provider, array $payload, Device $device): int
|
|
725
|
+
{
|
|
726
|
+
$rawSensors = $payload['sensors'] ?? $payload['sensors_last_val'] ?? [];
|
|
727
|
+
if (!is_array($rawSensors) || empty($rawSensors)) {
|
|
728
|
+
return 0;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
$stored = 0;
|
|
732
|
+
$externalId = $this->resolveExternalId($payload);
|
|
733
|
+
|
|
734
|
+
foreach ($this->normalizeRawSensorList($rawSensors) as $rawSensor) {
|
|
735
|
+
try {
|
|
736
|
+
$sensorData = $provider->normalizeSensor(array_merge([
|
|
737
|
+
'device_id' => $externalId,
|
|
738
|
+
], $rawSensor));
|
|
739
|
+
$sensorData['device_id'] ??= $externalId;
|
|
740
|
+
|
|
741
|
+
$this->storeSensor($telematic, $sensorData, $device);
|
|
742
|
+
$stored++;
|
|
743
|
+
} catch (\Throwable) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return $stored;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
protected function normalizeRawSensorList(array $rawSensors): array
|
|
752
|
+
{
|
|
753
|
+
if (array_is_list($rawSensors)) {
|
|
754
|
+
return array_filter($rawSensors, fn ($sensor) => is_array($sensor));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return collect($rawSensors)
|
|
758
|
+
->map(function ($sensor, $name) {
|
|
759
|
+
if (is_array($sensor)) {
|
|
760
|
+
return array_merge(['name' => $name], $sensor);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return [
|
|
764
|
+
'name' => $name,
|
|
765
|
+
'type' => $name,
|
|
766
|
+
'value' => $sensor,
|
|
767
|
+
];
|
|
768
|
+
})
|
|
769
|
+
->values()
|
|
770
|
+
->all();
|
|
771
|
+
}
|
|
772
|
+
|
|
473
773
|
protected function defaultLocation(): array
|
|
474
774
|
{
|
|
475
775
|
return ['latitude' => 0, 'longitude' => 0];
|
package/server/src/routes.php
CHANGED
|
@@ -305,6 +305,8 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
305
305
|
],
|
|
306
306
|
],
|
|
307
307
|
function ($router) {
|
|
308
|
+
$router->get('search', 'SearchController@search');
|
|
309
|
+
|
|
308
310
|
$router->fleetbaseRoutes(
|
|
309
311
|
'contacts',
|
|
310
312
|
function ($router, $controller) {
|
|
@@ -610,6 +612,7 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
610
612
|
$router->get('coordinates', 'LiveController@coordinates');
|
|
611
613
|
$router->get('routes', 'LiveController@routes');
|
|
612
614
|
$router->get('orders', 'LiveController@orders');
|
|
615
|
+
$router->get('operations-monitor', 'LiveController@operationsMonitor');
|
|
613
616
|
$router->get('drivers', 'LiveController@drivers');
|
|
614
617
|
$router->get('vehicles', 'LiveController@vehicles');
|
|
615
618
|
$router->get('places', 'LiveController@places');
|
|
@@ -671,6 +674,13 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
671
674
|
$router->get('live-fleet', 'AnalyticsController@liveFleet');
|
|
672
675
|
}
|
|
673
676
|
);
|
|
677
|
+
$router->group(
|
|
678
|
+
['prefix' => 'hubs'],
|
|
679
|
+
function ($router) {
|
|
680
|
+
$router->get('resources', 'HubController@resources');
|
|
681
|
+
$router->get('maintenance', 'HubController@maintenance');
|
|
682
|
+
}
|
|
683
|
+
);
|
|
674
684
|
$router->group(
|
|
675
685
|
['prefix' => 'getting-started'],
|
|
676
686
|
function ($router) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
test('device filter exposes telematics detail filter contract', function () {
|
|
4
|
+
$filter = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/DeviceFilter.php');
|
|
5
|
+
$model = file_get_contents(dirname(__DIR__) . '/src/Models/Device.php');
|
|
6
|
+
$controller = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/DeviceController.php');
|
|
7
|
+
|
|
8
|
+
expect($model)
|
|
9
|
+
->toContain("'vehicle'")
|
|
10
|
+
->toContain("'connection_status'")
|
|
11
|
+
->toContain("'device_id'")
|
|
12
|
+
->toContain("'last_online_at'")
|
|
13
|
+
->toContain("'updated_at'");
|
|
14
|
+
|
|
15
|
+
expect($filter)
|
|
16
|
+
->toContain('public function query(?string $searchQuery)')
|
|
17
|
+
->toContain('public function deviceId(?string $deviceId)')
|
|
18
|
+
->toContain("where('device_id', 'like'")
|
|
19
|
+
->toContain('public function vehicle(?string $vehicle)')
|
|
20
|
+
->toContain("where('attachable_uuid', \$vehicle)")
|
|
21
|
+
->toContain('public function connectionStatus')
|
|
22
|
+
->toContain("'online'")
|
|
23
|
+
->toContain("'recently_offline'")
|
|
24
|
+
->toContain("'offline'")
|
|
25
|
+
->toContain("'long_offline'")
|
|
26
|
+
->toContain("'never_connected'")
|
|
27
|
+
->toContain('public function lastOnlineAt')
|
|
28
|
+
->toContain('public function updatedAt')
|
|
29
|
+
->toContain('Utils::dateRange')
|
|
30
|
+
->toContain('protected function filterDate');
|
|
31
|
+
|
|
32
|
+
expect($controller)
|
|
33
|
+
->toContain("filled('connection_status')")
|
|
34
|
+
->toContain("filled('last_online_at')")
|
|
35
|
+
->toContain("filled('updated_at')");
|
|
36
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
test('hub routes are registered', function () {
|
|
4
|
+
$routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
|
|
5
|
+
|
|
6
|
+
expect($routes)
|
|
7
|
+
->toContain("['prefix' => 'hubs']")
|
|
8
|
+
->toContain("\$router->get('resources', 'HubController@resources');")
|
|
9
|
+
->toContain("\$router->get('maintenance', 'HubController@maintenance');");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('hub controller exposes resources and maintenance contracts', function () {
|
|
13
|
+
$controller = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/HubController.php');
|
|
14
|
+
|
|
15
|
+
expect($controller)
|
|
16
|
+
->toContain('public function resources(Request $request)')
|
|
17
|
+
->toContain('public function maintenance(Request $request)')
|
|
18
|
+
->toContain("'kpis'")
|
|
19
|
+
->toContain("'actions'")
|
|
20
|
+
->toContain("'sections'")
|
|
21
|
+
->toContain("'docs'");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('resources hub action queue includes cross workflow resource actions', function () {
|
|
25
|
+
$controller = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/HubController.php');
|
|
26
|
+
|
|
27
|
+
expect($controller)
|
|
28
|
+
->toContain("'drivers_without_vehicles'")
|
|
29
|
+
->toContain("'vehicles_without_drivers'")
|
|
30
|
+
->toContain("'vehicles_without_devices'")
|
|
31
|
+
->toContain("'unattached_devices'")
|
|
32
|
+
->toContain("'resource_issues'")
|
|
33
|
+
->toContain("'overdue_vehicle_schedules'")
|
|
34
|
+
->toContain("'open_resource_work_orders'")
|
|
35
|
+
->toContain("'low_stock_parts'")
|
|
36
|
+
->toContain("'unmatched_fuel_transactions'")
|
|
37
|
+
->toContain("'assign_vehicles_to_drivers', 'Assign vehicles to drivers'")
|
|
38
|
+
->toContain("'assign_drivers_to_vehicles', 'Assign drivers to vehicles'")
|
|
39
|
+
->toContain("'attach_devices_to_vehicles', 'Attach devices to vehicles'")
|
|
40
|
+
->toContain("'prepare_vehicle_maintenance', 'Prepare upcoming maintenance'")
|
|
41
|
+
->toContain("'close_overdue_work_orders', 'Close overdue work orders'")
|
|
42
|
+
->toContain("'replenish_low_stock_parts', 'Replenish low-stock parts'")
|
|
43
|
+
->toContain("'review_unmatched_fuel_transactions', 'Review unmatched fuel transactions'")
|
|
44
|
+
->toContain("'add_operating_places', 'Add operating places'")
|
|
45
|
+
->toContain("'add_service_vendors', 'Add vendors for service work'")
|
|
46
|
+
->toContain("'Core resources look ready'");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('resource action target filters are supported', function () {
|
|
50
|
+
$deviceFilter = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/DeviceFilter.php');
|
|
51
|
+
$driverFilter = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/DriverFilter.php');
|
|
52
|
+
$vehicleFilter = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/VehicleFilter.php');
|
|
53
|
+
|
|
54
|
+
expect($deviceFilter)
|
|
55
|
+
->toContain('public function attachmentState')
|
|
56
|
+
->toContain("attachmentState === 'unattached'")
|
|
57
|
+
->toContain('public function status')
|
|
58
|
+
->toContain('public function telematic');
|
|
59
|
+
|
|
60
|
+
expect($driverFilter)->toContain("\$vehicle === 'unassigned'");
|
|
61
|
+
expect($vehicleFilter)->toContain("\$driverId === 'unassigned'");
|
|
62
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
test('operations monitor live route is registered on the internal fleetops surface', function () {
|
|
4
|
+
$routes = file_get_contents(__DIR__ . '/../src/routes.php');
|
|
5
|
+
|
|
6
|
+
expect($routes)
|
|
7
|
+
->toContain("\$router->get('operations-monitor', 'LiveController@operationsMonitor');")
|
|
8
|
+
->toContain("['prefix' => 'live']");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('operations monitor endpoint returns a cached flat resource snapshot with id linked fleets', function () {
|
|
12
|
+
$controller = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/LiveController.php');
|
|
13
|
+
|
|
14
|
+
expect($controller)
|
|
15
|
+
->toContain("LiveCacheService::remember('operations-monitor'")
|
|
16
|
+
->toContain("'drivers' => \$drivers->map")
|
|
17
|
+
->toContain("'vehicles' => \$vehicles->map")
|
|
18
|
+
->toContain("'fleets' => \$this->buildOperationsMonitorFleetTree")
|
|
19
|
+
->toContain("'driver_ids' => \$driverIds->all()")
|
|
20
|
+
->toContain("'vehicle_ids' => \$vehicleIds->all()")
|
|
21
|
+
->toContain("'meta' => [")
|
|
22
|
+
->not->toContain("'drivers' => \$this->whenLoaded('drivers'")
|
|
23
|
+
->not->toContain("'vehicles' => \$this->whenLoaded('vehicles'");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('operations monitor cache is invalidated by live cache and resource mutations', function () {
|
|
27
|
+
$cache = file_get_contents(__DIR__ . '/../src/Support/LiveCacheService.php');
|
|
28
|
+
$driverObserver = file_get_contents(__DIR__ . '/../src/Observers/DriverObserver.php');
|
|
29
|
+
$vehicleObserver = file_get_contents(__DIR__ . '/../src/Observers/VehicleObserver.php');
|
|
30
|
+
$fleetObserver = file_get_contents(__DIR__ . '/../src/Observers/FleetObserver.php');
|
|
31
|
+
$fleetController = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/FleetController.php');
|
|
32
|
+
|
|
33
|
+
expect($cache)->toContain("'operations-monitor'");
|
|
34
|
+
expect($driverObserver)->toContain("LiveCacheService::invalidateMultiple(['drivers', 'operations-monitor'])");
|
|
35
|
+
expect($vehicleObserver)->toContain("LiveCacheService::invalidateMultiple(['vehicles', 'operations-monitor'])");
|
|
36
|
+
expect($fleetObserver)->toContain("LiveCacheService::invalidate('operations-monitor')");
|
|
37
|
+
expect(substr_count($fleetController, "LiveCacheService::invalidate('operations-monitor')"))->toBe(4);
|
|
38
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Jobs\FinalizeApiOrderCreation;
|
|
4
|
+
use Fleetbase\FleetOps\Jobs\FinalizeInternalOrderCreation;
|
|
5
|
+
use Fleetbase\FleetOps\Jobs\NotifyBulkAssignedDriver;
|
|
6
|
+
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
7
|
+
|
|
8
|
+
test('order workflows dispatch named queue jobs instead of signed closures', function () {
|
|
9
|
+
$apiController = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/OrderController.php');
|
|
10
|
+
$internalController = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/OrderController.php');
|
|
11
|
+
|
|
12
|
+
expect($apiController)
|
|
13
|
+
->not->toContain('dispatch(function')
|
|
14
|
+
->toContain('FinalizeApiOrderCreation::dispatch(')
|
|
15
|
+
->toContain(')->afterCommit()')
|
|
16
|
+
->and($internalController)
|
|
17
|
+
->not->toContain('dispatch(function')
|
|
18
|
+
->toContain('FinalizeInternalOrderCreation::dispatch($order->uuid)->afterCommit()')
|
|
19
|
+
->toContain('NotifyBulkAssignedDriver::dispatch($orderUuids->all(), $driver->uuid)->afterCommit()');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('order queue jobs serialize only scalar identifiers and options', function () {
|
|
23
|
+
$apiJob = new FinalizeApiOrderCreation('order-uuid', 'service-quote-uuid', true);
|
|
24
|
+
$internalJob = new FinalizeInternalOrderCreation('order-uuid');
|
|
25
|
+
$bulkJob = new NotifyBulkAssignedDriver(['order-uuid-1', 'order-uuid-2'], 'driver-uuid');
|
|
26
|
+
|
|
27
|
+
expect($apiJob)->toBeInstanceOf(ShouldQueue::class)
|
|
28
|
+
->and($apiJob->orderUuid)->toBe('order-uuid')
|
|
29
|
+
->and($apiJob->serviceQuoteUuid)->toBe('service-quote-uuid')
|
|
30
|
+
->and($apiJob->shouldDispatch)->toBeTrue()
|
|
31
|
+
->and($internalJob)->toBeInstanceOf(ShouldQueue::class)
|
|
32
|
+
->and($internalJob->orderUuid)->toBe('order-uuid')
|
|
33
|
+
->and($bulkJob)->toBeInstanceOf(ShouldQueue::class)
|
|
34
|
+
->and($bulkJob->orderUuids)->toBe(['order-uuid-1', 'order-uuid-2'])
|
|
35
|
+
->and($bulkJob->driverUuid)->toBe('driver-uuid');
|
|
36
|
+
});
|