@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
|
@@ -2,61 +2,104 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Exceptions\PublicRelationNotFoundException;
|
|
6
|
+
use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesFleetOpsApiResources;
|
|
7
|
+
use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesPublicExpansions;
|
|
5
8
|
use Fleetbase\FleetOps\Http\Requests\CreateFleetRequest;
|
|
6
9
|
use Fleetbase\FleetOps\Http\Requests\UpdateFleetRequest;
|
|
7
10
|
use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource;
|
|
8
11
|
use Fleetbase\FleetOps\Http\Resources\v1\Fleet as FleetResource;
|
|
12
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
9
13
|
use Fleetbase\FleetOps\Models\Fleet;
|
|
10
|
-
use Fleetbase\FleetOps\
|
|
14
|
+
use Fleetbase\FleetOps\Models\FleetDriver;
|
|
15
|
+
use Fleetbase\FleetOps\Models\FleetVehicle;
|
|
16
|
+
use Fleetbase\FleetOps\Models\ServiceArea;
|
|
17
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
18
|
+
use Fleetbase\FleetOps\Models\Vendor;
|
|
19
|
+
use Fleetbase\FleetOps\Models\Zone;
|
|
11
20
|
use Fleetbase\Http\Controllers\Controller;
|
|
21
|
+
use Fleetbase\Models\File;
|
|
22
|
+
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
23
|
+
use Illuminate\Database\UniqueConstraintViolationException;
|
|
12
24
|
use Illuminate\Http\Request;
|
|
13
25
|
|
|
14
26
|
class FleetController extends Controller
|
|
15
27
|
{
|
|
28
|
+
use ResolvesFleetOpsApiResources;
|
|
29
|
+
// Aliased so the fleet-specific override can still reach the shared
|
|
30
|
+
// resolution before adding the implicit subfleet nesting on top.
|
|
31
|
+
use ResolvesPublicExpansions {
|
|
32
|
+
resolvePublicExpansions as protected resolveAllowedExpansions;
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
/**
|
|
17
|
-
*
|
|
36
|
+
* Relationships eager loaded so the public resource can report each
|
|
37
|
+
* assignment as a public id without issuing a query per fleet.
|
|
18
38
|
*
|
|
19
|
-
*
|
|
39
|
+
* Loading them does not expand them: the resource returns the nested object
|
|
40
|
+
* only for a relation the caller named in `with`, which is the shape the
|
|
41
|
+
* endpoint has always had.
|
|
42
|
+
*/
|
|
43
|
+
protected const PUBLIC_RELATIONS = ['serviceArea', 'zone', 'vendor', 'parentFleet', 'photo'];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Public expansion name => Eloquent relation name.
|
|
47
|
+
*
|
|
48
|
+
* `subfleets` is the reason this map exists rather than a bare list: the
|
|
49
|
+
* public name and the relation differ only in case, and the automatic
|
|
50
|
+
* camelCase normalisation upstream turns `subfleets` into `subfleets`.
|
|
51
|
+
*/
|
|
52
|
+
public const EXPANDABLE = [
|
|
53
|
+
'service_area' => 'serviceArea',
|
|
54
|
+
'zone' => 'zone',
|
|
55
|
+
'vendor' => 'vendor',
|
|
56
|
+
'parent_fleet' => 'parentFleet',
|
|
57
|
+
'photo' => 'photo',
|
|
58
|
+
'subfleets' => 'subFleets',
|
|
59
|
+
'drivers' => 'drivers',
|
|
60
|
+
'vehicles' => 'vehicles',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a new Fleetbase Fleet resource.
|
|
20
65
|
*
|
|
21
66
|
* @return \Fleetbase\Http\Resources\Fleet
|
|
22
67
|
*/
|
|
23
68
|
public function create(CreateFleetRequest $request)
|
|
24
69
|
{
|
|
25
|
-
|
|
26
|
-
|
|
70
|
+
$this->applyPublicExpansions($request, static::EXPANDABLE);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
$input = $this->fleetInputFromRequest($request);
|
|
74
|
+
} catch (PublicRelationNotFoundException $exception) {
|
|
75
|
+
return $this->jsonResponse(['error' => $exception->getMessage()], 404);
|
|
76
|
+
}
|
|
27
77
|
|
|
28
78
|
// make sure company is set
|
|
29
79
|
$input['company_uuid'] = session('company');
|
|
30
80
|
|
|
31
|
-
// service area assignment
|
|
32
|
-
if ($request->has('service_area')) {
|
|
33
|
-
$input['service_area_uuid'] = $this->getServiceAreaUuid('service_areas', [
|
|
34
|
-
'public_id' => $request->input('service_area'),
|
|
35
|
-
'company_uuid' => session('company'),
|
|
36
|
-
]);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
81
|
// create the fleet
|
|
40
82
|
$fleet = $this->createFleet($input);
|
|
41
83
|
|
|
42
|
-
// response the
|
|
43
|
-
return $this->fleetResource($fleet);
|
|
84
|
+
// response the fleet resource
|
|
85
|
+
return $this->fleetResource($this->withPublicRelations($fleet));
|
|
44
86
|
}
|
|
45
87
|
|
|
46
88
|
/**
|
|
47
89
|
* Updates a Fleetbase Fleet resource.
|
|
48
90
|
*
|
|
49
|
-
* @param string
|
|
50
|
-
* @param \Fleetbase\Http\Requests\UpdateFleetRequest $request
|
|
91
|
+
* @param string $id
|
|
51
92
|
*
|
|
52
93
|
* @return \Fleetbase\Http\Resources\Fleet
|
|
53
94
|
*/
|
|
54
95
|
public function update($id, UpdateFleetRequest $request)
|
|
55
96
|
{
|
|
97
|
+
$this->applyPublicExpansions($request, static::EXPANDABLE);
|
|
98
|
+
|
|
56
99
|
// find for the fleet
|
|
57
100
|
try {
|
|
58
101
|
$fleet = $this->findFleet($id);
|
|
59
|
-
} catch (
|
|
102
|
+
} catch (ModelNotFoundException $exception) {
|
|
60
103
|
return $this->jsonResponse(
|
|
61
104
|
[
|
|
62
105
|
'error' => 'Fleet resource not found.',
|
|
@@ -65,22 +108,23 @@ class FleetController extends Controller
|
|
|
65
108
|
);
|
|
66
109
|
}
|
|
67
110
|
|
|
68
|
-
|
|
69
|
-
|
|
111
|
+
try {
|
|
112
|
+
$input = $this->fleetInputFromRequest($request);
|
|
113
|
+
} catch (PublicRelationNotFoundException $exception) {
|
|
114
|
+
return $this->jsonResponse(['error' => $exception->getMessage()], 404);
|
|
115
|
+
}
|
|
70
116
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'company_uuid' => session('company'),
|
|
76
|
-
]);
|
|
117
|
+
// a fleet may not be its own parent, nor sit beneath one of its own descendants
|
|
118
|
+
$hierarchyError = $this->hierarchyViolation($fleet, $input);
|
|
119
|
+
if ($hierarchyError) {
|
|
120
|
+
return $this->jsonResponse(['error' => $hierarchyError], 422);
|
|
77
121
|
}
|
|
78
122
|
|
|
79
123
|
// update the fleet
|
|
80
124
|
$fleet->update($input);
|
|
81
125
|
|
|
82
126
|
// response the fleet resource
|
|
83
|
-
return $this->fleetResource($fleet);
|
|
127
|
+
return $this->fleetResource($this->withPublicRelations($fleet));
|
|
84
128
|
}
|
|
85
129
|
|
|
86
130
|
/**
|
|
@@ -90,6 +134,8 @@ class FleetController extends Controller
|
|
|
90
134
|
*/
|
|
91
135
|
public function query(Request $request)
|
|
92
136
|
{
|
|
137
|
+
$this->applyPublicExpansions($request, static::EXPANDABLE);
|
|
138
|
+
|
|
93
139
|
$results = $this->queryFleets($request);
|
|
94
140
|
|
|
95
141
|
return $this->fleetResourceCollection($results);
|
|
@@ -102,10 +148,12 @@ class FleetController extends Controller
|
|
|
102
148
|
*/
|
|
103
149
|
public function find($id, Request $request)
|
|
104
150
|
{
|
|
151
|
+
$this->applyPublicExpansions($request, static::EXPANDABLE);
|
|
152
|
+
|
|
105
153
|
// find for the fleet
|
|
106
154
|
try {
|
|
107
155
|
$fleet = $this->findFleet($id);
|
|
108
|
-
} catch (
|
|
156
|
+
} catch (ModelNotFoundException $exception) {
|
|
109
157
|
return $this->jsonResponse(
|
|
110
158
|
[
|
|
111
159
|
'error' => 'Fleet resource not found.',
|
|
@@ -115,7 +163,7 @@ class FleetController extends Controller
|
|
|
115
163
|
}
|
|
116
164
|
|
|
117
165
|
// response the fleet resource
|
|
118
|
-
return $this->fleetResource($fleet);
|
|
166
|
+
return $this->fleetResource($this->withPublicRelations($fleet));
|
|
119
167
|
}
|
|
120
168
|
|
|
121
169
|
/**
|
|
@@ -128,7 +176,7 @@ class FleetController extends Controller
|
|
|
128
176
|
// find for the driver
|
|
129
177
|
try {
|
|
130
178
|
$fleet = $this->findFleet($id);
|
|
131
|
-
} catch (
|
|
179
|
+
} catch (ModelNotFoundException $exception) {
|
|
132
180
|
return $this->jsonResponse(
|
|
133
181
|
[
|
|
134
182
|
'error' => 'Fleet resource not found.',
|
|
@@ -144,9 +192,286 @@ class FleetController extends Controller
|
|
|
144
192
|
return $this->deletedFleetResource($fleet);
|
|
145
193
|
}
|
|
146
194
|
|
|
147
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Adds a vehicle to a fleet.
|
|
197
|
+
*
|
|
198
|
+
* Idempotent: assigning a vehicle that is already a member answers exactly as
|
|
199
|
+
* the first assignment did and creates no second pivot row. A membership that
|
|
200
|
+
* was previously removed is restored rather than duplicated.
|
|
201
|
+
*
|
|
202
|
+
* @return \Illuminate\Http\Response
|
|
203
|
+
*/
|
|
204
|
+
public function assignVehicle(string $id, string $vehicleId)
|
|
205
|
+
{
|
|
206
|
+
try {
|
|
207
|
+
$fleet = $this->findFleet($id);
|
|
208
|
+
$vehicle = $this->findVehicle($vehicleId);
|
|
209
|
+
} catch (ModelNotFoundException $exception) {
|
|
210
|
+
return $this->jsonResponse(['error' => 'Fleet or vehicle resource not found.'], 404);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
$this->assignVehicleToFleet($fleet, $vehicle);
|
|
214
|
+
|
|
215
|
+
return $this->jsonResponse($this->vehicleMembershipPayload($fleet, $vehicle, true), 200);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Removes a vehicle from a fleet.
|
|
220
|
+
*
|
|
221
|
+
* Removing a membership that is not there is a successful no-op, and removing
|
|
222
|
+
* a membership never deletes the vehicle itself, its driver assignment, or its
|
|
223
|
+
* membership of any other fleet.
|
|
224
|
+
*
|
|
225
|
+
* @return \Illuminate\Http\Response
|
|
226
|
+
*/
|
|
227
|
+
public function removeVehicle(string $id, string $vehicleId)
|
|
228
|
+
{
|
|
229
|
+
try {
|
|
230
|
+
$fleet = $this->findFleet($id);
|
|
231
|
+
$vehicle = $this->findVehicle($vehicleId);
|
|
232
|
+
} catch (ModelNotFoundException $exception) {
|
|
233
|
+
return $this->jsonResponse(['error' => 'Fleet or vehicle resource not found.'], 404);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
$this->removeVehicleFromFleet($fleet, $vehicle);
|
|
237
|
+
|
|
238
|
+
return $this->jsonResponse($this->vehicleMembershipPayload($fleet, $vehicle, false), 200);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Adds a driver to a fleet.
|
|
243
|
+
*
|
|
244
|
+
* @return \Illuminate\Http\Response
|
|
245
|
+
*/
|
|
246
|
+
public function assignDriver(string $id, string $driverId)
|
|
247
|
+
{
|
|
248
|
+
try {
|
|
249
|
+
$fleet = $this->findFleet($id);
|
|
250
|
+
$driver = $this->findDriver($driverId);
|
|
251
|
+
} catch (ModelNotFoundException $exception) {
|
|
252
|
+
return $this->jsonResponse(['error' => 'Fleet or driver resource not found.'], 404);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
$this->assignDriverToFleet($fleet, $driver);
|
|
256
|
+
|
|
257
|
+
return $this->jsonResponse($this->driverMembershipPayload($fleet, $driver, true), 200);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Removes a driver from a fleet.
|
|
262
|
+
*
|
|
263
|
+
* The driver keeps its vehicle assignment and every other fleet membership.
|
|
264
|
+
*
|
|
265
|
+
* @return \Illuminate\Http\Response
|
|
266
|
+
*/
|
|
267
|
+
public function removeDriver(string $id, string $driverId)
|
|
268
|
+
{
|
|
269
|
+
try {
|
|
270
|
+
$fleet = $this->findFleet($id);
|
|
271
|
+
$driver = $this->findDriver($driverId);
|
|
272
|
+
} catch (ModelNotFoundException $exception) {
|
|
273
|
+
return $this->jsonResponse(['error' => 'Fleet or driver resource not found.'], 404);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
$this->removeDriverFromFleet($fleet, $driver);
|
|
277
|
+
|
|
278
|
+
return $this->jsonResponse($this->driverMembershipPayload($fleet, $driver, false), 200);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Preserve the implicit subfleet nesting the released contract had.
|
|
283
|
+
*
|
|
284
|
+
* `?with[]=subfleets&with[]=drivers` used to load the subfleets' drivers as
|
|
285
|
+
* well as the fleet's own, so the nested collections were part of that
|
|
286
|
+
* response. The explicit `?with[]=subfleets.drivers` form is the better
|
|
287
|
+
* spelling, but dropping the implicit one would remove data a caller is
|
|
288
|
+
* already receiving.
|
|
289
|
+
*
|
|
290
|
+
* @param array<string, string> $allowed
|
|
291
|
+
*
|
|
292
|
+
* @return array<int, string>
|
|
293
|
+
*/
|
|
294
|
+
protected function resolvePublicExpansions(Request $request, array $allowed): array
|
|
295
|
+
{
|
|
296
|
+
$resolved = $this->resolveAllowedExpansions($request, $allowed);
|
|
297
|
+
|
|
298
|
+
if (!in_array('subFleets', $resolved, true)) {
|
|
299
|
+
return $resolved;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
foreach (['drivers', 'vehicles'] as $nested) {
|
|
303
|
+
$path = 'subFleets.' . $nested;
|
|
304
|
+
|
|
305
|
+
if (in_array($nested, $resolved, true) && !in_array($path, $resolved, true)) {
|
|
306
|
+
$resolved[] = $path;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return $resolved;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The explicit public input allowlist for a fleet.
|
|
315
|
+
*
|
|
316
|
+
* Everything outside this list is either generated (`public_id`, `slug`,
|
|
317
|
+
* `uuid`, `_key`), tenancy (`company_uuid`) or a raw relation column, and is
|
|
318
|
+
* resolved from a public id rather than accepted directly.
|
|
319
|
+
*
|
|
320
|
+
* @throws PublicRelationNotFoundException
|
|
321
|
+
*/
|
|
322
|
+
protected function fleetInputFromRequest(Request $request): array
|
|
323
|
+
{
|
|
324
|
+
$input = $request->only(['name', 'color', 'task', 'status']);
|
|
325
|
+
|
|
326
|
+
$this->applyPublicIdRelations($input, [
|
|
327
|
+
'service_area' => ['service_area_uuid', ServiceArea::class],
|
|
328
|
+
'zone' => ['zone_uuid', Zone::class],
|
|
329
|
+
'vendor' => ['vendor_uuid', Vendor::class],
|
|
330
|
+
'parent_fleet' => ['parent_fleet_uuid', Fleet::class],
|
|
331
|
+
'photo' => ['image_uuid', File::class],
|
|
332
|
+
], $request);
|
|
333
|
+
|
|
334
|
+
return $input;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Reject a parent assignment that would make the tree cyclic.
|
|
339
|
+
*
|
|
340
|
+
* Returns the error message to answer with, or null when the assignment is
|
|
341
|
+
* sound. Walking upward from the proposed parent is enough: a cycle exists
|
|
342
|
+
* exactly when this fleet is already somewhere on that chain.
|
|
343
|
+
*/
|
|
344
|
+
protected function hierarchyViolation(Fleet $fleet, array $input): ?string
|
|
345
|
+
{
|
|
346
|
+
if (!array_key_exists('parent_fleet_uuid', $input) || empty($input['parent_fleet_uuid'])) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
$parentUuid = $input['parent_fleet_uuid'];
|
|
351
|
+
|
|
352
|
+
if ($parentUuid === $fleet->uuid) {
|
|
353
|
+
return 'A fleet cannot be its own parent fleet.';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
$seen = [$parentUuid => true];
|
|
357
|
+
$ancestor = $this->parentUuidOf($parentUuid);
|
|
358
|
+
|
|
359
|
+
while ($ancestor && !isset($seen[$ancestor])) {
|
|
360
|
+
if ($ancestor === $fleet->uuid) {
|
|
361
|
+
return 'A fleet cannot be assigned beneath one of its own subfleets.';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
$seen[$ancestor] = true;
|
|
365
|
+
$ancestor = $this->parentUuidOf($ancestor);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
protected function parentUuidOf(string $uuid): ?string
|
|
372
|
+
{
|
|
373
|
+
return Fleet::where('uuid', $uuid)->value('parent_fleet_uuid');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
protected function withPublicRelations(Fleet $fleet): Fleet
|
|
377
|
+
{
|
|
378
|
+
$fleet->loadMissing(static::PUBLIC_RELATIONS);
|
|
379
|
+
|
|
380
|
+
return $fleet;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
protected function assignVehicleToFleet(Fleet $fleet, Vehicle $vehicle): void
|
|
384
|
+
{
|
|
385
|
+
$this->assignMembership(FleetVehicle::class, [
|
|
386
|
+
'fleet_uuid' => $fleet->uuid,
|
|
387
|
+
'vehicle_uuid' => $vehicle->uuid,
|
|
388
|
+
]);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Create or restore exactly one membership row for a pivot pair.
|
|
393
|
+
*
|
|
394
|
+
* Read-then-write is idempotent only against itself. Two requests arriving
|
|
395
|
+
* together both see no membership and both insert, which is precisely what
|
|
396
|
+
* an importer retrying a timed-out call produces. The composite unique index
|
|
397
|
+
* is the actual guarantee; this catches the violation the loser gets and
|
|
398
|
+
* finishes the winner's work, so both callers still receive the same
|
|
399
|
+
* successful answer.
|
|
400
|
+
*
|
|
401
|
+
* @param class-string $pivotClass
|
|
402
|
+
* @param array<string, string> $attributes
|
|
403
|
+
*/
|
|
404
|
+
protected function assignMembership(string $pivotClass, array $attributes): void
|
|
405
|
+
{
|
|
406
|
+
$membership = $pivotClass::withTrashed()->firstOrNew($attributes);
|
|
407
|
+
|
|
408
|
+
if ($membership->trashed()) {
|
|
409
|
+
$membership->restore();
|
|
410
|
+
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if ($membership->exists) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
$membership->save();
|
|
420
|
+
} catch (UniqueConstraintViolationException $exception) {
|
|
421
|
+
// Only this violation is swallowed. Anything else is a real failure
|
|
422
|
+
// and must not be reported as a successful assignment.
|
|
423
|
+
$winner = $pivotClass::withTrashed()->where($attributes)->first();
|
|
424
|
+
|
|
425
|
+
if (!$winner) {
|
|
426
|
+
throw $exception;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if ($winner->trashed()) {
|
|
430
|
+
$winner->restore();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
protected function removeVehicleFromFleet(Fleet $fleet, Vehicle $vehicle): void
|
|
148
436
|
{
|
|
149
|
-
|
|
437
|
+
FleetVehicle::where([
|
|
438
|
+
'fleet_uuid' => $fleet->uuid,
|
|
439
|
+
'vehicle_uuid' => $vehicle->uuid,
|
|
440
|
+
])->delete();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
protected function assignDriverToFleet(Fleet $fleet, Driver $driver): void
|
|
444
|
+
{
|
|
445
|
+
$this->assignMembership(FleetDriver::class, [
|
|
446
|
+
'fleet_uuid' => $fleet->uuid,
|
|
447
|
+
'driver_uuid' => $driver->uuid,
|
|
448
|
+
]);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
protected function removeDriverFromFleet(Fleet $fleet, Driver $driver): void
|
|
452
|
+
{
|
|
453
|
+
FleetDriver::where([
|
|
454
|
+
'fleet_uuid' => $fleet->uuid,
|
|
455
|
+
'driver_uuid' => $driver->uuid,
|
|
456
|
+
])->delete();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
protected function vehicleMembershipPayload(Fleet $fleet, Vehicle $vehicle, bool $assigned): array
|
|
460
|
+
{
|
|
461
|
+
return [
|
|
462
|
+
'fleet' => $fleet->public_id,
|
|
463
|
+
'vehicle' => $vehicle->public_id,
|
|
464
|
+
'assigned' => $assigned,
|
|
465
|
+
];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
protected function driverMembershipPayload(Fleet $fleet, Driver $driver, bool $assigned): array
|
|
469
|
+
{
|
|
470
|
+
return [
|
|
471
|
+
'fleet' => $fleet->public_id,
|
|
472
|
+
'driver' => $driver->public_id,
|
|
473
|
+
'assigned' => $assigned,
|
|
474
|
+
];
|
|
150
475
|
}
|
|
151
476
|
|
|
152
477
|
protected function createFleet(array $input): Fleet
|
|
@@ -159,9 +484,21 @@ class FleetController extends Controller
|
|
|
159
484
|
return Fleet::findRecordOrFail($id);
|
|
160
485
|
}
|
|
161
486
|
|
|
487
|
+
protected function findVehicle(string $id): Vehicle
|
|
488
|
+
{
|
|
489
|
+
return Vehicle::findRecordOrFail($id);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
protected function findDriver(string $id): Driver
|
|
493
|
+
{
|
|
494
|
+
return Driver::findRecordOrFail($id);
|
|
495
|
+
}
|
|
496
|
+
|
|
162
497
|
protected function queryFleets(Request $request)
|
|
163
498
|
{
|
|
164
|
-
return Fleet::queryWithRequest($request)
|
|
499
|
+
return Fleet::queryWithRequest($request, function (&$query) {
|
|
500
|
+
$query->with(static::PUBLIC_RELATIONS);
|
|
501
|
+
});
|
|
165
502
|
}
|
|
166
503
|
|
|
167
504
|
protected function fleetResource(Fleet $fleet)
|
|
@@ -157,7 +157,7 @@ class FuelReportController extends Controller
|
|
|
157
157
|
return $this->deletedFuelReportResource($fuelReport);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
protected function findDriverRecord(string $id): Driver
|
|
160
|
+
protected function findDriverRecord(?string $id): Driver
|
|
161
161
|
{
|
|
162
162
|
return Driver::findRecordOrFail($id);
|
|
163
163
|
}
|
|
@@ -158,7 +158,7 @@ class IssueController extends Controller
|
|
|
158
158
|
return $this->deletedIssueResource($issue);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
protected function findDriverRecord(string $id): Driver
|
|
161
|
+
protected function findDriverRecord(?string $id): Driver
|
|
162
162
|
{
|
|
163
163
|
return Driver::findRecordOrFail($id);
|
|
164
164
|
}
|
|
@@ -1873,7 +1873,7 @@ class OrderController extends Controller
|
|
|
1873
1873
|
return new Payload();
|
|
1874
1874
|
}
|
|
1875
1875
|
|
|
1876
|
-
protected function findDriverByPublicId(string $publicId): ?Driver
|
|
1876
|
+
protected function findDriverByPublicId(?string $publicId): ?Driver
|
|
1877
1877
|
{
|
|
1878
1878
|
return Driver::where(['public_id' => $publicId, 'company_uuid' => session('company')])->first();
|
|
1879
1879
|
}
|
|
@@ -218,7 +218,7 @@ class ServiceAreaController extends Controller
|
|
|
218
218
|
return ServiceArea::createMultiPolygonFromPoint($point, $radius);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
protected function serviceAreaUuid(string $publicId, array $where): ?string
|
|
221
|
+
protected function serviceAreaUuid(?string $publicId, array $where): ?string
|
|
222
222
|
{
|
|
223
223
|
return Utils::getUuid('service_areas', $where);
|
|
224
224
|
}
|
|
@@ -187,7 +187,7 @@ class TrackingStatusController extends Controller
|
|
|
187
187
|
return Utils::getUuid($table, $where);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
protected function getOrderTrackingNumberUuid(string $orderId): ?string
|
|
190
|
+
protected function getOrderTrackingNumberUuid(?string $orderId): ?string
|
|
191
191
|
{
|
|
192
192
|
return Order::where('public_id', $orderId)->value('tracking_number_uuid');
|
|
193
193
|
}
|