@fleetbase/fleetops-engine 0.6.67 → 0.6.69
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/RELEASE.md +11 -15
- package/addon/components/contact/form.hbs +13 -2
- package/addon/components/customer/details.hbs +17 -17
- package/addon/components/customer/form.hbs +13 -2
- package/addon/components/customer/form.js +0 -56
- package/addon/components/driver/details.hbs +13 -19
- package/addon/components/driver/form.hbs +20 -35
- package/addon/components/driver/form.js +0 -58
- package/addon/components/fuel-integration/form.hbs +2 -2
- package/addon/components/fuel-integration/form.js +8 -0
- package/addon/components/fuel-integration/hub.js +1 -1
- package/addon/components/modals/driver-form.hbs +2 -2
- package/addon/components/modals/reset-profile-credentials.hbs +23 -0
- package/addon/components/modals/reset-profile-credentials.js +70 -0
- package/addon/components/profile-identity-hint.hbs +13 -0
- package/addon/components/profile-identity-hint.js +49 -0
- package/addon/controllers/management/drivers/index.js +4 -0
- package/addon/extension.js +4 -2
- package/addon/services/contact-actions.js +50 -89
- package/addon/services/driver-actions.js +98 -0
- package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
- package/addon/utils/fuel-transaction.js +2 -0
- package/addon/utils/profile-login-actions.js +172 -0
- package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
- package/app/components/profile-identity-hint.js +1 -0
- package/composer.json +1 -1
- package/docs/SAFEE.md +5 -0
- package/docs/TELEMATICS_QUEUES.md +59 -8
- package/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -0
- package/server/config/fuel-providers.php +53 -0
- package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
- package/server/resources/views/mail/driver-credentials.blade.php +21 -0
- package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
- package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
- package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
- package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
- package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
- package/server/src/Http/Resources/v1/Contact.php +3 -0
- package/server/src/Http/Resources/v1/Driver.php +3 -0
- package/server/src/Jobs/PollTelematicTelemetry.php +36 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +2 -2
- package/server/src/Mail/DriverCredentialsMail.php +55 -0
- package/server/src/Models/Contact.php +55 -90
- package/server/src/Models/Sensor.php +3 -5
- package/server/src/Observers/ContactObserver.php +2 -22
- package/server/src/Observers/DriverObserver.php +5 -6
- package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
- package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
- package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
- package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
- package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
- package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
- package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
- package/server/src/Support/ProfileAccountManager.php +405 -0
- package/server/src/Support/Telematics/TelematicService.php +6 -4
- package/server/src/Support/Telematics/Telemetry/Inbox.php +27 -9
- package/server/src/routes.php +5 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
- package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
- package/server/tests/ApiDriverControllerContractsTest.php +102 -20
- package/server/tests/CommandContractsTest.php +4 -4
- package/server/tests/ControllerHelperContractsTest.php +3 -11
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
- package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
- package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
- package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
- package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
- package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
- package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
- package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
- package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
- package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
- package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +88 -4
- package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
- package/server/tests/ModelAccessorContractsTest.php +82 -20
- package/server/tests/ObserverContractsTest.php +27 -2
- package/server/tests/RequestContractsTest.php +19 -11
- package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
- package/server/tests/TelematicControllerContractsTest.php +22 -4
- package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
- package/server/tests/Unit/Models/ContactTest.php +48 -5
- package/server/tests/Unit/Models/SensorTest.php +2 -2
- package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
- package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
- package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
- package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
- package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
- package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
- package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
- package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
- package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
- package/translations/en-us.yaml +37 -2
- package/translations/uk-ua.yaml +3475 -0
- package/addon/components/modals/reset-customer-credentials.hbs +0 -18
- package/addon/components/modals/reset-customer-credentials.js +0 -55
|
@@ -298,7 +298,7 @@ class FleetOpsInternalOrderLifecycleControllerProbe extends OrderController
|
|
|
298
298
|
return $this->order;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
protected function findOrderById(
|
|
301
|
+
protected function findOrderById(mixed $id, array $with = []): ?Order
|
|
302
302
|
{
|
|
303
303
|
$this->order?->setAttribute('lookup_id', $id);
|
|
304
304
|
$this->order?->setAttribute('lookup_with', $with);
|
|
@@ -990,10 +990,15 @@ test('internal order controller bulk dispatch only dispatches created uncanceled
|
|
|
990
990
|
});
|
|
991
991
|
|
|
992
992
|
test('internal order controller bulk assign driver deduplicates orders and queues notifications', function () {
|
|
993
|
-
$controller = fleetopsInternalOrderLifecycleController();
|
|
994
993
|
$driverUuid = '11111111-1111-4111-8111-111111111111';
|
|
995
994
|
$orderA = '22222222-2222-4222-8222-222222222222';
|
|
996
995
|
$orderB = '33333333-3333-4333-8333-333333333333';
|
|
996
|
+
// The ids are resolved through the company-scoped `ordersByUuid()` lookup, so
|
|
997
|
+
// only orders it returns are assigned, counted and notified.
|
|
998
|
+
$controller = fleetopsInternalOrderLifecycleController([
|
|
999
|
+
fleetopsInternalOrderLifecycleOrder($orderA),
|
|
1000
|
+
fleetopsInternalOrderLifecycleOrder($orderB),
|
|
1001
|
+
]);
|
|
997
1002
|
|
|
998
1003
|
$response = $controller->bulkAssignDriver(fleetopsBulkActionRequest([
|
|
999
1004
|
'ids' => [$orderA, $orderA, $orderB],
|
|
@@ -1011,7 +1016,9 @@ test('internal order controller bulk assign driver deduplicates orders and queue
|
|
|
1011
1016
|
->and($controller->assignedDriverUuid)->toBe($driverUuid)
|
|
1012
1017
|
->and($controller->bulkNotification)->toBe([[$orderA, $orderB], $driverUuid]);
|
|
1013
1018
|
|
|
1014
|
-
$controller = fleetopsInternalOrderLifecycleController(
|
|
1019
|
+
$controller = fleetopsInternalOrderLifecycleController([
|
|
1020
|
+
fleetopsInternalOrderLifecycleOrder($orderA),
|
|
1021
|
+
]);
|
|
1015
1022
|
$controller->bulkAssignDriver(fleetopsBulkActionRequest([
|
|
1016
1023
|
'ids' => [$orderA],
|
|
1017
1024
|
'driver' => $driverUuid,
|
|
@@ -177,3 +177,21 @@ test('invalid file types and unreadable spreadsheets return errors', function ()
|
|
|
177
177
|
]));
|
|
178
178
|
expect($unreadable->getStatusCode())->toBeGreaterThanOrEqual(400);
|
|
179
179
|
});
|
|
180
|
+
|
|
181
|
+
test('import ignores files uploaded by another company', function () {
|
|
182
|
+
$connection = fleetopsOrderImportFilesBoot();
|
|
183
|
+
$connection->table('files')->insert(['uuid' => '33333333-3333-4333-8333-333333333333', 'public_id' => 'file_ordimport4', 'company_uuid' => 'company-2', 'path' => 'uploads/orders.xlsx', 'disk' => 'local']);
|
|
184
|
+
fleetopsOrderImportFilesExcelFake([[
|
|
185
|
+
['name' => 'Victim Stop', 'street1' => 'Victim Rd 1', 'city' => 'Singapore'],
|
|
186
|
+
]]);
|
|
187
|
+
|
|
188
|
+
// The file uuid is caller-supplied and nothing upstream checks who owns it,
|
|
189
|
+
// so an unscoped lookup here would read a rival company's spreadsheet.
|
|
190
|
+
$response = (new OrderController())->importFromFiles(fleetopsOrderImportFilesRequest([
|
|
191
|
+
'files' => ['33333333-3333-4333-8333-333333333333'],
|
|
192
|
+
]));
|
|
193
|
+
|
|
194
|
+
$data = $response->getData(true);
|
|
195
|
+
expect($data['places'])->toHaveCount(0)
|
|
196
|
+
->and($data['entities'])->toHaveCount(0);
|
|
197
|
+
});
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
if (!function_exists('Fleetbase\Support\session')) {
|
|
4
|
+
eval('namespace Fleetbase\Support; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } }; } return \session($key, $default); }');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (!function_exists('Fleetbase\Support\auth')) {
|
|
8
|
+
eval('namespace Fleetbase\Support; function auth() { return new class { public function user() { return null; } public function id() { return null; } }; }');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
use Fleetbase\FleetOps\Http\Controllers\Internal\v1\OrderController;
|
|
12
|
+
use Fleetbase\FleetOps\Http\Requests\BulkDispatchRequest;
|
|
13
|
+
use Fleetbase\FleetOps\Http\Requests\CancelOrderRequest;
|
|
14
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
15
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
16
|
+
use Illuminate\Database\ConnectionResolver;
|
|
17
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
18
|
+
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
19
|
+
use Illuminate\Database\SQLiteConnection;
|
|
20
|
+
use Illuminate\Http\Request;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Covers tenant isolation on the internal OrderController.
|
|
24
|
+
*
|
|
25
|
+
* Every order-lifecycle action here takes its target as a caller-supplied
|
|
26
|
+
* identifier in the request body or query string rather than as a bound route
|
|
27
|
+
* parameter, and `fleetbase.protected` only checks that the caller holds the
|
|
28
|
+
* named RBAC capability — never which company the record belongs to. Nothing
|
|
29
|
+
* upstream narrows these queries, so each lookup carries the company
|
|
30
|
+
* constraint itself, and these tests pin that: an identifier belonging to
|
|
31
|
+
* another organization must resolve to nothing, and a request without a
|
|
32
|
+
* company session must fail closed rather than query every tenant at once.
|
|
33
|
+
*
|
|
34
|
+
* `company-1` is the caller throughout; `company-2` is the victim tenant.
|
|
35
|
+
*/
|
|
36
|
+
class FleetOpsInternalOrderTenantScopeProbe extends OrderController
|
|
37
|
+
{
|
|
38
|
+
public function callHelper(string $method, ...$arguments): mixed
|
|
39
|
+
{
|
|
40
|
+
return $this->{$method}(...$arguments);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const FLEETOPS_TENANT_OWN_ORDER = '55555555-5555-4555-8555-555555555501';
|
|
45
|
+
const FLEETOPS_TENANT_VICTIM_ORDER = '55555555-5555-4555-8555-555555555502';
|
|
46
|
+
const FLEETOPS_TENANT_OWN_DRIVER = '55555555-5555-4555-8555-555555555511';
|
|
47
|
+
const FLEETOPS_TENANT_VICTIM_DRIVER = '55555555-5555-4555-8555-555555555512';
|
|
48
|
+
|
|
49
|
+
function fleetopsOrderTenantBoot(): SQLiteConnection
|
|
50
|
+
{
|
|
51
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
52
|
+
$resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
53
|
+
$resolver->setDefaultConnection('mysql');
|
|
54
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
55
|
+
app()->instance('db', new class($connection) {
|
|
56
|
+
public function __construct(public SQLiteConnection $c)
|
|
57
|
+
{
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public function connection($name = null): SQLiteConnection
|
|
61
|
+
{
|
|
62
|
+
return $this->c;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public function __call($method, $arguments)
|
|
66
|
+
{
|
|
67
|
+
return $this->c->{$method}(...$arguments);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
Illuminate\Support\Facades\DB::clearResolvedInstance('db');
|
|
71
|
+
|
|
72
|
+
$schema = $connection->getSchemaBuilder();
|
|
73
|
+
$tables = [
|
|
74
|
+
'orders' => ['uuid', 'public_id', 'company_uuid', 'payload_uuid', 'order_config_uuid', 'tracking_number_uuid', 'driver_assigned_uuid', 'status', 'type', 'adhoc', 'dispatched', 'started', 'scheduled_at', 'meta', '_key'],
|
|
75
|
+
'payloads' => ['uuid', 'public_id', 'company_uuid', 'pickup_uuid', 'dropoff_uuid', 'current_waypoint_uuid', 'meta', 'type'],
|
|
76
|
+
'places' => ['uuid', 'public_id', 'company_uuid', 'name', 'location'],
|
|
77
|
+
'waypoints' => ['uuid', 'public_id', 'company_uuid', 'payload_uuid', 'place_uuid', 'tracking_number_uuid', 'order', 'type'],
|
|
78
|
+
'entities' => ['uuid', 'public_id', 'company_uuid', 'payload_uuid', 'destination_uuid', 'tracking_number_uuid', 'name', 'type'],
|
|
79
|
+
'drivers' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'vehicle_uuid', 'status', 'online', 'location', 'current_job_uuid'],
|
|
80
|
+
'users' => ['uuid', 'public_id', 'company_uuid', 'name', 'status', 'type'],
|
|
81
|
+
'tracking_numbers' => ['uuid', 'public_id', 'company_uuid', 'tracking_number', 'owner_uuid', 'owner_type', '_key'],
|
|
82
|
+
'tracking_statuses' => ['uuid', 'public_id', 'company_uuid', 'tracking_number_uuid', 'status', 'details', 'code', '_key'],
|
|
83
|
+
'proofs' => ['uuid', 'public_id', 'company_uuid', 'order_uuid', 'subject_uuid', 'subject_type', 'file_uuid', 'remarks', 'raw_data', 'data'],
|
|
84
|
+
'companies' => ['uuid', 'public_id', 'name', 'country'],
|
|
85
|
+
];
|
|
86
|
+
foreach ($tables as $table => $columns) {
|
|
87
|
+
$schema->create($table, function ($blueprint) use ($columns) {
|
|
88
|
+
$blueprint->increments('id');
|
|
89
|
+
foreach ($columns as $column) {
|
|
90
|
+
$blueprint->string($column)->nullable();
|
|
91
|
+
}
|
|
92
|
+
$blueprint->timestamps();
|
|
93
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
session(['company' => 'company-1']);
|
|
98
|
+
|
|
99
|
+
return $connection;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Seeds one order/driver/payload per tenant. The two tenants' records are
|
|
104
|
+
* identical apart from `company_uuid`, so any lookup that resolves the
|
|
105
|
+
* `company-2` identifier is crossing the tenant boundary.
|
|
106
|
+
*/
|
|
107
|
+
function fleetopsOrderTenantSeed(SQLiteConnection $connection): void
|
|
108
|
+
{
|
|
109
|
+
$connection->table('users')->insert([
|
|
110
|
+
['uuid' => 'user-own', 'company_uuid' => 'company-1', 'name' => 'Own Driver'],
|
|
111
|
+
['uuid' => 'user-victim', 'company_uuid' => 'company-2', 'name' => 'Victim Driver'],
|
|
112
|
+
]);
|
|
113
|
+
$connection->table('drivers')->insert([
|
|
114
|
+
['uuid' => FLEETOPS_TENANT_OWN_DRIVER, 'public_id' => 'driver_own1', 'company_uuid' => 'company-1', 'user_uuid' => 'user-own'],
|
|
115
|
+
['uuid' => FLEETOPS_TENANT_VICTIM_DRIVER, 'public_id' => 'driver_victim1', 'company_uuid' => 'company-2', 'user_uuid' => 'user-victim'],
|
|
116
|
+
]);
|
|
117
|
+
$connection->table('payloads')->insert([
|
|
118
|
+
['uuid' => 'payload-own', 'company_uuid' => 'company-1'],
|
|
119
|
+
['uuid' => 'payload-victim', 'company_uuid' => 'company-2'],
|
|
120
|
+
]);
|
|
121
|
+
$connection->table('tracking_numbers')->insert([
|
|
122
|
+
['uuid' => 'tn-own', 'company_uuid' => 'company-1', 'tracking_number' => 'FLB-OWN-1'],
|
|
123
|
+
['uuid' => 'tn-victim', 'company_uuid' => 'company-2', 'tracking_number' => 'FLB-VICTIM-1'],
|
|
124
|
+
]);
|
|
125
|
+
$connection->table('orders')->insert([
|
|
126
|
+
[
|
|
127
|
+
'uuid' => FLEETOPS_TENANT_OWN_ORDER,
|
|
128
|
+
'public_id' => 'order_own1',
|
|
129
|
+
'company_uuid' => 'company-1',
|
|
130
|
+
'payload_uuid' => 'payload-own',
|
|
131
|
+
'tracking_number_uuid' => 'tn-own',
|
|
132
|
+
'driver_assigned_uuid' => FLEETOPS_TENANT_OWN_DRIVER,
|
|
133
|
+
'status' => 'created',
|
|
134
|
+
'type' => 'transport',
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
'uuid' => FLEETOPS_TENANT_VICTIM_ORDER,
|
|
138
|
+
'public_id' => 'order_victim1',
|
|
139
|
+
'company_uuid' => 'company-2',
|
|
140
|
+
'payload_uuid' => 'payload-victim',
|
|
141
|
+
'tracking_number_uuid' => 'tn-victim',
|
|
142
|
+
'driver_assigned_uuid' => FLEETOPS_TENANT_VICTIM_DRIVER,
|
|
143
|
+
'status' => 'created',
|
|
144
|
+
'type' => 'transport',
|
|
145
|
+
],
|
|
146
|
+
]);
|
|
147
|
+
$connection->table('entities')->insert([
|
|
148
|
+
['uuid' => 'entity-own', 'company_uuid' => 'company-1', 'payload_uuid' => 'payload-own', 'name' => 'Own Parcel'],
|
|
149
|
+
['uuid' => 'entity-victim', 'company_uuid' => 'company-2', 'payload_uuid' => 'payload-victim', 'name' => 'Victim Parcel'],
|
|
150
|
+
]);
|
|
151
|
+
$connection->table('proofs')->insert([
|
|
152
|
+
['uuid' => 'proof-own', 'public_id' => 'proof_own1', 'company_uuid' => 'company-1', 'order_uuid' => FLEETOPS_TENANT_OWN_ORDER, 'subject_uuid' => FLEETOPS_TENANT_OWN_ORDER],
|
|
153
|
+
['uuid' => 'proof-victim', 'public_id' => 'proof_victim1', 'company_uuid' => 'company-2', 'order_uuid' => FLEETOPS_TENANT_VICTIM_ORDER, 'subject_uuid' => FLEETOPS_TENANT_VICTIM_ORDER],
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
test('order lookups resolve the callers own records', function () {
|
|
158
|
+
$connection = fleetopsOrderTenantBoot();
|
|
159
|
+
fleetopsOrderTenantSeed($connection);
|
|
160
|
+
$probe = new FleetOpsInternalOrderTenantScopeProbe();
|
|
161
|
+
|
|
162
|
+
expect($probe->callHelper('ordersByUuid', [FLEETOPS_TENANT_OWN_ORDER])->pluck('uuid')->all())->toBe([FLEETOPS_TENANT_OWN_ORDER])
|
|
163
|
+
->and($probe->callHelper('findOrderByUuid', FLEETOPS_TENANT_OWN_ORDER)?->public_id)->toBe('order_own1')
|
|
164
|
+
->and($probe->callHelper('findOrderById', FLEETOPS_TENANT_OWN_ORDER)?->public_id)->toBe('order_own1')
|
|
165
|
+
->and($probe->callHelper('findOrderById', 'order_own1')?->uuid)->toBe(FLEETOPS_TENANT_OWN_ORDER)
|
|
166
|
+
->and($probe->callHelper('findOrderRouteForEdit', FLEETOPS_TENANT_OWN_ORDER)?->uuid)->toBe(FLEETOPS_TENANT_OWN_ORDER)
|
|
167
|
+
->and($probe->callHelper('findOrderForStart', FLEETOPS_TENANT_OWN_ORDER)?->uuid)->toBe(FLEETOPS_TENANT_OWN_ORDER)
|
|
168
|
+
->and($probe->callHelper('findPayloadForStart', 'payload-own')?->uuid)->toBe('payload-own')
|
|
169
|
+
->and($probe->callHelper('findOrderForSchedule', 'order_own1')?->uuid)->toBe(FLEETOPS_TENANT_OWN_ORDER)
|
|
170
|
+
->and($probe->callHelper('findOrderForProofs', FLEETOPS_TENANT_OWN_ORDER)?->uuid)->toBe(FLEETOPS_TENANT_OWN_ORDER)
|
|
171
|
+
->and($probe->callHelper('findOrderForDriverPing', 'order_own1')?->uuid)->toBe(FLEETOPS_TENANT_OWN_ORDER)
|
|
172
|
+
->and($probe->callHelper('findEntityProofSubject', 'entity-own')?->uuid)->toBe('entity-own')
|
|
173
|
+
->and($probe->callHelper('resolveProof', 'proof_own1')?->uuid)->toBe('proof-own')
|
|
174
|
+
->and($probe->callHelper('resolveProof', 'proof-own')?->uuid)->toBe('proof-own');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('driver lookups resolve the callers own drivers', function () {
|
|
178
|
+
$connection = fleetopsOrderTenantBoot();
|
|
179
|
+
fleetopsOrderTenantSeed($connection);
|
|
180
|
+
$probe = new FleetOpsInternalOrderTenantScopeProbe();
|
|
181
|
+
|
|
182
|
+
expect($probe->callHelper('findDriverByUuid', FLEETOPS_TENANT_OWN_DRIVER)?->public_id)->toBe('driver_own1')
|
|
183
|
+
->and($probe->callHelper('findDriverForStart', FLEETOPS_TENANT_OWN_DRIVER)?->public_id)->toBe('driver_own1')
|
|
184
|
+
->and($probe->callHelper('findDriverForSchedule', FLEETOPS_TENANT_OWN_DRIVER)?->public_id)->toBe('driver_own1')
|
|
185
|
+
->and($probe->callHelper('findDriverForSchedule', 'driver_own1')?->uuid)->toBe(FLEETOPS_TENANT_OWN_DRIVER);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('order lookups refuse identifiers belonging to another company', function () {
|
|
189
|
+
$connection = fleetopsOrderTenantBoot();
|
|
190
|
+
fleetopsOrderTenantSeed($connection);
|
|
191
|
+
$probe = new FleetOpsInternalOrderTenantScopeProbe();
|
|
192
|
+
|
|
193
|
+
// Both identifier arms are covered: an unguarded
|
|
194
|
+
// `where(uuid)->orWhere(public_id)->where(company_uuid)` chain would read as
|
|
195
|
+
// `uuid = ? OR (public_id = ? AND company_uuid = ?)` and still resolve the
|
|
196
|
+
// victim by uuid.
|
|
197
|
+
expect($probe->callHelper('ordersByUuid', [FLEETOPS_TENANT_VICTIM_ORDER]))->toHaveCount(0)
|
|
198
|
+
->and($probe->callHelper('findOrderByUuid', FLEETOPS_TENANT_VICTIM_ORDER))->toBeNull()
|
|
199
|
+
->and($probe->callHelper('findOrderById', FLEETOPS_TENANT_VICTIM_ORDER))->toBeNull()
|
|
200
|
+
->and($probe->callHelper('findOrderById', 'order_victim1'))->toBeNull()
|
|
201
|
+
->and($probe->callHelper('findOrderRouteForEdit', FLEETOPS_TENANT_VICTIM_ORDER))->toBeNull()
|
|
202
|
+
->and($probe->callHelper('findOrderForStart', FLEETOPS_TENANT_VICTIM_ORDER))->toBeNull()
|
|
203
|
+
->and($probe->callHelper('findPayloadForStart', 'payload-victim'))->toBeNull()
|
|
204
|
+
->and($probe->callHelper('findOrderForSchedule', 'order_victim1'))->toBeNull()
|
|
205
|
+
->and($probe->callHelper('findOrderForProofs', FLEETOPS_TENANT_VICTIM_ORDER))->toBeNull()
|
|
206
|
+
->and($probe->callHelper('findEntityProofSubject', 'entity-victim'))->toBeNull()
|
|
207
|
+
->and($probe->callHelper('resolveProof', 'proof_victim1'))->toBeNull()
|
|
208
|
+
->and($probe->callHelper('resolveProof', 'proof-victim'))->toBeNull()
|
|
209
|
+
->and($probe->callHelper('findDriverByUuid', FLEETOPS_TENANT_VICTIM_DRIVER))->toBeNull()
|
|
210
|
+
->and($probe->callHelper('findDriverForStart', FLEETOPS_TENANT_VICTIM_DRIVER))->toBeNull()
|
|
211
|
+
->and($probe->callHelper('findDriverForSchedule', FLEETOPS_TENANT_VICTIM_DRIVER))->toBeNull()
|
|
212
|
+
->and($probe->callHelper('findDriverForSchedule', 'driver_victim1'))->toBeNull();
|
|
213
|
+
|
|
214
|
+
// The ping lookup reports a miss the same way it reports an unknown id, so
|
|
215
|
+
// the endpoint cannot be used to probe which ids exist in other tenants.
|
|
216
|
+
expect(fn () => $probe->callHelper('findOrderForDriverPing', 'order_victim1'))
|
|
217
|
+
->toThrow(ModelNotFoundException::class);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('the public tracking lookup finds an order by its tracking number alone', function () {
|
|
221
|
+
$connection = fleetopsOrderTenantBoot();
|
|
222
|
+
fleetopsOrderTenantSeed($connection);
|
|
223
|
+
$probe = new FleetOpsInternalOrderTenantScopeProbe();
|
|
224
|
+
|
|
225
|
+
// `fleet-ops/lookup` backs the public Track Order page: recipients have no session
|
|
226
|
+
// and the tracking number is the credential. It must work without a company, and
|
|
227
|
+
// for whichever company owns the order.
|
|
228
|
+
session(['company' => null]);
|
|
229
|
+
|
|
230
|
+
expect($probe->callHelper('findOrderByTrackingNumber', 'FLB-OWN-1')?->uuid)->toBe(FLEETOPS_TENANT_OWN_ORDER)
|
|
231
|
+
->and($probe->callHelper('findOrderByTrackingNumber', 'FLB-VICTIM-1')?->uuid)->toBe(FLEETOPS_TENANT_VICTIM_ORDER)
|
|
232
|
+
->and($probe->callHelper('findOrderByTrackingNumber', 'FLB-UNKNOWN'))->toBeNull();
|
|
233
|
+
|
|
234
|
+
session(['company' => 'company-1']);
|
|
235
|
+
|
|
236
|
+
expect($probe->callHelper('findOrderByTrackingNumber', 'FLB-VICTIM-1')?->uuid)->toBe(FLEETOPS_TENANT_VICTIM_ORDER);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('order lookups fail closed when no company session is present', function () {
|
|
240
|
+
$connection = fleetopsOrderTenantBoot();
|
|
241
|
+
fleetopsOrderTenantSeed($connection);
|
|
242
|
+
session(['company' => null]);
|
|
243
|
+
$probe = new FleetOpsInternalOrderTenantScopeProbe();
|
|
244
|
+
|
|
245
|
+
expect($probe->callHelper('ordersByUuid', [FLEETOPS_TENANT_OWN_ORDER, FLEETOPS_TENANT_VICTIM_ORDER]))->toHaveCount(0)
|
|
246
|
+
->and($probe->callHelper('findOrderByUuid', FLEETOPS_TENANT_OWN_ORDER))->toBeNull()
|
|
247
|
+
->and($probe->callHelper('findOrderById', 'order_own1'))->toBeNull()
|
|
248
|
+
->and($probe->callHelper('findDriverByUuid', FLEETOPS_TENANT_OWN_DRIVER))->toBeNull()
|
|
249
|
+
->and($probe->callHelper('findDriverForSchedule', 'driver_own1'))->toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('order resolution by id rejects empty identifiers without querying', function () {
|
|
253
|
+
$connection = fleetopsOrderTenantBoot();
|
|
254
|
+
fleetopsOrderTenantSeed($connection);
|
|
255
|
+
$probe = new FleetOpsInternalOrderTenantScopeProbe();
|
|
256
|
+
|
|
257
|
+
// The identifier is raw request input, so a non-string body value has to
|
|
258
|
+
// resolve to no order rather than raising out of the endpoint.
|
|
259
|
+
expect($probe->callHelper('findOrderById', null))->toBeNull()
|
|
260
|
+
->and($probe->callHelper('findOrderById', ''))->toBeNull()
|
|
261
|
+
->and($probe->callHelper('findOrderById', ['uuid' => FLEETOPS_TENANT_OWN_ORDER]))->toBeNull()
|
|
262
|
+
->and($probe->callHelper('findOrderById', 42))->toBeNull()
|
|
263
|
+
->and($probe->callHelper('findOrderForSchedule', null))->toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('bulk driver assignment only touches orders the caller owns', function () {
|
|
267
|
+
$connection = fleetopsOrderTenantBoot();
|
|
268
|
+
fleetopsOrderTenantSeed($connection);
|
|
269
|
+
$connection->table('orders')->update(['driver_assigned_uuid' => null]);
|
|
270
|
+
$controller = new OrderController();
|
|
271
|
+
|
|
272
|
+
$response = $controller->bulkAssignDriver(Request::create('/x', 'PATCH', [
|
|
273
|
+
'ids' => [FLEETOPS_TENANT_OWN_ORDER, FLEETOPS_TENANT_VICTIM_ORDER],
|
|
274
|
+
'driver' => FLEETOPS_TENANT_OWN_DRIVER,
|
|
275
|
+
'silent' => true,
|
|
276
|
+
]));
|
|
277
|
+
|
|
278
|
+
// The victim order is dropped before the update, so it is neither
|
|
279
|
+
// reassigned nor counted in the response.
|
|
280
|
+
expect($response->getData(true)['count'])->toBe(1)
|
|
281
|
+
->and($connection->table('orders')->where('uuid', FLEETOPS_TENANT_OWN_ORDER)->value('driver_assigned_uuid'))->toBe(FLEETOPS_TENANT_OWN_DRIVER)
|
|
282
|
+
->and($connection->table('orders')->where('uuid', FLEETOPS_TENANT_VICTIM_ORDER)->value('driver_assigned_uuid'))->toBeNull();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('bulk driver assignment refuses a driver from another company', function () {
|
|
286
|
+
$connection = fleetopsOrderTenantBoot();
|
|
287
|
+
fleetopsOrderTenantSeed($connection);
|
|
288
|
+
$controller = new OrderController();
|
|
289
|
+
|
|
290
|
+
$response = $controller->bulkAssignDriver(Request::create('/x', 'PATCH', [
|
|
291
|
+
'ids' => [FLEETOPS_TENANT_OWN_ORDER],
|
|
292
|
+
'driver' => FLEETOPS_TENANT_VICTIM_DRIVER,
|
|
293
|
+
'silent' => true,
|
|
294
|
+
]));
|
|
295
|
+
|
|
296
|
+
expect($response->getData(true)['error'] ?? '')->toContain('Invalid driver selected');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('the bulk assignment update is itself scoped to the callers company', function () {
|
|
300
|
+
$connection = fleetopsOrderTenantBoot();
|
|
301
|
+
fleetopsOrderTenantSeed($connection);
|
|
302
|
+
$connection->table('orders')->update(['driver_assigned_uuid' => null]);
|
|
303
|
+
$probe = new FleetOpsInternalOrderTenantScopeProbe();
|
|
304
|
+
$driver = Driver::where('uuid', FLEETOPS_TENANT_OWN_DRIVER)->first();
|
|
305
|
+
|
|
306
|
+
// Called directly with a victim uuid, standing in for any future caller
|
|
307
|
+
// that reaches this seam without pre-filtering the ids.
|
|
308
|
+
$probe->callHelper('assignDriverToOrders', [FLEETOPS_TENANT_OWN_ORDER, FLEETOPS_TENANT_VICTIM_ORDER], $driver);
|
|
309
|
+
|
|
310
|
+
expect($connection->table('orders')->where('uuid', FLEETOPS_TENANT_OWN_ORDER)->value('driver_assigned_uuid'))->toBe(FLEETOPS_TENANT_OWN_DRIVER)
|
|
311
|
+
->and($connection->table('orders')->where('uuid', FLEETOPS_TENANT_VICTIM_ORDER)->value('driver_assigned_uuid'))->toBeNull();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('bulk cancel and bulk dispatch skip orders from another company', function () {
|
|
315
|
+
$connection = fleetopsOrderTenantBoot();
|
|
316
|
+
fleetopsOrderTenantSeed($connection);
|
|
317
|
+
$controller = new OrderController();
|
|
318
|
+
|
|
319
|
+
$canceled = $controller->bulkCancel(Request::create('/x', 'PATCH', [
|
|
320
|
+
'ids' => [FLEETOPS_TENANT_VICTIM_ORDER],
|
|
321
|
+
]));
|
|
322
|
+
expect($canceled->getData(true)['count'])->toBe(0)
|
|
323
|
+
->and($connection->table('orders')->where('uuid', FLEETOPS_TENANT_VICTIM_ORDER)->value('status'))->toBe('created');
|
|
324
|
+
|
|
325
|
+
$dispatched = $controller->bulkDispatch(BulkDispatchRequest::create('/x', 'POST', [
|
|
326
|
+
'ids' => [FLEETOPS_TENANT_VICTIM_ORDER],
|
|
327
|
+
]));
|
|
328
|
+
expect($dispatched->getData(true)['count'])->toBe(0)
|
|
329
|
+
->and($connection->table('orders')->where('uuid', FLEETOPS_TENANT_VICTIM_ORDER)->value('dispatched'))->toBeNull();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('cancel rejects a known order uuid that belongs to another company', function () {
|
|
333
|
+
$connection = fleetopsOrderTenantBoot();
|
|
334
|
+
fleetopsOrderTenantSeed($connection);
|
|
335
|
+
|
|
336
|
+
// `exists:orders,uuid` on CancelOrderRequest is a global existence check, so
|
|
337
|
+
// this uuid passes validation and the controller itself has to refuse it.
|
|
338
|
+
$response = (new OrderController())->cancel(
|
|
339
|
+
CancelOrderRequest::create('/x', 'PATCH', ['order' => FLEETOPS_TENANT_VICTIM_ORDER])
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
expect($response->getData(true)['error'] ?? '')->toContain('No order found to cancel')
|
|
343
|
+
->and($connection->table('orders')->where('uuid', FLEETOPS_TENANT_VICTIM_ORDER)->value('status'))->toBe('created');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('dispatch start and schedule refuse another companys order', function () {
|
|
347
|
+
$connection = fleetopsOrderTenantBoot();
|
|
348
|
+
fleetopsOrderTenantSeed($connection);
|
|
349
|
+
$controller = new OrderController();
|
|
350
|
+
|
|
351
|
+
$dispatched = $controller->dispatchOrder(Request::create('/x', 'PATCH', ['order' => FLEETOPS_TENANT_VICTIM_ORDER]));
|
|
352
|
+
expect($dispatched->getData(true)['error'] ?? '')->toContain('No order found to dispatch');
|
|
353
|
+
|
|
354
|
+
$started = $controller->start(Request::create('/x', 'PATCH', ['order' => FLEETOPS_TENANT_VICTIM_ORDER]));
|
|
355
|
+
expect($started->getData(true)['error'] ?? '')->toContain('Unable to find order to start');
|
|
356
|
+
|
|
357
|
+
$scheduled = $controller->scheduleOrder(Request::create('/x', 'PATCH', [
|
|
358
|
+
'order' => FLEETOPS_TENANT_VICTIM_ORDER,
|
|
359
|
+
'scheduled_at' => '2026-01-01 09:00:00',
|
|
360
|
+
]));
|
|
361
|
+
expect($scheduled->getData(true)['error'] ?? '')->toContain('No order found to schedule')
|
|
362
|
+
->and($connection->table('orders')->where('uuid', FLEETOPS_TENANT_VICTIM_ORDER)->value('scheduled_at'))->toBeNull()
|
|
363
|
+
->and($connection->table('drivers')->where('uuid', FLEETOPS_TENANT_VICTIM_DRIVER)->value('current_job_uuid'))->toBeNull();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('schedule ignores a driver from another company', function () {
|
|
367
|
+
$connection = fleetopsOrderTenantBoot();
|
|
368
|
+
fleetopsOrderTenantSeed($connection);
|
|
369
|
+
$connection->table('orders')->where('uuid', FLEETOPS_TENANT_OWN_ORDER)->update(['driver_assigned_uuid' => null]);
|
|
370
|
+
|
|
371
|
+
$response = (new OrderController())->scheduleOrder(Request::create('/x', 'PATCH', [
|
|
372
|
+
'order' => FLEETOPS_TENANT_OWN_ORDER,
|
|
373
|
+
'driver_id' => 'driver_victim1',
|
|
374
|
+
]));
|
|
375
|
+
|
|
376
|
+
expect($response->getData(true)['status'])->toBe('OK')
|
|
377
|
+
->and($connection->table('orders')->where('uuid', FLEETOPS_TENANT_OWN_ORDER)->value('driver_assigned_uuid'))->toBeNull();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('activity destination and next-activity endpoints refuse another companys order', function () {
|
|
381
|
+
$connection = fleetopsOrderTenantBoot();
|
|
382
|
+
fleetopsOrderTenantSeed($connection);
|
|
383
|
+
$controller = new OrderController();
|
|
384
|
+
|
|
385
|
+
$nextActivity = $controller->nextActivity('order_victim1', Request::create('/x', 'GET'));
|
|
386
|
+
expect($nextActivity->getData(true))->toBe(['error' => 'No order found.']);
|
|
387
|
+
|
|
388
|
+
$updateActivity = $controller->updateActivity(FLEETOPS_TENANT_VICTIM_ORDER, Request::create('/x', 'PATCH', ['activity' => []]));
|
|
389
|
+
expect($updateActivity->getData(true))->toBe(['error' => 'No order found.']);
|
|
390
|
+
|
|
391
|
+
$setDestination = $controller->setDestination(FLEETOPS_TENANT_VICTIM_ORDER, 'place-victim');
|
|
392
|
+
expect($setDestination->getData(true))->toBe(['error' => 'No order found.']);
|
|
393
|
+
|
|
394
|
+
$trackerInfo = $controller->trackerInfo(Request::create('/x', 'GET'), FLEETOPS_TENANT_VICTIM_ORDER);
|
|
395
|
+
expect($trackerInfo->getData(true))->toBe(['error' => 'No order found.']);
|
|
396
|
+
|
|
397
|
+
$waypointEtas = $controller->waypointEtas(Request::create('/x', 'GET'), FLEETOPS_TENANT_VICTIM_ORDER);
|
|
398
|
+
expect($waypointEtas->getData(true))->toBe(['error' => 'No order found.']);
|
|
399
|
+
|
|
400
|
+
$editRoute = $controller->editOrderRoute(FLEETOPS_TENANT_VICTIM_ORDER, Request::create('/x', 'PATCH'));
|
|
401
|
+
expect($editRoute->getData(true)['error'] ?? '')->toContain('Unable to find order to update route for');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('proofs endpoint refuses another companys order', function () {
|
|
405
|
+
$connection = fleetopsOrderTenantBoot();
|
|
406
|
+
fleetopsOrderTenantSeed($connection);
|
|
407
|
+
|
|
408
|
+
$response = (new OrderController())->proofs(Request::create('/x', 'GET'), FLEETOPS_TENANT_VICTIM_ORDER);
|
|
409
|
+
|
|
410
|
+
expect($response->getData(true)['error'] ?? '')->toContain('Unable to retrieve proof');
|
|
411
|
+
});
|
|
@@ -6,27 +6,15 @@ use Illuminate\Http\Request;
|
|
|
6
6
|
/**
|
|
7
7
|
* Covers the not-found branch of Internal\v1\OrderController::nextActivity().
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* This branch used to depend on upstream behaviour that never fired: the
|
|
10
|
+
* controller wrapped `Order::findByIdOrFail($id)` in a
|
|
11
|
+
* `catch (ModelNotFoundException)`, but core-api's findByIdOrFail() raised a
|
|
12
|
+
* BadMethodCallException that escaped the catch and surfaced as a 500.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* so a missing order raises BadMethodCallException, escapes the catch, and
|
|
19
|
-
* surfaces as a 500 instead of the intended error response.
|
|
20
|
-
*
|
|
21
|
-
* core-api#231 (branch dev-v1.6.55) replaces that with
|
|
22
|
-
* `throw (new ModelNotFoundException())->setModel(static::class, [$identifier]);`
|
|
23
|
-
* which makes this branch live. The assertion below states the post-fix
|
|
24
|
-
* contract deliberately — asserting today's BadMethodCallException would
|
|
25
|
-
* codify the defect instead of the intent.
|
|
26
|
-
*
|
|
27
|
-
* If CI must be green before that release lands, neutralise this file with a
|
|
28
|
-
* single `->skip('pending fleetbase/core-api 1.6.55')` on the test below rather
|
|
29
|
-
* than changing what it asserts.
|
|
14
|
+
* nextActivity() now resolves the order through the controller's own
|
|
15
|
+
* company-scoped `findOrderById()` and returns the error response on a null
|
|
16
|
+
* result, so the branch is live here regardless of the upstream release: an
|
|
17
|
+
* unknown id and an id belonging to another company are reported identically.
|
|
30
18
|
*/
|
|
31
19
|
function fleetopsUpstreamNotFoundBoot(): Illuminate\Database\SQLiteConnection
|
|
32
20
|
{
|
|
@@ -104,10 +104,11 @@ class ManualTelemetryProvider extends ExampleTelemetryProvider
|
|
|
104
104
|
public array $requests = [];
|
|
105
105
|
public bool $fail = false;
|
|
106
106
|
public ?int $failOnCall = null;
|
|
107
|
+
public bool $polling = true;
|
|
107
108
|
|
|
108
109
|
public function telemetryOptions(): array
|
|
109
110
|
{
|
|
110
|
-
return array_merge(parent::telemetryOptions(), ['manual_batch_sync' => true, 'batch_size' => 1, 'webhooks_enabled' => false]);
|
|
111
|
+
return array_merge(parent::telemetryOptions(), ['manual_batch_sync' => true, 'batch_size' => 1, 'webhooks_enabled' => false, 'polling_enabled' => $this->polling]);
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
public function fetchDevices(array $options = []): array
|
|
@@ -213,13 +214,37 @@ test('old queued discovery jobs delegate without entering their legacy lock or H
|
|
|
213
214
|
}
|
|
214
215
|
});
|
|
215
216
|
|
|
216
|
-
test('
|
|
217
|
+
test('manual requests made while a scheduled sweep holds the poll lease are completed by the next sweep', function () {
|
|
217
218
|
[$connection, $provider, $registry, $service] = manualTelemetrySetup();
|
|
218
|
-
|
|
219
|
+
$scheduled = new PollTelematicTelemetry($connection->uuid);
|
|
220
|
+
expect(Fleetbase\FleetOps\Support\Telematics\Telemetry\Queue::dispatch($scheduled))->toBeTrue();
|
|
221
|
+
$id = $service->discoverDevices($connection);
|
|
222
|
+
// Old queued discovery jobs join the same pending request instead of failing.
|
|
223
|
+
(new SyncTelematicDevicesJob($connection->fresh(), [], 'old-job'))->handle($registry, $service);
|
|
224
|
+
$waiting = $connection->fresh();
|
|
225
|
+
expect(count($GLOBALS['manual_telemetry_jobs']))->toBe(1)
|
|
226
|
+
->and($waiting->status)->toBe('synchronizing')
|
|
227
|
+
->and(data_get($waiting->meta, 'last_sync_job_id'))->toBe($id)
|
|
228
|
+
->and(data_get($waiting->meta, 'last_sync_phase'))->toBe('waiting_for_sweep');
|
|
229
|
+
ExampleTelemetryProvider::$pages = [['devices' => [manualTelemetrySample()], 'has_more' => false, 'next_cursor' => null]];
|
|
230
|
+
$scheduled->handle($registry, new Inbox());
|
|
231
|
+
expect(data_get($connection->fresh()->meta, 'last_sync_result'))->toBe('running');
|
|
232
|
+
manualTelemetryProcessPending($service);
|
|
233
|
+
$fresh = $connection->fresh();
|
|
234
|
+
expect($fresh->status)->toBe('active')
|
|
235
|
+
->and(data_get($fresh->meta, 'last_sync_job_id'))->toBe($id)
|
|
236
|
+
->and(data_get($fresh->meta, 'last_sync_result'))->toBe('success')
|
|
237
|
+
->and(data_get($fresh->meta, 'last_sync_completed_at'))->toBe('2026-09-15 12:00:00')
|
|
238
|
+
->and(data_get($fresh->meta, 'last_sync_total'))->toBe(1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('old queued discovery jobs finish quietly when polling was paused after they were queued', function () {
|
|
242
|
+
[$connection, $provider, $registry, $service] = manualTelemetrySetup();
|
|
243
|
+
$provider->polling = false;
|
|
219
244
|
(new SyncTelematicDevicesJob($connection, [], 'old-job'))->handle($registry, $service);
|
|
220
245
|
$fresh = $connection->fresh();
|
|
221
246
|
// A thrown ValidationException would fail the tries=1 job and mark this connection as errored.
|
|
222
|
-
expect(count($GLOBALS['manual_telemetry_jobs']))->toBe(
|
|
247
|
+
expect(count($GLOBALS['manual_telemetry_jobs']))->toBe(0)
|
|
223
248
|
->and($fresh->status)->toBe('active')
|
|
224
249
|
->and(data_get($fresh->meta, 'last_sync_job_id'))->toBeNull();
|
|
225
250
|
});
|
|
@@ -459,3 +484,62 @@ test('manual sync re-checks the locked connection before queueing', function ()
|
|
|
459
484
|
expect(count($GLOBALS['manual_telemetry_jobs']))->toBe(0)
|
|
460
485
|
->and(DB::table('telematics')->value('status'))->toBe('disabled');
|
|
461
486
|
});
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
function manualTelemetryConnectionState(Telematic $connection, string $status, array $meta): void
|
|
490
|
+
{
|
|
491
|
+
DB::table('telematics')->where('uuid', $connection->uuid)->update(['status' => $status, 'meta' => json_encode($meta)]);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
test('completed scheduled sweeps replace a stale sync failure without saving through the model', function () {
|
|
495
|
+
[$connection, $provider, $registry, $service] = manualTelemetrySetup();
|
|
496
|
+
manualTelemetryConnectionState($connection, 'error', [
|
|
497
|
+
'last_sync_job_id' => 'lost-request', 'last_sync_result' => 'failed', 'last_sync_error' => 'Polling retries exhausted. MaxAttemptsExceededException',
|
|
498
|
+
'last_sync_completed_at' => '2026-06-24 07:32:18', 'preserved' => 'yes',
|
|
499
|
+
]);
|
|
500
|
+
ExampleTelemetryProvider::$pages = [['devices' => [manualTelemetrySample()], 'has_more' => false, 'next_cursor' => null]];
|
|
501
|
+
(new PollTelematicTelemetry($connection->uuid))->handle($registry, new Inbox());
|
|
502
|
+
manualTelemetryProcessPending($service);
|
|
503
|
+
$fresh = $connection->fresh();
|
|
504
|
+
expect($fresh->status)->toBe('active')
|
|
505
|
+
->and(data_get($fresh->meta, 'last_sync_result'))->toBe('success')
|
|
506
|
+
->and(data_get($fresh->meta, 'last_sync_error'))->toBeNull()
|
|
507
|
+
->and(data_get($fresh->meta, 'last_sync_completed_at'))->toBe('2026-09-15 12:00:00')
|
|
508
|
+
->and(data_get($fresh->meta, 'last_sync_fetched_total'))->toBe(1)
|
|
509
|
+
->and(data_get($fresh->meta, 'last_sync_job_id'))->toBe('lost-request')
|
|
510
|
+
->and(data_get($fresh->meta, 'preserved'))->toBe('yes')
|
|
511
|
+
->and($fresh->updated_at)->toBeNull();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test('partial scheduled sweeps leave the last reported sync state unchanged', function () {
|
|
515
|
+
[$connection, $provider, $registry, $service] = manualTelemetrySetup();
|
|
516
|
+
manualTelemetryConnectionState($connection, 'error', ['last_sync_result' => 'failed', 'last_sync_error' => 'Earlier failure']);
|
|
517
|
+
ExampleTelemetryProvider::$pages = [['devices' => [manualTelemetrySample()], 'has_more' => false, 'next_cursor' => null]];
|
|
518
|
+
(new PollTelematicTelemetry($connection->uuid))->handle($registry, new Inbox());
|
|
519
|
+
$run = DB::table('telematic_sync_runs')->value('uuid');
|
|
520
|
+
DB::table('telematic_deliveries')->update(['status' => 'quarantined', 'failed' => 1]);
|
|
521
|
+
Inbox::finishRun($run);
|
|
522
|
+
$fresh = $connection->fresh();
|
|
523
|
+
expect(DB::table('telematic_sync_runs')->value('status'))->toBe('partial')
|
|
524
|
+
->and($fresh->status)->toBe('error')
|
|
525
|
+
->and(data_get($fresh->meta, 'last_sync_error'))->toBe('Earlier failure');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('scheduled sweeps adopt abandoned requests but never one bound to a sweep in progress', function (string $boundStatus, bool $adopted) {
|
|
529
|
+
[$connection, $provider, $registry, $service] = manualTelemetrySetup();
|
|
530
|
+
DB::table('telematic_sync_runs')->insert(['uuid' => 'bound-run', 'telematic_uuid' => $connection->uuid, 'status' => $boundStatus, 'created_at' => now(), 'updated_at' => now()]);
|
|
531
|
+
manualTelemetryConnectionState($connection, 'synchronizing', [
|
|
532
|
+
'last_sync_job_id' => 'abandoned-request', 'last_sync_result' => 'retrying',
|
|
533
|
+
'last_sync_run_uuid' => 'bound-run', 'last_sync_run_job_id' => 'abandoned-request',
|
|
534
|
+
]);
|
|
535
|
+
ExampleTelemetryProvider::$pages = [['devices' => [manualTelemetrySample()], 'has_more' => false, 'next_cursor' => null]];
|
|
536
|
+
(new PollTelematicTelemetry($connection->uuid))->handle($registry, new Inbox());
|
|
537
|
+
manualTelemetryProcessPending($service);
|
|
538
|
+
$fresh = $connection->fresh();
|
|
539
|
+
expect($fresh->status)->toBe($adopted ? 'active' : 'synchronizing')
|
|
540
|
+
->and(data_get($fresh->meta, 'last_sync_result'))->toBe($adopted ? 'success' : 'retrying')
|
|
541
|
+
->and(data_get($fresh->meta, 'last_sync_run_uuid'))->toBe($adopted ? DB::table('telematic_sync_runs')->where('uuid', '!=', 'bound-run')->value('uuid') : 'bound-run');
|
|
542
|
+
})->with([
|
|
543
|
+
'interrupted run' => ['incomplete', true],
|
|
544
|
+
'run still ingesting' => ['ingesting', false],
|
|
545
|
+
]);
|