@fleetbase/fleetops-engine 0.6.61 → 0.6.63
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 +5 -14
- package/addon/controllers/management/drivers/index.js +4 -1
- package/addon/routes/management/fleets/index/details.js +1 -1
- package/addon/routes/management/fleets/index/edit.js +1 -1
- package/addon/routes/management/fleets/index.js +3 -1
- package/addon/services/map-settings.js +4 -1
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
- package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +93 -57
- package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
- package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +134 -35
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
- package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
- package/server/src/Http/Filter/DriverFilter.php +71 -15
- package/server/src/Http/Filter/FleetFilter.php +32 -33
- package/server/src/Http/Filter/VehicleFilter.php +57 -19
- package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
- package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
- package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
- package/server/src/Http/Requests/CreateVehicleRequest.php +150 -31
- package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
- package/server/src/Http/Resources/v1/Driver.php +22 -2
- package/server/src/Http/Resources/v1/Fleet.php +38 -25
- package/server/src/Http/Resources/v1/Vehicle.php +46 -4
- package/server/src/Models/Driver.php +2 -2
- package/server/src/routes.php +8 -0
- package/server/tests/ApiDriverControllerContractsTest.php +284 -13
- package/server/tests/ApiEntityControllerContractsTest.php +1 -1
- package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
- package/server/tests/ApiFleetControllerContractsTest.php +354 -21
- package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
- package/server/tests/ApiIssueControllerContractsTest.php +1 -1
- package/server/tests/ApiOrderControllerContractsTest.php +1 -1
- package/server/tests/ApiPartControllerContractsTest.php +2 -2
- package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
- package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
- package/server/tests/ApiVehicleControllerContractsTest.php +175 -8
- package/server/tests/ControllerFilterContractsTest.php +132 -13
- package/server/tests/ControllerHelperContractsTest.php +6 -3
- package/server/tests/DriverFilterExecutionTest.php +130 -6
- package/server/tests/ExceptionContractsTest.php +15 -0
- package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
- package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
- package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
- package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
- package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
- package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
- package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
- package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
- package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
- package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
- package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
- package/server/tests/FleetHierarchyResourceTest.php +56 -8
- package/server/tests/RequestContractsTest.php +100 -7
- package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
- package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Filter\DriverFilter;
|
|
4
|
+
use Fleetbase\FleetOps\Http\Filter\VehicleFilter;
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
7
|
+
use Illuminate\Database\ConnectionResolver;
|
|
8
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
9
|
+
use Illuminate\Database\SQLiteConnection;
|
|
10
|
+
use Illuminate\Http\Request;
|
|
11
|
+
use Illuminate\Support\Facades\DB;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `internal_id` means two different things to two different callers.
|
|
15
|
+
*
|
|
16
|
+
* The Fleet-Ops console types into a search box and expects `VEH-10` to find
|
|
17
|
+
* `VEH-100`. An importer asks whether `VEH-10` exists so it can decide between
|
|
18
|
+
* update and create, and a partial match answers yes because `VEH-100` does —
|
|
19
|
+
* so the importer updates the wrong vehicle, or skips a create it needed. The
|
|
20
|
+
* console keeps the loose behaviour; everything else gets equality.
|
|
21
|
+
*
|
|
22
|
+
* Asserted against a real connection: the branch depends on the resolved route,
|
|
23
|
+
* and a recording double would prove only that a method was called.
|
|
24
|
+
*/
|
|
25
|
+
function fleetopsIdentifierFilterBoot(): SQLiteConnection
|
|
26
|
+
{
|
|
27
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
28
|
+
$resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
29
|
+
$resolver->setDefaultConnection('mysql');
|
|
30
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
31
|
+
|
|
32
|
+
app()->instance('db', new class($connection) {
|
|
33
|
+
public function __construct(public SQLiteConnection $c)
|
|
34
|
+
{
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public function connection($name = null): SQLiteConnection
|
|
38
|
+
{
|
|
39
|
+
return $this->c;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public function __call($method, $arguments)
|
|
43
|
+
{
|
|
44
|
+
return $this->c->{$method}(...$arguments);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
app()->instance('db.schema', $connection->getSchemaBuilder());
|
|
48
|
+
DB::clearResolvedInstance('db');
|
|
49
|
+
|
|
50
|
+
// `searchWhere` is a Builder macro registered by core's service provider,
|
|
51
|
+
// which does not boot here. Mirrors the real one: case-insensitive LIKE with
|
|
52
|
+
// dots and commas treated as wildcards.
|
|
53
|
+
if (!Illuminate\Database\Eloquent\Builder::hasGlobalMacro('searchWhere')) {
|
|
54
|
+
Illuminate\Database\Eloquent\Builder::macro('searchWhere', function ($column, $search, $strict = false) {
|
|
55
|
+
if ($strict === true) {
|
|
56
|
+
return $this->where($column, $search);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
$needle = '%' . str_replace(['.', ','], '%', (string) $search) . '%';
|
|
60
|
+
|
|
61
|
+
return $this->where(DB::raw('lower(' . $column . ')'), 'like', strtolower($needle));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$schema = $connection->getSchemaBuilder();
|
|
66
|
+
foreach (['vehicles', 'drivers', 'users'] as $table) {
|
|
67
|
+
$schema->create($table, function ($blueprint) {
|
|
68
|
+
$blueprint->increments('id');
|
|
69
|
+
foreach (['uuid', 'public_id', 'internal_id', 'company_uuid', 'user_uuid', 'name', '_key'] as $column) {
|
|
70
|
+
$blueprint->string($column)->nullable();
|
|
71
|
+
}
|
|
72
|
+
$blueprint->timestamps();
|
|
73
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
$rows = ['VEH-10', 'VEH-100', 'VEH-101'];
|
|
78
|
+
$index = 0;
|
|
79
|
+
foreach ($rows as $internalId) {
|
|
80
|
+
$index++;
|
|
81
|
+
$connection->table('vehicles')->insert([
|
|
82
|
+
'uuid' => 'vehicle-uuid-' . $index,
|
|
83
|
+
'public_id' => 'vehicle_id' . $index,
|
|
84
|
+
'internal_id' => $internalId,
|
|
85
|
+
'company_uuid' => 'company-uuid',
|
|
86
|
+
]);
|
|
87
|
+
$connection->table('users')->insert(['uuid' => 'user-uuid-' . $index, 'company_uuid' => 'company-uuid']);
|
|
88
|
+
$connection->table('drivers')->insert([
|
|
89
|
+
'uuid' => 'driver-uuid-' . $index,
|
|
90
|
+
'public_id' => 'driver_id' . $index,
|
|
91
|
+
'internal_id' => str_replace('VEH', 'DRV', $internalId),
|
|
92
|
+
'company_uuid' => 'company-uuid',
|
|
93
|
+
'user_uuid' => 'user-uuid-' . $index,
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Another tenant holding the very same internal id.
|
|
98
|
+
$connection->table('vehicles')->insert([
|
|
99
|
+
'uuid' => 'vehicle-other-company',
|
|
100
|
+
'public_id' => 'vehicle_other01',
|
|
101
|
+
'internal_id' => 'VEH-10',
|
|
102
|
+
'company_uuid' => 'another-company',
|
|
103
|
+
]);
|
|
104
|
+
$connection->table('users')->insert(['uuid' => 'user-other', 'company_uuid' => 'another-company']);
|
|
105
|
+
$connection->table('drivers')->insert([
|
|
106
|
+
'uuid' => 'driver-other-company',
|
|
107
|
+
'public_id' => 'driver_other01',
|
|
108
|
+
'internal_id' => 'DRV-10',
|
|
109
|
+
'company_uuid' => 'another-company',
|
|
110
|
+
'user_uuid' => 'user-other',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
session(['company' => 'company-uuid']);
|
|
114
|
+
|
|
115
|
+
return $connection;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class FleetOpsIdentifierFilterRoute
|
|
119
|
+
{
|
|
120
|
+
public array $action = [];
|
|
121
|
+
|
|
122
|
+
public function __construct(private string $uri)
|
|
123
|
+
{
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public function uri(): string
|
|
127
|
+
{
|
|
128
|
+
return $this->uri;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function fleetopsIdentifierFilterRequest(string $uri, array $query): Request
|
|
133
|
+
{
|
|
134
|
+
$request = Request::create('/' . $uri, 'GET', $query);
|
|
135
|
+
$session = app('session.store');
|
|
136
|
+
$session->put('company', 'company-uuid');
|
|
137
|
+
$request->setLaravelSession($session);
|
|
138
|
+
$request->setRouteResolver(fn () => new FleetOpsIdentifierFilterRoute($uri));
|
|
139
|
+
|
|
140
|
+
return $request;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @return array<int, string>
|
|
145
|
+
*/
|
|
146
|
+
function fleetopsFilteredInternalIds(string $filterClass, string $modelClass, string $uri, array $query): array
|
|
147
|
+
{
|
|
148
|
+
$filter = new $filterClass(fleetopsIdentifierFilterRequest($uri, $query));
|
|
149
|
+
|
|
150
|
+
return $filter->apply($modelClass::query())->pluck('internal_id')->sort()->values()->all();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
test('a public vehicle lookup by internal id matches exactly and stays inside the tenant', function () {
|
|
154
|
+
fleetopsIdentifierFilterBoot();
|
|
155
|
+
|
|
156
|
+
$matched = fleetopsFilteredInternalIds(VehicleFilter::class, Vehicle::class, 'v1/vehicles', ['internal_id' => 'VEH-10']);
|
|
157
|
+
|
|
158
|
+
// VEH-100 and VEH-101 are different vehicles, and the other tenant's VEH-10
|
|
159
|
+
// is a different vehicle again.
|
|
160
|
+
expect($matched)->toBe(['VEH-10']);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('the console keeps partial internal id search on vehicles', function () {
|
|
164
|
+
fleetopsIdentifierFilterBoot();
|
|
165
|
+
|
|
166
|
+
$matched = fleetopsFilteredInternalIds(VehicleFilter::class, Vehicle::class, 'int/v1/fleet-ops/vehicles', ['internal_id' => 'VEH-10']);
|
|
167
|
+
|
|
168
|
+
expect($matched)->toBe(['VEH-10', 'VEH-100', 'VEH-101']);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('a public driver lookup by internal id matches exactly and stays inside the tenant', function () {
|
|
172
|
+
fleetopsIdentifierFilterBoot();
|
|
173
|
+
|
|
174
|
+
expect(fleetopsFilteredInternalIds(DriverFilter::class, Driver::class, 'v1/drivers', ['internal_id' => 'DRV-10']))
|
|
175
|
+
->toBe(['DRV-10']);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('the console keeps partial internal id search on drivers', function () {
|
|
179
|
+
fleetopsIdentifierFilterBoot();
|
|
180
|
+
|
|
181
|
+
expect(fleetopsFilteredInternalIds(DriverFilter::class, Driver::class, 'int/v1/fleet-ops/drivers', ['internal_id' => 'DRV-10']))
|
|
182
|
+
->toBe(['DRV-10', 'DRV-100', 'DRV-101']);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('a public lookup by public id is exact for vehicles and drivers', function () {
|
|
186
|
+
fleetopsIdentifierFilterBoot();
|
|
187
|
+
|
|
188
|
+
// vehicle_id1 is a prefix of nothing here, so the partial and exact forms
|
|
189
|
+
// agree — the assertion that matters is that a public request produces an
|
|
190
|
+
// equality comparison at all, which the tenant row below proves.
|
|
191
|
+
expect(fleetopsFilteredInternalIds(VehicleFilter::class, Vehicle::class, 'v1/vehicles', ['public_id' => 'vehicle_id1']))
|
|
192
|
+
->toBe(['VEH-10'])
|
|
193
|
+
->and(fleetopsFilteredInternalIds(DriverFilter::class, Driver::class, 'v1/drivers', ['public_id' => 'driver_id1']))
|
|
194
|
+
->toBe(['DRV-10'])
|
|
195
|
+
// Another tenant's record is never reachable, by either identifier.
|
|
196
|
+
->and(fleetopsFilteredInternalIds(VehicleFilter::class, Vehicle::class, 'v1/vehicles', ['public_id' => 'vehicle_other01']))
|
|
197
|
+
->toBe([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('vin and plate number keep the search behaviour they already had', function () {
|
|
201
|
+
$connection = fleetopsIdentifierFilterBoot();
|
|
202
|
+
$connection->getSchemaBuilder()->table('vehicles', function ($blueprint) {
|
|
203
|
+
$blueprint->string('vin')->nullable();
|
|
204
|
+
$blueprint->string('plate_number')->nullable();
|
|
205
|
+
});
|
|
206
|
+
$connection->table('vehicles')->where('uuid', 'vehicle-uuid-1')->update(['vin' => 'VIN1234567890', 'plate_number' => 'SG-1000']);
|
|
207
|
+
$connection->table('vehicles')->where('uuid', 'vehicle-uuid-2')->update(['vin' => 'VIN1234567891', 'plate_number' => 'SG-10001']);
|
|
208
|
+
|
|
209
|
+
// Untouched by this change: both still match on a prefix.
|
|
210
|
+
expect(fleetopsFilteredInternalIds(VehicleFilter::class, Vehicle::class, 'v1/vehicles', ['vin' => 'VIN123456789']))
|
|
211
|
+
->toBe(['VEH-10', 'VEH-100'])
|
|
212
|
+
->and(fleetopsFilteredInternalIds(VehicleFilter::class, Vehicle::class, 'v1/vehicles', ['plate_number' => 'SG-1000']))
|
|
213
|
+
->toBe(['VEH-10', 'VEH-100']);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('the console keeps partial public id search on vehicles drivers and fleets', function () {
|
|
217
|
+
$connection = fleetopsIdentifierFilterBoot();
|
|
218
|
+
$connection->getSchemaBuilder()->create('fleets', function ($blueprint) {
|
|
219
|
+
$blueprint->increments('id');
|
|
220
|
+
foreach (['uuid', 'public_id', 'company_uuid', 'name', '_key'] as $column) {
|
|
221
|
+
$blueprint->string($column)->nullable();
|
|
222
|
+
}
|
|
223
|
+
$blueprint->timestamps();
|
|
224
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
225
|
+
});
|
|
226
|
+
$connection->table('fleets')->insert([
|
|
227
|
+
['uuid' => 'fleet-1', 'public_id' => 'fleet_id1', 'company_uuid' => 'company-uuid', 'name' => 'One'],
|
|
228
|
+
['uuid' => 'fleet-2', 'public_id' => 'fleet_id12', 'company_uuid' => 'company-uuid', 'name' => 'Two'],
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
// The console's id column filter is a search box; the public API's is a
|
|
232
|
+
// lookup. Both behaviours are kept, chosen by the resolved route.
|
|
233
|
+
$vehicles = (new VehicleFilter(fleetopsIdentifierFilterRequest('int/v1/fleet-ops/vehicles', ['public_id' => 'vehicle_id1'])))
|
|
234
|
+
->apply(Vehicle::query())->pluck('public_id')->sort()->values()->all();
|
|
235
|
+
|
|
236
|
+
$drivers = (new DriverFilter(fleetopsIdentifierFilterRequest('int/v1/fleet-ops/drivers', ['public_id' => 'driver_id1'])))
|
|
237
|
+
->apply(Driver::query())->pluck('public_id')->sort()->values()->all();
|
|
238
|
+
|
|
239
|
+
$fleets = (new Fleetbase\FleetOps\Http\Filter\FleetFilter(fleetopsIdentifierFilterRequest('int/v1/fleet-ops/fleets', ['public_id' => 'fleet_id1'])))
|
|
240
|
+
->apply(Fleetbase\FleetOps\Models\Fleet::query())->pluck('public_id')->sort()->values()->all();
|
|
241
|
+
|
|
242
|
+
expect($vehicles)->toBe(['vehicle_id1'])
|
|
243
|
+
->and($drivers)->toBe(['driver_id1'])
|
|
244
|
+
// fleet_id1 is a prefix of fleet_id12, so the console returns both.
|
|
245
|
+
->and($fleets)->toBe(['fleet_id1', 'fleet_id12']);
|
|
246
|
+
|
|
247
|
+
// The public API returns only the exact match.
|
|
248
|
+
$publicFleets = (new Fleetbase\FleetOps\Http\Filter\FleetFilter(fleetopsIdentifierFilterRequest('v1/fleets', ['public_id' => 'fleet_id1'])))
|
|
249
|
+
->apply(Fleetbase\FleetOps\Models\Fleet::query())->pluck('public_id')->all();
|
|
250
|
+
|
|
251
|
+
expect($publicFleets)->toBe(['fleet_id1']);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('the driver update request scopes its uniqueness lookup to the caller company', function () {
|
|
255
|
+
fleetopsIdentifierFilterBoot();
|
|
256
|
+
|
|
257
|
+
$request = Fleetbase\FleetOps\Http\Requests\UpdateDriverRequest::create('/v1/drivers/driver_id1', 'PUT', ['email' => 'a@example.test']);
|
|
258
|
+
$request->setRouteResolver(fn () => new class {
|
|
259
|
+
public function parameter($name, $default = null)
|
|
260
|
+
{
|
|
261
|
+
return $name === 'id' ? 'driver_id1' : $default;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public function uri(): string
|
|
265
|
+
{
|
|
266
|
+
return 'v1/drivers/{id}';
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
app()->instance('request', $request);
|
|
270
|
+
|
|
271
|
+
$reflection = new ReflectionMethod($request, 'linkedUserUuid');
|
|
272
|
+
$reflection->setAccessible(true);
|
|
273
|
+
|
|
274
|
+
// Ignoring the driver's own user by uuid is what lets an unchanged address
|
|
275
|
+
// be resent without tripping the uniqueness rule.
|
|
276
|
+
expect($reflection->invoke($request))->toBe('user-uuid-1');
|
|
277
|
+
|
|
278
|
+
// Another tenant's driver is not reachable, so its user is never ignored.
|
|
279
|
+
$foreign = Fleetbase\FleetOps\Http\Requests\UpdateDriverRequest::create('/v1/drivers/driver_other01', 'PUT', []);
|
|
280
|
+
$foreign->setRouteResolver(fn () => new class {
|
|
281
|
+
public function parameter($name, $default = null)
|
|
282
|
+
{
|
|
283
|
+
return $name === 'id' ? 'driver_other01' : $default;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public function uri(): string
|
|
287
|
+
{
|
|
288
|
+
return 'v1/drivers/{id}';
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect($reflection->invoke($foreign))->toBeNull();
|
|
293
|
+
});
|
|
@@ -78,6 +78,9 @@ function fleetopsSmallApiHelpersBoot(): SQLiteConnection
|
|
|
78
78
|
'users' => ['uuid', 'public_id', 'company_uuid', 'name', '_key'],
|
|
79
79
|
'places' => ['uuid', 'public_id', 'company_uuid', 'name', 'location', '_key'],
|
|
80
80
|
'service_areas' => ['uuid', 'public_id', 'company_uuid', 'name', 'border', '_key'],
|
|
81
|
+
// ServiceArea eager loads its zones, so resolving one by public id
|
|
82
|
+
// reads this table too.
|
|
83
|
+
'zones' => ['uuid', 'public_id', 'company_uuid', 'service_area_uuid', 'name', 'border', '_key'],
|
|
81
84
|
'companies' => ['uuid', 'public_id', 'name', 'country', 'options'],
|
|
82
85
|
'service_rates' => ['uuid', 'public_id', 'company_uuid', 'service_area_uuid', 'zone_uuid', 'service_name', 'service_type', 'base_fee', 'per_km_flat_rate_fee', 'rate_calculation_method', 'currency', 'estimated_days', 'duration_terms', 'meta', 'slug', 'internal_id', '_key'],
|
|
83
86
|
'service_rate_fees' => ['uuid', 'public_id', 'service_rate_uuid', 'min', 'max', 'fee', 'distance_unit', '_key'],
|
|
@@ -146,10 +149,13 @@ test('vendor issue fleet and fuel report helper batteries execute against sqlite
|
|
|
146
149
|
|
|
147
150
|
// Fleet battery
|
|
148
151
|
$fleetHelper = fleetopsSmallApiHelper(new FleetController());
|
|
149
|
-
|
|
152
|
+
// Relationship inputs resolve from a public id, scoped to the caller's company.
|
|
153
|
+
expect($fleetHelper('resolveUuid', Fleetbase\FleetOps\Models\ServiceArea::class, 'sa_smallone1'))->toBe('sa-sm-1');
|
|
150
154
|
$fleet = $fleetHelper('createFleet', ['company_uuid' => 'company-1', 'name' => 'Battery Fleet']);
|
|
151
155
|
expect($connection->table('fleets')->count())->toBe(1);
|
|
152
156
|
$foundFleet = $fleetHelper('findFleet', (string) $connection->table('fleets')->value('public_id'));
|
|
157
|
+
// The hierarchy guard walks upward through this seam.
|
|
158
|
+
expect($fleetHelper('parentUuidOf', $foundFleet->uuid))->toBeNull();
|
|
153
159
|
expect($foundFleet->uuid)->toBe($fleet->uuid)
|
|
154
160
|
->and($fleetHelper('fleetResource', $foundFleet))->toBeInstanceOf(Fleetbase\FleetOps\Http\Resources\v1\Fleet::class)
|
|
155
161
|
->and($fleetHelper('fleetResourceCollection', collect([$foundFleet])))->toBeInstanceOf(Illuminate\Http\Resources\Json\ResourceCollection::class)
|
|
@@ -315,7 +315,9 @@ test('lookup create and response helpers execute their real implementations', fu
|
|
|
315
315
|
|
|
316
316
|
expect($probe->callProtected('findVehicle', ['vehicle_test']))->toBeInstanceOf(Vehicle::class)
|
|
317
317
|
->and($probe->callProtected('findDriver', ['driver_test']))->toBeInstanceOf(Driver::class)
|
|
318
|
-
|
|
318
|
+
// Vendor now resolves through the shared public-id resolver, which
|
|
319
|
+
// scopes the lookup to the session company.
|
|
320
|
+
->and($probe->callProtected('resolveUuid', [Fleetbase\FleetOps\Models\Vendor::class, 'vendor_test']))->toBe('vendor-1');
|
|
319
321
|
|
|
320
322
|
expect(fn () => $probe->callProtected('findVehicle', ['missing']))->toThrow(ModelNotFoundException::class)
|
|
321
323
|
->and(fn () => $probe->callProtected('findDriver', ['missing']))->toThrow(ModelNotFoundException::class);
|