@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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
3
|
use Fleetbase\FleetOps\Contracts\TelematicProviderDescriptor;
|
|
4
|
+
use Fleetbase\FleetOps\Support\Telematics\Providers\AfaqyProvider;
|
|
5
|
+
use Fleetbase\FleetOps\Support\Telematics\Providers\SafeeProvider;
|
|
6
|
+
use Illuminate\Http\Client\ConnectionException;
|
|
7
|
+
use Illuminate\Support\Facades\Http;
|
|
4
8
|
|
|
5
9
|
test('device event model and migration expose lifecycle fields used by telematics workflows', function () {
|
|
6
10
|
$model = file_get_contents(__DIR__ . '/../src/Models/DeviceEvent.php');
|
|
@@ -41,7 +45,19 @@ test('telematics service requires provider identity and stores idempotent event
|
|
|
41
45
|
|
|
42
46
|
expect($service)
|
|
43
47
|
->toContain('Provider device identity is required to link a telematics device.')
|
|
48
|
+
->toContain('public function ingestDeviceSnapshot')
|
|
44
49
|
->toContain('DeviceEvent::firstOrNew([\'_key\' => $eventKey])')
|
|
50
|
+
->toContain('reconcileDeviceTelemetry')
|
|
51
|
+
->toContain('PROTECTED_DEVICE_STATUSES')
|
|
52
|
+
->toContain("'provider_status'")
|
|
53
|
+
->toContain("'telemetry_summary'")
|
|
54
|
+
->toContain('connectionStatusForDevice')
|
|
55
|
+
->toContain('applyDeviceEventTelemetry')
|
|
56
|
+
->toContain('$event->createPosition($positionData)')
|
|
57
|
+
->toContain('updateVehicleTelemetry')
|
|
58
|
+
->toContain('broadcast(new VehicleLocationChanged')
|
|
59
|
+
->toContain('storeSnapshotSensors')
|
|
60
|
+
->toContain("\$payload['sensors'] ?? \$payload['sensors_last_val']")
|
|
45
61
|
->toContain('protected function makeEventKey')
|
|
46
62
|
->toContain('$telematic->public_id ?? $telematic->uuid')
|
|
47
63
|
->toContain('resolveWebhookTelematic')
|
|
@@ -49,6 +65,19 @@ test('telematics service requires provider identity and stores idempotent event
|
|
|
49
65
|
->toContain('meta->provider_account_id');
|
|
50
66
|
});
|
|
51
67
|
|
|
68
|
+
test('device details render consistent telematics connection state and timestamp', function () {
|
|
69
|
+
$details = file_get_contents(__DIR__ . '/../../addon/components/device/details.hbs');
|
|
70
|
+
$header = file_get_contents(__DIR__ . '/../../addon/components/device/panel-header.hbs');
|
|
71
|
+
|
|
72
|
+
expect($details)
|
|
73
|
+
->toContain('format-date-fns @resource.last_online_at "dd MMM yyyy, HH:mm"')
|
|
74
|
+
->not->toContain('format-date @resource.last_online_at');
|
|
75
|
+
|
|
76
|
+
expect($header)
|
|
77
|
+
->toContain('@resource.is_online')
|
|
78
|
+
->not->toContain('@resource.online "online"');
|
|
79
|
+
});
|
|
80
|
+
|
|
52
81
|
test('native providers normalize device payloads to canonical FleetOps keys', function () {
|
|
53
82
|
$afaqy = file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/AfaqyProvider.php');
|
|
54
83
|
$safee = file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/SafeeProvider.php');
|
|
@@ -129,19 +158,311 @@ test('device event mark processed action is routed and company scoped', function
|
|
|
129
158
|
->toContain("'Event was already processed.'");
|
|
130
159
|
});
|
|
131
160
|
|
|
132
|
-
test('afaqy sync stores compact device diagnostics and
|
|
161
|
+
test('afaqy sync stores compact device diagnostics and paginates complete units lists', function () {
|
|
133
162
|
$provider = file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/AfaqyProvider.php');
|
|
134
163
|
|
|
135
164
|
expect($provider)
|
|
165
|
+
->toContain('TelematicProviderException')
|
|
136
166
|
->toContain('compactLastUpdate')
|
|
137
167
|
->toContain("'provider_unit_id'")
|
|
138
168
|
->toContain("'plate_number'")
|
|
139
169
|
->toContain("'capabilities'")
|
|
140
170
|
->not->toContain("'raw' => \$payload")
|
|
141
171
|
->not->toContain("'sensors',")
|
|
142
|
-
->not->toContain("?token=' . urlencode")
|
|
143
172
|
->toContain("'Authorization' => 'Bearer ' . \$token")
|
|
144
|
-
->toContain(
|
|
173
|
+
->toContain('protected function authenticatedPost')
|
|
174
|
+
->toContain('protected function refreshToken')
|
|
175
|
+
->toContain('protected function isTokenRejected')
|
|
176
|
+
->toContain('protected int $dataTimeout = 120')
|
|
177
|
+
->toContain('protected int $connectTimeout = 15')
|
|
178
|
+
->toContain('protected int $connectionTestTimeout = 30')
|
|
179
|
+
->toContain('protected int $connectionTestConnectTimeout = 10')
|
|
180
|
+
->toContain('->timeout($timeout)')
|
|
181
|
+
->toContain('->connectTimeout($connectTimeout)')
|
|
182
|
+
->toContain('ConnectionException')
|
|
183
|
+
->toContain('transportErrorContext')
|
|
184
|
+
->toContain('extractBytesReceived')
|
|
185
|
+
->toContain("'requested_limit'")
|
|
186
|
+
->toContain("'requested_offset'")
|
|
187
|
+
->toContain("'bytes_received'")
|
|
188
|
+
->toContain('AFAQY token rejected; refreshing token and retrying request')
|
|
189
|
+
->toContain('AFAQY token rejected after refresh with status')
|
|
190
|
+
->toContain('AFAQY token rejected and username/password credentials are required to refresh it.')
|
|
191
|
+
->toContain('providerErrorContext')
|
|
192
|
+
->toContain("'endpoint'")
|
|
193
|
+
->toContain("'provider_code'")
|
|
194
|
+
->toContain("'provider_message'")
|
|
195
|
+
->toContain("'retry_attempted'")
|
|
196
|
+
->toContain('?? 500), 500')
|
|
197
|
+
->toContain('if (is_array($filters) && empty($filters))')
|
|
198
|
+
->toContain('$filters = new \stdClass();')
|
|
199
|
+
->toContain("'limit' => \$limit")
|
|
200
|
+
->toContain("'offset' => \$offset")
|
|
201
|
+
->not->toContain("\n 'limit' => \$limit,\n 'offset' => \$offset,")
|
|
202
|
+
->toContain("\$this->afaqyPost('/units/lists', [")
|
|
203
|
+
->toContain('], true);')
|
|
204
|
+
->toContain("http_build_query(['token' => \$this->credentials['token']])")
|
|
205
|
+
->toContain("\$body = \$tokenInQuery ? \$payload : array_merge(['token' => \$this->credentials['token']], \$payload)")
|
|
206
|
+
->toContain('$pageOffset = (int) ($pagination[\'offset\'] ?? $offset);')
|
|
207
|
+
->toContain('$pageLimit = (int) ($pagination[\'limit\'] ?? $limit);')
|
|
208
|
+
->toContain('$advanceBy = $resultCount > 0 ? $resultCount : max($pageLimit, count($devices), 1);')
|
|
209
|
+
->toContain('$nextCursor = ($pageOffset + $advanceBy) < $total ? $pageOffset + $advanceBy : null;')
|
|
210
|
+
->toContain("'requested_limit'")
|
|
211
|
+
->toContain("'provider_limit'")
|
|
212
|
+
->toContain("'provider_result_count'")
|
|
213
|
+
->toContain("'pagination' => [")
|
|
214
|
+
->toContain("'allCount'")
|
|
215
|
+
->toContain("'filtersCount'")
|
|
216
|
+
->toContain("'resultCount'")
|
|
217
|
+
->toContain("'online' => \$payload['active'] ?? null")
|
|
218
|
+
->toContain("'altitude' => \$lastUpdate['alt'] ?? null");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('afaqy sync keeps default limit and uses extended data request path', function () {
|
|
222
|
+
$requests = [];
|
|
223
|
+
|
|
224
|
+
Http::fake(function ($request) use (&$requests) {
|
|
225
|
+
$requests[] = $request;
|
|
226
|
+
|
|
227
|
+
if (str_ends_with($request->url(), '/auth/login')) {
|
|
228
|
+
return Http::response(['data' => ['token' => 'testing-token']], 200);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Http::response([
|
|
232
|
+
'data' => [],
|
|
233
|
+
'pagination' => ['resultCount' => 0],
|
|
234
|
+
], 200);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
$provider = new class extends AfaqyProvider {
|
|
238
|
+
public function fetchDevicesForTest(array $credentials): array
|
|
239
|
+
{
|
|
240
|
+
$this->credentials = $credentials;
|
|
241
|
+
$this->prepareAuthentication();
|
|
242
|
+
|
|
243
|
+
return $this->fetchDevices();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
$result = $provider->fetchDevicesForTest([
|
|
248
|
+
'base_url' => 'https://api.afaqy.test',
|
|
249
|
+
'username' => 'testing-user',
|
|
250
|
+
'password' => 'testing-password',
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
expect($result['pagination']['resultCount'])->toBe(0);
|
|
254
|
+
expect($requests)->toHaveCount(2);
|
|
255
|
+
expect($requests[0]->url())->toBe('https://api.afaqy.test/auth/login');
|
|
256
|
+
expect($requests[1]->url())->toContain('/units/lists?token=testing-token');
|
|
257
|
+
expect($requests[1]->body())->toContain('"limit":500');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('afaqy sync timeout failures are converted to sanitized provider metadata', function () {
|
|
261
|
+
Http::fake(function ($request) {
|
|
262
|
+
if (str_ends_with($request->url(), '/auth/login')) {
|
|
263
|
+
return Http::response(['data' => ['token' => 'timeout-testing-token']], 200);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
throw new ConnectionException('cURL error 28: Operation timed out after 30000 milliseconds with 1186621 bytes received for https://api.afaqy.test/units/lists?token=timeout-testing-token');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
$provider = new class extends AfaqyProvider {
|
|
270
|
+
public function fetchDevicesForTest(array $credentials): array
|
|
271
|
+
{
|
|
272
|
+
$this->credentials = $credentials;
|
|
273
|
+
$this->prepareAuthentication();
|
|
274
|
+
|
|
275
|
+
return $this->fetchDevices();
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
$provider->fetchDevicesForTest([
|
|
281
|
+
'base_url' => 'https://api.afaqy.test',
|
|
282
|
+
'username' => 'testing-user',
|
|
283
|
+
'password' => 'testing-password',
|
|
284
|
+
]);
|
|
285
|
+
} catch (Throwable $e) {
|
|
286
|
+
$result = [
|
|
287
|
+
'success' => false,
|
|
288
|
+
'message' => $e->getMessage(),
|
|
289
|
+
'metadata' => method_exists($e, 'context') ? $e->context() : [],
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
expect($result['success'])->toBeFalse();
|
|
294
|
+
expect($result['message'])->toBe('AFAQY API request timed out while waiting for provider response.');
|
|
295
|
+
expect($result['metadata'])->toMatchArray([
|
|
296
|
+
'provider' => 'afaqy',
|
|
297
|
+
'endpoint' => '/units/lists',
|
|
298
|
+
'requested_limit' => 500,
|
|
299
|
+
'requested_offset' => 0,
|
|
300
|
+
'timeout' => 120,
|
|
301
|
+
'connect_timeout' => 15,
|
|
302
|
+
'bytes_received' => 1186621,
|
|
303
|
+
'retry_attempted' => false,
|
|
304
|
+
'transport_error' => 'connection_exception',
|
|
305
|
+
]);
|
|
306
|
+
expect(json_encode($result))
|
|
307
|
+
->not->toContain('timeout-testing-token')
|
|
308
|
+
->not->toContain('token=')
|
|
309
|
+
->not->toContain('testing-password')
|
|
310
|
+
->not->toContain('testing-user');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('afaqy credential test refreshes token once when units list rejects token', function () {
|
|
314
|
+
$authCount = 0;
|
|
315
|
+
$requests = [];
|
|
316
|
+
|
|
317
|
+
Http::fake(function ($request) use (&$authCount, &$requests) {
|
|
318
|
+
$requests[] = $request;
|
|
319
|
+
|
|
320
|
+
if (str_ends_with($request->url(), '/auth/login')) {
|
|
321
|
+
$authCount++;
|
|
322
|
+
|
|
323
|
+
return Http::response(['data' => ['token' => $authCount === 1 ? 'first-testing-token' : 'second-testing-token']], 200);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (str_contains($request->url(), '/units/lists?token=first-testing-token')) {
|
|
327
|
+
return Http::response(['message' => 'Token expired', 'code' => 'TOKEN_EXPIRED'], 401);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return Http::response([
|
|
331
|
+
'data' => [['_id' => 'unit-1', 'name' => 'Truck 1']],
|
|
332
|
+
'pagination' => ['resultCount' => 1],
|
|
333
|
+
], 200);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
$result = (new AfaqyProvider())->testConnection([
|
|
337
|
+
'base_url' => 'https://api.afaqy.test',
|
|
338
|
+
'username' => 'testing-user',
|
|
339
|
+
'password' => 'testing-password',
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
expect($result['success'])->toBeTrue();
|
|
343
|
+
expect($authCount)->toBe(2);
|
|
344
|
+
expect(collect($requests)->filter(fn ($request) => str_contains($request->url(), '/units/lists'))->count())->toBe(2);
|
|
345
|
+
expect($requests[1]->url())->toContain('/units/lists?token=first-testing-token');
|
|
346
|
+
expect($requests[2]->url())->toContain('/auth/login');
|
|
347
|
+
expect($requests[3]->url())->toContain('/units/lists?token=second-testing-token');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('afaqy token rejection failure metadata is sanitized', function () {
|
|
351
|
+
$authCount = 0;
|
|
352
|
+
|
|
353
|
+
Http::fake(function ($request) use (&$authCount) {
|
|
354
|
+
if (str_ends_with($request->url(), '/auth/login')) {
|
|
355
|
+
$authCount++;
|
|
356
|
+
|
|
357
|
+
return Http::response(['data' => ['token' => 'rejected-testing-token-' . $authCount]], 200);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return Http::response(['message' => 'Token rejected', 'code' => 'TOKEN_REJECTED'], 401);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
$result = (new AfaqyProvider())->testConnection([
|
|
364
|
+
'base_url' => 'https://api.afaqy.test',
|
|
365
|
+
'username' => 'testing-user',
|
|
366
|
+
'password' => 'testing-password',
|
|
367
|
+
]);
|
|
368
|
+
|
|
369
|
+
expect($result['success'])->toBeFalse();
|
|
370
|
+
expect($result['message'])->toBe('AFAQY token rejected after refresh with status 401');
|
|
371
|
+
expect($result['metadata'])->toMatchArray([
|
|
372
|
+
'provider' => 'afaqy',
|
|
373
|
+
'endpoint' => '/units/lists',
|
|
374
|
+
'status_code' => 401,
|
|
375
|
+
'provider_code' => 'TOKEN_REJECTED',
|
|
376
|
+
'provider_message' => 'Token rejected',
|
|
377
|
+
'retry_attempted' => true,
|
|
378
|
+
]);
|
|
379
|
+
expect(json_encode($result))
|
|
380
|
+
->not->toContain('rejected-testing-token')
|
|
381
|
+
->not->toContain('testing-password')
|
|
382
|
+
->not->toContain('testing-user');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('afaqy supplied token rejection requires password credentials for refresh', function () {
|
|
386
|
+
Http::fake([
|
|
387
|
+
'https://api.afaqy.test/units/lists?token=static-testing-token' => Http::response(['message' => 'Token rejected'], 401),
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
$result = (new AfaqyProvider())->testConnection([
|
|
391
|
+
'base_url' => 'https://api.afaqy.test',
|
|
392
|
+
'token' => 'static-testing-token',
|
|
393
|
+
]);
|
|
394
|
+
|
|
395
|
+
expect($result['success'])->toBeFalse();
|
|
396
|
+
expect($result['message'])->toBe('AFAQY token rejected and username/password credentials are required to refresh it.');
|
|
397
|
+
expect($result['metadata'])->toMatchArray([
|
|
398
|
+
'provider' => 'afaqy',
|
|
399
|
+
'endpoint' => '/units/lists',
|
|
400
|
+
'status_code' => 401,
|
|
401
|
+
'provider_message' => 'Token rejected',
|
|
402
|
+
'retry_attempted' => false,
|
|
403
|
+
]);
|
|
404
|
+
expect(json_encode($result))->not->toContain('static-testing-token');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('telematics device sync records provider pagination and skipped device counts', function () {
|
|
408
|
+
$job = file_get_contents(__DIR__ . '/../src/Jobs/SyncTelematicDevicesJob.php');
|
|
409
|
+
$controller = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/TelematicController.php');
|
|
410
|
+
|
|
411
|
+
expect($controller)
|
|
412
|
+
->toContain("'limit' => \$request->input('limit')")
|
|
413
|
+
->not->toContain("'limit' => \$request->input('limit', 100)");
|
|
414
|
+
|
|
415
|
+
expect($job)
|
|
416
|
+
->toContain('public int $tries = 1')
|
|
417
|
+
->toContain("'limit' => \$this->options['limit'] ?? null")
|
|
418
|
+
->toContain('Cache::lock($lockKey, $this->timeout + 60)')
|
|
419
|
+
->toContain("'fleetops:sync-telematic-devices:' . \$this->telematic->uuid")
|
|
420
|
+
->toContain('Device discovery skipped because another sync is already running')
|
|
421
|
+
->toContain("'last_sync_skipped_reason'")
|
|
422
|
+
->toContain("'sync_already_running'")
|
|
423
|
+
->toContain('$totalFetched')
|
|
424
|
+
->toContain('$totalLinked')
|
|
425
|
+
->toContain('$totalEvents')
|
|
426
|
+
->toContain('$totalSensors')
|
|
427
|
+
->toContain('$totalSkipped')
|
|
428
|
+
->toContain('$pageCount')
|
|
429
|
+
->toContain('$service->ingestDeviceSnapshot($this->telematic, $provider, $devicePayload)')
|
|
430
|
+
->toContain('Device discovery page fetched')
|
|
431
|
+
->toContain("'provider_unit_id'")
|
|
432
|
+
->toContain("'last_sync_fetched_total'")
|
|
433
|
+
->toContain("'last_sync_linked_total'")
|
|
434
|
+
->toContain("'last_sync_events_total'")
|
|
435
|
+
->toContain("'last_sync_sensors_total'")
|
|
436
|
+
->toContain("'last_sync_skipped_total'")
|
|
437
|
+
->toContain("'last_sync_page_count'")
|
|
438
|
+
->toContain("'last_sync_provider_total'")
|
|
439
|
+
->toContain("'last_sync_provider_all_count'")
|
|
440
|
+
->toContain("'last_sync_provider_filters_count'")
|
|
441
|
+
->toContain("'last_sync_error_context'")
|
|
442
|
+
->toContain('safeSyncErrorMessage')
|
|
443
|
+
->toContain('token=|password|client_secret|authorization')
|
|
444
|
+
->toContain("method_exists(\$e, 'context') ? \$e->context() : []");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('telematics polling command is registered and scheduled for no webhook providers', function () {
|
|
448
|
+
$command = file_get_contents(__DIR__ . '/../src/Console/Commands/SyncTelematics.php');
|
|
449
|
+
$provider = file_get_contents(__DIR__ . '/../src/Providers/FleetOpsServiceProvider.php');
|
|
450
|
+
$details = file_get_contents(__DIR__ . '/../../addon/components/telematic/details.hbs');
|
|
451
|
+
|
|
452
|
+
expect($command)
|
|
453
|
+
->toContain("protected \$signature = 'fleetops:sync-telematics")
|
|
454
|
+
->toContain('SyncTelematicDevicesJob::dispatch($telematic')
|
|
455
|
+
->toContain('!$descriptor->supportsWebhooks')
|
|
456
|
+
->toContain('$descriptor->supportsDiscovery')
|
|
457
|
+
->toContain("whereIn('status', ['active', 'connected'])");
|
|
458
|
+
|
|
459
|
+
expect($provider)
|
|
460
|
+
->toContain('Console\\Commands\\SyncTelematics::class')
|
|
461
|
+
->toContain("command('fleetops:sync-telematics')->everyMinute()");
|
|
462
|
+
|
|
463
|
+
expect($details)
|
|
464
|
+
->toContain('Provider polling')
|
|
465
|
+
->toContain('FleetOps polls this provider for device snapshots and telemetry updates.');
|
|
145
466
|
});
|
|
146
467
|
|
|
147
468
|
test('native endpoint fields are advanced optional overrides with provider defaults', function () {
|
|
@@ -162,6 +483,90 @@ test('native endpoint fields are advanced optional overrides with provider defau
|
|
|
162
483
|
}
|
|
163
484
|
});
|
|
164
485
|
|
|
486
|
+
test('safee credential test sends documented form auth request to custom server uri', function () {
|
|
487
|
+
$requests = [];
|
|
488
|
+
|
|
489
|
+
Http::fake(function ($request) use (&$requests) {
|
|
490
|
+
$requests[] = $request;
|
|
491
|
+
|
|
492
|
+
if (str_ends_with($request->url(), '/protocol/openid-connect/token')) {
|
|
493
|
+
return Http::response(['access_token' => 'testing-access-token'], 200);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return Http::response([
|
|
497
|
+
'code' => 0,
|
|
498
|
+
'time' => 1509946353.033,
|
|
499
|
+
'status' => 'success',
|
|
500
|
+
'message' => 'operation completed successfully',
|
|
501
|
+
], 200);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
$result = (new SafeeProvider())->testConnection([
|
|
505
|
+
'server_uri' => ' https://fms.example.test/ ',
|
|
506
|
+
'realm_id' => 'dsco',
|
|
507
|
+
'client_id' => 'api',
|
|
508
|
+
'client_secret' => 'testing-client-secret',
|
|
509
|
+
'username' => 'testing-user',
|
|
510
|
+
'password' => 'testing-password',
|
|
511
|
+
]);
|
|
512
|
+
|
|
513
|
+
expect($result['success'])->toBeTrue();
|
|
514
|
+
expect($result['metadata'])
|
|
515
|
+
->toMatchArray([
|
|
516
|
+
'auth_host' => 'https://fms.example.test',
|
|
517
|
+
'auth_path' => '/auth/realms/dsco/protocol/openid-connect/token',
|
|
518
|
+
'realm_id' => 'dsco',
|
|
519
|
+
]);
|
|
520
|
+
|
|
521
|
+
expect($requests)->toHaveCount(2);
|
|
522
|
+
|
|
523
|
+
$tokenRequest = $requests[0];
|
|
524
|
+
parse_str($tokenRequest->body(), $tokenBody);
|
|
525
|
+
|
|
526
|
+
expect($tokenRequest->method())->toBe('POST');
|
|
527
|
+
expect($tokenRequest->url())->toBe('https://fms.example.test/auth/realms/dsco/protocol/openid-connect/token');
|
|
528
|
+
expect(implode(' ', (array) $tokenRequest->header('Content-Type')))->toContain('application/x-www-form-urlencoded');
|
|
529
|
+
expect($tokenBody)->toMatchArray([
|
|
530
|
+
'grant_type' => 'password',
|
|
531
|
+
'client_secret' => 'testing-client-secret',
|
|
532
|
+
'client_id' => 'api',
|
|
533
|
+
'username' => 'testing-user',
|
|
534
|
+
'password' => 'testing-password',
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
expect($requests[1]->method())->toBe('GET');
|
|
538
|
+
expect($requests[1]->url())->toBe('https://fms.example.test/api/v2/status');
|
|
539
|
+
expect(implode(' ', (array) $requests[1]->header('Authorization')))->toBe('Bearer testing-access-token');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('safee credential test reports token endpoint 401 with sanitized auth context', function () {
|
|
543
|
+
Http::fake([
|
|
544
|
+
'https://fms.example.test/auth/realms/dsco/protocol/openid-connect/token' => Http::response(['error' => 'unauthorized'], 401),
|
|
545
|
+
]);
|
|
546
|
+
|
|
547
|
+
$result = (new SafeeProvider())->testConnection([
|
|
548
|
+
'server_uri' => 'https://fms.example.test',
|
|
549
|
+
'realm_id' => 'dsco',
|
|
550
|
+
'client_id' => 'api',
|
|
551
|
+
'client_secret' => 'testing-client-secret',
|
|
552
|
+
'username' => 'testing-user',
|
|
553
|
+
'password' => 'testing-password',
|
|
554
|
+
]);
|
|
555
|
+
|
|
556
|
+
expect($result['success'])->toBeFalse();
|
|
557
|
+
expect($result['message'])->toBe('Safee authentication failed with status 401');
|
|
558
|
+
expect($result['metadata'])
|
|
559
|
+
->toMatchArray([
|
|
560
|
+
'auth_host' => 'https://fms.example.test',
|
|
561
|
+
'auth_path' => '/auth/realms/dsco/protocol/openid-connect/token',
|
|
562
|
+
'realm_id' => 'dsco',
|
|
563
|
+
])
|
|
564
|
+
->not->toHaveKey('client_secret')
|
|
565
|
+
->not->toHaveKey('password')
|
|
566
|
+
->not->toHaveKey('access_token')
|
|
567
|
+
->not->toHaveKey('refresh_token');
|
|
568
|
+
});
|
|
569
|
+
|
|
165
570
|
test('telematics activity logging excludes large json and spatial payloads', function () {
|
|
166
571
|
$device = telematics_activity_log_method(file_get_contents(__DIR__ . '/../src/Models/Device.php'));
|
|
167
572
|
$sensor = telematics_activity_log_method(file_get_contents(__DIR__ . '/../src/Models/Sensor.php'));
|
|
@@ -260,6 +665,53 @@ test('telematics overview attention items render as full width stacked alerts',
|
|
|
260
665
|
->not->toContain('lg:grid-cols-2');
|
|
261
666
|
});
|
|
262
667
|
|
|
668
|
+
test('device attachment morph types are normalized and legacy aliases are tolerated', function () {
|
|
669
|
+
$migration = file_get_contents(__DIR__ . '/../migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php');
|
|
670
|
+
$command = file_get_contents(__DIR__ . '/../src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php');
|
|
671
|
+
$serviceProvider = file_get_contents(__DIR__ . '/../src/Providers/FleetOpsServiceProvider.php');
|
|
672
|
+
|
|
673
|
+
expect($migration)
|
|
674
|
+
->toContain("whereNotNull('attachable_uuid')")
|
|
675
|
+
->toContain("'Fleetbase\\\\Models\\\\Vehicle'")
|
|
676
|
+
->toContain("'\\\\Fleetbase\\\\Models\\\\Vehicle'")
|
|
677
|
+
->toContain("'attachable_type' => Vehicle::class")
|
|
678
|
+
->toContain('Intentionally do not restore invalid legacy morph class names.');
|
|
679
|
+
|
|
680
|
+
expect($command)
|
|
681
|
+
->toContain('\\Fleetbase\\FleetOps\\Models\\Device::class')
|
|
682
|
+
->toContain("'columns' => ['attachable_type']");
|
|
683
|
+
|
|
684
|
+
expect($serviceProvider)
|
|
685
|
+
->toContain('use Illuminate\Database\Eloquent\Relations\Relation;')
|
|
686
|
+
->toContain('$this->registerMorphMap();')
|
|
687
|
+
->toContain("'Fleetbase\\\\Models\\\\Vehicle' => \\Fleetbase\\FleetOps\\Models\\Vehicle::class")
|
|
688
|
+
->toContain("'\\\\Fleetbase\\\\Models\\\\Vehicle' => \\Fleetbase\\FleetOps\\Models\\Vehicle::class");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test('internal device attachment endpoints return specific api errors instead of raw model misses', function () {
|
|
692
|
+
$vehicleController = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/VehicleController.php');
|
|
693
|
+
$deviceController = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/DeviceController.php');
|
|
694
|
+
|
|
695
|
+
expect($vehicleController)
|
|
696
|
+
->toContain('$vehicle = $this->resolveVehicle($id);')
|
|
697
|
+
->toContain('$device = $this->resolveDevice($deviceId);')
|
|
698
|
+
->toContain('Vehicle not found or not available for this organization.')
|
|
699
|
+
->toContain('Device not found or not available for this organization.')
|
|
700
|
+
->toContain('Unable to attach device to vehicle. Please try again or contact support.')
|
|
701
|
+
->toContain('Unable to detach device from vehicle. Please try again or contact support.')
|
|
702
|
+
->toContain('logDeviceAttachmentLookupFailure')
|
|
703
|
+
->toContain('logDeviceAttachmentFailure');
|
|
704
|
+
|
|
705
|
+
expect($deviceController)
|
|
706
|
+
->toContain('$device = $this->resolveDevice($id);')
|
|
707
|
+
->toContain('$vehicle = $this->resolveVehicle($vehicleId);')
|
|
708
|
+
->toContain('Vehicle not found or not available for this organization.')
|
|
709
|
+
->toContain('Device not found or not available for this organization.')
|
|
710
|
+
->toContain('Unable to attach device to vehicle. Please try again or contact support.')
|
|
711
|
+
->toContain('Unable to detach device from vehicle. Please try again or contact support.')
|
|
712
|
+
->not->toContain('firstOrFail();');
|
|
713
|
+
});
|
|
714
|
+
|
|
263
715
|
function telematics_activity_log_method(string $model): string
|
|
264
716
|
{
|
|
265
717
|
preg_match('/public function getActivitylogOptions\(\): LogOptions\s+\{(?P<body>.*?)\n \}/s', $model, $matches);
|
|
@@ -17,6 +17,33 @@ test('vehicle updates preserve driver assignment unless driver input is explicit
|
|
|
17
17
|
->toContain("Driver::where('vehicle_uuid', \$this->uuid)->update(['vehicle_uuid' => null])");
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
test('driver vehicle assignment preserves license expiry', function () {
|
|
21
|
+
$apiController = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/DriverController.php');
|
|
22
|
+
$internalController = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/DriverController.php');
|
|
23
|
+
$driverModel = file_get_contents(dirname(__DIR__) . '/src/Models/Driver.php');
|
|
24
|
+
$driverResource = file_get_contents(dirname(__DIR__) . '/src/Http/Resources/v1/Driver.php');
|
|
25
|
+
|
|
26
|
+
expect($apiController)
|
|
27
|
+
->not->toContain('normalizeDriverUpdateInput')
|
|
28
|
+
->not->toContain("array_key_exists('license_expiry', \$input)")
|
|
29
|
+
->and($internalController)
|
|
30
|
+
->not->toContain('normalizeDriverUpdateInput')
|
|
31
|
+
->toContain('public function assignVehicle(Request $request, string $id): JsonResponse')
|
|
32
|
+
->toContain('$driver->assignVehicle($vehicle)')
|
|
33
|
+
->not->toContain("'license_expiry' => null")
|
|
34
|
+
->and($driverModel)
|
|
35
|
+
->toContain('public function setLicenseExpiryAttribute($value): void')
|
|
36
|
+
->toContain("!empty(\$this->getOriginal('license_expiry'))")
|
|
37
|
+
->toContain('Carbon::parse($value)->toDateString()')
|
|
38
|
+
->toContain('public function assignVehicle(Vehicle $vehicle): self')
|
|
39
|
+
->toContain('$this->setVehicle($vehicle)')
|
|
40
|
+
->toContain('$this->save()')
|
|
41
|
+
->not->toContain('$this->license_expiry = null')
|
|
42
|
+
->and($driverResource)
|
|
43
|
+
->toContain("'license_expiry' => \$this->formatDateOnly(\$this->license_expiry)")
|
|
44
|
+
->toContain('protected function formatDateOnly($date): ?string');
|
|
45
|
+
});
|
|
46
|
+
|
|
20
47
|
test('driver assigned-order workflows expose list and multi-unassign endpoints', function () {
|
|
21
48
|
$routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
|
|
22
49
|
$controller = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/DriverController.php');
|
package/translations/ar-ae.yml
CHANGED
|
@@ -1051,7 +1051,7 @@ fleet-ops:
|
|
|
1051
1051
|
more-waypoints: المزيد من نقاط الطريق
|
|
1052
1052
|
settings-window:
|
|
1053
1053
|
success-message: تم حفظ الإعدادات بنجاح.
|
|
1054
|
-
title: إعدادات
|
|
1054
|
+
title: إعدادات Fleet-Ops
|
|
1055
1055
|
save: حفظ التغييرات
|
|
1056
1056
|
name: مسافة التنبيه الافتراضية للطلبات الفورية
|
|
1057
1057
|
name-text: يحدد هذا الإعداد المسافة الافتراضية بالأمتار التي يجب أن تنبه الطلبات الفورية السائقين ضمن محيط نقطة استلام الطلب.
|
package/translations/en-us.yaml
CHANGED
|
@@ -668,6 +668,7 @@ order:
|
|
|
668
668
|
invalid-coordinates: 'Invalid coordinates!'
|
|
669
669
|
items-drop: Items drop at
|
|
670
670
|
loading-message: Loading service quotes...
|
|
671
|
+
updating-service-quotes-message: Updating service quotes...
|
|
671
672
|
notes-placeholder: Enter order notes here....
|
|
672
673
|
notes-title: Notes
|
|
673
674
|
optimize-route: Optimize Route
|
package/translations/vi-vn.yaml
CHANGED
|
@@ -1054,7 +1054,7 @@ fleet-ops:
|
|
|
1054
1054
|
more-waypoints: thêm điểm dừng
|
|
1055
1055
|
settings-window:
|
|
1056
1056
|
success-message: Cài đặt đã được lưu thành công.
|
|
1057
|
-
title: Cài đặt
|
|
1057
|
+
title: Cài đặt Fleet-Ops
|
|
1058
1058
|
save: Lưu Thay đổi
|
|
1059
1059
|
name: Khoảng cách Ping Adhoc Mặc định
|
|
1060
1060
|
name-text: Cài đặt này xác định khoảng cách mặc định tính bằng mét mà các đơn hàng adhoc nên ping tài xế trong khu vực của điểm đón đơn hàng.
|