@fleetbase/fleetops-engine 0.6.60 → 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 (109) hide show
  1. package/RELEASE.md +21 -17
  2. package/addon/components/ai/route-preview-map.js +1 -5
  3. package/addon/components/customer/orders.js +2 -1
  4. package/addon/components/driver/card.hbs +49 -0
  5. package/addon/components/driver/card.js +30 -0
  6. package/addon/components/map/leaflet-live-map.js +4 -19
  7. package/addon/components/modals/place-details.hbs +1 -1
  8. package/addon/components/modals/point-map.hbs +1 -1
  9. package/addon/components/orchestrator-workbench.js +2 -1
  10. package/addon/components/order/form/route.js +3 -0
  11. package/addon/components/order-tracking-lookup.js +2 -1
  12. package/addon/components/place/details.hbs +1 -1
  13. package/addon/components/positions-replay.js +4 -1
  14. package/addon/components/service-area/details.hbs +1 -1
  15. package/addon/components/widget/live-fleet.js +7 -7
  16. package/addon/components/zone/details.hbs +1 -1
  17. package/addon/controllers/management/drivers/index.js +22 -1
  18. package/addon/controllers/settings/map.js +16 -0
  19. package/addon/helpers/leaflet-tile-url.js +19 -0
  20. package/addon/services/map-settings.js +26 -1
  21. package/addon/templates/management/drivers/index.hbs +67 -33
  22. package/addon/templates/settings/map.hbs +10 -0
  23. package/addon/utils/leaflet-tile-url.js +31 -0
  24. package/addon/utils/prepare-place-for-save.js +13 -0
  25. package/app/components/driver/card.js +1 -0
  26. package/app/helpers/leaflet-tile-url.js +1 -0
  27. package/app/utils/leaflet-tile-url.js +1 -0
  28. package/composer.json +1 -1
  29. package/extension.json +1 -1
  30. package/index.js +11 -0
  31. package/package.json +2 -1
  32. package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
  33. package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
  34. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
  35. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
  36. package/server/src/Http/Controllers/Api/v1/DriverController.php +312 -59
  37. package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
  38. package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
  39. package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
  40. package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
  41. package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
  42. package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
  43. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
  44. package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
  45. package/server/src/Http/Controllers/Api/v1/VehicleController.php +139 -34
  46. package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
  47. package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
  48. package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
  49. package/server/src/Http/Filter/DriverFilter.php +71 -15
  50. package/server/src/Http/Filter/FleetFilter.php +32 -33
  51. package/server/src/Http/Filter/FuelReportFilter.php +29 -0
  52. package/server/src/Http/Filter/IssueFilter.php +29 -0
  53. package/server/src/Http/Filter/VehicleFilter.php +57 -19
  54. package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
  55. package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
  56. package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
  57. package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
  58. package/server/src/Http/Requests/CreateVehicleRequest.php +150 -26
  59. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
  60. package/server/src/Http/Resources/v1/Driver.php +22 -2
  61. package/server/src/Http/Resources/v1/Fleet.php +38 -25
  62. package/server/src/Http/Resources/v1/Manifest.php +52 -0
  63. package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
  64. package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
  65. package/server/src/Http/Resources/v1/Vehicle.php +46 -4
  66. package/server/src/Models/Driver.php +2 -2
  67. package/server/src/Models/Place.php +1 -1
  68. package/server/src/routes.php +29 -0
  69. package/server/tests/ApiDriverControllerContractsTest.php +317 -13
  70. package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
  71. package/server/tests/ApiEntityControllerContractsTest.php +1 -1
  72. package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
  73. package/server/tests/ApiFleetControllerContractsTest.php +354 -21
  74. package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
  75. package/server/tests/ApiIssueControllerContractsTest.php +1 -1
  76. package/server/tests/ApiManifestControllerContractsTest.php +599 -0
  77. package/server/tests/ApiOrderControllerContractsTest.php +1 -1
  78. package/server/tests/ApiPartControllerContractsTest.php +2 -2
  79. package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
  80. package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
  81. package/server/tests/ApiVehicleControllerContractsTest.php +210 -8
  82. package/server/tests/CompactResourceSerializationTest.php +13 -7
  83. package/server/tests/ControllerFilterContractsTest.php +132 -13
  84. package/server/tests/ControllerHelperContractsTest.php +6 -3
  85. package/server/tests/DriverFilterExecutionTest.php +130 -6
  86. package/server/tests/ExceptionContractsTest.php +15 -0
  87. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
  88. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
  89. package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
  90. package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
  91. package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
  92. package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
  93. package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
  94. package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
  95. package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
  96. package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
  97. package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
  98. package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
  99. package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
  100. package/server/tests/FleetHierarchyResourceTest.php +56 -8
  101. package/server/tests/RequestContractsTest.php +105 -7
  102. package/server/tests/SettingControllerContractsTest.php +33 -0
  103. package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
  104. package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
  105. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
  106. package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
  107. package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
  108. package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
  109. package/translations/en-us.yaml +4 -0
@@ -0,0 +1,457 @@
1
+ <?php
2
+
3
+ /*
4
+ * illuminate/foundation is not installed locally — CI installs it — so the
5
+ * controller's base class cannot load without these. Guarded, so they do
6
+ * nothing where the real ones exist.
7
+ */
8
+ if (!trait_exists('Illuminate\Foundation\Auth\Access\AuthorizesRequests')) {
9
+ eval('namespace Illuminate\Foundation\Auth\Access; trait AuthorizesRequests {}');
10
+ }
11
+ if (!trait_exists('Illuminate\Foundation\Bus\DispatchesJobs')) {
12
+ eval('namespace Illuminate\Foundation\Bus; trait DispatchesJobs {}');
13
+ }
14
+ if (!trait_exists('Illuminate\Foundation\Validation\ValidatesRequests')) {
15
+ eval('namespace Illuminate\Foundation\Validation; trait ValidatesRequests {}');
16
+ }
17
+ if (!class_exists('Illuminate\Foundation\Auth\User')) {
18
+ eval('namespace Illuminate\Foundation\Auth; class User extends \Illuminate\Database\Eloquent\Model {}');
19
+ }
20
+
21
+ if (!function_exists('Fleetbase\Traits\config')) {
22
+ eval('namespace Fleetbase\Traits; function config($key = null, $default = null) { return $key === "api.cache.enabled" ? false : $default; }');
23
+ }
24
+ if (!function_exists('Fleetbase\Models\config')) {
25
+ eval('namespace Fleetbase\Models; function config($key = null, $default = null) { return $default; }');
26
+ }
27
+ if (!function_exists('Fleetbase\FleetOps\Models\config')) {
28
+ eval('namespace Fleetbase\FleetOps\Models; function config($key = null, $default = null) { return $default; }');
29
+ }
30
+
31
+ if (!class_exists('FleetOpsPasswordTestResponse')) {
32
+ class FleetOpsPasswordTestResponse
33
+ {
34
+ public function __construct(public mixed $payload = null, public int $status = 200)
35
+ {
36
+ }
37
+
38
+ public function getStatusCode(): int
39
+ {
40
+ return $this->status;
41
+ }
42
+
43
+ public function getData(): mixed
44
+ {
45
+ return $this->payload;
46
+ }
47
+ }
48
+
49
+ class FleetOpsPasswordTestResponseFactory
50
+ {
51
+ public function apiError(string $message, int $status = 400): FleetOpsPasswordTestResponse
52
+ {
53
+ return new FleetOpsPasswordTestResponse(['error' => $message], $status);
54
+ }
55
+
56
+ public function json(mixed $payload = null, int $status = 200): FleetOpsPasswordTestResponse
57
+ {
58
+ return new FleetOpsPasswordTestResponse($payload, $status);
59
+ }
60
+ }
61
+ }
62
+
63
+ if (!function_exists('Fleetbase\FleetOps\Http\Controllers\Api\v1\response')) {
64
+ eval('namespace Fleetbase\FleetOps\Http\Controllers\Api\v1; function response() { return new \FleetOpsPasswordTestResponseFactory(); }');
65
+ }
66
+
67
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\DriverController;
68
+ use Fleetbase\FleetOps\Models\Driver;
69
+ use Fleetbase\Models\User;
70
+ use Illuminate\Http\Request;
71
+
72
+ /** A user whose password writes and token operations are observable. */
73
+ class FleetOpsPasswordUserFake extends User
74
+ {
75
+ public array $saved = [];
76
+ public bool $tokensDeleted = false;
77
+ public ?string $issuedTokenFor = null;
78
+
79
+ /** core-api hashes on write; hashing is not wired in this harness. */
80
+ public function setPasswordAttribute($value): void
81
+ {
82
+ $this->attributes['password'] = 'hashed:' . $value;
83
+ }
84
+
85
+ public function save(array $options = []): bool
86
+ {
87
+ $this->saved[] = $this->attributes['password'] ?? null;
88
+
89
+ return true;
90
+ }
91
+
92
+ public function tokens()
93
+ {
94
+ $user = $this;
95
+
96
+ return new class($user) {
97
+ public function __construct(private FleetOpsPasswordUserFake $user)
98
+ {
99
+ }
100
+
101
+ public function delete(): bool
102
+ {
103
+ $this->user->tokensDeleted = true;
104
+
105
+ return true;
106
+ }
107
+ };
108
+ }
109
+
110
+ public function createToken($name, array $abilities = ['*'], $expiresAt = null)
111
+ {
112
+ $this->issuedTokenFor = is_string($name) ? $name : 'unknown';
113
+
114
+ return new class {
115
+ public string $plainTextToken = 'fresh-token';
116
+ };
117
+ }
118
+ }
119
+
120
+ /** A driver that answers with the user the test supplied. */
121
+ class FleetOpsPasswordDriverFake extends Driver
122
+ {
123
+ public ?User $userForTest = null;
124
+
125
+ public function getUser(): ?User
126
+ {
127
+ return $this->userForTest;
128
+ }
129
+ }
130
+
131
+ class FleetOpsPasswordControllerProbe extends DriverController
132
+ {
133
+ public static ?Driver $driver = null;
134
+ public static bool $driverMissing = false;
135
+ public static ?User $identityUser = null;
136
+ public static mixed $resetCode = null;
137
+ public static array $sentCodes = [];
138
+ public static bool $sendThrows = false;
139
+
140
+ public function findDriver(string $id, array $with = []): Driver
141
+ {
142
+ if (static::$driverMissing || !static::$driver) {
143
+ throw new Illuminate\Database\Eloquent\ModelNotFoundException();
144
+ }
145
+
146
+ return static::$driver;
147
+ }
148
+
149
+ protected static function findDriverUserByIdentity(string $identity): ?User
150
+ {
151
+ return static::$identityUser;
152
+ }
153
+
154
+ protected static function findDriverUserRecord(Driver $driver): ?User
155
+ {
156
+ return $driver->getUser();
157
+ }
158
+
159
+ protected static function findResetCode(User $user, string $code)
160
+ {
161
+ return static::$resetCode;
162
+ }
163
+
164
+ /**
165
+ * Hashing is not wired in this harness, and asserting that bcrypt works is
166
+ * not what these tests are for — the question is whether a mismatch is
167
+ * refused.
168
+ */
169
+ protected static function passwordMatches(User $user, string $plain): bool
170
+ {
171
+ return $plain === static::$correctPassword;
172
+ }
173
+
174
+ public static string $correctPassword = 'correct-horse';
175
+
176
+ protected static function debugEnabled(): bool
177
+ {
178
+ return false;
179
+ }
180
+
181
+ protected static function sendResetCode(User $user, string $identity): void
182
+ {
183
+ if (static::$sendThrows) {
184
+ throw new RuntimeException('sms gateway down');
185
+ }
186
+
187
+ static::$sentCodes[] = $identity;
188
+ }
189
+ }
190
+
191
+ function fleetopsPasswordUser(string $plainPassword = 'correct-horse'): FleetOpsPasswordUserFake
192
+ {
193
+ FleetOpsPasswordControllerProbe::$correctPassword = $plainPassword;
194
+
195
+ $user = new FleetOpsPasswordUserFake();
196
+ $user->setRawAttributes(['uuid' => 'user-uuid', 'password' => 'stored-hash'], true);
197
+
198
+ return $user;
199
+ }
200
+
201
+ beforeEach(function () {
202
+ FleetOpsPasswordControllerProbe::$driver = null;
203
+ FleetOpsPasswordControllerProbe::$driverMissing = false;
204
+ FleetOpsPasswordControllerProbe::$identityUser = null;
205
+ FleetOpsPasswordControllerProbe::$resetCode = null;
206
+ FleetOpsPasswordControllerProbe::$sentCodes = [];
207
+ FleetOpsPasswordControllerProbe::$sendThrows = false;
208
+ });
209
+
210
+ test('change password requires both the current and the new one', function () {
211
+ $response = (new FleetOpsPasswordControllerProbe())->changePassword(new Request(['password' => 'newpassword']), 'driver_a');
212
+
213
+ expect($response->getStatusCode())->toBe(400);
214
+ });
215
+
216
+ test('change password refuses a new password that is too short to be worth having', function () {
217
+ $response = (new FleetOpsPasswordControllerProbe())->changePassword(
218
+ new Request(['current_password' => 'correct-horse', 'password' => 'short']),
219
+ 'driver_a'
220
+ );
221
+
222
+ expect($response->getStatusCode())->toBe(400);
223
+ });
224
+
225
+ test('change password refuses a confirmation that does not match', function () {
226
+ $response = (new FleetOpsPasswordControllerProbe())->changePassword(
227
+ new Request(['current_password' => 'correct-horse', 'password' => 'newpassword', 'password_confirmation' => 'newpassward']),
228
+ 'driver_a'
229
+ );
230
+
231
+ expect($response->getStatusCode())->toBe(422);
232
+ });
233
+
234
+ test('change password reports a missing driver', function () {
235
+ FleetOpsPasswordControllerProbe::$driverMissing = true;
236
+
237
+ $response = (new FleetOpsPasswordControllerProbe())->changePassword(
238
+ new Request(['current_password' => 'correct-horse', 'password' => 'newpassword']),
239
+ 'driver_missing'
240
+ );
241
+
242
+ expect($response->getStatusCode())->toBe(404);
243
+ });
244
+
245
+ test('change password refuses a driver with no user account', function () {
246
+ $driver = new FleetOpsPasswordDriverFake();
247
+ $driver->userForTest = null;
248
+ FleetOpsPasswordControllerProbe::$driver = $driver;
249
+
250
+ $response = (new FleetOpsPasswordControllerProbe())->changePassword(
251
+ new Request(['current_password' => 'correct-horse', 'password' => 'newpassword']),
252
+ 'driver_a'
253
+ );
254
+
255
+ expect($response->getStatusCode())->toBe(422);
256
+ });
257
+
258
+ test('change password refuses when the current password is wrong, and changes nothing', function () {
259
+ /*
260
+ * The whole point of the endpoint. Without this check a borrowed handset or
261
+ * a leaked token is a permanent account takeover.
262
+ */
263
+ $user = fleetopsPasswordUser('correct-horse');
264
+ $driver = new FleetOpsPasswordDriverFake();
265
+ $driver->userForTest = $user;
266
+ FleetOpsPasswordControllerProbe::$driver = $driver;
267
+
268
+ $response = (new FleetOpsPasswordControllerProbe())->changePassword(
269
+ new Request(['current_password' => 'guessing', 'password' => 'newpassword']),
270
+ 'driver_a'
271
+ );
272
+
273
+ expect($response->getStatusCode())->toBe(422)
274
+ ->and($user->saved)->toBeEmpty()
275
+ ->and($user->tokensDeleted)->toBeFalse();
276
+ });
277
+
278
+ test('change password sets it, ends other sessions, and hands back a fresh token', function () {
279
+ $user = fleetopsPasswordUser('correct-horse');
280
+ $driver = new FleetOpsPasswordDriverFake();
281
+ $driver->userForTest = $user;
282
+ FleetOpsPasswordControllerProbe::$driver = $driver;
283
+
284
+ $response = (new FleetOpsPasswordControllerProbe())->changePassword(
285
+ new Request(['current_password' => 'correct-horse', 'password' => 'newpassword', 'device_name' => 'navigator']),
286
+ 'driver_a'
287
+ );
288
+
289
+ expect($response->getStatusCode())->toBe(200)
290
+ ->and($response->getData()['token'])->toBe('fresh-token')
291
+ ->and($user->saved)->toHaveCount(1)
292
+ // The old sessions die; the caller keeps working.
293
+ ->and($user->tokensDeleted)->toBeTrue()
294
+ ->and($user->issuedTokenFor)->toBe('navigator');
295
+ });
296
+
297
+ test('forgot password requires an identity', function () {
298
+ $response = (new FleetOpsPasswordControllerProbe())->forgotPassword(new Request());
299
+
300
+ expect($response->getStatusCode())->toBe(400);
301
+ });
302
+
303
+ test('forgot password answers the same for an unknown identity, and sends nothing', function () {
304
+ /*
305
+ * A reset endpoint that 404s on an unknown number is a way to enumerate a
306
+ * company's drivers.
307
+ */
308
+ FleetOpsPasswordControllerProbe::$identityUser = null;
309
+
310
+ $response = (new FleetOpsPasswordControllerProbe())->forgotPassword(new Request(['identity' => 'nobody@example.test']));
311
+
312
+ expect($response->getStatusCode())->toBe(200)
313
+ ->and(FleetOpsPasswordControllerProbe::$sentCodes)->toBeEmpty();
314
+ });
315
+
316
+ test('forgot password sends a code to a driver that exists', function () {
317
+ FleetOpsPasswordControllerProbe::$identityUser = fleetopsPasswordUser();
318
+
319
+ $response = (new FleetOpsPasswordControllerProbe())->forgotPassword(new Request(['identity' => 'driver@example.test']));
320
+
321
+ expect($response->getStatusCode())->toBe(200)
322
+ ->and(FleetOpsPasswordControllerProbe::$sentCodes)->toBe(['driver@example.test']);
323
+ });
324
+
325
+ test('forgot password reports a delivery failure rather than pretending it sent', function () {
326
+ FleetOpsPasswordControllerProbe::$identityUser = fleetopsPasswordUser();
327
+ FleetOpsPasswordControllerProbe::$sendThrows = true;
328
+
329
+ $response = (new FleetOpsPasswordControllerProbe())->forgotPassword(new Request(['identity' => '+6581000001']));
330
+
331
+ expect($response->getStatusCode())->toBe(400);
332
+ });
333
+
334
+ test('reset password requires identity, code and a new password', function () {
335
+ $response = (new FleetOpsPasswordControllerProbe())->resetPassword(new Request(['identity' => 'a@b.test']));
336
+
337
+ expect($response->getStatusCode())->toBe(400);
338
+ });
339
+
340
+ test('reset password refuses a password too short to be worth having', function () {
341
+ $response = (new FleetOpsPasswordControllerProbe())->resetPassword(
342
+ new Request(['identity' => 'a@b.test', 'code' => '123456', 'password' => 'short'])
343
+ );
344
+
345
+ expect($response->getStatusCode())->toBe(400);
346
+ });
347
+
348
+ test('reset password gives one answer for an unknown identity and a bad code alike', function () {
349
+ // Neither may be used as an oracle.
350
+ $unknownIdentity = (new FleetOpsPasswordControllerProbe())->resetPassword(
351
+ new Request(['identity' => 'nobody@example.test', 'code' => '123456', 'password' => 'newpassword'])
352
+ );
353
+
354
+ FleetOpsPasswordControllerProbe::$identityUser = fleetopsPasswordUser();
355
+ FleetOpsPasswordControllerProbe::$resetCode = null;
356
+ $badCode = (new FleetOpsPasswordControllerProbe())->resetPassword(
357
+ new Request(['identity' => 'driver@example.test', 'code' => 'wrong', 'password' => 'newpassword'])
358
+ );
359
+
360
+ expect($unknownIdentity->getStatusCode())->toBe(422)
361
+ ->and($badCode->getStatusCode())->toBe(422)
362
+ ->and($unknownIdentity->getData())->toBe($badCode->getData());
363
+ });
364
+
365
+ test('reset password sets the password, spends the code, and ends every session', function () {
366
+ $user = fleetopsPasswordUser();
367
+ FleetOpsPasswordControllerProbe::$identityUser = $user;
368
+
369
+ $deleted = false;
370
+ FleetOpsPasswordControllerProbe::$resetCode = new class($deleted) {
371
+ public function __construct(public bool &$deleted)
372
+ {
373
+ }
374
+
375
+ public function delete(): bool
376
+ {
377
+ $this->deleted = true;
378
+
379
+ return true;
380
+ }
381
+ };
382
+
383
+ $response = (new FleetOpsPasswordControllerProbe())->resetPassword(
384
+ new Request(['identity' => 'driver@example.test', 'code' => '123456', 'password' => 'newpassword'])
385
+ );
386
+
387
+ expect($response->getStatusCode())->toBe(200)
388
+ ->and($user->saved)->toHaveCount(1)
389
+ ->and($deleted)->toBeTrue()
390
+ // A reset is a recovery from losing control; nothing keeps working.
391
+ ->and($user->tokensDeleted)->toBeTrue();
392
+ });
393
+
394
+ /**
395
+ * Reaches the real helpers, which the probe above replaces.
396
+ *
397
+ * Each is a one-line delegation — to Eloquent, to the verification-code
398
+ * generator, to the hasher, to the application. What matters is that they
399
+ * delegate: a password comparison that quietly returned true, or a code sender
400
+ * that quietly did nothing, would each turn a security control into decoration.
401
+ */
402
+ class FleetOpsPasswordRealHelperProbe extends DriverController
403
+ {
404
+ public static function callFindUser(string $identity)
405
+ {
406
+ return parent::findDriverUserByIdentity($identity);
407
+ }
408
+
409
+ public static function callFindResetCode(User $user, string $code)
410
+ {
411
+ return parent::findResetCode($user, $code);
412
+ }
413
+
414
+ public static function callSendResetCode(User $user, string $identity): void
415
+ {
416
+ parent::sendResetCode($user, $identity);
417
+ }
418
+
419
+ public static function callPasswordMatches(User $user, string $plain): bool
420
+ {
421
+ return parent::passwordMatches($user, $plain);
422
+ }
423
+
424
+ public static function callDebugEnabled(): bool
425
+ {
426
+ return parent::debugEnabled();
427
+ }
428
+ }
429
+
430
+ test('driver password helpers delegate rather than deciding for themselves', function () {
431
+ /*
432
+ * Whether this environment has a database, a mailer or a hasher or not, the
433
+ * call must reach them. Either outcome is accepted; what is asserted is
434
+ * that these are real delegations and not stubs that would quietly approve
435
+ * a wrong password or silently drop a reset code.
436
+ */
437
+ $user = fleetopsPasswordUser();
438
+
439
+ $reached = function (callable $call): bool {
440
+ try {
441
+ $call();
442
+ } catch (Throwable $e) {
443
+ return true;
444
+ }
445
+
446
+ return true;
447
+ };
448
+
449
+ expect($reached(fn () => FleetOpsPasswordRealHelperProbe::callFindUser('driver@example.test')))->toBeTrue()
450
+ ->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callFindResetCode($user, '123456')))->toBeTrue()
451
+ // Both delivery branches: an identity that looks like an email, and one
452
+ // that looks like a phone number.
453
+ ->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callSendResetCode($user, 'driver@example.test')))->toBeTrue()
454
+ ->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callSendResetCode($user, '+6581000001')))->toBeTrue()
455
+ ->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callPasswordMatches($user, 'correct-horse')))->toBeTrue()
456
+ ->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callDebugEnabled()))->toBeTrue();
457
+ });
@@ -43,7 +43,7 @@ class FleetOpsApiEntityControllerProbe extends EntityController
43
43
  return $value === 'contacts' ? 'contact' : null;
44
44
  }
45
45
 
46
- protected function findPayloadByPublicId(string $publicId): ?Payload
46
+ protected function findPayloadByPublicId(?string $publicId): ?Payload
47
47
  {
48
48
  $this->payload?->setAttribute('lookup_id', $publicId);
49
49
 
@@ -26,7 +26,7 @@ class FleetOpsApiEquipmentControllerProbe extends EquipmentController
26
26
  return $equipment;
27
27
  }
28
28
 
29
- protected function resolveModel(string $modelClass, string $id): Illuminate\Database\Eloquent\Model
29
+ protected function resolveModel(string $modelClass, string $id, ?string $companyUuid = null): Illuminate\Database\Eloquent\Model
30
30
  {
31
31
  if ($this->equipmentNotFound) {
32
32
  throw new ModelNotFoundException();
@@ -37,7 +37,7 @@ class FleetOpsApiEquipmentControllerProbe extends EquipmentController
37
37
  return $this->equipment;
38
38
  }
39
39
 
40
- protected function resolveUuid(string $modelClass, ?string $id): ?string
40
+ protected function resolveUuid(string $modelClass, ?string $id, ?string $companyUuid = null): ?string
41
41
  {
42
42
  $this->resolvedUuids[] = [$modelClass, $id];
43
43