@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.
Files changed (64) hide show
  1. package/RELEASE.md +23 -8
  2. package/addon/services/map-settings.js +4 -1
  3. package/composer.json +1 -1
  4. package/extension.json +1 -1
  5. package/package.json +1 -1
  6. package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
  7. package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
  8. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
  9. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
  10. package/server/src/Http/Controllers/Api/v1/DriverController.php +93 -57
  11. package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
  12. package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
  13. package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
  14. package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
  15. package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
  16. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
  17. package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
  18. package/server/src/Http/Controllers/Api/v1/VehicleController.php +134 -35
  19. package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
  20. package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
  21. package/server/src/Http/Filter/DriverFilter.php +71 -15
  22. package/server/src/Http/Filter/FleetFilter.php +32 -33
  23. package/server/src/Http/Filter/VehicleFilter.php +57 -19
  24. package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
  25. package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
  26. package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
  27. package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
  28. package/server/src/Http/Requests/CreateVehicleRequest.php +150 -31
  29. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
  30. package/server/src/Http/Resources/v1/Driver.php +22 -2
  31. package/server/src/Http/Resources/v1/Fleet.php +38 -25
  32. package/server/src/Http/Resources/v1/Vehicle.php +46 -4
  33. package/server/src/Models/Driver.php +2 -2
  34. package/server/src/routes.php +8 -0
  35. package/server/tests/ApiDriverControllerContractsTest.php +284 -13
  36. package/server/tests/ApiEntityControllerContractsTest.php +1 -1
  37. package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
  38. package/server/tests/ApiFleetControllerContractsTest.php +354 -21
  39. package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
  40. package/server/tests/ApiIssueControllerContractsTest.php +1 -1
  41. package/server/tests/ApiOrderControllerContractsTest.php +1 -1
  42. package/server/tests/ApiPartControllerContractsTest.php +2 -2
  43. package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
  44. package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
  45. package/server/tests/ApiVehicleControllerContractsTest.php +175 -8
  46. package/server/tests/ControllerFilterContractsTest.php +132 -13
  47. package/server/tests/ControllerHelperContractsTest.php +6 -3
  48. package/server/tests/DriverFilterExecutionTest.php +130 -6
  49. package/server/tests/ExceptionContractsTest.php +15 -0
  50. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
  51. package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
  52. package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
  53. package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
  54. package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
  55. package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
  56. package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
  57. package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
  58. package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
  59. package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
  60. package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
  61. package/server/tests/FleetHierarchyResourceTest.php +56 -8
  62. package/server/tests/RequestContractsTest.php +100 -7
  63. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
  64. package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
@@ -5,6 +5,9 @@ namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
5
5
  use Fleetbase\FleetOps\Events\GeofenceEntered;
6
6
  use Fleetbase\FleetOps\Events\GeofenceExited;
7
7
  use Fleetbase\FleetOps\Events\VehicleLocationChanged;
8
+ use Fleetbase\FleetOps\Exceptions\PublicRelationNotFoundException;
9
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesFleetOpsApiResources;
10
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesPublicExpansions;
8
11
  use Fleetbase\FleetOps\Http\Requests\CreateVehicleRequest;
9
12
  use Fleetbase\FleetOps\Http\Requests\UpdateVehicleRequest;
10
13
  use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource;
@@ -12,16 +15,45 @@ use Fleetbase\FleetOps\Http\Resources\v1\Vehicle as VehicleResource;
12
15
  use Fleetbase\FleetOps\Jobs\CheckGeofenceDwell;
13
16
  use Fleetbase\FleetOps\Models\Driver;
14
17
  use Fleetbase\FleetOps\Models\Vehicle;
18
+ use Fleetbase\FleetOps\Models\Vendor;
19
+ use Fleetbase\FleetOps\Models\Warranty;
15
20
  use Fleetbase\FleetOps\Support\GeofenceIntersectionService;
16
21
  use Fleetbase\FleetOps\Support\Utils;
17
22
  use Fleetbase\Http\Controllers\Controller;
18
23
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
24
+ use Fleetbase\Models\Category;
25
+ use Fleetbase\Models\File;
19
26
  use Illuminate\Http\Request;
20
27
  use Illuminate\Support\Carbon;
21
28
  use Illuminate\Support\Facades\DB;
22
29
 
23
30
  class VehicleController extends Controller
24
31
  {
32
+ use ResolvesFleetOpsApiResources;
33
+ use ResolvesPublicExpansions;
34
+
35
+ /**
36
+ * Relationships eager loaded so the public resource can report each
37
+ * assignment as a public id without a query per vehicle.
38
+ *
39
+ * Loading them does not expand them: the nested object is returned only for
40
+ * a relation the caller named in `with`, which is the shape the endpoint has
41
+ * always had.
42
+ */
43
+ protected const PUBLIC_RELATIONS = ['vendor', 'category', 'warranty', 'driver', 'photo'];
44
+
45
+ /**
46
+ * Public expansion name => Eloquent relation name.
47
+ */
48
+ public const EXPANDABLE = [
49
+ 'driver' => 'driver',
50
+ 'vendor' => 'vendor',
51
+ 'category' => 'category',
52
+ 'warranty' => 'warranty',
53
+ 'photo' => 'photo',
54
+ 'devices' => 'devices',
55
+ ];
56
+
25
57
  /**
26
58
  * Creates a new Fleetbase Vehicle resource.
27
59
  *
@@ -31,8 +63,14 @@ class VehicleController extends Controller
31
63
  */
32
64
  public function create(CreateVehicleRequest $request)
33
65
  {
66
+ $this->applyPublicExpansions($request, static::EXPANDABLE);
67
+
34
68
  // get request input
35
- $input = $this->vehicleInputFromRequest($request);
69
+ try {
70
+ $input = $this->vehicleInputFromRequest($request);
71
+ } catch (PublicRelationNotFoundException $exception) {
72
+ return $this->jsonResponse(['error' => $exception->getMessage()], 404);
73
+ }
36
74
 
37
75
  // make sure company is set
38
76
  $input['company_uuid'] = session('company');
@@ -40,14 +78,6 @@ class VehicleController extends Controller
40
78
  // set default online
41
79
  $input = $this->withDefaultOnline($input);
42
80
 
43
- // vendor assignment
44
- if ($request->has('vendor')) {
45
- $input['vendor_uuid'] = $this->getVendorUuid('vendors', [
46
- 'public_id' => $request->input('vendor'),
47
- 'company_uuid' => session('company'),
48
- ]);
49
- }
50
-
51
81
  // latitude / longitude
52
82
  $input = $this->withCoordinateLocation($input, $request);
53
83
 
@@ -85,6 +115,8 @@ class VehicleController extends Controller
85
115
  */
86
116
  public function update($id, UpdateVehicleRequest $request)
87
117
  {
118
+ $this->applyPublicExpansions($request, static::EXPANDABLE);
119
+
88
120
  // find for the vehicle
89
121
  try {
90
122
  $vehicle = $this->findVehicle($id);
@@ -98,19 +130,12 @@ class VehicleController extends Controller
98
130
  }
99
131
 
100
132
  // get request input
101
- $input = $this->vehicleInputFromRequest($request);
102
-
103
- // vendor assignment
104
- if ($request->has('vendor')) {
105
- $input['vendor_uuid'] = $this->getVendorUuid('vendors', [
106
- 'public_id' => $request->input('vendor'),
107
- 'company_uuid' => session('company'),
108
- ]);
133
+ try {
134
+ $input = $this->vehicleInputFromRequest($request);
135
+ } catch (PublicRelationNotFoundException $exception) {
136
+ return $this->jsonResponse(['error' => $exception->getMessage()], 404);
109
137
  }
110
138
 
111
- // set default online
112
- $input = $this->withDefaultOnline($input);
113
-
114
139
  // latitude / longitude
115
140
  $input = $this->withCoordinateLocation($input, $request);
116
141
 
@@ -157,6 +182,8 @@ class VehicleController extends Controller
157
182
  */
158
183
  public function query(Request $request)
159
184
  {
185
+ $this->applyPublicExpansions($request, static::EXPANDABLE);
186
+
160
187
  $results = $this->queryVehicles($request);
161
188
 
162
189
  return $this->vehicleResourceCollection($results);
@@ -169,8 +196,17 @@ class VehicleController extends Controller
169
196
  *
170
197
  * @return \Fleetbase\Http\Resources\VehicleCollection
171
198
  */
172
- public function find($id)
199
+ public function find($id, ?Request $request = null)
173
200
  {
201
+ // Falls back to the container's request rather than trusting injection:
202
+ // the parameter carries a default, and Laravel's controller dispatcher
203
+ // skips resolving a type-hinted dependency that has one. It arrives null,
204
+ // the expansions are never mapped, and an unsupported name reaches
205
+ // Eloquent — a 500 for a typo, on the one endpoint most likely to be
206
+ // handed one.
207
+ $request = $request instanceof Request ? $request : request();
208
+ $this->applyPublicExpansions($request, static::EXPANDABLE);
209
+
174
210
  // find for the vehicle
175
211
  try {
176
212
  $vehicle = $this->findVehicle($id);
@@ -347,23 +383,75 @@ class VehicleController extends Controller
347
383
  }
348
384
  }
349
385
 
386
+ /**
387
+ * The explicit public input allowlist for a vehicle.
388
+ *
389
+ * Mirrors every safe business field the model and the Fleet-Ops vehicle form
390
+ * expose. Deliberately absent: `company_uuid` and the raw `*_uuid` relation
391
+ * columns (resolved from public ids below), `public_id` / `uuid` / `_key` /
392
+ * `slug` (generated), `avatar_url` when given as a uuid, `vin_data` (written
393
+ * by the VIN decoder) and `telematics` (written by telematics ingestion).
394
+ *
395
+ * @throws PublicRelationNotFoundException
396
+ */
350
397
  protected function vehicleInputFromRequest(Request $request): array
351
398
  {
352
- return $request->only([
353
- 'status', 'make', 'model', 'year', 'trim', 'type', 'plate_number', 'vin',
354
- 'meta', 'online', 'location', 'altitude', 'heading', 'speed',
399
+ $input = $request->only([
400
+ // Identity and description
401
+ 'internal_id', 'name', 'description', 'make', 'model', 'model_type', 'year',
402
+ 'trim', 'color', 'type', 'class', 'plate_number', 'vin', 'serial_number',
403
+ 'call_sign', 'fuel_card_number',
404
+ // Measurement and operation.
405
+ //
355
406
  // Odometer is fillable on the model and unrestricted by the request
356
407
  // rules, but was absent here — so a caller sending one received a
357
408
  // 200 and a response body that looked correct while the reading was
358
409
  // discarded. Recording mileage is the single most common write a
359
410
  // driver app makes against a vehicle.
360
- 'odometer', 'odometer_unit',
361
- 'payload_capacity', 'payload_capacity_volume',
362
- 'payload_capacity_pallets', 'payload_capacity_parcels',
411
+ 'odometer', 'odometer_unit', 'odometer_at_purchase', 'measurement_system',
412
+ 'fuel_type', 'fuel_volume_unit', 'online', 'status',
413
+ 'location', 'altitude', 'heading', 'speed',
414
+ // Body, capacity and dimensions
415
+ 'transmission', 'body_type', 'body_sub_type', 'usage_type', 'ownership_type',
416
+ 'cargo_volume', 'passenger_volume', 'interior_volume', 'weight', 'width',
417
+ 'length', 'height', 'towing_capacity', 'payload_capacity', 'seating_capacity',
418
+ 'ground_clearance', 'bed_length', 'fuel_capacity',
419
+ // Lifecycle and financing
420
+ 'financing_status', 'loan_number_of_payments', 'loan_first_payment', 'loan_amount',
421
+ 'estimated_service_life_distance_unit', 'estimated_service_life_distance',
422
+ 'estimated_service_life_months', 'insurance_value', 'depreciation_rate',
423
+ 'current_value', 'acquisition_cost', 'currency', 'purchased_at', 'lease_expires_at',
424
+ // Regulatory and engine specifications
425
+ 'emission_standard', 'dpf_equipped', 'scr_equipped', 'gvwr', 'gcwr',
426
+ 'engine_number', 'engine_model', 'engine_make', 'engine_family',
427
+ 'engine_configuration', 'engine_displacement', 'engine_size', 'horsepower',
428
+ 'horsepower_rpm', 'torque', 'torque_rpm', 'number_of_cylinders',
429
+ 'cylinder_arrangement',
430
+ // Structured and descriptive fields
431
+ 'specs', 'details', 'notes', 'meta',
432
+ // Orchestrator
433
+ 'payload_capacity_volume', 'payload_capacity_pallets', 'payload_capacity_parcels',
363
434
  'skills', 'max_tasks', 'time_window_start', 'time_window_end', 'return_to_depot',
364
435
  ]);
436
+
437
+ $this->applyPublicIdRelations($input, [
438
+ 'vendor' => ['vendor_uuid', Vendor::class],
439
+ 'category' => ['category_uuid', Category::class],
440
+ 'warranty' => ['warranty_uuid', Warranty::class],
441
+ 'photo' => ['photo_uuid', File::class],
442
+ ], $request);
443
+
444
+ return $input;
365
445
  }
366
446
 
447
+ /**
448
+ * A vehicle that does not say otherwise starts offline.
449
+ *
450
+ * Applied on create only. Applying it on update too meant any partial write —
451
+ * a plate correction, an odometer reading — silently knocked the vehicle
452
+ * offline, because the absent key was read as "set it to false" rather than
453
+ * "leave it alone".
454
+ */
367
455
  protected function withDefaultOnline(array $input): array
368
456
  {
369
457
  if (!isset($input['online'])) {
@@ -394,11 +482,6 @@ class VehicleController extends Controller
394
482
  ];
395
483
  }
396
484
 
397
- protected function getVendorUuid(string $table, array $where): ?string
398
- {
399
- return Utils::getUuid($table, $where);
400
- }
401
-
402
485
  protected function createVehicle(array $input): Vehicle
403
486
  {
404
487
  return Vehicle::create($input);
@@ -409,19 +492,35 @@ class VehicleController extends Controller
409
492
  return Vehicle::findRecordOrFail($id);
410
493
  }
411
494
 
412
- protected function findDriver(string $id): Driver
495
+ protected function findDriver(?string $id): Driver
413
496
  {
414
497
  return Driver::findRecordOrFail($id);
415
498
  }
416
499
 
417
500
  protected function queryVehicles(Request $request)
418
501
  {
419
- return Vehicle::queryWithRequest($request);
502
+ return Vehicle::queryWithRequest($request, function (&$query) {
503
+ $query->with(static::PUBLIC_RELATIONS);
504
+ });
420
505
  }
421
506
 
422
507
  protected function vehicleResource(Vehicle $vehicle)
423
508
  {
424
- return new VehicleResource($vehicle);
509
+ return new VehicleResource($this->withPublicRelations($vehicle));
510
+ }
511
+
512
+ /**
513
+ * Load the relations the public resource reports as identifiers.
514
+ *
515
+ * One query per relation instead of one per relation per read, and it makes
516
+ * an assignment made moments earlier in the same request readable back
517
+ * immediately.
518
+ */
519
+ protected function withPublicRelations(Vehicle $vehicle): Vehicle
520
+ {
521
+ $vehicle->loadMissing(static::PUBLIC_RELATIONS);
522
+
523
+ return $vehicle;
425
524
  }
426
525
 
427
526
  protected function vehicleResourceCollection($results)
@@ -212,7 +212,7 @@ class ZoneController extends Controller
212
212
  return (int) $request->input('radius', 500);
213
213
  }
214
214
 
215
- protected function serviceAreaUuid(string $publicId, array $where): ?string
215
+ protected function serviceAreaUuid(?string $publicId, array $where): ?string
216
216
  {
217
217
  return Utils::getUuid('service_areas', $where);
218
218
  }
@@ -0,0 +1,54 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Filter\Concerns;
4
+
5
+ use Fleetbase\FleetOps\Support\Utils;
6
+ use Fleetbase\Support\Http;
7
+
8
+ /**
9
+ * Resolves a relationship filter value expressed as a public identifier into the
10
+ * internal uuids the column actually stores.
11
+ *
12
+ * The public API never hands out uuids, so `?vendor=vendor_abc` has to be
13
+ * translated before it can be compared against `vendor_uuid`. Internal console
14
+ * requests deliberately keep passing uuids straight through, so the uuid arm is
15
+ * gated on the request kind rather than on the shape of the value.
16
+ */
17
+ trait ResolvesPublicRelationUuids
18
+ {
19
+ /**
20
+ * Resolve one or more public identifiers to uuids within the session company.
21
+ *
22
+ * @param class-string $modelClass the model the identifiers belong to
23
+ * @param string|array<int, string> $identifiers public_id / internal_id values (uuid when internal)
24
+ *
25
+ * @return array<int, string>
26
+ */
27
+ protected function resolvePublicRelationUuids(string $modelClass, string|array $identifiers, ?bool $allowUuid = null): array
28
+ {
29
+ $identifiers = array_values(array_filter(Utils::arrayFrom($identifiers), static fn ($identifier) => filled($identifier)));
30
+
31
+ if ($identifiers === []) {
32
+ return [];
33
+ }
34
+
35
+ $allowUuid = $allowUuid ?? Http::isInternalRequest($this->request);
36
+ $instance = new $modelClass();
37
+
38
+ return $modelClass::query()
39
+ ->where('company_uuid', $this->session->get('company'))
40
+ ->where(function ($query) use ($identifiers, $instance, $allowUuid) {
41
+ $query->whereIn('public_id', $identifiers);
42
+
43
+ if (in_array('internal_id', $instance->getFillable())) {
44
+ $query->orWhereIn('internal_id', $identifiers);
45
+ }
46
+
47
+ if ($allowUuid) {
48
+ $query->orWhereIn('uuid', $identifiers);
49
+ }
50
+ })
51
+ ->pluck('uuid')
52
+ ->all();
53
+ }
54
+ }
@@ -2,14 +2,21 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Filter;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
6
+ use Fleetbase\FleetOps\Models\Fleet;
5
7
  use Fleetbase\FleetOps\Models\Place;
8
+ use Fleetbase\FleetOps\Models\Vehicle;
9
+ use Fleetbase\FleetOps\Models\Vendor;
6
10
  use Fleetbase\FleetOps\Support\Utils;
7
11
  use Fleetbase\Http\Filter\Filter;
8
12
  use Fleetbase\Models\Company;
13
+ use Fleetbase\Support\Http;
9
14
  use Illuminate\Support\Str;
10
15
 
11
16
  class DriverFilter extends Filter
12
17
  {
18
+ use ResolvesPublicRelationUuids;
19
+
13
20
  public function queryForInternal()
14
21
  {
15
22
  $this->builder->where(
@@ -43,9 +50,28 @@ class DriverFilter extends Filter
43
50
  });
44
51
  }
45
52
 
53
+ /**
54
+ * Match a driver by the identifier the operator's own system uses.
55
+ *
56
+ * Two callers, two meanings. The Fleet-Ops console types into a search box
57
+ * and expects `VEH-10` to find `VEH-100`; an importer asks whether `VEH-10`
58
+ * exists and must not be told yes because `VEH-100` does. A partial match
59
+ * there produces a duplicate on every rerun.
60
+ *
61
+ * Core's ordinary fillable-column filtering is already equality — this
62
+ * method exists only because the console needs the looser behaviour, so it
63
+ * is the console that gets the exception. An unknown request context is
64
+ * treated as public: exact is the safe default of the two.
65
+ */
46
66
  public function internalId(?string $internalId)
47
67
  {
48
- $this->builder->searchWhere('internal_id', $internalId);
68
+ if (Http::isInternalRequest($this->request)) {
69
+ $this->builder->searchWhere('internal_id', $internalId);
70
+
71
+ return;
72
+ }
73
+
74
+ $this->builder->where('internal_id', '=', $internalId);
49
75
  }
50
76
 
51
77
  public function name(?string $name)
@@ -58,14 +84,25 @@ class DriverFilter extends Filter
58
84
  );
59
85
  }
60
86
 
87
+ /**
88
+ * A public id identifies exactly one record, so a public lookup matches it
89
+ * exactly. The console keeps the partial search its id column filter box
90
+ * has always had.
91
+ */
61
92
  public function publicId(?string $publicId)
62
93
  {
63
- $this->builder->searchWhere('public_id', $publicId);
94
+ if (Http::isInternalRequest($this->request)) {
95
+ $this->builder->searchWhere('public_id', $publicId);
96
+
97
+ return;
98
+ }
99
+
100
+ $this->builder->where('public_id', '=', $publicId);
64
101
  }
65
102
 
66
103
  public function facilitator(string $facilitator)
67
104
  {
68
- $this->builder->where('vendor_uuid', $facilitator);
105
+ $this->builder->whereIn('vendor_uuid', $this->resolvePublicRelationUuids(Vendor::class, $facilitator));
69
106
  }
70
107
 
71
108
  public function vehicle(string $vehicle)
@@ -76,16 +113,28 @@ class DriverFilter extends Filter
76
113
  return;
77
114
  }
78
115
 
79
- if (Str::isUuid($vehicle)) {
116
+ // The console passes a uuid; the public API passes a public or internal id.
117
+ if (Str::isUuid($vehicle) && Http::isInternalRequest($this->request)) {
80
118
  $this->builder->where('vehicle_uuid', $vehicle);
81
- } else {
82
- $this->builder->whereHas(
83
- 'vehicle',
84
- function ($query) use ($vehicle) {
85
- $query->search($vehicle);
86
- }
87
- );
119
+
120
+ return;
121
+ }
122
+
123
+ $vehicleUuids = $this->resolvePublicRelationUuids(Vehicle::class, $vehicle);
124
+
125
+ if ($vehicleUuids !== []) {
126
+ $this->builder->whereIn('vehicle_uuid', $vehicleUuids);
127
+
128
+ return;
88
129
  }
130
+
131
+ // Fall back to a search so a partial plate or model still narrows a list.
132
+ $this->builder->whereHas(
133
+ 'vehicle',
134
+ function ($query) use ($vehicle) {
135
+ $query->search($vehicle);
136
+ }
137
+ );
89
138
  }
90
139
 
91
140
  public function driversLicenseNumber(?string $driversLicenseNumber)
@@ -93,12 +142,17 @@ class DriverFilter extends Filter
93
142
  $this->builder->searchWhere('drivers_license_number', $driversLicenseNumber);
94
143
  }
95
144
 
145
+ /**
146
+ * `phone` is an accessor sourced from the linked user, not a relation — the
147
+ * previous `whereHas('phone')` asked Eloquent for a relation that does not
148
+ * exist and raised a 500 for every caller of `?phone=`.
149
+ */
96
150
  public function phone(string $phone)
97
151
  {
98
152
  $this->builder->whereHas(
99
- 'phone',
153
+ 'user',
100
154
  function ($query) use ($phone) {
101
- $query->search($phone);
155
+ $query->searchWhere('phone', $phone);
102
156
  }
103
157
  );
104
158
  }
@@ -127,10 +181,12 @@ class DriverFilter extends Filter
127
181
 
128
182
  public function fleet(string $fleet)
129
183
  {
184
+ $fleetUuids = $this->resolvePublicRelationUuids(Fleet::class, $fleet);
185
+
130
186
  $this->builder->whereHas(
131
187
  'fleets',
132
- function ($q) use ($fleet) {
133
- $q->where('fleet_uuid', $fleet);
188
+ function ($q) use ($fleetUuids) {
189
+ $q->whereIn('fleet_uuid', $fleetUuids);
134
190
  }
135
191
  );
136
192
  }
@@ -2,11 +2,19 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Filter;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
6
+ use Fleetbase\FleetOps\Models\Fleet;
7
+ use Fleetbase\FleetOps\Models\ServiceArea;
8
+ use Fleetbase\FleetOps\Models\Vendor;
9
+ use Fleetbase\FleetOps\Models\Zone;
5
10
  use Fleetbase\FleetOps\Support\Utils;
6
11
  use Fleetbase\Http\Filter\Filter;
12
+ use Fleetbase\Support\Http;
7
13
 
8
14
  class FleetFilter extends Filter
9
15
  {
16
+ use ResolvesPublicRelationUuids;
17
+
10
18
  public function queryForInternal()
11
19
  {
12
20
  $this->builder->where('company_uuid', $this->session->get('company'))->with(['serviceArea', 'zone']);
@@ -17,15 +25,17 @@ class FleetFilter extends Filter
17
25
  $this->builder->where('company_uuid', $this->session->get('company'));
18
26
  }
19
27
 
28
+ /**
29
+ * Free-text search across a fleet's own columns.
30
+ *
31
+ * Previously matched against a `user` relation, which Fleet does not have —
32
+ * every `?query=` on the fleets endpoint raised a relation-not-found error
33
+ * rather than returning results.
34
+ */
20
35
  public function query(?string $searchQuery)
21
36
  {
22
37
  $this->builder->where(function ($query) use ($searchQuery) {
23
- $query->orWhereHas(
24
- 'user',
25
- function ($query) use ($searchQuery) {
26
- $query->searchWhere(['name', 'email', 'phone'], $searchQuery);
27
- }
28
- );
38
+ $query->searchWhere(['name', 'task', 'public_id'], $searchQuery);
29
39
  });
30
40
  }
31
41
 
@@ -38,49 +48,38 @@ class FleetFilter extends Filter
38
48
 
39
49
  public function serviceArea(?string $serviceArea)
40
50
  {
41
- $this->builder->whereHas(
42
- 'serviceArea',
43
- function ($query) use ($serviceArea) {
44
- $query->where('uuid', $serviceArea);
45
- }
46
- );
51
+ $this->builder->whereIn('service_area_uuid', $this->resolvePublicRelationUuids(ServiceArea::class, $serviceArea));
47
52
  }
48
53
 
49
54
  public function zone(?string $zone)
50
55
  {
51
- $this->builder->whereHas(
52
- 'zone',
53
- function ($query) use ($zone) {
54
- $query->where('zone_uuid', $zone);
55
- }
56
- );
56
+ $this->builder->whereIn('zone_uuid', $this->resolvePublicRelationUuids(Zone::class, $zone));
57
57
  }
58
58
 
59
59
  public function parentFleet(?string $fleet)
60
60
  {
61
- $this->builder->whereHas(
62
- 'parent_fleet',
63
- function ($query) use ($fleet) {
64
- $query->where('uuid', $fleet);
65
- }
66
- );
67
-
68
- $this->builder->searchWhere('parent_fleet_uuid', $fleet);
61
+ $this->builder->whereIn('parent_fleet_uuid', $this->resolvePublicRelationUuids(Fleet::class, $fleet));
69
62
  }
70
63
 
71
64
  public function vendor(?string $vendor)
72
65
  {
73
- $this->builder->whereHas(
74
- 'vendor',
75
- function ($query) use ($vendor) {
76
- $query->where('uuid', $vendor);
77
- }
78
- );
66
+ $this->builder->whereIn('vendor_uuid', $this->resolvePublicRelationUuids(Vendor::class, $vendor));
79
67
  }
80
68
 
69
+ /**
70
+ * A public id identifies exactly one record, so a public lookup matches it
71
+ * exactly. The console keeps the partial search its id column filter box
72
+ * has always had.
73
+ */
81
74
  public function publicId(?string $publicId)
82
75
  {
83
- $this->builder->searchWhere('public_id', $publicId);
76
+ if (Http::isInternalRequest($this->request)) {
77
+ $this->builder->searchWhere('public_id', $publicId);
78
+
79
+ return;
80
+ }
81
+
82
+ $this->builder->where('public_id', '=', $publicId);
84
83
  }
85
84
 
86
85
  public function task(?string $task)