@fleetbase/fleetops-engine 0.6.61 → 0.6.62
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 +23 -8
- 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
|
@@ -290,6 +290,8 @@ namespace {
|
|
|
290
290
|
});
|
|
291
291
|
|
|
292
292
|
test('vehicle and fuel report requests expose core validation contracts', function () {
|
|
293
|
+
session(['company' => 'company-uuid']);
|
|
294
|
+
|
|
293
295
|
$vehicleRules = requestRules(CreateVehicleRequest::class);
|
|
294
296
|
$fuelReportRules = requestRules(CreateFuelReportRequest::class);
|
|
295
297
|
$fuelReportUpdateRules = requestRules(UpdateFuelReportRequest::class, 'PATCH');
|
|
@@ -305,6 +307,53 @@ namespace {
|
|
|
305
307
|
// error. These rules are what stop that.
|
|
306
308
|
->and($vehicleRules['odometer'])->toBe('nullable|numeric|min:0')
|
|
307
309
|
->and($vehicleRules['odometer_unit'])->toBe('nullable|string|max:12')
|
|
310
|
+
// Postman documented three statuses while the API accepted nineteen.
|
|
311
|
+
// This enum is the source of truth both must agree with; dropping a
|
|
312
|
+
// value would silently break an integration already sending it.
|
|
313
|
+
->and(CreateVehicleRequest::STATUSES)->toContain(
|
|
314
|
+
'active',
|
|
315
|
+
'available',
|
|
316
|
+
'in_use',
|
|
317
|
+
'maintenance',
|
|
318
|
+
'out_of_service',
|
|
319
|
+
'reserved',
|
|
320
|
+
'retired',
|
|
321
|
+
'staging',
|
|
322
|
+
'on_route',
|
|
323
|
+
'idle',
|
|
324
|
+
'cleaning',
|
|
325
|
+
'awaiting_parts',
|
|
326
|
+
'inspection_due',
|
|
327
|
+
'inspection_failed',
|
|
328
|
+
'accident',
|
|
329
|
+
'compliance_hold',
|
|
330
|
+
'stolen',
|
|
331
|
+
'operational',
|
|
332
|
+
'decommissioned',
|
|
333
|
+
)
|
|
334
|
+
// Every newly exposed vehicle field carries a type-appropriate rule.
|
|
335
|
+
->and($vehicleRules['year'])->toBe('nullable|integer|min:1900|max:2100')
|
|
336
|
+
->and($vehicleRules['dpf_equipped'])->toBe('nullable|boolean')
|
|
337
|
+
->and($vehicleRules['purchased_at'])->toBe('nullable|date')
|
|
338
|
+
->and($vehicleRules['currency'])->toBe('nullable|string|size:3')
|
|
339
|
+
->and($vehicleRules['specs'])->toBe('nullable|array')
|
|
340
|
+
->and($vehicleRules['meta'])->toBe('nullable|array')
|
|
341
|
+
->and($vehicleRules['seating_capacity'])->toBe('nullable|integer|min:0')
|
|
342
|
+
->and($vehicleRules['weight'])->toBe('nullable|numeric|min:0')
|
|
343
|
+
->and($vehicleRules['return_to_depot'])->toBe('nullable|boolean')
|
|
344
|
+
// Relationship inputs are confined to the caller's own company.
|
|
345
|
+
->and(ruleStrings($vehicleRules['vendor']))->toContain('exists:vendors,public_id')
|
|
346
|
+
->and(ruleStrings($vehicleRules['driver']))->toContain('exists:drivers,public_id')
|
|
347
|
+
->and(ruleStrings($vehicleRules['category']))->toContain('exists:categories,public_id')
|
|
348
|
+
->and(ruleStrings($vehicleRules['warranty']))->toContain('exists:warranties,public_id')
|
|
349
|
+
->and($vehicleRules['vendor'][2]->constraints)->toBe([
|
|
350
|
+
['where', 'company_uuid', 'company-uuid'],
|
|
351
|
+
['whereNull', 'deleted_at'],
|
|
352
|
+
])
|
|
353
|
+
->and($vehicleRules['driver'][2]->constraints)->toBe([
|
|
354
|
+
['where', 'company_uuid', 'company-uuid'],
|
|
355
|
+
['whereNull', 'deleted_at'],
|
|
356
|
+
])
|
|
308
357
|
->and($fuelReportRules['driver'])->toBe(['required'])
|
|
309
358
|
->and($fuelReportRules['odometer'])->toBe(['required'])
|
|
310
359
|
->and($fuelReportRules['volume'])->toBe(['required'])
|
|
@@ -434,7 +483,30 @@ namespace {
|
|
|
434
483
|
])
|
|
435
484
|
->and(ruleStrings($fleetCreateRules['name']))->toContain('required')
|
|
436
485
|
->and(ruleStrings($fleetPatchRules['name']))->not->toContain('required')
|
|
437
|
-
->and($fleetCreateRules['service_area'])->
|
|
486
|
+
->and(ruleStrings($fleetCreateRules['service_area']))->toContain('nullable', 'exists:service_areas,public_id')
|
|
487
|
+
->and(ruleStrings($fleetCreateRules['zone']))->toContain('exists:zones,public_id')
|
|
488
|
+
->and(ruleStrings($fleetCreateRules['vendor']))->toContain('exists:vendors,public_id')
|
|
489
|
+
->and(ruleStrings($fleetCreateRules['parent_fleet']))->toContain('exists:fleets,public_id')
|
|
490
|
+
->and($fleetCreateRules['color'])->toBe('nullable|string|max:64')
|
|
491
|
+
->and($fleetCreateRules['task'])->toBe('nullable|string|max:191')
|
|
492
|
+
->and($fleetCreateRules['status'])->toBe('nullable|string|max:64')
|
|
493
|
+
// Every fleet relationship input is confined to the caller's own
|
|
494
|
+
// company, so another organization's public id cannot be assigned
|
|
495
|
+
// — and cannot be probed for existence either.
|
|
496
|
+
->and($fleetCreateRules['service_area'][2]->constraints)->toBe([
|
|
497
|
+
['where', 'company_uuid', 'company-uuid'],
|
|
498
|
+
['whereNull', 'deleted_at'],
|
|
499
|
+
])
|
|
500
|
+
->and($fleetCreateRules['parent_fleet'][2]->constraints)->toBe([
|
|
501
|
+
['where', 'company_uuid', 'company-uuid'],
|
|
502
|
+
['whereNull', 'deleted_at'],
|
|
503
|
+
])
|
|
504
|
+
// A validation message naming `parent_fleet` reads worse than one
|
|
505
|
+
// naming "parent fleet".
|
|
506
|
+
->and(CreateFleetRequest::create('/fleetops-test', 'POST')->attributes())->toBe([
|
|
507
|
+
'service_area' => 'service area',
|
|
508
|
+
'parent_fleet' => 'parent fleet',
|
|
509
|
+
])
|
|
438
510
|
->and(requestRules(CancelOrderRequest::class))->toBe(['order' => 'required|exists:orders,uuid'])
|
|
439
511
|
->and(requestRules(DecodeTrackingNumberQR::class))->toBe(['code' => 'required|string'])
|
|
440
512
|
->and(requestRules(CreateServiceQuoteRequest::class))->toBe([]);
|
|
@@ -838,6 +910,9 @@ namespace {
|
|
|
838
910
|
});
|
|
839
911
|
|
|
840
912
|
test('driver request authorizes api sanctum and navigator sessions with identity rules', function () {
|
|
913
|
+
// The harness `session()` is a process-wide static independent of the
|
|
914
|
+
// bound store, and the company-scoped rule closures read it.
|
|
915
|
+
session(['company' => 'company-uuid']);
|
|
841
916
|
bindFleetOpsRequestSession();
|
|
842
917
|
|
|
843
918
|
$request = CreateDriverRequest::create('/fleetops-test', 'POST', [
|
|
@@ -854,15 +929,27 @@ namespace {
|
|
|
854
929
|
expect($request->authorize())->toBeTrue()
|
|
855
930
|
->and(ruleStrings($createRules['name']))->toContain('required')
|
|
856
931
|
->and(ruleStrings($patchRules['name']))->not->toContain('required')
|
|
857
|
-
|
|
858
|
-
|
|
932
|
+
// Contact details are optional but still validated and still unique:
|
|
933
|
+
// an operational driver record may legitimately have neither.
|
|
934
|
+
->and(ruleStrings($createRules['email']))->toContain('nullable', 'email', 'unique:users')
|
|
935
|
+
->and(ruleStrings($createRules['email']))->not->toContain('required')
|
|
936
|
+
->and(ruleStrings($createRules['phone']))->toContain('nullable', 'unique:users')
|
|
937
|
+
->and(ruleStrings($createRules['phone']))->not->toContain('required')
|
|
859
938
|
->and($createRules['password'])->toBe('nullable|string')
|
|
860
939
|
->and($createRules['country'])->toBe('nullable|size:2')
|
|
861
|
-
->and($createRules['vehicle'])->
|
|
940
|
+
->and(ruleStrings($createRules['vehicle']))->toContain('nullable', 'string', 'starts_with:vehicle_', 'exists:vehicles,public_id')
|
|
862
941
|
->and($createRules['license_expiry'])->toBe('nullable|date')
|
|
863
942
|
->and($createRules['status'])->toBe('nullable|string|in:active,available,inactive')
|
|
864
|
-
->and($createRules['vendor'])->
|
|
865
|
-
->and($createRules['job'])->
|
|
943
|
+
->and(ruleStrings($createRules['vendor']))->toContain('nullable', 'exists:vendors,public_id')
|
|
944
|
+
->and(ruleStrings($createRules['job']))->toContain('nullable', 'exists:orders,public_id')
|
|
945
|
+
->and($createRules['vehicle'][3]->constraints)->toBe([
|
|
946
|
+
['where', 'company_uuid', 'company-uuid'],
|
|
947
|
+
['whereNull', 'deleted_at'],
|
|
948
|
+
])
|
|
949
|
+
->and($createRules['internal_id'])->toBe('nullable|string|max:191')
|
|
950
|
+
->and($createRules['meta'])->toBe('nullable|array')
|
|
951
|
+
->and($createRules['current_status'])->toBe('nullable|string|max:64')
|
|
952
|
+
->and($createRules['max_travel_time'])->toBe('nullable|integer|min:0')
|
|
866
953
|
->and($createRules['location'][1])->toBeInstanceOf(ResolvablePoint::class)
|
|
867
954
|
->and($createRules['latitude'])->toBe(['nullable', 'required_with:longitude'])
|
|
868
955
|
->and($createRules['longitude'])->toBe(['nullable', 'required_with:latitude'])
|
|
@@ -954,7 +1041,13 @@ namespace {
|
|
|
954
1041
|
->and(ruleStrings($rateRules['peak_hours_calculation_method']))->toContain('required', 'in:percentage,flat')
|
|
955
1042
|
->and(ruleStrings($rateRules['peak_hours_percent']))->toContain('required', 'integer')
|
|
956
1043
|
->and(ruleStrings($rateRules['peak_hours_start']))->toContain('required', 'date_format:H:i')
|
|
957
|
-
->and(ruleStrings($rateRules['peak_hours_end']))->toContain('required', 'date_format:H:i')
|
|
1044
|
+
->and(ruleStrings($rateRules['peak_hours_end']))->toContain('required', 'date_format:H:i')
|
|
1045
|
+
// Both relationships are optional, so an empty one has to read as
|
|
1046
|
+
// absent. Without `nullable` the `exists` rule runs against the null
|
|
1047
|
+
// that ConvertEmptyStringsToNull produced and answers "the selected
|
|
1048
|
+
// service area is invalid" for a field the caller left blank.
|
|
1049
|
+
->and(ruleStrings($rateRules['service_area']))->toContain('nullable')
|
|
1050
|
+
->and(ruleStrings($rateRules['zone']))->toContain('nullable');
|
|
958
1051
|
|
|
959
1052
|
// A street address alone satisfies the name requirement on create, and
|
|
960
1053
|
// updates never require either field since the record already exists
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
|
+
use Fleetbase\FleetOps\Http\Controllers\Api\v1\FleetController;
|
|
3
4
|
use Fleetbase\FleetOps\Http\Resources\v1\Fleet as FleetResource;
|
|
4
5
|
use Fleetbase\FleetOps\Models\Fleet;
|
|
5
6
|
use Illuminate\Database\ConnectionResolver;
|
|
@@ -81,22 +82,65 @@ function fleetopsFleetResourceBoot(): SQLiteConnection
|
|
|
81
82
|
return $connection;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Resolve expansions the way a request does, then serialize.
|
|
87
|
+
*/
|
|
88
|
+
function fleetopsFleetResourcePayload(Fleet $fleet, array $query): array
|
|
89
|
+
{
|
|
90
|
+
$request = Request::create('/v1/fleets/fleet_parentone', 'GET', $query);
|
|
91
|
+
$controller = new FleetController();
|
|
92
|
+
$reflection = new ReflectionMethod(FleetController::class, 'applyPublicExpansions');
|
|
93
|
+
$reflection->setAccessible(true);
|
|
94
|
+
$reflection->invoke($controller, $request, FleetController::EXPANDABLE);
|
|
95
|
+
|
|
96
|
+
return (new FleetResource($fleet))->resolve($request);
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
test('fleet resource preloads requested subfleet driver and vehicle relations', function () {
|
|
85
100
|
fleetopsFleetResourceBoot();
|
|
86
101
|
|
|
87
|
-
$fleet
|
|
88
|
-
$
|
|
89
|
-
|
|
90
|
-
$resolved = (new FleetResource($fleet))->resolve($request);
|
|
102
|
+
$fleet = Fleet::where('uuid', 'fleet-parent-1')->first();
|
|
103
|
+
$resolved = fleetopsFleetResourcePayload($fleet, ['with' => ['subfleets', 'drivers', 'vehicles']]);
|
|
91
104
|
|
|
105
|
+
// `subFleets` is the relation Eloquent actually has. The public name differs
|
|
106
|
+
// from it only in case, and PHP method calls are case-insensitive, so
|
|
107
|
+
// `load('subfleets')` used to succeed and store a *second* copy under the
|
|
108
|
+
// mis-cased key — which `whenLoaded('subFleets')` then could not see.
|
|
92
109
|
expect($resolved['name'])->toBe('Parent Fleet')
|
|
93
|
-
->and($fleet->relationLoaded('
|
|
94
|
-
->and($
|
|
110
|
+
->and($fleet->relationLoaded('subFleets'))->toBeTrue()
|
|
111
|
+
->and($resolved)->toHaveKey('subfleets');
|
|
95
112
|
|
|
96
113
|
$subFleet = $fleet->getRelation('subFleets')->first();
|
|
114
|
+
|
|
115
|
+
// Asking for subfleets alongside drivers and vehicles still nests them, as
|
|
116
|
+
// the released contract did.
|
|
97
117
|
expect($subFleet)->not->toBeNull()
|
|
98
118
|
->and($subFleet->relationLoaded('drivers'))->toBeTrue()
|
|
99
119
|
->and($subFleet->relationLoaded('vehicles'))->toBeTrue()
|
|
100
120
|
->and($subFleet->drivers)->toHaveCount(1)
|
|
101
121
|
->and($subFleet->vehicles)->toHaveCount(1);
|
|
102
122
|
});
|
|
123
|
+
|
|
124
|
+
test('fleet resource resolves subfleets on its own, which the mis-cased load never did', function () {
|
|
125
|
+
fleetopsFleetResourceBoot();
|
|
126
|
+
|
|
127
|
+
$fleet = Fleet::where('uuid', 'fleet-parent-1')->first();
|
|
128
|
+
$resolved = fleetopsFleetResourcePayload($fleet, ['with' => 'subfleets']);
|
|
129
|
+
|
|
130
|
+
// Before the mapping, this returned no `subfleets` key at all: the load
|
|
131
|
+
// landed under `subfleets` and the resource looked for `subFleets`.
|
|
132
|
+
expect($resolved)->toHaveKey('subfleets')
|
|
133
|
+
->and($fleet->relationLoaded('subFleets'))->toBeTrue();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('fleet resource accepts the explicit nested expansion spelling', function () {
|
|
137
|
+
fleetopsFleetResourceBoot();
|
|
138
|
+
|
|
139
|
+
$fleet = Fleet::where('uuid', 'fleet-parent-1')->first();
|
|
140
|
+
fleetopsFleetResourcePayload($fleet, ['with' => ['subfleets.drivers']]);
|
|
141
|
+
|
|
142
|
+
$subFleet = $fleet->getRelation('subFleets')->first();
|
|
143
|
+
|
|
144
|
+
expect($subFleet->relationLoaded('drivers'))->toBeTrue()
|
|
145
|
+
->and($subFleet->relationLoaded('vehicles'))->toBeFalse();
|
|
146
|
+
});
|