@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.
- package/addon/components/admin/routing-settings.hbs +2 -1
- package/addon/components/cell/attached-vehicle.hbs +10 -0
- package/addon/components/cell/attached-vehicle.js +58 -0
- package/addon/components/cell/device-identity.hbs +24 -0
- package/addon/components/cell/device-identity.js +129 -0
- package/addon/components/cell/driver-identity.hbs +33 -0
- package/addon/components/cell/driver-identity.js +143 -0
- package/addon/components/cell/equipment-identity.hbs +5 -0
- package/addon/components/cell/equipment-identity.js +48 -0
- package/addon/components/cell/part-identity.hbs +5 -0
- package/addon/components/cell/part-identity.js +63 -0
- package/addon/components/cell/telematic-device.hbs +38 -0
- package/addon/components/cell/telematic-device.js +44 -0
- package/addon/components/cell/telematic-provider.hbs +33 -16
- package/addon/components/cell/telematic-provider.js +50 -8
- package/addon/components/cell/vehicle-identity.hbs +33 -0
- package/addon/components/cell/vehicle-identity.js +138 -0
- package/addon/components/device/form.hbs +2 -2
- package/addon/components/device/form.js +7 -0
- package/addon/components/device/panel-header.hbs +45 -20
- package/addon/components/device/panel-header.js +49 -1
- package/addon/components/device/panel-tabs/events.hbs +30 -0
- package/addon/components/device/panel-tabs/events.js +128 -0
- package/addon/components/device/panel-tabs/sensors.hbs +30 -0
- package/addon/components/device/panel-tabs/sensors.js +93 -0
- package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
- package/addon/components/device/panel-tabs/vehicle.js +90 -0
- package/addon/components/equipment/form.js +8 -1
- package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +8 -0
- package/addon/components/map/drawer/device-event-listing.js +2 -0
- package/addon/components/map/drawer/driver-listing.js +2 -0
- package/addon/components/map/drawer/place-listing.js +2 -0
- package/addon/components/map/drawer/position-listing.hbs +11 -6
- package/addon/components/map/drawer/position-listing.js +17 -4
- package/addon/components/map/drawer/vehicle-listing.js +2 -0
- package/addon/components/sensor/panel-header.hbs +47 -15
- package/addon/components/sensor/panel-header.js +59 -1
- package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
- package/addon/components/table/cell/dropdown/action-item.js +62 -0
- package/addon/components/table/cell/dropdown.hbs +28 -0
- package/addon/components/table/cell/dropdown.js +91 -0
- package/addon/components/vendor/panel-header.hbs +82 -0
- package/addon/components/vendor/panel-header.js +48 -0
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +49 -1
- package/addon/controllers/connectivity/devices/index.js +144 -129
- package/addon/controllers/connectivity/events/details.js +3 -0
- package/addon/controllers/connectivity/events/index.js +187 -11
- package/addon/controllers/connectivity/sensors/index.js +156 -14
- package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
- package/addon/controllers/maintenance/equipment/index.js +1 -2
- package/addon/controllers/maintenance/parts/index.js +1 -2
- package/addon/controllers/management/drivers/index.js +82 -42
- package/addon/controllers/management/fuel-reports/index.js +63 -4
- package/addon/controllers/management/issues/index.js +63 -4
- package/addon/controllers/management/vehicles/index.js +33 -6
- package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
- package/addon/routes/connectivity/devices/index.js +7 -0
- package/addon/routes/connectivity/events/details.js +3 -0
- package/addon/routes/connectivity/events/index.js +6 -2
- package/addon/routes/connectivity/sensors/index.js +7 -2
- package/addon/routes/connectivity/telematics/details/devices.js +2 -0
- package/addon/services/device-actions.js +30 -0
- package/addon/services/device-event-actions.js +1 -1
- package/addon/services/equipment-actions.js +1 -0
- package/addon/services/sensor-actions.js +3 -0
- package/addon/services/service-rate-actions.js +39 -1
- package/addon/services/vehicle-actions.js +24 -12
- package/addon/services/vendor-actions.js +3 -0
- package/addon/styles/fleetops-engine.css +8 -0
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +64 -1
- package/addon/templates/management/vendors/index/details.hbs +2 -2
- package/addon/templates/settings/notifications.hbs +2 -1
- package/addon/templates/settings/routing.hbs +1 -0
- package/addon/utils/device-table-columns.js +288 -0
- package/addon/utils/identity-cell-resource.js +20 -0
- package/addon/utils/map-drawer-dropdown-position.js +47 -0
- package/addon/utils/trackable-option.js +93 -0
- package/app/components/cell/attached-vehicle.js +1 -0
- package/app/components/cell/device-identity.js +1 -0
- package/app/components/cell/driver-identity.js +1 -0
- package/app/components/cell/equipment-identity.js +1 -0
- package/app/components/cell/part-identity.js +1 -0
- package/app/components/cell/telematic-device.js +1 -0
- package/app/components/cell/vehicle-identity.js +1 -0
- package/app/components/device/panel-tabs/events.hbs +30 -0
- package/app/components/device/panel-tabs/events.js +128 -0
- package/app/components/device/panel-tabs/sensors.hbs +30 -0
- package/app/components/device/panel-tabs/sensors.js +93 -0
- package/app/components/device/panel-tabs/vehicle.hbs +68 -0
- package/app/components/device/panel-tabs/vehicle.js +90 -0
- package/app/components/table/cell/dropdown/action-item.js +1 -0
- package/app/components/table/cell/dropdown.js +1 -0
- package/app/components/vendor/panel-header.js +1 -0
- package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
- package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
- package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
- package/app/utils/map-drawer-dropdown-position.js +1 -0
- package/app/utils/trackable-option.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +4 -4
- package/server/src/Console/Commands/SyncTelematics.php +4 -4
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -1
- package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
- package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
- package/server/src/Http/Filter/DeviceFilter.php +24 -1
- package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
- package/server/src/Models/Device.php +2 -0
- package/server/src/Models/DeviceEvent.php +81 -0
- package/server/src/Models/Equipment.php +55 -0
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +1 -0
- package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
- package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +26 -2
- package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
- package/server/src/routes.php +0 -1
- package/server/tests/DeviceFilterTest.php +9 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
- package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
- package/server/tests/ServiceRateRouteValidationTest.php +101 -0
- package/server/tests/TelematicsHardeningTest.php +238 -7
- package/addon/controllers/connectivity/events/index/details.js +0 -3
- package/addon/routes/connectivity/events/index/details.js +0 -3
- /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
4
4
|
|
|
5
|
+
use Illuminate\Support\Carbon;
|
|
5
6
|
use Illuminate\Support\Facades\Http;
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -75,19 +76,21 @@ class GeotabProvider extends AbstractProvider
|
|
|
75
76
|
{
|
|
76
77
|
$limit = $options['limit'] ?? 100;
|
|
77
78
|
|
|
78
|
-
$response =
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
|
|
82
|
-
'database' => $this->credentials['database'],
|
|
83
|
-
'sessionId' => $this->sessionId,
|
|
84
|
-
],
|
|
85
|
-
'typeName' => 'Device',
|
|
86
|
-
'resultsLimit' => $limit,
|
|
87
|
-
],
|
|
88
|
-
])->json();
|
|
79
|
+
$response = $this->apiCall('Get', [
|
|
80
|
+
'typeName' => 'Device',
|
|
81
|
+
'resultsLimit' => $limit,
|
|
82
|
+
]);
|
|
89
83
|
|
|
90
|
-
$devices
|
|
84
|
+
$devices = $response['result'] ?? [];
|
|
85
|
+
$logsByDevice = $this->fetchLatestLogRecords($devices, $options);
|
|
86
|
+
$devices = array_map(function ($device) use ($logsByDevice) {
|
|
87
|
+
$deviceId = $device['id'] ?? null;
|
|
88
|
+
if ($deviceId && isset($logsByDevice[$deviceId])) {
|
|
89
|
+
$device['latest_log_record'] = $logsByDevice[$deviceId];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return $device;
|
|
93
|
+
}, $devices);
|
|
91
94
|
|
|
92
95
|
return [
|
|
93
96
|
'devices' => $devices,
|
|
@@ -98,43 +101,76 @@ class GeotabProvider extends AbstractProvider
|
|
|
98
101
|
|
|
99
102
|
public function fetchDeviceDetails(string $externalId): array
|
|
100
103
|
{
|
|
101
|
-
$response =
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
|
|
105
|
-
'database' => $this->credentials['database'],
|
|
106
|
-
'sessionId' => $this->sessionId,
|
|
107
|
-
],
|
|
108
|
-
'typeName' => 'Device',
|
|
109
|
-
'search' => ['id' => $externalId],
|
|
110
|
-
],
|
|
111
|
-
])->json();
|
|
104
|
+
$response = $this->apiCall('Get', [
|
|
105
|
+
'typeName' => 'Device',
|
|
106
|
+
'search' => ['id' => $externalId],
|
|
107
|
+
]);
|
|
112
108
|
|
|
113
109
|
return $response['result'][0] ?? [];
|
|
114
110
|
}
|
|
115
111
|
|
|
116
112
|
public function normalizeDevice(array $payload): array
|
|
117
113
|
{
|
|
114
|
+
$latestLog = $payload['latest_log_record'] ?? [];
|
|
115
|
+
$hasTelemetry = isset($latestLog['dateTime']) || isset($latestLog['latitude'], $latestLog['longitude']);
|
|
116
|
+
|
|
118
117
|
return [
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
118
|
+
'device_id' => $payload['id'] ?? null,
|
|
119
|
+
'external_id' => $payload['id'] ?? null,
|
|
120
|
+
'name' => $payload['name'] ?? 'Unknown Device',
|
|
121
|
+
'provider' => 'geotab',
|
|
122
|
+
'model' => $payload['deviceType'] ?? null,
|
|
123
|
+
'imei' => $payload['serialNumber'] ?? null,
|
|
124
|
+
'vin' => $payload['vehicleIdentificationNumber'] ?? null,
|
|
125
|
+
'serial_number' => $payload['serialNumber'] ?? null,
|
|
126
|
+
'status' => 'active',
|
|
127
|
+
'online' => $hasTelemetry ? true : null,
|
|
128
|
+
'last_seen_at' => $latestLog ? $this->parseTimestamp($latestLog['dateTime'] ?? null) : null,
|
|
129
|
+
'location' => [
|
|
130
|
+
'lat' => $latestLog['latitude'] ?? null,
|
|
131
|
+
'lng' => $latestLog['longitude'] ?? null,
|
|
132
|
+
],
|
|
133
|
+
'speed' => $latestLog['speed'] ?? null,
|
|
134
|
+
'heading' => $latestLog['bearing'] ?? $latestLog['heading'] ?? null,
|
|
135
|
+
'altitude' => $latestLog['altitude'] ?? null,
|
|
136
|
+
'meta' => [
|
|
137
|
+
'raw' => $payload,
|
|
138
|
+
'provider_status' => array_filter([
|
|
139
|
+
'active_from' => $payload['activeFrom'] ?? null,
|
|
140
|
+
'active_to' => $payload['activeTo'] ?? null,
|
|
141
|
+
'groups' => $payload['groups'] ?? null,
|
|
142
|
+
'has_log' => $hasTelemetry,
|
|
143
|
+
], fn ($value) => $value !== null),
|
|
144
|
+
],
|
|
127
145
|
];
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
public function normalizeEvent(array $payload): array
|
|
131
149
|
{
|
|
150
|
+
$latestLog = $payload['latest_log_record'] ?? $payload;
|
|
151
|
+
$deviceId = data_get($latestLog, 'device.id') ?? $payload['deviceId'] ?? $payload['id'] ?? null;
|
|
152
|
+
$hasTelemetry = isset($latestLog['dateTime']) || isset($latestLog['latitude'], $latestLog['longitude']);
|
|
153
|
+
|
|
132
154
|
return [
|
|
133
|
-
'external_id' => $payload['id'] ?? null,
|
|
134
|
-
'device_id' => $
|
|
155
|
+
'external_id' => $latestLog['id'] ?? $payload['id'] ?? null,
|
|
156
|
+
'device_id' => $deviceId,
|
|
135
157
|
'event_type' => $payload['type'] ?? 'status_data',
|
|
136
|
-
'occurred_at' => $
|
|
137
|
-
'
|
|
158
|
+
'occurred_at' => $this->parseTimestamp($latestLog['dateTime'] ?? null) ?? now(),
|
|
159
|
+
'online' => $hasTelemetry ? true : null,
|
|
160
|
+
'location' => [
|
|
161
|
+
'lat' => $latestLog['latitude'] ?? null,
|
|
162
|
+
'lng' => $latestLog['longitude'] ?? null,
|
|
163
|
+
],
|
|
164
|
+
'speed' => $latestLog['speed'] ?? null,
|
|
165
|
+
'heading' => $latestLog['bearing'] ?? $latestLog['heading'] ?? null,
|
|
166
|
+
'altitude' => $latestLog['altitude'] ?? null,
|
|
167
|
+
'meta' => [
|
|
168
|
+
'raw' => $payload,
|
|
169
|
+
'provider_status' => array_filter([
|
|
170
|
+
'has_log' => $hasTelemetry,
|
|
171
|
+
'device' => data_get($latestLog, 'device.id'),
|
|
172
|
+
], fn ($value) => $value !== null),
|
|
173
|
+
],
|
|
138
174
|
];
|
|
139
175
|
}
|
|
140
176
|
|
|
@@ -179,4 +215,56 @@ class GeotabProvider extends AbstractProvider
|
|
|
179
215
|
{
|
|
180
216
|
return false;
|
|
181
217
|
}
|
|
218
|
+
|
|
219
|
+
protected function apiCall(string $method, array $params): array
|
|
220
|
+
{
|
|
221
|
+
$params['credentials'] = [
|
|
222
|
+
'database' => $this->credentials['database'],
|
|
223
|
+
'sessionId' => $this->sessionId,
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
return Http::post($this->baseUrl, [
|
|
227
|
+
'method' => $method,
|
|
228
|
+
'params' => $params,
|
|
229
|
+
])->json() ?? [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
protected function fetchLatestLogRecords(array $devices, array $options = []): array
|
|
233
|
+
{
|
|
234
|
+
$deviceIds = array_values(array_filter(array_map(fn ($device) => $device['id'] ?? null, $devices)));
|
|
235
|
+
if (empty($deviceIds)) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
$fromDate = $options['from_date'] ?? Carbon::now()->subHours(24)->toIso8601String();
|
|
240
|
+
$response = $this->apiCall('Get', [
|
|
241
|
+
'typeName' => 'LogRecord',
|
|
242
|
+
'search' => ['fromDate' => $fromDate],
|
|
243
|
+
'resultsLimit' => max(count($deviceIds) * 5, 100),
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
$logsByDevice = [];
|
|
247
|
+
foreach ($response['result'] ?? [] as $record) {
|
|
248
|
+
$deviceId = data_get($record, 'device.id');
|
|
249
|
+
if (!$deviceId || !in_array($deviceId, $deviceIds, true)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
$current = $logsByDevice[$deviceId] ?? null;
|
|
254
|
+
if (!$current || Carbon::parse($record['dateTime'] ?? now())->greaterThan(Carbon::parse($current['dateTime'] ?? now()))) {
|
|
255
|
+
$logsByDevice[$deviceId] = $record;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return $logsByDevice;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
protected function parseTimestamp($value): ?string
|
|
263
|
+
{
|
|
264
|
+
if (!$value) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return Carbon::parse($value)->toDateTimeString();
|
|
269
|
+
}
|
|
182
270
|
}
|
|
@@ -110,8 +110,19 @@ class SafeeProvider extends AbstractProvider
|
|
|
110
110
|
'lat' => $position['lat'] ?? null,
|
|
111
111
|
'lng' => $position['lng'] ?? null,
|
|
112
112
|
],
|
|
113
|
-
'
|
|
114
|
-
|
|
113
|
+
'speed' => $payload['speed'] ?? $payload['lastSpeed'] ?? null,
|
|
114
|
+
'heading' => $payload['heading'] ?? $payload['angle'] ?? null,
|
|
115
|
+
'altitude' => $payload['altitude'] ?? $payload['alt'] ?? null,
|
|
116
|
+
'odometer' => $this->extractOdometer($payload),
|
|
117
|
+
'ignition' => $this->extractIgnition($payload),
|
|
118
|
+
'fuel_level' => $this->extractFuelLevel($payload),
|
|
119
|
+
'meta' => [
|
|
120
|
+
'raw' => $payload,
|
|
121
|
+
'provider_status' => array_filter([
|
|
122
|
+
'status' => $rawStatus,
|
|
123
|
+
'normalized' => $status,
|
|
124
|
+
'vehicleStatus' => $payload['vehicleStatus'] ?? null,
|
|
125
|
+
], fn ($value) => $value !== null),
|
|
115
126
|
'plate_number' => $payload['plateNumber'] ?? $payload['plate_number'] ?? null,
|
|
116
127
|
'door_number' => $payload['doorNumber'] ?? null,
|
|
117
128
|
'driver' => $payload['driver'] ?? null,
|
|
@@ -136,12 +147,14 @@ class SafeeProvider extends AbstractProvider
|
|
|
136
147
|
'event_type' => data_get($payload, 'event.code') ?? data_get($payload, 'event.name') ?? 'telemetry_update',
|
|
137
148
|
'message' => data_get($payload, 'event.name') ?? data_get($payload, 'event.message') ?? null,
|
|
138
149
|
'occurred_at' => $this->parseTimestamp($payload['date'] ?? $payload['deviceTime'] ?? $payload['time'] ?? null),
|
|
150
|
+
'online' => $this->resolveOnline($payload),
|
|
139
151
|
'location' => [
|
|
140
152
|
'lat' => $position['lat'] ?? null,
|
|
141
153
|
'lng' => $position['lng'] ?? null,
|
|
142
154
|
],
|
|
143
155
|
'speed' => $payload['speed'] ?? $payload['lastSpeed'] ?? null,
|
|
144
156
|
'heading' => $payload['heading'] ?? $payload['angle'] ?? null,
|
|
157
|
+
'altitude' => $payload['altitude'] ?? $payload['alt'] ?? null,
|
|
145
158
|
'odometer' => $this->extractOdometer($payload),
|
|
146
159
|
'ignition' => $this->extractIgnition($payload),
|
|
147
160
|
'fuel_level' => $this->extractFuelLevel($payload),
|
|
@@ -347,6 +360,17 @@ class SafeeProvider extends AbstractProvider
|
|
|
347
360
|
return in_array(strtolower($status), ['offline', 'inactive', 'deleted', 'expired'], true) ? 'inactive' : 'active';
|
|
348
361
|
}
|
|
349
362
|
|
|
363
|
+
protected function resolveOnline(array $payload): ?bool
|
|
364
|
+
{
|
|
365
|
+
if (array_key_exists('online', $payload)) {
|
|
366
|
+
return filter_var($payload['online'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $payload['online'];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
$status = $payload['status'] ?? $payload['vehicleStatus'] ?? null;
|
|
370
|
+
|
|
371
|
+
return $status ? $this->normalizeVehicleStatus($status) === 'active' : null;
|
|
372
|
+
}
|
|
373
|
+
|
|
350
374
|
protected function extractOdometer(array $payload): mixed
|
|
351
375
|
{
|
|
352
376
|
return data_get($payload, 'canbus.odometer')
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
4
4
|
|
|
5
|
+
use Illuminate\Support\Carbon;
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Class SamsaraProvider.
|
|
7
9
|
*
|
|
@@ -78,30 +80,68 @@ class SamsaraProvider extends AbstractProvider
|
|
|
78
80
|
|
|
79
81
|
public function normalizeDevice(array $payload): array
|
|
80
82
|
{
|
|
83
|
+
$position = $this->extractPosition($payload);
|
|
84
|
+
|
|
81
85
|
return [
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
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'] ??
|
|
97
|
-
'device_id' => $
|
|
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' => $
|
|
102
|
-
'lng' => $
|
|
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
|
}
|
package/server/src/routes.php
CHANGED
|
@@ -476,7 +476,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
476
476
|
$router->delete('bulk-delete', $controller('bulkDelete'));
|
|
477
477
|
$router->get('for-route', $controller('getServicesForRoute'));
|
|
478
478
|
$router->match(['get', 'post'], 'export', $controller('export'));
|
|
479
|
-
$router->get('for-route', $controller('getServicesForRoute'));
|
|
480
479
|
}
|
|
481
480
|
);
|
|
482
481
|
$router->fleetbaseRoutes('tracking-numbers');
|
|
@@ -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
|
|
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
|
|
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,101 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Controllers\Internal\v1\ServiceRateController;
|
|
4
|
+
use Illuminate\Http\Request;
|
|
5
|
+
use Illuminate\Support\Collection;
|
|
6
|
+
|
|
7
|
+
function fleetopsServiceRateRouteRequest(array $query = []): Request
|
|
8
|
+
{
|
|
9
|
+
$request = Request::create('/int/v1/service-rates/for-route', 'GET', $query);
|
|
10
|
+
$session = app('session.store');
|
|
11
|
+
$session->put('company', 'company_test');
|
|
12
|
+
$request->setLaravelSession($session);
|
|
13
|
+
|
|
14
|
+
return $request;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fleetopsServiceRateRouteController()
|
|
18
|
+
{
|
|
19
|
+
return new class extends ServiceRateController {
|
|
20
|
+
public ?Collection $waypoints = null;
|
|
21
|
+
public ?Closure $queryCallback = null;
|
|
22
|
+
|
|
23
|
+
protected function getServicableForWaypoints(Collection $waypoints, Closure $queryCallback): array
|
|
24
|
+
{
|
|
25
|
+
$this->waypoints = $waypoints;
|
|
26
|
+
$this->queryCallback = $queryCallback;
|
|
27
|
+
|
|
28
|
+
return [['id' => 'service_rate_test']];
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('service rates for route requires coordinates', function ($query) {
|
|
34
|
+
$response = fleetopsServiceRateRouteController()->getServicesForRoute(fleetopsServiceRateRouteRequest($query));
|
|
35
|
+
|
|
36
|
+
expect($response->getStatusCode())->toBe(422);
|
|
37
|
+
})->with([
|
|
38
|
+
'missing coordinates' => [[]],
|
|
39
|
+
'empty coordinates' => [['coordinates' => '']],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
test('service rates for route requires at least two coordinates', function () {
|
|
43
|
+
$response = fleetopsServiceRateRouteController()->getServicesForRoute(fleetopsServiceRateRouteRequest([
|
|
44
|
+
'coordinates' => '1.3621663,103.8845049',
|
|
45
|
+
]));
|
|
46
|
+
|
|
47
|
+
expect($response->getStatusCode())->toBe(422);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('service rates for route rejects malformed coordinates', function ($coordinates) {
|
|
51
|
+
$response = fleetopsServiceRateRouteController()->getServicesForRoute(fleetopsServiceRateRouteRequest([
|
|
52
|
+
'coordinates' => $coordinates,
|
|
53
|
+
]));
|
|
54
|
+
|
|
55
|
+
expect($response->getStatusCode())->toBe(422);
|
|
56
|
+
})->with([
|
|
57
|
+
'missing longitude' => ['1.3621663;1.353151,103.86458'],
|
|
58
|
+
'missing latitude' => [',103.8845049;1.353151,103.86458'],
|
|
59
|
+
'blank segment' => ['1.3621663,103.8845049;;1.353151,103.86458'],
|
|
60
|
+
'non numeric' => ['north,103.8845049;1.353151,103.86458'],
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
test('service rates for route converts coordinate pairs to waypoints', function () {
|
|
64
|
+
$controller = fleetopsServiceRateRouteController();
|
|
65
|
+
$response = $controller->getServicesForRoute(fleetopsServiceRateRouteRequest([
|
|
66
|
+
'coordinates' => '1.3621663,103.8845049;1.353151,103.86458',
|
|
67
|
+
'service_type' => 'delivery',
|
|
68
|
+
]));
|
|
69
|
+
|
|
70
|
+
expect($response->getStatusCode())->toBe(200)
|
|
71
|
+
->and($controller->waypoints)->toHaveCount(2)
|
|
72
|
+
->and($controller->waypoints->first()->x())->toBe(103.8845049)
|
|
73
|
+
->and($controller->waypoints->first()->y())->toBe(1.3621663);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('service rates for route ignores placeholder service type values', function ($serviceType) {
|
|
77
|
+
$controller = fleetopsServiceRateRouteController();
|
|
78
|
+
$controller->getServicesForRoute(fleetopsServiceRateRouteRequest([
|
|
79
|
+
'coordinates' => '1.3621663,103.8845049;1.353151,103.86458',
|
|
80
|
+
'service_type' => $serviceType,
|
|
81
|
+
]));
|
|
82
|
+
|
|
83
|
+
$query = new class {
|
|
84
|
+
public array $wheres = [];
|
|
85
|
+
|
|
86
|
+
public function where($column, $value)
|
|
87
|
+
{
|
|
88
|
+
$this->wheres[] = [$column, $value];
|
|
89
|
+
|
|
90
|
+
return $this;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
($controller->queryCallback)($query);
|
|
95
|
+
|
|
96
|
+
expect($query->wheres)->toBe([['company_uuid', 'company_test']]);
|
|
97
|
+
})->with([
|
|
98
|
+
'undefined' => ['undefined'],
|
|
99
|
+
'null' => ['null'],
|
|
100
|
+
'empty' => [''],
|
|
101
|
+
]);
|