@fleetbase/fleetops-engine 0.6.68 → 0.6.69

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 (100) hide show
  1. package/RELEASE.md +11 -9
  2. package/addon/components/contact/form.hbs +13 -2
  3. package/addon/components/customer/details.hbs +17 -17
  4. package/addon/components/customer/form.hbs +13 -2
  5. package/addon/components/customer/form.js +0 -56
  6. package/addon/components/driver/details.hbs +13 -19
  7. package/addon/components/driver/form.hbs +20 -35
  8. package/addon/components/driver/form.js +0 -58
  9. package/addon/components/modals/driver-form.hbs +2 -2
  10. package/addon/components/modals/reset-profile-credentials.hbs +23 -0
  11. package/addon/components/modals/reset-profile-credentials.js +70 -0
  12. package/addon/components/profile-identity-hint.hbs +13 -0
  13. package/addon/components/profile-identity-hint.js +49 -0
  14. package/addon/controllers/management/drivers/index.js +4 -0
  15. package/addon/extension.js +4 -2
  16. package/addon/services/contact-actions.js +50 -89
  17. package/addon/services/driver-actions.js +98 -0
  18. package/addon/utils/profile-login-actions.js +172 -0
  19. package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
  20. package/app/components/profile-identity-hint.js +1 -0
  21. package/composer.json +1 -1
  22. package/extension.json +1 -1
  23. package/package.json +3 -3
  24. package/scripts/pest-bootstrap.php +12 -0
  25. package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
  26. package/server/resources/views/mail/driver-credentials.blade.php +21 -0
  27. package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
  28. package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
  29. package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
  30. package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
  31. package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
  32. package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
  33. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
  34. package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
  35. package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
  36. package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
  37. package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
  38. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
  39. package/server/src/Http/Resources/v1/Contact.php +3 -0
  40. package/server/src/Http/Resources/v1/Driver.php +3 -0
  41. package/server/src/Mail/DriverCredentialsMail.php +55 -0
  42. package/server/src/Models/Contact.php +55 -90
  43. package/server/src/Models/Sensor.php +3 -5
  44. package/server/src/Observers/ContactObserver.php +2 -22
  45. package/server/src/Observers/DriverObserver.php +5 -6
  46. package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
  47. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
  48. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
  49. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
  50. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
  51. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
  52. package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
  53. package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
  54. package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
  55. package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
  56. package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
  57. package/server/src/Support/ProfileAccountManager.php +405 -0
  58. package/server/src/routes.php +5 -0
  59. package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
  60. package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
  61. package/server/tests/ApiDriverControllerContractsTest.php +102 -20
  62. package/server/tests/CommandContractsTest.php +4 -4
  63. package/server/tests/ControllerHelperContractsTest.php +3 -11
  64. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
  65. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
  66. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
  67. package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
  68. package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
  69. package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
  70. package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
  71. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
  72. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
  73. package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
  74. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
  75. package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
  76. package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
  77. package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
  78. package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
  79. package/server/tests/ModelAccessorContractsTest.php +82 -20
  80. package/server/tests/ObserverContractsTest.php +27 -2
  81. package/server/tests/RequestContractsTest.php +19 -11
  82. package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
  83. package/server/tests/TelematicControllerContractsTest.php +22 -4
  84. package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
  85. package/server/tests/Unit/Models/ContactTest.php +48 -5
  86. package/server/tests/Unit/Models/SensorTest.php +2 -2
  87. package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
  88. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
  89. package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
  90. package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
  91. package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
  92. package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
  93. package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
  94. package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
  95. package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
  96. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
  97. package/translations/en-us.yaml +37 -2
  98. package/translations/uk-ua.yaml +3475 -0
  99. package/addon/components/modals/reset-customer-credentials.hbs +0 -18
  100. package/addon/components/modals/reset-customer-credentials.js +0 -55
@@ -0,0 +1,716 @@
1
+ <?php
2
+
3
+ if (!function_exists('Fleetbase\\Models\\env')) {
4
+ eval('namespace Fleetbase\\Models; function env($key = null, $default = null) { return $default; }');
5
+ }
6
+
7
+ if (!function_exists('Fleetbase\\Notifications\\env')) {
8
+ eval('namespace Fleetbase\\Notifications; function env($key = null, $default = null) { return $default; }');
9
+ }
10
+
11
+ if (!function_exists('Fleetbase\\Support\\env')) {
12
+ eval('namespace Fleetbase\\Support; function env($key = null, $default = null) { return $default; }');
13
+ }
14
+
15
+ if (!function_exists('Fleetbase\\FleetOps\\Http\\Controllers\\Internal\\v1\\env')) {
16
+ eval('namespace Fleetbase\\FleetOps\\Http\\Controllers\\Internal\\v1; function env($key = null, $default = null) { return $key === \'DEBUG\' ? true : $default; }');
17
+ }
18
+
19
+ use Fleetbase\FleetOps\Http\Controllers\Internal\v1\DriverController;
20
+ use Fleetbase\FleetOps\Http\Controllers\Internal\v1\FleetOpsLookupController;
21
+ use Fleetbase\Services\SmsService;
22
+ use Illuminate\Database\ConnectionResolver;
23
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
24
+ use Illuminate\Database\SQLiteConnection;
25
+ use Illuminate\Http\Request;
26
+ use Illuminate\Support\MessageBag;
27
+ use Illuminate\Support\Str;
28
+
29
+ /**
30
+ * Covers the internal DriverController login management endpoints against
31
+ * SQLite with real spatie roles: sending and resetting credentials,
32
+ * deactivating (and signing out) and reactivating a driver login, refusing
33
+ * all of them for a driver linked to a team member's account, the proxy
34
+ * field sync on update, and FleetOpsLookupController::profileIdentity.
35
+ */
36
+ if (!function_exists('Fleetbase\Observers\event')) {
37
+ eval('namespace Fleetbase\Observers; function event($event = null, $payload = []) { return []; }');
38
+ }
39
+
40
+ if (!function_exists('Fleetbase\FleetOps\Observers\event')) {
41
+ eval('namespace Fleetbase\FleetOps\Observers; function event($event = null, $payload = []) { return []; }');
42
+ }
43
+
44
+ if (!function_exists('Fleetbase\Support\session')) {
45
+ eval('namespace Fleetbase\Support; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } }; } return \session($key, $default); }');
46
+ }
47
+
48
+ if (!function_exists('Fleetbase\Models\session')) {
49
+ eval('namespace Fleetbase\Models; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } public function missing($k) { return \session($k) === null; } }; } return \session($key, $default); }');
50
+ }
51
+
52
+ if (!function_exists('Fleetbase\Support\auth')) {
53
+ eval('namespace Fleetbase\Support; function auth() { return new class { public function user() { return null; } public function id() { return null; } }; }');
54
+ }
55
+
56
+ if (!function_exists('Fleetbase\FleetOps\Http\Controllers\Internal\v1\env')) {
57
+ eval('namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; function env($key, $default = null) { return $default; }');
58
+ }
59
+
60
+ if (!function_exists('__')) {
61
+ function __($key = null, $replace = [], $locale = null)
62
+ {
63
+ return $key;
64
+ }
65
+ }
66
+
67
+ if (!class_exists('Illuminate\Validation\Rule')) {
68
+ eval('namespace Illuminate\Validation; class Rule { public function __construct(private string $rule = "") {} public static function requiredIf($c): string { return (is_callable($c) ? $c() : $c) ? "required" : "nullable"; } public static function in(array $v): self { return new self("in:" . implode(",", $v)); } public static function exists($t, $c = null): self { return new self("exists:" . $t . ($c ? "," . $c : "")); } public static function unique($t, $c = null): self { return new self("unique:" . $t . ($c ? "," . $c : "")); } public static function when($c, array $r): array { return (is_callable($c) ? $c() : $c) ? $r : []; } public function where($cb): self { return $this; } public function whereNull($col): self { return $this; } public function ignore($v, $c = null): self { return $this; } public function __toString(): string { return $this->rule; } }');
69
+ }
70
+
71
+ if (!Str::hasMacro('humanize')) {
72
+ Str::macro('humanize', fn ($value) => ucfirst(str_replace(['_', '-'], ' ', Str::snake((string) $value))));
73
+ }
74
+
75
+ if (!Request::hasMacro('getController')) {
76
+ Request::macro('getController', fn () => new DriverController());
77
+ }
78
+
79
+ if (!Request::hasMacro('or')) {
80
+ Request::macro('or', function (array $params = [], $default = null) {
81
+ foreach ($params as $param) {
82
+ if ($this->has($param)) {
83
+ return $this->input($param);
84
+ }
85
+ }
86
+
87
+ return $default;
88
+ });
89
+ }
90
+
91
+ function fleetopsDriverLoginContainer(): void
92
+ {
93
+ $current = Illuminate\Container\Container::getInstance();
94
+ if (method_exists($current, 'hasDebugModeEnabled')) {
95
+ return;
96
+ }
97
+
98
+ // Core record mutation surfaces exceptions through app()->hasDebugModeEnabled()
99
+ // and app()->environment(), which the harness container lacks
100
+ $replacement = new class extends Illuminate\Container\Container {
101
+ public function environment(...$environments)
102
+ {
103
+ if (empty($environments)) {
104
+ return 'testing';
105
+ }
106
+
107
+ $checks = is_array($environments[0]) ? $environments[0] : $environments;
108
+
109
+ return in_array('testing', $checks, true);
110
+ }
111
+
112
+ public function hasDebugModeEnabled()
113
+ {
114
+ return true;
115
+ }
116
+ };
117
+
118
+ foreach (['bindings', 'instances', 'aliases', 'abstractAliases', 'resolved', 'extenders', 'tags', 'contextual', 'scopedInstances', 'reboundCallbacks', 'globalBeforeResolvingCallbacks', 'globalResolvingCallbacks', 'globalAfterResolvingCallbacks', 'beforeResolvingCallbacks', 'resolvingCallbacks', 'afterResolvingCallbacks'] as $property) {
119
+ if (!property_exists(Illuminate\Container\Container::class, $property)) {
120
+ continue;
121
+ }
122
+ $reflection = new ReflectionProperty(Illuminate\Container\Container::class, $property);
123
+ $reflection->setAccessible(true);
124
+ if ($reflection->isInitialized($current)) {
125
+ $reflection->setValue($replacement, $reflection->getValue($current));
126
+ }
127
+ }
128
+
129
+ Illuminate\Container\Container::setInstance($replacement);
130
+ Illuminate\Support\Facades\Facade::setFacadeApplication($replacement);
131
+ }
132
+
133
+ function fleetopsDriverLoginBoot(array $validatorErrors): SQLiteConnection
134
+ {
135
+ fleetopsDriverLoginContainer();
136
+ $pdo = new PDO('sqlite::memory:');
137
+ $pdo->sqliteCreateFunction('ST_PointFromText', fn ($wkt, $srid = 0, $axisOrder = null) => $wkt);
138
+ $pdo->sqliteCreateFunction('ST_GeomFromText', fn ($wkt, $srid = 0, $axisOrder = null) => $wkt);
139
+ $connection = new SQLiteConnection($pdo);
140
+ $resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
141
+ $resolver->setDefaultConnection('mysql');
142
+ EloquentModel::setConnectionResolver($resolver);
143
+ if (!EloquentModel::getEventDispatcher()) {
144
+ EloquentModel::setEventDispatcher(new Illuminate\Events\Dispatcher());
145
+ }
146
+ app()->instance('db', new class($connection) {
147
+ public function __construct(public SQLiteConnection $c)
148
+ {
149
+ }
150
+
151
+ public function connection($name = null): SQLiteConnection
152
+ {
153
+ return $this->c;
154
+ }
155
+
156
+ public function __call($method, $arguments)
157
+ {
158
+ return $this->c->{$method}(...$arguments);
159
+ }
160
+ });
161
+ app()->instance('db.schema', $connection->getSchemaBuilder());
162
+ app()->instance('responsecache', new class {
163
+ public function __call($method, $arguments)
164
+ {
165
+ return null;
166
+ }
167
+ });
168
+ $GLOBALS['fleetopsDriverLoginErrors'] = $validatorErrors;
169
+ app()->instance('validator', new class {
170
+ public function make($data = [], $rules = [], $messages = [], $attributes = [])
171
+ {
172
+ return new class implements Illuminate\Contracts\Validation\Validator {
173
+ public function fails()
174
+ {
175
+ return !empty($GLOBALS['fleetopsDriverLoginErrors']);
176
+ }
177
+
178
+ public function errors()
179
+ {
180
+ return new MessageBag($GLOBALS['fleetopsDriverLoginErrors']);
181
+ }
182
+
183
+ public function validated()
184
+ {
185
+ return [];
186
+ }
187
+
188
+ public function validate()
189
+ {
190
+ return [];
191
+ }
192
+
193
+ public function failed()
194
+ {
195
+ return array_keys($GLOBALS['fleetopsDriverLoginErrors']);
196
+ }
197
+
198
+ public function sometimes($attribute, $rules, callable $callback)
199
+ {
200
+ return $this;
201
+ }
202
+
203
+ public function after($callback)
204
+ {
205
+ return $this;
206
+ }
207
+
208
+ public function getMessageBag()
209
+ {
210
+ return new MessageBag($GLOBALS['fleetopsDriverLoginErrors']);
211
+ }
212
+ };
213
+ }
214
+ });
215
+ Illuminate\Support\Facades\DB::clearResolvedInstance('db');
216
+ app()->instance('hash', new class implements Illuminate\Contracts\Hashing\Hasher {
217
+ public function info($hashedValue): array
218
+ {
219
+ return [];
220
+ }
221
+
222
+ public function make($value, array $options = []): string
223
+ {
224
+ return 'hashed:' . $value;
225
+ }
226
+
227
+ public function check($value, $hashedValue, array $options = []): bool
228
+ {
229
+ return 'hashed:' . $value === $hashedValue;
230
+ }
231
+
232
+ public function needsRehash($hashedValue, array $options = []): bool
233
+ {
234
+ return false;
235
+ }
236
+ });
237
+ Illuminate\Support\Facades\Hash::clearResolvedInstance('hash');
238
+
239
+ $schema = $connection->getSchemaBuilder();
240
+ $tables = [
241
+ 'drivers' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'user_uuid', 'vehicle_uuid', 'vendor_uuid', 'current_job_uuid', 'auth_token', 'signup_token_used', 'avatar_url', 'drivers_license_number', 'license_expiry', 'location', 'heading', 'bearing', 'altitude', 'speed', 'currency', 'current_status', 'meta', 'location_updated_at', 'slug', 'status', 'country', 'city', 'online', '_key'],
242
+ 'users' => ['uuid', 'public_id', 'company_uuid', 'name', 'email', 'phone', 'password', 'status', 'type', 'username', 'avatar_uuid', 'slug', 'timezone', 'country', 'ip_address', 'meta', '_key'],
243
+ 'companies' => ['uuid', 'public_id', 'name', 'owner_uuid', 'timezone', 'options', 'status', '_key'],
244
+ 'company_users' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'status', '_key'],
245
+ 'vehicles' => ['uuid', 'public_id', 'company_uuid', 'driver_uuid'],
246
+ 'custom_fields' => ['uuid', 'public_id', 'company_uuid', 'subject_uuid', 'subject_type', 'name', 'label'],
247
+ 'custom_field_values' => ['uuid', 'public_id', 'company_uuid', 'custom_field_uuid', 'subject_uuid', 'subject_type', 'value', 'value_type', '_key'],
248
+ 'settings' => ['key', 'value'],
249
+ 'permissions' => ['name', 'guard_name', 'service', 'description'],
250
+ 'roles' => ['uuid', 'public_id', 'name', 'guard_name', 'company_uuid', 'service', 'description', '_key'],
251
+ 'model_has_roles' => ['role_id', 'model_type', 'model_uuid'],
252
+ 'model_has_permissions' => ['permission_id', 'model_type', 'model_uuid'],
253
+ 'role_has_permissions' => ['permission_id', 'role_id'],
254
+ 'policies' => ['name', 'guard_name', 'company_uuid'],
255
+ 'directives' => ['uuid', 'public_id', 'company_uuid', 'permission_uuid', 'subject_type', 'subject_uuid', 'key', 'rules'],
256
+ 'files' => ['uuid', 'public_id', 'company_uuid', 'uploader_uuid', 'subject_uuid', 'subject_type', 'name', 'original_filename', 'extension', 'content_type', 'path', 'bucket', 'disk', 'size', 'type', 'meta', '_key'],
257
+ 'invites' => ['uuid', 'public_id', 'company_uuid', 'created_by_uuid', 'subject_uuid', 'subject_type', 'code', 'uri', 'protocol', 'recipients', 'reason', 'expires_at', '_key'],
258
+ 'notifications' => ['type', 'notifiable_type', 'notifiable_id', 'data', 'read_at'],
259
+ 'contacts' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'name', 'email', 'phone', 'type'],
260
+ 'personal_access_tokens' => ['tokenable_type', 'tokenable_id', 'name', 'token', 'abilities', 'last_used_at', 'expires_at'],
261
+ ];
262
+ foreach ($tables as $table => $columns) {
263
+ $schema->create($table, function ($blueprint) use ($columns) {
264
+ $blueprint->increments('id');
265
+ foreach ($columns as $column) {
266
+ $blueprint->string($column)->nullable();
267
+ }
268
+ $blueprint->timestamps();
269
+ $blueprint->timestamp('deleted_at')->nullable();
270
+ });
271
+ }
272
+
273
+ app()->instance('cache', new class {
274
+ public function tags($tags = null)
275
+ {
276
+ return $this;
277
+ }
278
+
279
+ public function flush()
280
+ {
281
+ return true;
282
+ }
283
+
284
+ public function remember($key, $ttl, $callback)
285
+ {
286
+ return $callback();
287
+ }
288
+
289
+ public function store($name = null)
290
+ {
291
+ return $this;
292
+ }
293
+
294
+ public function __call($method, $arguments)
295
+ {
296
+ return null;
297
+ }
298
+ });
299
+ Illuminate\Support\Facades\Cache::clearResolvedInstance('cache');
300
+ app()->instance(Illuminate\Contracts\Notifications\Dispatcher::class, new class {
301
+ public array $sent = [];
302
+
303
+ public function send($notifiables, $notification)
304
+ {
305
+ $this->sent[] = $notification;
306
+ }
307
+
308
+ public function sendNow($notifiables, $notification, ?array $channels = null)
309
+ {
310
+ $this->sent[] = $notification;
311
+ }
312
+
313
+ public function __call($method, $arguments)
314
+ {
315
+ return null;
316
+ }
317
+ });
318
+ config()->set('cache.default', 'array');
319
+ config()->set('cache.stores.array', ['driver' => 'array']);
320
+ config()->set('permission.cache.expiration_time', 60);
321
+ config()->set('permission.cache.key', 'spatie.permission.cache');
322
+ config()->set('permission.cache.store', 'default');
323
+ config()->set('permission.models.permission', Fleetbase\Models\Permission::class);
324
+ config()->set('permission.models.role', Fleetbase\Models\Role::class);
325
+ config()->set('permission.table_names', ['roles' => 'roles', 'permissions' => 'permissions', 'model_has_permissions' => 'model_has_permissions', 'model_has_roles' => 'model_has_roles', 'role_has_permissions' => 'role_has_permissions']);
326
+ config()->set('permission.column_names', ['role_pivot_key' => null, 'permission_pivot_key' => null, 'model_morph_key' => 'model_uuid', 'team_foreign_key' => 'team_id']);
327
+ config()->set('permission.teams', false);
328
+ config()->set('permission.events_enabled', false);
329
+ $cacheManager = new Illuminate\Cache\CacheManager(app());
330
+ app()->instance(Illuminate\Cache\CacheManager::class, $cacheManager);
331
+ app()->instance(Spatie\Permission\PermissionRegistrar::class, new Spatie\Permission\PermissionRegistrar($cacheManager));
332
+ config()->set('activitylog.enabled', false);
333
+ config()->set('activitylog.default_auth_driver', 'web');
334
+ config()->set('auth.defaults.guard', 'web');
335
+ config()->set('permission.models.role', Fleetbase\Models\Role::class);
336
+ app()->bind(Illuminate\Contracts\Config\Repository::class, fn () => config());
337
+
338
+ session(['company' => 'company-1']);
339
+ $connection->table('companies')->insert(['uuid' => 'company-1', 'public_id' => 'company_adopt1', 'name' => 'Acme']);
340
+ $connection->table('roles')->insert([
341
+ ['uuid' => 'role-1', 'name' => 'Driver', 'guard_name' => 'web', 'company_uuid' => 'company-1'],
342
+ ['uuid' => 'role-2', 'name' => 'Driver', 'guard_name' => 'sanctum', 'company_uuid' => 'company-1'],
343
+ ['uuid' => 'role-3', 'name' => 'Administrator', 'guard_name' => 'web', 'company_uuid' => 'company-1'],
344
+ ['uuid' => 'role-4', 'name' => 'Administrator', 'guard_name' => 'sanctum', 'company_uuid' => 'company-1'],
345
+ ]);
346
+
347
+ return $connection;
348
+ }
349
+
350
+ class FleetOpsDriverLoginSmsFake extends SmsService
351
+ {
352
+ public array $sent = [];
353
+
354
+ public function __construct()
355
+ {
356
+ }
357
+
358
+ public function send(string $to, string $text, array $options = [], ?string $provider = null): array
359
+ {
360
+ $this->sent[] = [$to, $text];
361
+
362
+ return ['success' => true];
363
+ }
364
+ }
365
+
366
+ function fleetopsDriverLoginFixture(): SQLiteConnection
367
+ {
368
+ $connection = fleetopsDriverLoginBoot([]);
369
+
370
+ Illuminate\Support\Facades\Mail::swap(new class {
371
+ public array $sent = [];
372
+
373
+ public function to($users)
374
+ {
375
+ return $this;
376
+ }
377
+
378
+ public function send($mailable)
379
+ {
380
+ $this->sent[] = $mailable;
381
+
382
+ return null;
383
+ }
384
+ });
385
+ app()->instance(SmsService::class, new FleetOpsDriverLoginSmsFake());
386
+
387
+ $users = [
388
+ ['uuid' => '11111111-1111-4111-8111-111111111111', 'public_id' => 'user_managed', 'company_uuid' => 'company-1', 'name' => 'Managed Driver', 'email' => 'managed@example.com', 'phone' => '+6590000001', 'password' => 'hashed:old', 'status' => 'pending', 'type' => 'driver'],
389
+ ['uuid' => '22222222-2222-4222-8222-222222222222', 'public_id' => 'user_staff', 'company_uuid' => 'company-1', 'name' => 'Staff Driver', 'email' => 'staff@example.com', 'phone' => '+6590000002', 'password' => 'hashed:staff', 'status' => 'active', 'type' => 'user'],
390
+ ['uuid' => '33333333-3333-4333-8333-333333333333', 'public_id' => 'user_nocontact', 'company_uuid' => 'company-1', 'name' => 'Unreachable', 'status' => 'active', 'type' => 'driver'],
391
+ ];
392
+ foreach ($users as $user) {
393
+ $connection->table('users')->insert($user);
394
+ }
395
+ $connection->table('company_users')->insert(['uuid' => 'cu-managed', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111', 'status' => 'pending']);
396
+ $drivers = [
397
+ ['uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', 'public_id' => 'driver_managed', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111'],
398
+ ['uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2', 'public_id' => 'driver_staff', 'company_uuid' => 'company-1', 'user_uuid' => '22222222-2222-4222-8222-222222222222'],
399
+ ['uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa3', 'public_id' => 'driver_nocontact', 'company_uuid' => 'company-1', 'user_uuid' => '33333333-3333-4333-8333-333333333333'],
400
+ ];
401
+ foreach ($drivers as $driver) {
402
+ $connection->table('drivers')->insert($driver);
403
+ }
404
+
405
+ return $connection;
406
+ }
407
+
408
+ function fleetopsDriverLoginUser(SQLiteConnection $connection, string $uuid = '11111111-1111-4111-8111-111111111111'): object
409
+ {
410
+ return $connection->table('users')->where('uuid', $uuid)->first();
411
+ }
412
+
413
+ test('send credentials sets a new password activates the login and emails it', function () {
414
+ $connection = fleetopsDriverLoginFixture();
415
+
416
+ $response = (new DriverController())->sendCredentials('driver_managed');
417
+ $data = $response->getData(true);
418
+ $user = fleetopsDriverLoginUser($connection);
419
+
420
+ expect($data['status'])->toBe('ok')
421
+ ->and($data['sent_via'])->toBe('email')
422
+ ->and($data['driver'])->toMatchArray([
423
+ 'id' => 'driver_managed',
424
+ 'uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1',
425
+ 'user_uuid' => '11111111-1111-4111-8111-111111111111',
426
+ 'is_staff_linked' => false,
427
+ 'login_status' => 'active',
428
+ ])
429
+ ->and($data['driver']['user'])->toMatchArray([
430
+ 'id' => 'user_managed',
431
+ 'email' => 'managed@example.com',
432
+ 'phone' => '+6590000001',
433
+ 'status' => 'active',
434
+ ])
435
+ ->and($user->status)->toBe('active')
436
+ ->and($user->password)->not->toBe('hashed:old')
437
+ ->and(Illuminate\Support\Facades\Mail::getFacadeRoot()->sent[0])->toBeInstanceOf(Fleetbase\FleetOps\Mail\DriverCredentialsMail::class);
438
+
439
+ // A driver with neither an email nor a phone can't be sent credentials
440
+ $unreachable = (new DriverController())->sendCredentials('driver_nocontact');
441
+ expect($unreachable->getData(true))->toBe(['error' => 'This profile has no email or phone to send credentials to.']);
442
+ });
443
+
444
+ test('login management endpoints report missing drivers accounts and staff-linked logins', function () {
445
+ $connection = fleetopsDriverLoginFixture();
446
+ // The driver scope keeps drivers whose account was deleted (the user
447
+ // relation includes trashed accounts), but such a driver has no login
448
+ $connection->table('users')->insert(['uuid' => '44444444-4444-4444-8444-444444444444', 'company_uuid' => 'company-1', 'type' => 'driver', 'deleted_at' => '2026-01-01 00:00:00']);
449
+ $connection->table('drivers')->insert(['uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa4', 'public_id' => 'driver_orphan', 'company_uuid' => 'company-1', 'user_uuid' => '44444444-4444-4444-8444-444444444444']);
450
+ $controller = new DriverController();
451
+ $reset = fn (string $id) => $controller->resetCredentials(Request::create('/x', 'POST', ['password' => 'long-enough', 'password_confirmation' => 'long-enough']), $id);
452
+
453
+ foreach ([
454
+ fn (string $id) => $controller->sendCredentials($id),
455
+ $reset,
456
+ fn (string $id) => $controller->deactivateLogin($id),
457
+ fn (string $id) => $controller->reactivateLogin($id),
458
+ ] as $endpoint) {
459
+ $missing = $endpoint('driver_unknown');
460
+ $orphan = $endpoint('driver_orphan');
461
+ $staff = $endpoint('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2');
462
+
463
+ expect($missing->getStatusCode())->toBe(404)
464
+ ->and($missing->getData(true))->toBe(['error' => 'Driver not found.'])
465
+ ->and($orphan->getData(true))->toBe(['error' => 'This driver has no login account.'])
466
+ ->and($staff->getStatusCode())->toBe(422)
467
+ ->and($staff->getData(true))->toBe(['error' => 'This driver signs in with a team member account. Manage this login in IAM.']);
468
+ }
469
+
470
+ // The team member's account is never touched
471
+ $staffUser = fleetopsDriverLoginUser($connection, '22222222-2222-4222-8222-222222222222');
472
+ expect($staffUser->password)->toBe('hashed:staff')
473
+ ->and($staffUser->status)->toBe('active')
474
+ ->and(Illuminate\Support\Facades\Mail::getFacadeRoot()->sent)->toBe([]);
475
+ });
476
+
477
+ test('reset credentials validates the password and optionally sends it', function () {
478
+ $connection = fleetopsDriverLoginFixture();
479
+ $controller = new DriverController();
480
+ $request = fn (array $input) => Request::create('/x', 'POST', $input);
481
+
482
+ expect($controller->resetCredentials($request(['password' => 'short', 'password_confirmation' => 'short']), 'driver_managed')->getData(true))
483
+ ->toBe(['error' => 'Password must be at least 8 characters.'])
484
+ ->and($controller->resetCredentials($request([]), 'driver_managed')->getData(true))
485
+ ->toBe(['error' => 'Password must be at least 8 characters.'])
486
+ ->and($controller->resetCredentials($request(['password' => 'long-enough', 'password_confirmation' => 'different']), 'driver_managed')->getData(true))
487
+ ->toBe(['error' => 'Passwords do not match.']);
488
+
489
+ $quiet = $controller->resetCredentials($request(['password' => 'quiet-secret', 'password_confirmation' => 'quiet-secret']), 'driver_managed');
490
+ expect($quiet->getData(true)['status'])->toBe('ok')
491
+ ->and(fleetopsDriverLoginUser($connection)->password)->toBe('hashed:quiet-secret')
492
+ ->and(Illuminate\Support\Facades\Mail::getFacadeRoot()->sent)->toBe([]);
493
+
494
+ $sent = $controller->resetCredentials($request(['password' => 'mailed-secret', 'password_confirmation' => 'mailed-secret', 'send_credentials' => true]), 'driver_managed');
495
+ expect($sent->getData(true)['driver']['user_uuid'])->toBe('11111111-1111-4111-8111-111111111111')
496
+ ->and(fleetopsDriverLoginUser($connection)->password)->toBe('hashed:mailed-secret')
497
+ ->and(Illuminate\Support\Facades\Mail::getFacadeRoot()->sent)->toHaveCount(1);
498
+
499
+ // The password is still changed when there's nowhere to send it
500
+ $unsendable = $controller->resetCredentials($request(['password' => 'kept-secret', 'password_confirmation' => 'kept-secret', 'send_credentials' => '1']), 'driver_nocontact');
501
+ expect($unsendable->getData(true))->toBe(['error' => 'This profile has no email or phone to send credentials to.'])
502
+ ->and(fleetopsDriverLoginUser($connection, '33333333-3333-4333-8333-333333333333')->password)->toBe('hashed:kept-secret');
503
+ });
504
+
505
+ test('deactivating a login signs the driver out and reactivating restores it', function () {
506
+ $connection = fleetopsDriverLoginFixture();
507
+ $connection->table('users')->where('uuid', '11111111-1111-4111-8111-111111111111')->update(['status' => 'active']);
508
+ $connection->table('personal_access_tokens')->insert([
509
+ 'tokenable_type' => Fleetbase\Models\User::class,
510
+ 'tokenable_id' => '11111111-1111-4111-8111-111111111111',
511
+ 'name' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1',
512
+ 'token' => 'token-hash',
513
+ ]);
514
+ $connection->table('personal_access_tokens')->insert([
515
+ 'tokenable_type' => Fleetbase\Models\User::class,
516
+ 'tokenable_id' => '22222222-2222-4222-8222-222222222222',
517
+ 'name' => 'staff-session',
518
+ 'token' => 'staff-token-hash',
519
+ ]);
520
+ $controller = new DriverController();
521
+
522
+ $deactivated = $controller->deactivateLogin('driver_managed')->getData(true);
523
+
524
+ expect($deactivated['status'])->toBe('ok')
525
+ ->and($deactivated['driver']['login_status'])->toBe('inactive')
526
+ ->and(fleetopsDriverLoginUser($connection)->status)->toBe('inactive')
527
+ ->and($connection->table('company_users')->where('uuid', 'cu-managed')->value('status'))->toBe('inactive')
528
+ // Only the driver's own tokens are revoked
529
+ ->and($connection->table('personal_access_tokens')->pluck('name')->all())->toBe(['staff-session']);
530
+
531
+ $reactivated = $controller->reactivateLogin('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1')->getData(true);
532
+
533
+ expect($reactivated['driver']['login_status'])->toBe('active')
534
+ ->and($reactivated['driver']['user']['session_status'])->toBe('active')
535
+ ->and(fleetopsDriverLoginUser($connection)->status)->toBe('active');
536
+ });
537
+
538
+ function fleetopsDriverLoginUpdateRequest(string $id, array $driver): Request
539
+ {
540
+ $request = Request::create('/int/v1/drivers/' . $id, 'PUT', ['driver' => $driver]);
541
+ $store = app('session.store');
542
+ $store->put('company', 'company-1');
543
+ $request->setLaravelSession($store);
544
+ $request->setRouteResolver(fn () => new class($id) {
545
+ public function __construct(private string $id)
546
+ {
547
+ }
548
+
549
+ public function getAction($key = null)
550
+ {
551
+ return DriverController::class . '@updateRecord';
552
+ }
553
+
554
+ public function getActionMethod()
555
+ {
556
+ return 'updateRecord';
557
+ }
558
+
559
+ public function uri()
560
+ {
561
+ return 'int/v1/drivers/{id}';
562
+ }
563
+
564
+ public function getName()
565
+ {
566
+ return 'int.v1.drivers.update';
567
+ }
568
+
569
+ public function parameters()
570
+ {
571
+ return ['id' => $this->id];
572
+ }
573
+
574
+ public function parameter($key, $default = null)
575
+ {
576
+ return $key === 'id' ? $this->id : $default;
577
+ }
578
+ });
579
+ app()->instance('request', $request);
580
+
581
+ return $request;
582
+ }
583
+
584
+ test('update record syncs proxy fields to a managed account and only the name to a team member', function () {
585
+ $connection = fleetopsDriverLoginFixture();
586
+ $controller = new DriverController();
587
+
588
+ $managed = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', [
589
+ 'name' => 'Renamed Driver',
590
+ 'email' => 'Renamed@Example.com',
591
+ 'phone' => null,
592
+ 'photo_uuid' => 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
593
+ 'user_uuid' => '22222222-2222-4222-8222-222222222222',
594
+ ]), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
595
+
596
+ $user = fleetopsDriverLoginUser($connection);
597
+
598
+ expect($managed)->toBeArray()
599
+ ->and($managed['driver']->resource->user_uuid)->toBe('11111111-1111-4111-8111-111111111111')
600
+ ->and($user->name)->toBe('Renamed Driver')
601
+ ->and($user->email)->toBe('renamed@example.com')
602
+ ->and($user->phone)->toBeNull()
603
+ ->and($user->avatar_uuid)->toBe('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb');
604
+
605
+ $staff = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2', [
606
+ 'name' => 'Staff As Driver',
607
+ 'email' => 'changed@example.com',
608
+ ]), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2');
609
+
610
+ $staffUser = fleetopsDriverLoginUser($connection, '22222222-2222-4222-8222-222222222222');
611
+
612
+ expect($staff)->toBeArray()
613
+ ->and($staffUser->name)->toBe('Staff As Driver')
614
+ ->and($staffUser->email)->toBe('staff@example.com');
615
+
616
+ // Taking another account's email is refused
617
+ $conflict = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', [
618
+ 'email' => 'staff@example.com',
619
+ ]), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
620
+
621
+ expect($conflict->getStatusCode())->toBe(422)
622
+ ->and($conflict->getData(true))->toBe(['error' => 'This email is already in use by another account.'])
623
+ ->and(fleetopsDriverLoginUser($connection)->email)->toBe('renamed@example.com');
624
+ });
625
+
626
+ function fleetopsDriverLoginLookup(array $input): array
627
+ {
628
+ return (new FleetOpsLookupController())->profileIdentity(Request::create('/int/v1/fleet-ops/lookup/profile-identity', 'GET', $input))->getData(true);
629
+ }
630
+
631
+ test('profile identity lookup reports the team member to link or the conflict', function () {
632
+ $connection = fleetopsDriverLoginFixture();
633
+ $connection->table('users')->insert(['uuid' => '55555555-5555-4555-8555-555555555555', 'company_uuid' => 'company-1', 'name' => 'Office Staff', 'email' => 'office@example.com', 'phone' => '+6590000005', 'type' => 'admin']);
634
+ $connection->table('users')->insert(['uuid' => '66666666-6666-4666-8666-666666666666', 'company_uuid' => 'company-1', 'name' => 'Customer', 'email' => 'customer@example.com', 'type' => 'customer']);
635
+ $connection->table('contacts')->insert(['uuid' => 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', 'public_id' => 'contact_customer', 'company_uuid' => 'company-1', 'user_uuid' => '66666666-6666-4666-8666-666666666666', 'type' => 'customer']);
636
+ $connection->table('contacts')->insert(['uuid' => 'cccccccc-cccc-4ccc-8ccc-ccccccccccc2', 'public_id' => 'contact_nouser', 'company_uuid' => 'company-1', 'type' => 'contact']);
637
+
638
+ expect((new FleetOpsLookupController())->profileIdentity(Request::create('/x', 'GET', ['type' => 'user']))->getStatusCode())->toBe(422)
639
+ ->and(fleetopsDriverLoginLookup(['type' => 'staff']))->toBe(['error' => 'Invalid profile type.'])
640
+ // Nothing entered, nothing found
641
+ ->and(fleetopsDriverLoginLookup([]))->toBe(['staff' => null, 'conflict' => null])
642
+ // A new driver with a team member's email links to them
643
+ ->and(fleetopsDriverLoginLookup(['email' => 'office@example.com']))->toBe([
644
+ 'staff' => ['uuid' => '55555555-5555-4555-8555-555555555555', 'name' => 'Office Staff', 'email' => 'office@example.com', 'phone' => '+6590000005'],
645
+ 'conflict' => null,
646
+ ])
647
+ // A new driver can't take a customer's email
648
+ ->and(fleetopsDriverLoginLookup(['type' => 'driver', 'email' => 'customer@example.com']))->toBe(['staff' => null, 'conflict' => 'This email is already used by a customer.'])
649
+ // An existing driver's own email isn't taken, another account's is
650
+ ->and(fleetopsDriverLoginLookup(['ignore' => 'driver_managed', 'email' => 'managed@example.com']))->toBe(['staff' => null, 'conflict' => null])
651
+ ->and(fleetopsDriverLoginLookup(['ignore' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', 'phone' => '+6590000005']))->toBe(['staff' => null, 'conflict' => 'This phone number is already in use by another account.'])
652
+ // Customer profiles are looked up among contacts
653
+ ->and(fleetopsDriverLoginLookup(['type' => 'customer', 'ignore' => 'contact_customer', 'email' => 'customer@example.com']))->toBe(['staff' => null, 'conflict' => null])
654
+ // A profile without an account, or an unknown profile id, is treated as a new profile
655
+ ->and(fleetopsDriverLoginLookup(['type' => 'contact', 'ignore' => 'contact_nouser', 'email' => 'customer@example.com']))->toBe(['staff' => null, 'conflict' => 'This email is already used by a customer.'])
656
+ ->and(fleetopsDriverLoginLookup(['type' => 'contact', 'ignore' => 'contact_unknown', 'email' => 'customer@example.com']))->toBe(['staff' => null, 'conflict' => 'This email is already used by a customer.']);
657
+ });
658
+
659
+ test('update record syncs custom field values and reports failures', function () {
660
+ $connection = fleetopsDriverLoginFixture();
661
+ $connection->table('custom_fields')->insert(['uuid' => 'dddddddd-dddd-4ddd-8ddd-dddddddddddd', 'company_uuid' => 'company-1', 'name' => 'badge', 'label' => 'Badge']);
662
+ $controller = new DriverController();
663
+
664
+ $updated = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', [
665
+ 'custom_field_values' => [['custom_field_uuid' => 'dddddddd-dddd-4ddd-8ddd-dddddddddddd', 'value' => 'B-12']],
666
+ ]), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
667
+
668
+ expect($updated)->toBeArray()
669
+ ->and($connection->table('custom_field_values')->where('subject_uuid', 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1')->value('value'))->toBe('B-12');
670
+
671
+ // An unknown driver surfaces the model's not-found error
672
+ $missing = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa9', ['name' => 'Ghost']), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa9');
673
+ expect($missing->getData(true)['error'])->toContain('not found');
674
+
675
+ // A request validation failure raised while updating the account is returned
676
+ $GLOBALS['fleetopsDriverLoginHashException'] = new Fleetbase\Exceptions\FleetbaseRequestValidationException(['password' => ['The password is too weak.']]);
677
+ app()->instance('hash', new class implements Illuminate\Contracts\Hashing\Hasher {
678
+ public function info($hashedValue): array
679
+ {
680
+ return [];
681
+ }
682
+
683
+ public function make($value, array $options = []): string
684
+ {
685
+ throw $GLOBALS['fleetopsDriverLoginHashException'];
686
+ }
687
+
688
+ public function check($value, $hashedValue, array $options = []): bool
689
+ {
690
+ return false;
691
+ }
692
+
693
+ public function needsRehash($hashedValue, array $options = []): bool
694
+ {
695
+ return false;
696
+ }
697
+ });
698
+ Illuminate\Support\Facades\Hash::clearResolvedInstance('hash');
699
+
700
+ $weak = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', ['password' => 'weak-password']), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
701
+ expect($weak->getData(true))->toBe(['error' => ['password' => ['The password is too weak.']]]);
702
+ });
703
+
704
+ test('update record returns validation failures', function () {
705
+ fleetopsDriverLoginFixture();
706
+ $GLOBALS['fleetopsDriverLoginErrors'] = ['email' => ['The email must be a valid email address.']];
707
+
708
+ $failure = null;
709
+ try {
710
+ (new DriverController())->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', ['email' => 'not-an-email']), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
711
+ } catch (Illuminate\Validation\ValidationException $exception) {
712
+ $failure = $exception;
713
+ }
714
+
715
+ expect($failure?->errors())->toBe(['email' => ['The email must be a valid email address.']]);
716
+ });