@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.
Files changed (68) hide show
  1. package/RELEASE.md +5 -14
  2. package/addon/controllers/management/drivers/index.js +4 -1
  3. package/addon/routes/management/fleets/index/details.js +1 -1
  4. package/addon/routes/management/fleets/index/edit.js +1 -1
  5. package/addon/routes/management/fleets/index.js +3 -1
  6. package/addon/services/map-settings.js +4 -1
  7. package/composer.json +1 -1
  8. package/extension.json +1 -1
  9. package/package.json +1 -1
  10. package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
  11. package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
  12. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
  13. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
  14. package/server/src/Http/Controllers/Api/v1/DriverController.php +93 -57
  15. package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
  16. package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
  17. package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
  18. package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
  19. package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
  20. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
  21. package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
  22. package/server/src/Http/Controllers/Api/v1/VehicleController.php +134 -35
  23. package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
  24. package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
  25. package/server/src/Http/Filter/DriverFilter.php +71 -15
  26. package/server/src/Http/Filter/FleetFilter.php +32 -33
  27. package/server/src/Http/Filter/VehicleFilter.php +57 -19
  28. package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
  29. package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
  30. package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
  31. package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
  32. package/server/src/Http/Requests/CreateVehicleRequest.php +150 -31
  33. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
  34. package/server/src/Http/Resources/v1/Driver.php +22 -2
  35. package/server/src/Http/Resources/v1/Fleet.php +38 -25
  36. package/server/src/Http/Resources/v1/Vehicle.php +46 -4
  37. package/server/src/Models/Driver.php +2 -2
  38. package/server/src/routes.php +8 -0
  39. package/server/tests/ApiDriverControllerContractsTest.php +284 -13
  40. package/server/tests/ApiEntityControllerContractsTest.php +1 -1
  41. package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
  42. package/server/tests/ApiFleetControllerContractsTest.php +354 -21
  43. package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
  44. package/server/tests/ApiIssueControllerContractsTest.php +1 -1
  45. package/server/tests/ApiOrderControllerContractsTest.php +1 -1
  46. package/server/tests/ApiPartControllerContractsTest.php +2 -2
  47. package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
  48. package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
  49. package/server/tests/ApiVehicleControllerContractsTest.php +175 -8
  50. package/server/tests/ControllerFilterContractsTest.php +132 -13
  51. package/server/tests/ControllerHelperContractsTest.php +6 -3
  52. package/server/tests/DriverFilterExecutionTest.php +130 -6
  53. package/server/tests/ExceptionContractsTest.php +15 -0
  54. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
  55. package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
  56. package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
  57. package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
  58. package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
  59. package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
  60. package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
  61. package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
  62. package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
  63. package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
  64. package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
  65. package/server/tests/FleetHierarchyResourceTest.php +56 -8
  66. package/server/tests/RequestContractsTest.php +100 -7
  67. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
  68. package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
@@ -6,6 +6,9 @@ use Fleetbase\FleetOps\Events\DriverLocationChanged;
6
6
  use Fleetbase\FleetOps\Events\GeofenceEntered;
7
7
  use Fleetbase\FleetOps\Events\GeofenceExited;
8
8
  use Fleetbase\FleetOps\Events\VehicleLocationChanged;
9
+ use Fleetbase\FleetOps\Exceptions\PublicRelationNotFoundException;
10
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesFleetOpsApiResources;
11
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesPublicExpansions;
9
12
  use Fleetbase\FleetOps\Http\Requests\CreateDriverRequest;
10
13
  use Fleetbase\FleetOps\Http\Requests\DriverSimulationRequest;
11
14
  use Fleetbase\FleetOps\Http\Requests\UpdateDriverRequest;
@@ -16,6 +19,7 @@ use Fleetbase\FleetOps\Jobs\SimulateDrivingRoute;
16
19
  use Fleetbase\FleetOps\Models\Driver;
17
20
  use Fleetbase\FleetOps\Models\Order;
18
21
  use Fleetbase\FleetOps\Models\Vehicle;
22
+ use Fleetbase\FleetOps\Models\Vendor;
19
23
  use Fleetbase\FleetOps\Support\GeofenceIntersectionService;
20
24
  use Fleetbase\FleetOps\Support\OSRM;
21
25
  use Fleetbase\FleetOps\Support\Utils;
@@ -40,6 +44,23 @@ use Illuminate\Support\Str;
40
44
  class DriverController extends Controller
41
45
  {
42
46
  use \Fleetbase\FleetOps\Http\Controllers\Concerns\ResolvesReviewAccountBypass;
47
+ use ResolvesFleetOpsApiResources;
48
+ use ResolvesPublicExpansions;
49
+
50
+ /**
51
+ * Public expansion name => Eloquent relation name.
52
+ *
53
+ * `user` and `company` are deliberately absent. Both are already published
54
+ * as public-id strings, and Navigator interpolates `driver.user` directly
55
+ * into a socket channel name — expanding either would retype a released
56
+ * field and break it silently.
57
+ */
58
+ public const EXPANDABLE = [
59
+ 'vehicle' => 'vehicle',
60
+ 'vendor' => 'vendor',
61
+ 'current_job' => 'currentJob',
62
+ 'fleets' => 'fleets',
63
+ ];
43
64
 
44
65
  /**
45
66
  * Creates a new Fleetbase Driver resource.
@@ -48,11 +69,7 @@ class DriverController extends Controller
48
69
  */
49
70
  public function create(CreateDriverRequest $request)
50
71
  {
51
- // get request input
52
- $input = $request->except(['name', 'password', 'email', 'phone', 'location', 'altitude', 'heading', 'speed', 'meta']);
53
-
54
- // Add default status
55
- $input['status'] = $request->input('status', 'available');
72
+ $this->applyPublicExpansions($request, static::EXPANDABLE);
56
73
 
57
74
  // get user details for driver
58
75
  $userDetails = $request->only(['name', 'password', 'email', 'phone', 'timezone']);
@@ -65,6 +82,18 @@ class DriverController extends Controller
65
82
  return $this->apiError('Company not found.');
66
83
  }
67
84
 
85
+ // get request input. Relationship inputs resolve against the company the
86
+ // driver is being created in, which is not always the session company —
87
+ // a request may name one explicitly.
88
+ try {
89
+ $input = $this->driverInputFromRequest($request, $company->uuid);
90
+ } catch (PublicRelationNotFoundException $exception) {
91
+ return $this->jsonResponse(['error' => $exception->getMessage()], 404);
92
+ }
93
+
94
+ // Add default status
95
+ $input['status'] = $request->input('status', 'available');
96
+
68
97
  // Apply user infos
69
98
  $userDetails = $this->applyUserInfoFromRequest($request, $userDetails);
70
99
 
@@ -87,30 +116,6 @@ class DriverController extends Controller
87
116
  $input['user_uuid'] = $user->uuid;
88
117
  $input['company_uuid'] = $company->uuid; // Ensure correct company_uuid is set
89
118
 
90
- // vehicle assignment public_id -> uuid
91
- if ($request->has('vehicle')) {
92
- $input['vehicle_uuid'] = $this->getUuid('vehicles', [
93
- 'public_id' => $request->input('vehicle'),
94
- 'company_uuid' => $company->uuid, // Use $company->uuid instead of session
95
- ]);
96
- }
97
-
98
- // vendor assignment public_id -> uuid
99
- if ($request->has('vendor')) {
100
- $input['vendor_uuid'] = $this->getUuid('vendors', [
101
- 'public_id' => $request->input('vendor'),
102
- 'company_uuid' => $company->uuid, // Use $company->uuid instead of session
103
- ]);
104
- }
105
-
106
- // order|alias:job assignment public_id -> uuid
107
- if ($request->has('job')) {
108
- $input['current_job_uuid'] = $this->getUuid('orders', [
109
- 'public_id' => $request->input('job'),
110
- 'company_uuid' => $company->uuid, // Use $company->uuid instead of session
111
- ]);
112
- }
113
-
114
119
  // set default online
115
120
  if (!isset($input['online'])) {
116
121
  $input['online'] = 0;
@@ -130,7 +135,11 @@ class DriverController extends Controller
130
135
  $file = $this->resolveFile($request->input('photo'), $path);
131
136
 
132
137
  if ($file) {
133
- $user->update(['photo_uuid' => $file->uuid]);
138
+ // `photo_uuid` is not a column on users — the avatar lives in
139
+ // `avatar_uuid`, and User guards mass assignment by fillable, so
140
+ // the old key was dropped without a word and every driver photo
141
+ // uploaded through the public API was discarded.
142
+ $user->update(['avatar_uuid' => $file->uuid]);
134
143
  }
135
144
  }
136
145
 
@@ -151,6 +160,8 @@ class DriverController extends Controller
151
160
  */
152
161
  public function update($id, UpdateDriverRequest $request)
153
162
  {
163
+ $this->applyPublicExpansions($request, static::EXPANDABLE);
164
+
154
165
  // find for the driver
155
166
  try {
156
167
  $driver = $this->findDriver($id, ['user']);
@@ -164,7 +175,11 @@ class DriverController extends Controller
164
175
  }
165
176
 
166
177
  // get request input
167
- $input = $request->except(['name', 'password', 'email', 'phone', 'location', 'altitude', 'heading', 'speed', 'meta']);
178
+ try {
179
+ $input = $this->driverInputFromRequest($request);
180
+ } catch (PublicRelationNotFoundException $exception) {
181
+ return $this->jsonResponse(['error' => $exception->getMessage()], 404);
182
+ }
168
183
 
169
184
  /*
170
185
  * Deliberately no `password` here. Setting one through a general update
@@ -173,7 +188,10 @@ class DriverController extends Controller
173
188
  * password. Changing a password is its own operation with its own
174
189
  * proof: see changePassword(), forgotPassword() and resetPassword().
175
190
  */
176
- $userDetails = $request->only(['name', 'email', 'phone']);
191
+ // `timezone` is accepted and documented on update, but was never copied
192
+ // to the linked user — the request validated, answered 200, and dropped
193
+ // it. The driver's own record has no timezone column; the user's does.
194
+ $userDetails = $request->only(['name', 'email', 'phone', 'timezone']);
177
195
 
178
196
  // update driver user details
179
197
  $driverUser = $driver->getUser();
@@ -181,30 +199,6 @@ class DriverController extends Controller
181
199
  $driverUser->update($userDetails);
182
200
  }
183
201
 
184
- // vehicle assignment public_id -> uuid
185
- if ($request->has('vehicle')) {
186
- $input['vehicle_uuid'] = $this->getUuid('vehicles', [
187
- 'public_id' => $request->input('vehicle'),
188
- 'company_uuid' => $this->sessionCompany(),
189
- ]);
190
- }
191
-
192
- // vendor assignment public_id -> uuid
193
- if ($request->has('vendor')) {
194
- $input['vendor_uuid'] = $this->getUuid('vendors', [
195
- 'public_id' => $request->input('vendor'),
196
- 'company_uuid' => $this->sessionCompany(),
197
- ]);
198
- }
199
-
200
- // order|alias:job assignment public_id -> uuid
201
- if ($request->has('job')) {
202
- $input['current_job_uuid'] = $this->getUuid('orders', [
203
- 'public_id' => $request->input('job'),
204
- 'company_uuid' => $this->sessionCompany(),
205
- ]);
206
- }
207
-
208
202
  // latitude / longitude
209
203
  if ($request->has(['latitude', 'longitude'])) {
210
204
  $input['location'] = $this->pointFromCoordinates($request->only(['latitude', 'longitude']));
@@ -220,7 +214,7 @@ class DriverController extends Controller
220
214
  $file = $this->resolveFile($request->input('photo'), $path);
221
215
 
222
216
  if ($file) {
223
- $driver->user->update(['photo_uuid' => $file->uuid]);
217
+ $driver->user->update(['avatar_uuid' => $file->uuid]);
224
218
  }
225
219
  }
226
220
 
@@ -238,6 +232,8 @@ class DriverController extends Controller
238
232
  */
239
233
  public function query(Request $request)
240
234
  {
235
+ $this->applyPublicExpansions($request, static::EXPANDABLE);
236
+
241
237
  $results = $this->queryDrivers($request);
242
238
 
243
239
  return $this->driverResourceCollection($results);
@@ -252,6 +248,11 @@ class DriverController extends Controller
252
248
  */
253
249
  public function find($id)
254
250
  {
251
+ // Retrieve carries no Request parameter, so the container's is used —
252
+ // otherwise an unsupported `with` on this endpoint would reach Eloquent
253
+ // unmapped.
254
+ $this->applyPublicExpansions(request(), static::EXPANDABLE);
255
+
255
256
  // find for the driver
256
257
  try {
257
258
  $driver = $this->findDriver($id, ['user', 'vehicle', 'vendor', 'currentJob']);
@@ -919,6 +920,41 @@ class DriverController extends Controller
919
920
  return response()->json($route);
920
921
  }
921
922
 
923
+ /**
924
+ * The explicit public input allowlist for a driver.
925
+ *
926
+ * Replaces an `except()` blocklist: anything not named here — `user_uuid`,
927
+ * `company_uuid`, `auth_token`, `signup_token_used`, `public_id`, `uuid`,
928
+ * `_key`, the generated `slug`, and the raw `*_uuid` relation columns — used
929
+ * to reach `Driver::create()` intact simply because nobody had thought to
930
+ * exclude it. `location`, `heading`, `altitude`, `speed` and `meta` were on
931
+ * that blocklist and so were dropped on every write, which is why a driver's
932
+ * metadata never persisted through the public API.
933
+ *
934
+ * @throws PublicRelationNotFoundException
935
+ */
936
+ protected function driverInputFromRequest(Request $request, ?string $companyUuid = null): array
937
+ {
938
+ $input = $request->only([
939
+ // Identity
940
+ 'internal_id', 'drivers_license_number', 'license_expiry',
941
+ // Operational
942
+ 'country', 'currency', 'city', 'online', 'current_status', 'status',
943
+ 'location', 'heading', 'bearing', 'altitude', 'speed',
944
+ // Structured / orchestrator
945
+ 'meta', 'skills', 'max_travel_time', 'max_distance',
946
+ 'time_window_start', 'time_window_end',
947
+ ]);
948
+
949
+ $this->applyPublicIdRelations($input, [
950
+ 'vehicle' => ['vehicle_uuid', Vehicle::class],
951
+ 'vendor' => ['vendor_uuid', Vendor::class],
952
+ 'job' => ['current_job_uuid', Order::class],
953
+ ], $request, $companyUuid);
954
+
955
+ return $input;
956
+ }
957
+
922
958
  protected function companyFromRequest(Request $request): ?Company
923
959
  {
924
960
  return Auth::getCompanyFromRequest($request);
@@ -271,7 +271,7 @@ class EntityController extends Controller
271
271
  return $value ? Utils::singularize($value) : null;
272
272
  }
273
273
 
274
- protected function findPayloadByPublicId(string $publicId): ?Payload
274
+ protected function findPayloadByPublicId(?string $publicId): ?Payload
275
275
  {
276
276
  return Payload::where('public_id', $publicId)->first();
277
277
  }