@fleetbase/fleetops-engine 0.6.67 → 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 (115) hide show
  1. package/RELEASE.md +11 -15
  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/fuel-integration/form.hbs +2 -2
  10. package/addon/components/fuel-integration/form.js +8 -0
  11. package/addon/components/fuel-integration/hub.js +1 -1
  12. package/addon/components/modals/driver-form.hbs +2 -2
  13. package/addon/components/modals/reset-profile-credentials.hbs +23 -0
  14. package/addon/components/modals/reset-profile-credentials.js +70 -0
  15. package/addon/components/profile-identity-hint.hbs +13 -0
  16. package/addon/components/profile-identity-hint.js +49 -0
  17. package/addon/controllers/management/drivers/index.js +4 -0
  18. package/addon/extension.js +4 -2
  19. package/addon/services/contact-actions.js +50 -89
  20. package/addon/services/driver-actions.js +98 -0
  21. package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
  22. package/addon/utils/fuel-transaction.js +2 -0
  23. package/addon/utils/profile-login-actions.js +172 -0
  24. package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
  25. package/app/components/profile-identity-hint.js +1 -0
  26. package/composer.json +1 -1
  27. package/docs/SAFEE.md +5 -0
  28. package/docs/TELEMATICS_QUEUES.md +59 -8
  29. package/extension.json +1 -1
  30. package/package.json +3 -3
  31. package/scripts/pest-bootstrap.php +12 -0
  32. package/server/config/fuel-providers.php +53 -0
  33. package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
  34. package/server/resources/views/mail/driver-credentials.blade.php +21 -0
  35. package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
  36. package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
  37. package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
  38. package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
  39. package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
  40. package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
  41. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
  42. package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
  43. package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
  44. package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
  45. package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
  46. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
  47. package/server/src/Http/Resources/v1/Contact.php +3 -0
  48. package/server/src/Http/Resources/v1/Driver.php +3 -0
  49. package/server/src/Jobs/PollTelematicTelemetry.php +36 -0
  50. package/server/src/Jobs/SyncTelematicDevicesJob.php +2 -2
  51. package/server/src/Mail/DriverCredentialsMail.php +55 -0
  52. package/server/src/Models/Contact.php +55 -90
  53. package/server/src/Models/Sensor.php +3 -5
  54. package/server/src/Observers/ContactObserver.php +2 -22
  55. package/server/src/Observers/DriverObserver.php +5 -6
  56. package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
  57. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
  58. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
  59. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
  60. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
  61. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
  62. package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
  63. package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
  64. package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
  65. package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
  66. package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
  67. package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
  68. package/server/src/Support/ProfileAccountManager.php +405 -0
  69. package/server/src/Support/Telematics/TelematicService.php +6 -4
  70. package/server/src/Support/Telematics/Telemetry/Inbox.php +27 -9
  71. package/server/src/routes.php +5 -0
  72. package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
  73. package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
  74. package/server/tests/ApiDriverControllerContractsTest.php +102 -20
  75. package/server/tests/CommandContractsTest.php +4 -4
  76. package/server/tests/ControllerHelperContractsTest.php +3 -11
  77. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
  78. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
  79. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
  80. package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
  81. package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
  82. package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
  83. package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
  84. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
  85. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
  86. package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
  87. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
  88. package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
  89. package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
  90. package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
  91. package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +88 -4
  92. package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
  93. package/server/tests/ModelAccessorContractsTest.php +82 -20
  94. package/server/tests/ObserverContractsTest.php +27 -2
  95. package/server/tests/RequestContractsTest.php +19 -11
  96. package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
  97. package/server/tests/TelematicControllerContractsTest.php +22 -4
  98. package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
  99. package/server/tests/Unit/Models/ContactTest.php +48 -5
  100. package/server/tests/Unit/Models/SensorTest.php +2 -2
  101. package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
  102. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
  103. package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
  104. package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
  105. package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
  106. package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
  107. package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
  108. package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
  109. package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
  110. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
  111. package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
  112. package/translations/en-us.yaml +37 -2
  113. package/translations/uk-ua.yaml +3475 -0
  114. package/addon/components/modals/reset-customer-credentials.hbs +0 -18
  115. package/addon/components/modals/reset-customer-credentials.js +0 -55
@@ -25,12 +25,13 @@ use Illuminate\Support\MessageBag;
25
25
  use Illuminate\Support\Str;
26
26
 
27
27
  /**
28
- * Covers the internal DriverController createRecord unique-conflict branch
29
- * against SQLite with a failing validator: adopting an existing
30
- * organization member by creating a driver profile, returning the existing
31
- * driver profile when one already exists, assigning non-members to the
32
- * company, and falling through to the validation error response when the
33
- * conflict is not a phone or email collision.
28
+ * Covers how the internal DriverController createRecord resolves the new
29
+ * driver's login account through ProfileAccountManager against SQLite with
30
+ * real spatie roles: creating a managed driver account without an invite,
31
+ * linking a staff member of the organization without touching their role,
32
+ * linking a managed driver account of another organization, rejecting an
33
+ * email/phone held by another organization's staff, another profile type or
34
+ * an existing driver of the organization, and returning validation failures.
34
35
  */
35
36
  if (!function_exists('Fleetbase\Observers\event')) {
36
37
  eval('namespace Fleetbase\Observers; function event($event = null, $payload = []) { return []; }');
@@ -212,6 +213,28 @@ function fleetopsDriverAdoptionBoot(array $validatorErrors): SQLiteConnection
212
213
  }
213
214
  });
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');
215
238
 
216
239
  $schema = $connection->getSchemaBuilder();
217
240
  $tables = [
@@ -327,66 +350,121 @@ function fleetopsDriverAdoptionRequest(array $driver): Request
327
350
  return Request::create('/int/v1/drivers', 'POST', ['driver' => $driver]);
328
351
  }
329
352
 
330
- test('phone conflicts adopt existing organization members as drivers', function () {
331
- $connection = fleetopsDriverAdoptionBoot(['phone' => ['The phone has already been taken.']]);
332
- $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'name' => 'Member', 'phone' => '+6591234567', 'slug' => 'member', 'type' => 'driver']);
333
- $connection->table('company_users')->insert(['uuid' => 'cu-1', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111']);
353
+ function fleetopsDriverAdoptionRoles(SQLiteConnection $connection, string $userUuid): array
354
+ {
355
+ return $connection->table('model_has_roles')
356
+ ->join('roles', 'roles.id', '=', 'model_has_roles.role_id')
357
+ ->join('company_users', 'company_users.uuid', '=', 'model_has_roles.model_uuid')
358
+ ->where('company_users.user_uuid', $userUuid)
359
+ ->pluck('roles.name')
360
+ ->unique()
361
+ ->values()
362
+ ->all();
363
+ }
364
+
365
+ test('a new driver gets a managed driver account added to the company without an invite', function () {
366
+ $connection = fleetopsDriverAdoptionBoot([]);
334
367
 
335
368
  $result = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest([
336
- 'name' => 'Member',
337
- 'phone' => '+6591234567',
369
+ 'name' => 'New Driver',
370
+ 'email' => 'New.Driver@Example.com',
371
+ 'phone' => '+65 9123-0000',
372
+ 'password' => 'chosen-secret',
338
373
  ]));
339
374
 
375
+ $user = $connection->table('users')->first();
376
+
340
377
  expect($result)->toBeArray()
341
- ->and($result['driver']->resource->user_uuid)->toBe('11111111-1111-4111-8111-111111111111')
342
- ->and($connection->table('drivers')->count())->toBe(1)
343
- ->and($connection->table('drivers')->value('company_uuid'))->toBe('company-1');
378
+ ->and($result['driver']->resource->user_uuid)->toBe($user->uuid)
379
+ ->and($user->type)->toBe('driver')
380
+ ->and($user->status)->toBe('active')
381
+ ->and($user->email)->toBe('new.driver@example.com')
382
+ ->and($user->phone)->toBe('+6591230000')
383
+ ->and($user->password)->toBe('hashed:chosen-secret')
384
+ ->and($connection->table('company_users')->where(['company_uuid' => 'company-1', 'user_uuid' => $user->uuid])->count())->toBe(1)
385
+ ->and(fleetopsDriverAdoptionRoles($connection, $user->uuid))->toBe(['Driver'])
386
+ ->and($connection->table('invites')->count())->toBe(0);
344
387
  });
345
388
 
346
- test('phone conflicts assign non members to the session company', function () {
347
- $connection = fleetopsDriverAdoptionBoot(['phone' => ['The phone has already been taken.']]);
348
-
349
- // Same conflict as above, but with no company_users row the user is not yet
350
- // an organization member, so adoption must also attach them to the company
351
- $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111112', 'name' => 'Outsider', 'phone' => '+6591234568', 'slug' => 'outsider', 'type' => 'driver']);
352
-
353
- expect($connection->table('company_users')->count())->toBe(0);
389
+ test('a staff member of the organization is linked without changing their account or role', function () {
390
+ $connection = fleetopsDriverAdoptionBoot([]);
391
+ $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'name' => 'Staff Member', 'email' => 'staff@example.com', 'phone' => '+6590001111', 'slug' => 'staff', 'type' => 'user']);
354
392
 
355
393
  $result = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest([
356
- 'name' => 'Outsider',
357
- 'phone' => '+6591234568',
394
+ 'name' => 'Different Name',
395
+ 'email' => 'staff@example.com',
396
+ 'phone' => '+6599998888',
358
397
  ]));
359
398
 
399
+ $staff = $connection->table('users')->where('uuid', '11111111-1111-4111-8111-111111111111')->first();
400
+
360
401
  expect($result)->toBeArray()
361
- ->and($result['driver']->resource->user_uuid)->toBe('11111111-1111-4111-8111-111111111112')
362
- ->and($connection->table('drivers')->count())->toBe(1)
363
- // the assignCompany branch ran: the user is now a member of company-1
364
- ->and($connection->table('company_users')->where([
365
- 'company_uuid' => 'company-1',
366
- 'user_uuid' => '11111111-1111-4111-8111-111111111112',
367
- ])->count())->toBe(1);
402
+ ->and($result['driver']->resource->user_uuid)->toBe('11111111-1111-4111-8111-111111111111')
403
+ ->and($connection->table('users')->count())->toBe(1)
404
+ ->and($staff->type)->toBe('user')
405
+ ->and($staff->phone)->toBe('+6590001111')
406
+ ->and($staff->name)->toBe('Staff Member')
407
+ ->and($connection->table('model_has_roles')->count())->toBe(0)
408
+ ->and($connection->table('drivers')->value('company_uuid'))->toBe('company-1');
368
409
  });
369
410
 
370
- test('email conflicts return the existing driver profile when present', function () {
371
- $connection = fleetopsDriverAdoptionBoot(['email' => ['The email has already been taken.']]);
372
- $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'name' => 'Member', 'email' => 'member@example.com', 'type' => 'driver']);
373
- $connection->table('drivers')->insert(['uuid' => 'driver-1', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111']);
411
+ test('a managed driver account of another organization is linked and joins the company', function () {
412
+ $connection = fleetopsDriverAdoptionBoot([]);
413
+ $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111112', 'company_uuid' => 'company-2', 'name' => 'Shared Driver', 'phone' => '+6591234568', 'slug' => 'shared', 'type' => 'driver']);
414
+ $connection->table('drivers')->insert(['uuid' => 'driver-elsewhere', 'company_uuid' => 'company-2', 'user_uuid' => '11111111-1111-4111-8111-111111111112']);
374
415
 
375
416
  $result = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest([
376
- 'name' => 'Member',
377
- 'email' => 'member@example.com',
417
+ 'name' => 'Shared Driver',
418
+ 'phone' => '+6591234568',
378
419
  ]));
379
420
 
380
421
  expect($result)->toBeArray()
381
- ->and($result['driver']->resource->uuid)->toBe('driver-1')
382
- ->and($connection->table('drivers')->count())->toBe(1);
422
+ ->and($result['driver']->resource->user_uuid)->toBe('11111111-1111-4111-8111-111111111112')
423
+ ->and($connection->table('drivers')->where('company_uuid', 'company-1')->count())->toBe(1)
424
+ ->and($connection->table('company_users')->where(['company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111112'])->count())->toBe(1)
425
+ ->and(fleetopsDriverAdoptionRoles($connection, '11111111-1111-4111-8111-111111111112'))->toBe(['Driver'])
426
+ ->and($connection->table('invites')->count())->toBe(0);
383
427
  });
384
428
 
385
- test('non phone or email conflicts fall through to the error response', function () {
429
+ test('an email or phone that cannot be linked is rejected with a 422', function (array $user, ?array $driver, array $input, string $message) {
430
+ $connection = fleetopsDriverAdoptionBoot([]);
431
+ $connection->table('users')->insert(array_merge(['uuid' => '11111111-1111-4111-8111-111111111111', 'name' => 'Existing'], $user));
432
+ if ($driver) {
433
+ $connection->table('drivers')->insert($driver);
434
+ }
435
+
436
+ $result = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest(array_merge(['name' => 'New Driver'], $input)));
437
+
438
+ expect($result)->toBeInstanceOf(Illuminate\Http\JsonResponse::class)
439
+ ->and($result->getStatusCode())->toBe(422)
440
+ ->and($result->getData(true)['error'])->toBe($message)
441
+ ->and($connection->table('drivers')->where('company_uuid', 'company-1')->where('user_uuid', '!=', '11111111-1111-4111-8111-111111111111')->count())->toBe(0)
442
+ ->and($connection->table('users')->count())->toBe(1);
443
+ })->with([
444
+ 'staff of another organization' => [
445
+ ['company_uuid' => 'company-2', 'email' => 'outsider@example.com', 'type' => 'admin'],
446
+ null,
447
+ ['email' => 'outsider@example.com'],
448
+ 'This email is already in use by another account.',
449
+ ],
450
+ 'a customer account' => [
451
+ ['company_uuid' => 'company-1', 'phone' => '+6591112222', 'type' => 'customer'],
452
+ null,
453
+ ['phone' => '+6591112222'],
454
+ 'This phone number is already used by a customer.',
455
+ ],
456
+ 'an existing driver of the organization' => [
457
+ ['company_uuid' => 'company-1', 'email' => 'member@example.com', 'type' => 'driver'],
458
+ ['uuid' => 'driver-1', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111'],
459
+ ['email' => 'member@example.com'],
460
+ 'A driver with this email already exists.',
461
+ ],
462
+ ]);
463
+
464
+ test('validation failures return the error response', function () {
386
465
  fleetopsDriverAdoptionBoot(['name' => ['The name field is required.']]);
387
466
 
388
- // The error response seam raises the validation failure once the
389
- // fall-through branch executes
467
+ // The error response seam raises the validation failure
390
468
  $failure = null;
391
469
 
392
470
  try {
@@ -398,104 +476,67 @@ test('non phone or email conflicts fall through to the error response', function
398
476
  expect($failure?->errors())->toBe(['name' => ['The name field is required.']]);
399
477
  });
400
478
 
401
- test('valid create requests build the driver user and profile', function () {
402
- $connection = fleetopsDriverAdoptionBoot([]);
403
-
404
- putenv('DEBUG=1');
405
- $_ENV['DEBUG'] = $_SERVER['DEBUG'] = '1';
406
- $result = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest([
407
- 'name' => 'Fresh Driver',
408
- 'email' => 'fresh@example.com',
409
- 'phone' => '+6590001111',
410
- ]));
479
+ function fleetopsDriverAdoptionThrowingHasher(Throwable $exception): void
480
+ {
481
+ $GLOBALS['fleetopsDriverAdoptionHashException'] = $exception;
482
+ app()->instance('hash', new class implements Illuminate\Contracts\Hashing\Hasher {
483
+ public function info($hashedValue): array
484
+ {
485
+ return [];
486
+ }
411
487
 
412
- expect($result)->toBeArray()->toHaveKey('driver')
413
- ->and($connection->table('users')->where('email', 'fresh@example.com')->value('type'))->toBe('driver')
414
- ->and($connection->table('drivers')->whereNotNull('user_uuid')->count())->toBe(1)
415
- ->and($connection->table('model_has_roles')->count())->toBeGreaterThanOrEqual(1);
416
- });
488
+ public function make($value, array $options = []): string
489
+ {
490
+ throw $GLOBALS['fleetopsDriverAdoptionHashException'];
491
+ }
417
492
 
418
- test('valid update requests refresh driver and user details', function () {
419
- $connection = fleetopsDriverAdoptionBoot([]);
420
- $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'name' => 'Old Name', 'email' => 'old@example.com', 'type' => 'driver']);
421
- $connection->table('drivers')->insert(['uuid' => '22222222-2222-4222-8222-222222222222', 'public_id' => 'driver_updone1', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111']);
422
-
423
- $request = Request::create('/int/v1/drivers/driver_updone1', 'PUT', ['driver' => [
424
- 'name' => 'New Name',
425
- 'status' => 'active',
426
- ]]);
427
- $request->setRouteResolver(function () {
428
- return new class {
429
- public function getAction($key = null)
430
- {
431
- return $key === null ? ['controller' => DriverController::class . '@updateRecord'] : DriverController::class . '@updateRecord';
432
- }
493
+ public function check($value, $hashedValue, array $options = []): bool
494
+ {
495
+ return false;
496
+ }
433
497
 
434
- public function getActionMethod()
435
- {
436
- return 'updateRecord';
437
- }
498
+ public function needsRehash($hashedValue, array $options = []): bool
499
+ {
500
+ return false;
501
+ }
502
+ });
503
+ Illuminate\Support\Facades\Hash::clearResolvedInstance('hash');
504
+ }
438
505
 
439
- public function getName()
440
- {
441
- return 'internal.drivers.update';
442
- }
506
+ test('create record reports a missing organization', function () {
507
+ fleetopsDriverAdoptionBoot([]);
508
+ session(['company' => 'company-missing']);
443
509
 
444
- public function uri()
445
- {
446
- return 'int/v1/drivers/{id}';
447
- }
510
+ $result = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest(['name' => 'Nowhere Driver']));
448
511
 
449
- public function parameters()
450
- {
451
- return ['id' => 'driver_updone1'];
452
- }
512
+ expect($result->getData(true))->toBe(['error' => 'Unable to create driver.']);
513
+ });
453
514
 
454
- public function parameter($name = null, $default = null)
455
- {
456
- return $name === 'id' ? 'driver_updone1' : $default;
457
- }
458
- };
459
- });
515
+ test('create record syncs custom field values onto the new driver', function () {
516
+ $connection = fleetopsDriverAdoptionBoot([]);
517
+ $connection->table('custom_fields')->insert(['uuid' => 'dddddddd-dddd-4ddd-8ddd-dddddddddddd', 'company_uuid' => 'company-1', 'name' => 'badge', 'label' => 'Badge']);
460
518
 
461
- $result = (new DriverController())->updateRecord($request, 'driver_updone1');
519
+ $result = (new DriverController())->createRecord(Request::create('/int/v1/drivers', 'POST', ['driver' => [
520
+ 'name' => 'Badged Driver',
521
+ 'custom_field_values' => [['custom_field_uuid' => 'dddddddd-dddd-4ddd-8ddd-dddddddddddd', 'value' => 'B-12']],
522
+ ]]));
462
523
 
463
- expect($result)->toBeArray()->toHaveKey('driver')
464
- ->and($connection->table('users')->value('name'))->toBe('New Name')
465
- ->and($connection->table('drivers')->value('status'))->toBe('available');
524
+ expect($result)->toBeArray()
525
+ ->and($connection->table('custom_field_values')->where('subject_uuid', $result['driver']->resource->uuid)->value('value'))->toBe('B-12');
466
526
  });
467
527
 
468
- test('create with user uuids provisions users guards conflicts and companies', function () {
528
+ test('create record reports database and request validation failures', function () {
469
529
  $connection = fleetopsDriverAdoptionBoot([]);
470
- Illuminate\Support\Facades\Cache::clearResolvedInstance('cache');
530
+ $connection->getSchemaBuilder()->drop('drivers');
471
531
 
472
- // Unknown user uuids provision a fresh user with generated credentials
473
- $created = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest([
474
- 'user_uuid' => '99999999-9999-4999-8999-999999999901',
475
- 'name' => 'Provisioned Driver',
476
- 'email' => 'provisioned@example.com',
477
- 'phone' => '+6591112222',
478
- 'photo_uuid' => '99999999-9999-4999-8999-999999999902',
479
- ]));
480
- expect($created)->toBeArray()->toHaveKey('driver')
481
- ->and($connection->table('users')->where('email', 'provisioned@example.com')->count())->toBe(1)
482
- ->and($connection->table('users')->where('email', 'provisioned@example.com')->value('avatar_uuid'))->toBe('99999999-9999-4999-8999-999999999902');
483
-
484
- // Users already holding a driver profile in the company are rejected
485
- $existingUserUuid = $connection->table('users')->where('email', 'provisioned@example.com')->value('uuid');
486
- $conflict = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest([
487
- 'user_uuid' => $existingUserUuid,
488
- 'name' => 'Duplicate Driver',
489
- ]));
490
- expect($conflict->getData(true)['error'] ?? '')->toContain('already belongs');
532
+ // The account is created before the driver row fails to insert
533
+ $queryFailure = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest(['name' => 'Tableless Driver']));
534
+ expect($queryFailure->getData(true)['error'])->toContain('drivers');
491
535
 
492
- // Without a session company driver creation fails outright
493
- session(['company' => null]);
494
- $noCompany = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest([
495
- 'name' => 'Companyless Driver',
496
- ]));
497
- expect($noCompany->getData(true)['error'] ?? '')->toContain('Unable to create driver');
498
- session(['company' => 'company-1']);
536
+ fleetopsDriverAdoptionThrowingHasher(new Fleetbase\Exceptions\FleetbaseRequestValidationException(['password' => ['The password is too weak.']]));
537
+ $validationFailure = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest(['name' => 'Weak Password', 'password' => 'weak-password']));
538
+
539
+ expect($validationFailure->getData(true))->toBe(['error' => ['password' => ['The password is too weak.']]]);
499
540
  });
500
541
 
501
542
  test('auth can resolves real permissions for requests and ping guards', function () {
@@ -558,153 +599,3 @@ test('auth can resolves real permissions for requests and ping guards', function
558
599
 
559
600
  session(['user' => null]);
560
601
  });
561
-
562
- function fleetopsDriverAdoptionUpdateRequest(string $publicId, array $driver): Request
563
- {
564
- $request = Request::create('/int/v1/drivers/' . $publicId, 'PUT', ['driver' => $driver]);
565
- $request->setRouteResolver(function () use ($publicId) {
566
- return new class($publicId) {
567
- public function __construct(private string $publicId)
568
- {
569
- }
570
-
571
- public function getAction($key = null)
572
- {
573
- return $key === null ? ['controller' => DriverController::class . '@updateRecord'] : DriverController::class . '@updateRecord';
574
- }
575
-
576
- public function getActionMethod()
577
- {
578
- return 'updateRecord';
579
- }
580
-
581
- public function getName()
582
- {
583
- return 'internal.drivers.update';
584
- }
585
-
586
- public function uri()
587
- {
588
- return 'int/v1/drivers/{id}';
589
- }
590
-
591
- public function parameters()
592
- {
593
- return ['id' => $this->publicId];
594
- }
595
-
596
- public function parameter($name = null, $default = null)
597
- {
598
- return $name === 'id' ? $this->publicId : $default;
599
- }
600
- };
601
- });
602
-
603
- return $request;
604
- }
605
-
606
- test('creation applies photo avatars and syncs custom field values', function () {
607
- $connection = fleetopsDriverAdoptionBoot([]);
608
- $connection->table('custom_fields')->insert(['uuid' => '33333333-3333-4333-8333-333333333301', 'company_uuid' => 'company-1', 'name' => 'badge_number', 'label' => 'Badge Number']);
609
-
610
- $result = (new DriverController())->createRecord(fleetopsDriverAdoptionRequest([
611
- 'name' => 'Photographed Driver',
612
- 'email' => 'photographed@example.com',
613
- 'phone' => '+6590002222',
614
- 'photo_uuid' => '44444444-4444-4444-8444-444444444401',
615
- 'custom_field_values' => [
616
- ['custom_field_uuid' => '33333333-3333-4333-8333-333333333301', 'value' => 'BADGE-7', 'value_type' => 'text'],
617
- ],
618
- ]));
619
-
620
- expect($result)->toBeArray()->toHaveKey('driver')
621
- ->and($connection->table('users')->where('email', 'photographed@example.com')->value('avatar_uuid'))->toBe('44444444-4444-4444-8444-444444444401')
622
- ->and($connection->table('custom_field_values')->where('value', 'BADGE-7')->count())->toBe(1);
623
- });
624
-
625
- test('creation maps query and validation failures onto error responses', function () {
626
- fleetopsDriverAdoptionBoot([]);
627
-
628
- // Query failures from the persistence pipeline report a generic message
629
- $queryFailure = new DriverController();
630
- $queryFailure->model = new class extends Fleetbase\FleetOps\Models\Driver {
631
- public function createRecordFromRequest($request, ?callable $onBefore = null, ?callable $onAfter = null, array $options = [])
632
- {
633
- throw new Illuminate\Database\QueryException('mysql', 'insert into drivers', [], new RuntimeException('constraint violation'));
634
- }
635
- };
636
- $queryResponse = $queryFailure->createRecord(fleetopsDriverAdoptionRequest(['name' => 'Query Failure']));
637
- expect($queryResponse->getData(true)['error'] ?? '')->not->toBeEmpty();
638
-
639
- // Validation exceptions surface their individual error messages
640
- $validationFailure = new DriverController();
641
- $validationFailure->model = new class extends Fleetbase\FleetOps\Models\Driver {
642
- public function createRecordFromRequest($request, ?callable $onBefore = null, ?callable $onAfter = null, array $options = [])
643
- {
644
- throw new Fleetbase\Exceptions\FleetbaseRequestValidationException(['The name field is required.']);
645
- }
646
- };
647
- $validationResponse = $validationFailure->createRecord(fleetopsDriverAdoptionRequest(['name' => 'Validation Failure']));
648
- expect($validationResponse->getData(true)['error'])->toBe(['The name field is required.']);
649
- });
650
-
651
- test('updates apply photo avatars and sync custom field values', function () {
652
- $connection = fleetopsDriverAdoptionBoot([]);
653
- $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111121', 'company_uuid' => 'company-1', 'name' => 'Photo Driver', 'email' => 'photodriver@example.com', 'type' => 'driver']);
654
- $connection->table('drivers')->insert(['uuid' => '22222222-2222-4222-8222-222222222221', 'public_id' => 'driver_photoone', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111121']);
655
- $connection->table('custom_fields')->insert(['uuid' => '33333333-3333-4333-8333-333333333302', 'company_uuid' => 'company-1', 'name' => 'route_code', 'label' => 'Route Code']);
656
-
657
- $result = (new DriverController())->updateRecord(fleetopsDriverAdoptionUpdateRequest('driver_photoone', [
658
- 'name' => 'Photo Driver Updated',
659
- 'photo_uuid' => '44444444-4444-4444-8444-444444444402',
660
- 'custom_field_values' => [
661
- ['custom_field_uuid' => '33333333-3333-4333-8333-333333333302', 'value' => 'ROUTE-9', 'value_type' => 'text'],
662
- ],
663
- ]), 'driver_photoone');
664
-
665
- expect($result)->toBeArray()->toHaveKey('driver')
666
- ->and($connection->table('users')->where('uuid', '11111111-1111-4111-8111-111111111121')->value('avatar_uuid'))->toBe('44444444-4444-4444-8444-444444444402')
667
- ->and($connection->table('custom_field_values')->where('value', 'ROUTE-9')->count())->toBe(1);
668
- });
669
-
670
- test('updates map validation and generic failures onto error responses', function () {
671
- fleetopsDriverAdoptionBoot([]);
672
-
673
- // Validation exceptions from the pipeline surface their error list
674
- $validationFailure = new DriverController();
675
- $validationFailure->model = new class extends Fleetbase\FleetOps\Models\Driver {
676
- public function updateRecordFromRequest(Request $request, $id, ?callable $onBefore = null, ?callable $onAfter = null, array $options = [])
677
- {
678
- throw new Fleetbase\Exceptions\FleetbaseRequestValidationException(['The status field is invalid.']);
679
- }
680
- };
681
- $validationResponse = $validationFailure->updateRecord(fleetopsDriverAdoptionUpdateRequest('driver_missing1', ['name' => 'Validation Failure']), 'driver_missing1');
682
- expect($validationResponse->getData(true)['error'])->toBe(['The status field is invalid.']);
683
-
684
- // Any other failure reports its own message
685
- $genericFailure = new DriverController();
686
- $genericFailure->model = new class extends Fleetbase\FleetOps\Models\Driver {
687
- public function updateRecordFromRequest(Request $request, $id, ?callable $onBefore = null, ?callable $onAfter = null, array $options = [])
688
- {
689
- throw new Exception('Driver profile is locked.');
690
- }
691
- };
692
- $genericResponse = $genericFailure->updateRecord(fleetopsDriverAdoptionUpdateRequest('driver_missing1', ['name' => 'Generic Failure']), 'driver_missing1');
693
- expect($genericResponse->getData(true)['error'] ?? '')->toBe('Driver profile is locked.');
694
- });
695
-
696
- test('update rejects requests that fail validation before persistence', function () {
697
- fleetopsDriverAdoptionBoot(['status' => ['The selected status is invalid.']]);
698
-
699
- // The error-response seam raises the validation failure once the rejection
700
- // branch executes, so the request never reaches persistence
701
- $failure = null;
702
-
703
- try {
704
- (new DriverController())->updateRecord(fleetopsDriverAdoptionUpdateRequest('driver_missing1', ['status' => 'bogus']), 'driver_missing1');
705
- } catch (Illuminate\Validation\ValidationException $exception) {
706
- $failure = $exception;
707
- }
708
-
709
- expect($failure?->errors())->toBe(['status' => ['The selected status is invalid.']]);
710
- });