@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.
- package/RELEASE.md +11 -15
- package/addon/components/contact/form.hbs +13 -2
- package/addon/components/customer/details.hbs +17 -17
- package/addon/components/customer/form.hbs +13 -2
- package/addon/components/customer/form.js +0 -56
- package/addon/components/driver/details.hbs +13 -19
- package/addon/components/driver/form.hbs +20 -35
- package/addon/components/driver/form.js +0 -58
- package/addon/components/fuel-integration/form.hbs +2 -2
- package/addon/components/fuel-integration/form.js +8 -0
- package/addon/components/fuel-integration/hub.js +1 -1
- package/addon/components/modals/driver-form.hbs +2 -2
- package/addon/components/modals/reset-profile-credentials.hbs +23 -0
- package/addon/components/modals/reset-profile-credentials.js +70 -0
- package/addon/components/profile-identity-hint.hbs +13 -0
- package/addon/components/profile-identity-hint.js +49 -0
- package/addon/controllers/management/drivers/index.js +4 -0
- package/addon/extension.js +4 -2
- package/addon/services/contact-actions.js +50 -89
- package/addon/services/driver-actions.js +98 -0
- package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
- package/addon/utils/fuel-transaction.js +2 -0
- package/addon/utils/profile-login-actions.js +172 -0
- package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
- package/app/components/profile-identity-hint.js +1 -0
- package/composer.json +1 -1
- package/docs/SAFEE.md +5 -0
- package/docs/TELEMATICS_QUEUES.md +59 -8
- package/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -0
- package/server/config/fuel-providers.php +53 -0
- package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
- package/server/resources/views/mail/driver-credentials.blade.php +21 -0
- package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
- package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
- package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
- package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
- package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
- package/server/src/Http/Resources/v1/Contact.php +3 -0
- package/server/src/Http/Resources/v1/Driver.php +3 -0
- package/server/src/Jobs/PollTelematicTelemetry.php +36 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +2 -2
- package/server/src/Mail/DriverCredentialsMail.php +55 -0
- package/server/src/Models/Contact.php +55 -90
- package/server/src/Models/Sensor.php +3 -5
- package/server/src/Observers/ContactObserver.php +2 -22
- package/server/src/Observers/DriverObserver.php +5 -6
- package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
- package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
- package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
- package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
- package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
- package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
- package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
- package/server/src/Support/ProfileAccountManager.php +405 -0
- package/server/src/Support/Telematics/TelematicService.php +6 -4
- package/server/src/Support/Telematics/Telemetry/Inbox.php +27 -9
- package/server/src/routes.php +5 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
- package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
- package/server/tests/ApiDriverControllerContractsTest.php +102 -20
- package/server/tests/CommandContractsTest.php +4 -4
- package/server/tests/ControllerHelperContractsTest.php +3 -11
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
- package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
- package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
- package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
- package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
- package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
- package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
- package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
- package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
- package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
- package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +88 -4
- package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
- package/server/tests/ModelAccessorContractsTest.php +82 -20
- package/server/tests/ObserverContractsTest.php +27 -2
- package/server/tests/RequestContractsTest.php +19 -11
- package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
- package/server/tests/TelematicControllerContractsTest.php +22 -4
- package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
- package/server/tests/Unit/Models/ContactTest.php +48 -5
- package/server/tests/Unit/Models/SensorTest.php +2 -2
- package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
- package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
- package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
- package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
- package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
- package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
- package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
- package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
- package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
- package/translations/en-us.yaml +37 -2
- package/translations/uk-ua.yaml +3475 -0
- package/addon/components/modals/reset-customer-credentials.hbs +0 -18
- package/addon/components/modals/reset-customer-credentials.js +0 -55
|
@@ -122,11 +122,12 @@ function fleetopsInternalCustomer(): FleetOpsInternalCustomerContactFake
|
|
|
122
122
|
return $customer;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function fleetopsInternalCustomerUser(string $status = 'active'): FleetOpsInternalCustomerUserFake
|
|
125
|
+
function fleetopsInternalCustomerUser(string $status = 'active', string $type = 'customer'): FleetOpsInternalCustomerUserFake
|
|
126
126
|
{
|
|
127
127
|
$user = new FleetOpsInternalCustomerUserFake();
|
|
128
128
|
$user->setRawAttributes([
|
|
129
129
|
'uuid' => 'user-uuid',
|
|
130
|
+
'type' => $type,
|
|
130
131
|
'public_id' => 'user_public',
|
|
131
132
|
'name' => 'Ada Customer',
|
|
132
133
|
'email' => 'ada@example.test',
|
|
@@ -236,3 +237,33 @@ test('internal customer controller validates reset credential error branches', f
|
|
|
236
237
|
'password_confirmation' => 'one',
|
|
237
238
|
])))['error'])->toBe('Unable to reset customer credentials');
|
|
238
239
|
});
|
|
240
|
+
|
|
241
|
+
test('internal customer controller refuses login management for staff-linked customers', function () {
|
|
242
|
+
$staff = fleetopsInternalCustomerUser('active', 'user');
|
|
243
|
+
$controller = fleetopsInternalCustomerController($staff);
|
|
244
|
+
$message = 'This customer signs in with a team member account. Manage this login in IAM.';
|
|
245
|
+
$request = new Request(['customer' => 'customer_public']);
|
|
246
|
+
|
|
247
|
+
$responses = [
|
|
248
|
+
$controller->createPortalLogin($request),
|
|
249
|
+
$controller->sendCredentials($request),
|
|
250
|
+
$controller->deactivatePortalLogin($request),
|
|
251
|
+
$controller->reactivatePortalLogin($request),
|
|
252
|
+
$controller->resetCredentials(new Request([
|
|
253
|
+
'customer' => 'customer_public',
|
|
254
|
+
'password' => 'chosen-secret',
|
|
255
|
+
'password_confirmation' => 'chosen-secret',
|
|
256
|
+
])),
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
foreach ($responses as $response) {
|
|
260
|
+
expect($response->getStatusCode())->toBe(422)
|
|
261
|
+
->and(fleetopsInternalCustomerJson($response)['error'])->toBe($message);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The team member's account is never touched
|
|
265
|
+
expect($staff->passwords)->toBe([])
|
|
266
|
+
->and($staff->activations)->toBe(0)
|
|
267
|
+
->and($staff->deactivations)->toBe(0)
|
|
268
|
+
->and($controller->sentCredentials)->toBe([]);
|
|
269
|
+
});
|
|
@@ -130,14 +130,51 @@ class FleetOpsContactSyncUserFake extends Fleetbase\Models\User
|
|
|
130
130
|
return true;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
public function save(array $options = []): bool
|
|
134
|
+
{
|
|
135
|
+
$this->updates[] = $this->getDirty();
|
|
136
|
+
$this->syncOriginal();
|
|
137
|
+
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
public function delete()
|
|
134
142
|
{
|
|
135
143
|
$this->deleted = true;
|
|
144
|
+
$this->setAttribute('deleted_at', '2026-01-01 00:00:00');
|
|
136
145
|
|
|
137
146
|
return true;
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
|
|
150
|
+
function fleetopsModelAccessorsUseProfileAccountTables(): void
|
|
151
|
+
{
|
|
152
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
153
|
+
$schema = $connection->getSchemaBuilder();
|
|
154
|
+
$schema->create('users', function ($table) {
|
|
155
|
+
$table->string('uuid')->nullable();
|
|
156
|
+
$table->string('type')->nullable();
|
|
157
|
+
$table->string('email')->nullable();
|
|
158
|
+
$table->string('phone')->nullable();
|
|
159
|
+
$table->timestamp('deleted_at')->nullable();
|
|
160
|
+
});
|
|
161
|
+
foreach (['drivers', 'contacts'] as $profileTable) {
|
|
162
|
+
$schema->create($profileTable, function ($table) {
|
|
163
|
+
$table->string('uuid')->nullable();
|
|
164
|
+
$table->string('user_uuid')->nullable();
|
|
165
|
+
$table->string('company_uuid')->nullable();
|
|
166
|
+
$table->timestamp('deleted_at')->nullable();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
$resolver = new ConnectionResolver([
|
|
171
|
+
'default' => $connection,
|
|
172
|
+
'mysql' => $connection,
|
|
173
|
+
]);
|
|
174
|
+
$resolver->setDefaultConnection('mysql');
|
|
175
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
176
|
+
}
|
|
177
|
+
|
|
141
178
|
class FleetOpsContactAccessorFake extends Contact
|
|
142
179
|
{
|
|
143
180
|
public array $loadedMissing = [];
|
|
@@ -858,6 +895,8 @@ test('contact accessors imports notifications and customer identity helpers are
|
|
|
858
895
|
});
|
|
859
896
|
|
|
860
897
|
test('contact user sync lookup and deletion guards are stable', function () {
|
|
898
|
+
fleetopsModelAccessorsUseProfileAccountTables();
|
|
899
|
+
|
|
861
900
|
$user = new FleetOpsContactSyncUserFake();
|
|
862
901
|
$user->setRawAttributes([
|
|
863
902
|
'uuid' => 'user-uuid',
|
|
@@ -868,22 +907,25 @@ test('contact user sync lookup and deletion guards are stable', function () {
|
|
|
868
907
|
], true);
|
|
869
908
|
|
|
870
909
|
$contact = new FleetOpsContactAccessorFake();
|
|
910
|
+
$contact->exists = true;
|
|
871
911
|
$contact->fakeUser = $user;
|
|
872
912
|
$contact->setRawAttributes([
|
|
873
|
-
'uuid'
|
|
874
|
-
'
|
|
875
|
-
'
|
|
876
|
-
'
|
|
877
|
-
'
|
|
878
|
-
'
|
|
879
|
-
'
|
|
913
|
+
'uuid' => 'contact-uuid',
|
|
914
|
+
'company_uuid' => 'company-uuid',
|
|
915
|
+
'user_uuid' => 'not-a-uuid',
|
|
916
|
+
'type' => 'customer',
|
|
917
|
+
'name' => 'Old Name',
|
|
918
|
+
'email' => 'old@example.com',
|
|
919
|
+
'phone' => '+15550000000',
|
|
920
|
+
'timezone' => 'UTC',
|
|
880
921
|
], true);
|
|
881
922
|
|
|
882
923
|
$contact->name = 'New Name';
|
|
883
|
-
$contact->email = '
|
|
884
|
-
$contact->phone = '+
|
|
924
|
+
$contact->email = 'New@Example.com';
|
|
925
|
+
$contact->phone = '+1 555-111-2222';
|
|
885
926
|
$contact->timezone = 'Asia/Singapore';
|
|
886
927
|
|
|
928
|
+
// Proxy fields are normalized and pushed to the managed account
|
|
887
929
|
expect($contact->syncWithUser())->toBeTrue()
|
|
888
930
|
->and($user->updates[0])->toBe([
|
|
889
931
|
'name' => 'New Name',
|
|
@@ -895,26 +937,46 @@ test('contact user sync lookup and deletion guards are stable', function () {
|
|
|
895
937
|
->and($contact->hasUser())->toBeTrue()
|
|
896
938
|
->and($contact->doesntHaveUser())->toBeFalse();
|
|
897
939
|
|
|
898
|
-
|
|
899
|
-
$
|
|
900
|
-
$
|
|
940
|
+
// An unsaved contact's account was just resolved from these values
|
|
941
|
+
$newContact = new FleetOpsContactAccessorFake();
|
|
942
|
+
$newContact->fakeUser = $user;
|
|
943
|
+
$newContact->name = 'Brand New';
|
|
944
|
+
|
|
945
|
+
// A saved contact with no proxy changes has nothing to sync
|
|
946
|
+
$cleanContact = new FleetOpsContactAccessorFake();
|
|
947
|
+
$cleanContact->exists = true;
|
|
948
|
+
$cleanContact->fakeUser = $user;
|
|
949
|
+
$cleanContact->setRawAttributes(['name' => 'New Name', 'type' => 'customer'], true);
|
|
950
|
+
|
|
951
|
+
expect($newContact->syncWithUser())->toBeFalse()
|
|
952
|
+
->and($cleanContact->syncWithUser())->toBeFalse()
|
|
953
|
+
->and($user->updates)->toHaveCount(1);
|
|
954
|
+
|
|
955
|
+
// Deleting the only profile holding a managed account deletes the account
|
|
956
|
+
$deleteContact = new FleetOpsContactAccessorFake();
|
|
957
|
+
$deleteContact->type = 'customer';
|
|
958
|
+
$deleteContact->company_uuid = 'company-uuid';
|
|
959
|
+
$deleteContact->fakeUser = $user;
|
|
901
960
|
|
|
902
961
|
expect($deleteContact->deleteUser())->toBeTrue()
|
|
903
|
-
->and($user->deleted)->toBeTrue()
|
|
904
|
-
->and($deleteContact->loadedMissing)->toBe(['user']);
|
|
962
|
+
->and($user->deleted)->toBeTrue();
|
|
905
963
|
|
|
906
|
-
|
|
907
|
-
$
|
|
964
|
+
// A team member's account linked to the contact is never deleted
|
|
965
|
+
$staffUser = new FleetOpsContactSyncUserFake();
|
|
966
|
+
$staffUser->setRawAttributes(['uuid' => 'staff-uuid', 'type' => 'admin'], true);
|
|
908
967
|
|
|
909
|
-
$
|
|
910
|
-
$
|
|
911
|
-
$
|
|
968
|
+
$staffContact = new FleetOpsContactAccessorFake();
|
|
969
|
+
$staffContact->type = 'customer';
|
|
970
|
+
$staffContact->fakeUser = $staffUser;
|
|
912
971
|
|
|
913
972
|
$emptyContact = new FleetOpsContactAccessorFake();
|
|
973
|
+
$emptyContact->exists = true;
|
|
914
974
|
$emptyContact->fakeUser = null;
|
|
915
975
|
$emptyContact->setRawAttributes(['user_uuid' => 'not-a-uuid'], true);
|
|
976
|
+
$emptyContact->name = 'Changed';
|
|
916
977
|
|
|
917
|
-
expect($
|
|
978
|
+
expect($staffContact->deleteUser())->toBeFalse()
|
|
979
|
+
->and($staffUser->deleted)->toBeFalse()
|
|
918
980
|
->and($emptyContact->syncWithUser())->toBeFalse()
|
|
919
981
|
->and($emptyContact->getUser())->toBeNull()
|
|
920
982
|
->and($emptyContact->hasUser())->toBeFalse()
|
|
@@ -263,6 +263,7 @@ class FleetOpsDriverObserverUserFake extends User
|
|
|
263
263
|
public function delete()
|
|
264
264
|
{
|
|
265
265
|
$this->deleted = true;
|
|
266
|
+
$this->setAttribute('deleted_at', '2026-01-01 00:00:00');
|
|
266
267
|
|
|
267
268
|
return true;
|
|
268
269
|
}
|
|
@@ -1149,14 +1150,30 @@ test('contact observer prevents changing existing customer contact type', functi
|
|
|
1149
1150
|
});
|
|
1150
1151
|
|
|
1151
1152
|
test('driver observer defaults location unassigns related records and deletes driver users', function () {
|
|
1153
|
+
// releaseForProfile looks for the account's remaining driver/contact profiles
|
|
1154
|
+
$connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
|
|
1155
|
+
$resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
1156
|
+
$resolver->setDefaultConnection('mysql');
|
|
1157
|
+
Illuminate\Database\Eloquent\Model::setConnectionResolver($resolver);
|
|
1158
|
+
foreach (['drivers', 'contacts'] as $profileTable) {
|
|
1159
|
+
$connection->getSchemaBuilder()->create($profileTable, function ($table) {
|
|
1160
|
+
$table->string('uuid')->nullable();
|
|
1161
|
+
$table->string('user_uuid')->nullable();
|
|
1162
|
+
$table->string('company_uuid')->nullable();
|
|
1163
|
+
$table->timestamp('deleted_at')->nullable();
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1152
1167
|
$driver = new Driver();
|
|
1153
1168
|
$driver->setRawAttributes([
|
|
1154
|
-
'uuid'
|
|
1155
|
-
'user_uuid'
|
|
1169
|
+
'uuid' => 'driver-uuid',
|
|
1170
|
+
'user_uuid' => 'user-uuid',
|
|
1171
|
+
'company_uuid' => 'company-uuid',
|
|
1156
1172
|
], true);
|
|
1157
1173
|
|
|
1158
1174
|
$observer = new FleetOpsDriverObserverProbe();
|
|
1159
1175
|
$user = new FleetOpsDriverObserverUserFake();
|
|
1176
|
+
$user->setRawAttributes(['uuid' => 'user-uuid', 'type' => 'driver'], true);
|
|
1160
1177
|
|
|
1161
1178
|
$observer->user = $user;
|
|
1162
1179
|
$observer->creating($driver);
|
|
@@ -1175,6 +1192,14 @@ test('driver observer defaults location unassigns related records and deletes dr
|
|
|
1175
1192
|
->and($observer->unassigned)->toBe(['driver-uuid'])
|
|
1176
1193
|
->and($user->deleted)->toBeTrue();
|
|
1177
1194
|
|
|
1195
|
+
// A team member's account linked to the driver is never deleted
|
|
1196
|
+
$staff = new FleetOpsDriverObserverUserFake();
|
|
1197
|
+
$staff->setRawAttributes(['uuid' => 'staff-uuid', 'type' => 'user'], true);
|
|
1198
|
+
$observer->user = $staff;
|
|
1199
|
+
$observer->deleted($driver);
|
|
1200
|
+
|
|
1201
|
+
expect($staff->deleted)->toBeFalse();
|
|
1202
|
+
|
|
1178
1203
|
$driverWithLocation = new Driver();
|
|
1179
1204
|
$location = new Fleetbase\LaravelMysqlSpatial\Types\Point(1.3, 103.8);
|
|
1180
1205
|
$driverWithLocation->location = $location;
|
|
@@ -534,10 +534,13 @@ namespace {
|
|
|
534
534
|
$request = new InternalCreateDriverRequest();
|
|
535
535
|
$updateProbe = new FleetOpsInternalUpdateDriverRequestProbe();
|
|
536
536
|
|
|
537
|
+
// Email/phone availability is decided by ProfileAccountManager (a team
|
|
538
|
+
// member of the company is linked), so they're optional and not unique here.
|
|
537
539
|
expect(ruleStrings($createRules['name']))->toContain('required', 'nullable', 'string', 'max:255')
|
|
538
|
-
->and(ruleStrings($createRules['email']))->
|
|
539
|
-
->and(ruleStrings($createRules['phone']))->
|
|
540
|
-
->and(ruleStrings($userRules['name']))->
|
|
540
|
+
->and(ruleStrings($createRules['email']))->toBe(['nullable'])
|
|
541
|
+
->and(ruleStrings($createRules['phone']))->toBe(['nullable', 'string'])
|
|
542
|
+
->and(ruleStrings($userRules['name']))->toContain('required')
|
|
543
|
+
->and(ruleStrings(InternalCreateDriverRequest::create('/fleetops-test', 'POST', ['email' => 'driver@example.test'])->rules()['email']))->toBe(['nullable', 'email'])
|
|
541
544
|
->and(ruleStrings($patchRules['name']))->not->toContain('required')
|
|
542
545
|
->and($updateProbe->authorize())->toBeFalse();
|
|
543
546
|
|
|
@@ -556,9 +559,10 @@ namespace {
|
|
|
556
559
|
])
|
|
557
560
|
->and($request->messages())->toMatchArray([
|
|
558
561
|
'name.required' => 'Driver name is required.',
|
|
559
|
-
'email.
|
|
562
|
+
'email.email' => 'Please provide a valid email address.',
|
|
560
563
|
'password.min' => 'Password must be at least 8 characters.',
|
|
561
|
-
])
|
|
564
|
+
])
|
|
565
|
+
->and($request->messages())->not->toHaveKeys(['email.required', 'email.unique', 'phone.required', 'phone.unique']);
|
|
562
566
|
});
|
|
563
567
|
|
|
564
568
|
test('public create order request validates payload alternatives and pod methods', function () {
|
|
@@ -929,12 +933,16 @@ namespace {
|
|
|
929
933
|
expect($request->authorize())->toBeTrue()
|
|
930
934
|
->and(ruleStrings($createRules['name']))->toContain('required')
|
|
931
935
|
->and(ruleStrings($patchRules['name']))->not->toContain('required')
|
|
932
|
-
// Contact details are optional but still validated
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
->and(ruleStrings($createRules['
|
|
937
|
-
->and(ruleStrings($createRules['
|
|
936
|
+
// Contact details are optional but still validated: an operational
|
|
937
|
+
// driver record may legitimately have neither. On create, availability
|
|
938
|
+
// is decided by ProfileAccountManager (a team member is linked); on
|
|
939
|
+
// update they must stay unique among users.
|
|
940
|
+
->and(ruleStrings($createRules['email']))->toContain('nullable', 'email')
|
|
941
|
+
->and(ruleStrings($createRules['email']))->not->toContain('required', 'unique:users')
|
|
942
|
+
->and(ruleStrings($createRules['phone']))->toContain('nullable')
|
|
943
|
+
->and(ruleStrings($createRules['phone']))->not->toContain('required', 'unique:users')
|
|
944
|
+
->and(ruleStrings($patchRules['email']))->toContain('nullable', 'unique:users')
|
|
945
|
+
->and(ruleStrings($patchRules['phone']))->toContain('nullable', 'string', 'unique:users')
|
|
938
946
|
->and($createRules['password'])->toBe('nullable|string')
|
|
939
947
|
->and($createRules['country'])->toBe('nullable|size:2')
|
|
940
948
|
->and(ruleStrings($createRules['vehicle']))->toContain('nullable', 'string', 'starts_with:vehicle_', 'exists:vehicles,public_id')
|
|
@@ -889,8 +889,8 @@ test('search resources capability metadata prompts terms and denied branches are
|
|
|
889
889
|
->and($capability->shouldResolve($task))->toBeTrue()
|
|
890
890
|
->and($capability->promptMatches('tell me about sensor SENSOR-1'))->toBeTrue()
|
|
891
891
|
->and($capability->promptMatches('compose a friendly email'))->toBeFalse()
|
|
892
|
-
->and($capability->termsFor('Find order ORDER-123 for driver driver_456'))->toBe(['ORDER-123', '
|
|
893
|
-
->and($capability->termsFor('show order'))->toBe([
|
|
892
|
+
->and($capability->termsFor('Find order ORDER-123 for driver driver_456'))->toBe(['ORDER-123', 'driver_456'])
|
|
893
|
+
->and($capability->termsFor('show order'))->toBe([])
|
|
894
894
|
->and($capability->genericWhenDenied())->toBe([])
|
|
895
895
|
->and($capability->allResourceBranchesWhenDenied(['needle']))->toBe([
|
|
896
896
|
'orders' => [],
|
|
@@ -903,7 +903,7 @@ test('search resources capability metadata prompts terms and denied branches are
|
|
|
903
903
|
'telematics' => [],
|
|
904
904
|
])
|
|
905
905
|
->and($capability->resolve($task))->toBe([
|
|
906
|
-
'query_terms' => ['DRV-123', '
|
|
906
|
+
'query_terms' => ['DRV-123', 'TRUCK_9'],
|
|
907
907
|
'results' => [],
|
|
908
908
|
]);
|
|
909
909
|
});
|
|
@@ -159,6 +159,7 @@ test('internal customer controller serializes portal login payloads', function (
|
|
|
159
159
|
$customer->setRelation('user', (object) [
|
|
160
160
|
'uuid' => 'user-uuid',
|
|
161
161
|
'public_id' => 'user-public',
|
|
162
|
+
'type' => 'customer',
|
|
162
163
|
'name' => 'Ada Customer',
|
|
163
164
|
'email' => 'ada@example.test',
|
|
164
165
|
'phone' => '+15551234567',
|
|
@@ -168,10 +169,12 @@ test('internal customer controller serializes portal login payloads', function (
|
|
|
168
169
|
]);
|
|
169
170
|
|
|
170
171
|
expect($controller->callHelper('customerPayload', $customer))->toBe([
|
|
171
|
-
'id'
|
|
172
|
-
'uuid'
|
|
173
|
-
'user_uuid'
|
|
174
|
-
'
|
|
172
|
+
'id' => 'customer-public',
|
|
173
|
+
'uuid' => 'customer-uuid',
|
|
174
|
+
'user_uuid' => 'user-uuid',
|
|
175
|
+
'is_staff_linked' => false,
|
|
176
|
+
'login_status' => 'active',
|
|
177
|
+
'user' => [
|
|
175
178
|
'id' => 'user-public',
|
|
176
179
|
'uuid' => 'user-uuid',
|
|
177
180
|
'name' => 'Ada Customer',
|
|
@@ -182,4 +185,19 @@ test('internal customer controller serializes portal login payloads', function (
|
|
|
182
185
|
'avatar_url' => 'https://example.test/avatar.png',
|
|
183
186
|
],
|
|
184
187
|
]);
|
|
188
|
+
|
|
189
|
+
// A customer linked to a team member's account is reported as staff-linked
|
|
190
|
+
$customer->setRelation('user', (object) ['uuid' => 'staff-uuid', 'public_id' => 'staff-public', 'type' => 'user', 'status' => 'inactive']);
|
|
191
|
+
$staffPayload = $controller->callHelper('customerPayload', $customer);
|
|
192
|
+
|
|
193
|
+
expect($staffPayload['is_staff_linked'])->toBeTrue()
|
|
194
|
+
->and($staffPayload['login_status'])->toBe('inactive');
|
|
195
|
+
|
|
196
|
+
// A customer without a login account is neither
|
|
197
|
+
$customer->setRelation('user', null);
|
|
198
|
+
$emptyPayload = $controller->callHelper('customerPayload', $customer);
|
|
199
|
+
|
|
200
|
+
expect($emptyPayload['is_staff_linked'])->toBeFalse()
|
|
201
|
+
->and($emptyPayload['login_status'])->toBeNull()
|
|
202
|
+
->and($emptyPayload['user'])->toBeNull();
|
|
185
203
|
});
|
|
@@ -9,7 +9,7 @@ if (!Illuminate\Support\Str::hasMacro('humanize')) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
use Fleetbase\FleetOps\Exceptions\CustomerUserConflictException;
|
|
12
|
-
use Fleetbase\FleetOps\Exceptions\
|
|
12
|
+
use Fleetbase\FleetOps\Exceptions\ProfileIdentityConflictException;
|
|
13
13
|
use Fleetbase\FleetOps\Models\Contact;
|
|
14
14
|
use Fleetbase\Models\User;
|
|
15
15
|
use Illuminate\Database\ConnectionResolver;
|
|
@@ -75,9 +75,38 @@ function fleetopsContactCustomerUserBoot(): SQLiteConnection
|
|
|
75
75
|
});
|
|
76
76
|
Illuminate\Support\Facades\DB::clearResolvedInstance('db');
|
|
77
77
|
|
|
78
|
+
app()->instance('hash', new class implements Illuminate\Contracts\Hashing\Hasher {
|
|
79
|
+
public function info($hashedValue): array
|
|
80
|
+
{
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public function make($value, array $options = []): string
|
|
85
|
+
{
|
|
86
|
+
return md5((string) $value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public function check($value, $hashedValue, array $options = []): bool
|
|
90
|
+
{
|
|
91
|
+
return md5((string) $value) === $hashedValue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public function needsRehash($hashedValue, array $options = []): bool
|
|
95
|
+
{
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public function verifyConfiguration($value): bool
|
|
100
|
+
{
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
Illuminate\Support\Facades\Hash::clearResolvedInstance('hash');
|
|
105
|
+
|
|
78
106
|
$schema = $connection->getSchemaBuilder();
|
|
79
107
|
$tables = [
|
|
80
108
|
'contacts' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'name', 'email', 'phone', 'type', 'title'],
|
|
109
|
+
'drivers' => ['uuid', 'public_id', 'company_uuid', 'user_uuid'],
|
|
81
110
|
'users' => ['uuid', 'public_id', 'company_uuid', 'name', 'email', 'phone', 'username', 'password', 'timezone', 'status', 'type', 'slug', 'avatar_uuid', 'last_login'],
|
|
82
111
|
'companies' => ['uuid', 'public_id', 'name', 'timezone', 'owner_uuid'],
|
|
83
112
|
'company_users' => ['uuid', 'company_uuid', 'user_uuid', 'status'],
|
|
@@ -188,24 +217,45 @@ test('create user from contact adopts an existing matching user', function () {
|
|
|
188
217
|
->and($connection->table('users')->count())->toBe(1);
|
|
189
218
|
});
|
|
190
219
|
|
|
191
|
-
test('create user from contact rejects
|
|
220
|
+
test('create user from contact rejects an account that already holds a contact in the company', function () {
|
|
192
221
|
$connection = fleetopsContactCustomerUserBoot();
|
|
193
|
-
|
|
194
|
-
// contact type, which is null in the static whereHas context — a null-typed
|
|
195
|
-
// user keeps the linked-contact subquery matchable.
|
|
196
|
-
$connection->table('users')->insert(['uuid' => 'user-9', 'company_uuid' => 'company-1', 'email' => 'taken@example.com', 'type' => null]);
|
|
222
|
+
$connection->table('users')->insert(['uuid' => 'user-9', 'company_uuid' => 'company-1', 'email' => 'taken@example.com', 'type' => 'contact']);
|
|
197
223
|
$connection->table('contacts')->insert(['uuid' => 'contact-owner', 'company_uuid' => 'company-1', 'user_uuid' => 'user-9', 'name' => 'Owner', 'type' => 'contact']);
|
|
198
224
|
$contact = fleetopsContactCustomerUserContact(['uuid' => 'contact-2', 'email' => 'taken@example.com']);
|
|
199
225
|
|
|
200
|
-
expect(fn () => Contact::createUserFromContact($contact))
|
|
226
|
+
expect(fn () => Contact::createUserFromContact($contact))
|
|
227
|
+
->toThrow(ProfileIdentityConflictException::class, 'A contact with this email already exists.');
|
|
201
228
|
});
|
|
202
229
|
|
|
203
|
-
test('create user from contact rejects staff users for customer contacts', function () {
|
|
230
|
+
test('create user from contact rejects staff users of another organization for customer contacts', function () {
|
|
204
231
|
$connection = fleetopsContactCustomerUserBoot();
|
|
205
|
-
$connection->table('users')->insert(['uuid' => 'user-9', 'company_uuid' => 'company-
|
|
232
|
+
$connection->table('users')->insert(['uuid' => 'user-9', 'company_uuid' => 'company-other', 'email' => 'staff@example.com', 'type' => 'user']);
|
|
206
233
|
$contact = fleetopsContactCustomerUserContact(['type' => 'customer', 'email' => 'staff@example.com']);
|
|
207
234
|
|
|
208
|
-
expect(fn () => Contact::createUserFromContact($contact))
|
|
235
|
+
expect(fn () => Contact::createUserFromContact($contact))
|
|
236
|
+
->toThrow(ProfileIdentityConflictException::class, 'This email is already in use by another account.');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('create user from contact links a staff member of the organization without changing their role', function () {
|
|
240
|
+
$connection = fleetopsContactCustomerUserBoot();
|
|
241
|
+
$connection->table('users')->insert(['uuid' => 'user-9', 'company_uuid' => 'company-1', 'email' => 'staff@example.com', 'type' => 'user']);
|
|
242
|
+
$contact = fleetopsContactCustomerUserContact(['type' => 'customer', 'email' => 'Staff@Example.com']);
|
|
243
|
+
|
|
244
|
+
$user = Contact::createUserFromContact($contact);
|
|
245
|
+
|
|
246
|
+
expect($user->uuid)->toBe('user-9')
|
|
247
|
+
->and($user->type)->toBe('user')
|
|
248
|
+
->and($contact->user_uuid)->toBe('user-9')
|
|
249
|
+
->and($connection->table('model_has_roles')->count())->toBe(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('create user from contact rejects a managed account of another type', function () {
|
|
253
|
+
$connection = fleetopsContactCustomerUserBoot();
|
|
254
|
+
$connection->table('users')->insert(['uuid' => 'user-9', 'company_uuid' => 'company-1', 'phone' => '+6591112222', 'type' => 'driver']);
|
|
255
|
+
$contact = fleetopsContactCustomerUserContact(['type' => 'customer', 'phone' => '+65 9111-2222']);
|
|
256
|
+
|
|
257
|
+
expect(fn () => Contact::createUserFromContact($contact))
|
|
258
|
+
->toThrow(ProfileIdentityConflictException::class, 'This phone number is already used by a driver.');
|
|
209
259
|
});
|
|
210
260
|
|
|
211
261
|
test('create user from contact assigns the customer role and persists the link', function () {
|
|
@@ -305,17 +355,27 @@ test('identity lookup helpers resolve users by email phone and uuid', function (
|
|
|
305
355
|
|
|
306
356
|
test('assert customer identity is available detects staff conflicts', function () {
|
|
307
357
|
$connection = fleetopsContactCustomerUserBoot();
|
|
308
|
-
$connection->table('users')->insert([
|
|
358
|
+
$connection->table('users')->insert([
|
|
359
|
+
['uuid' => 'user-1', 'company_uuid' => 'company-1', 'email' => 'staff@example.com', 'type' => 'staff'],
|
|
360
|
+
['uuid' => 'user-2', 'company_uuid' => 'company-1', 'email' => 'driver@example.com', 'type' => 'driver'],
|
|
361
|
+
]);
|
|
309
362
|
|
|
310
363
|
$nonCustomer = fleetopsContactCustomerUserContact();
|
|
311
364
|
$nonCustomer->assertCustomerIdentityIsAvailable();
|
|
312
365
|
|
|
313
|
-
|
|
314
|
-
|
|
366
|
+
// A staff member of the organization can hold a customer profile
|
|
367
|
+
$staffMember = fleetopsContactCustomerUserContact(['type' => 'customer', 'email' => 'staff@example.com']);
|
|
368
|
+
$staffMember->assertCustomerIdentityIsAvailable();
|
|
369
|
+
|
|
370
|
+
// A driver's managed account cannot
|
|
371
|
+
$conflicted = fleetopsContactCustomerUserContact(['type' => 'customer', 'email' => 'driver@example.com']);
|
|
372
|
+
expect(fn () => $conflicted->assertCustomerIdentityIsAvailable())
|
|
373
|
+
->toThrow(CustomerUserConflictException::class, 'This email is already used by a driver and cannot be used for a customer account.');
|
|
315
374
|
|
|
316
375
|
// An already-assigned user is checked ahead of the identity lookup, so a
|
|
317
376
|
// contact whose email matches nothing still trips on its own assignment
|
|
318
|
-
|
|
377
|
+
// (a staff account of another organization)
|
|
378
|
+
$connection->table('users')->insert(['uuid' => '33333333-3333-4333-8333-333333333333', 'company_uuid' => 'company-other', 'email' => 'assigned@example.com', 'type' => 'staff']);
|
|
319
379
|
$assigned = fleetopsContactCustomerUserContact([
|
|
320
380
|
'type' => 'customer',
|
|
321
381
|
'email' => 'unmatched@example.com',
|
|
@@ -368,3 +428,19 @@ test('normalize customer user creates company membership and assigns the custome
|
|
|
368
428
|
expect($connection->table('company_users')->where('user_uuid', 'user-n1')->count())->toBe(1)
|
|
369
429
|
->and($connection->table('model_has_roles')->count())->toBeGreaterThanOrEqual(1);
|
|
370
430
|
});
|
|
431
|
+
|
|
432
|
+
test('normalize customer user keeps a staff member of the organization untouched', function () {
|
|
433
|
+
$connection = fleetopsContactCustomerUserBoot();
|
|
434
|
+
$connection->table('companies')->insert(['uuid' => 'company-1', 'name' => 'Acme']);
|
|
435
|
+
$connection->table('users')->insert(['uuid' => 'user-s1', 'company_uuid' => 'company-1', 'name' => 'Staff', 'email' => 'staff@example.com', 'type' => 'user', 'status' => 'active']);
|
|
436
|
+
|
|
437
|
+
$contact = fleetopsContactCustomerUserContact(['type' => 'customer', 'user_uuid' => 'user-s1']);
|
|
438
|
+
$user = User::where('uuid', 'user-s1')->first();
|
|
439
|
+
|
|
440
|
+
expect($contact->normalizeCustomerUser($user))->toBe($user)
|
|
441
|
+
->and($contact->getRelation('user'))->toBe($user)
|
|
442
|
+
// A team member's account keeps its own organization membership and role
|
|
443
|
+
->and($connection->table('company_users')->count())->toBe(0)
|
|
444
|
+
->and($connection->table('model_has_roles')->count())->toBe(0)
|
|
445
|
+
->and($connection->table('users')->where('uuid', 'user-s1')->value('type'))->toBe('user');
|
|
446
|
+
});
|
|
@@ -25,7 +25,8 @@ class FleetOpsContactUnitCompanyFake extends Company
|
|
|
25
25
|
public array $addUserCalls = [];
|
|
26
26
|
public ?FleetOpsContactUnitCompanyUserFake $companyUser = null;
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// Matches core-api 1.6.63+, where no role is granted unless one is given.
|
|
29
|
+
public function addUser(User $user, ?string $role = null, string $status = 'active'): CompanyUser
|
|
29
30
|
{
|
|
30
31
|
$this->addUserCalls[] = [$user, $role, $status];
|
|
31
32
|
|
|
@@ -71,9 +72,18 @@ class FleetOpsContactUnitUserFake extends User
|
|
|
71
72
|
return true;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
public function save(array $options = []): bool
|
|
76
|
+
{
|
|
77
|
+
$this->updates[] = $this->getDirty();
|
|
78
|
+
$this->syncOriginal();
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
public function delete()
|
|
75
84
|
{
|
|
76
85
|
$this->deleted = true;
|
|
86
|
+
$this->setAttribute('deleted_at', '2026-01-01 00:00:00');
|
|
77
87
|
|
|
78
88
|
return true;
|
|
79
89
|
}
|
|
@@ -180,6 +190,16 @@ class FleetOpsContactUnitAssignableFake extends FleetOpsContactUnitFake
|
|
|
180
190
|
}
|
|
181
191
|
}
|
|
182
192
|
|
|
193
|
+
function fleetopsContactUnitUseProfileTables(): SQLiteConnection
|
|
194
|
+
{
|
|
195
|
+
$connection = fleetopsContactUnitUseInMemoryConnection();
|
|
196
|
+
$connection->statement('create table drivers (uuid varchar(64), user_uuid varchar(64) null, company_uuid varchar(64) null, deleted_at datetime null)');
|
|
197
|
+
$connection->statement('create table contacts (uuid varchar(64), user_uuid varchar(64) null, company_uuid varchar(64) null, deleted_at datetime null)');
|
|
198
|
+
$connection->statement('create table company_users (uuid varchar(64), user_uuid varchar(64) null, company_uuid varchar(64) null, created_at datetime null, updated_at datetime null, deleted_at datetime null)');
|
|
199
|
+
|
|
200
|
+
return $connection;
|
|
201
|
+
}
|
|
202
|
+
|
|
183
203
|
function fleetopsContactUnitUseInMemoryConnection(): SQLiteConnection
|
|
184
204
|
{
|
|
185
205
|
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
@@ -496,6 +516,8 @@ test('contact user conflict helpers guard staff users and allow customers', func
|
|
|
496
516
|
});
|
|
497
517
|
|
|
498
518
|
test('contact sync delete and user presence helpers use loaded user relations', function () {
|
|
519
|
+
fleetopsContactUnitUseProfileTables();
|
|
520
|
+
|
|
499
521
|
$user = new FleetOpsContactUnitUserFake();
|
|
500
522
|
$user->setRawAttributes([
|
|
501
523
|
'uuid' => 'user-uuid',
|
|
@@ -521,6 +543,7 @@ test('contact sync delete and user presence helpers use loaded user relations',
|
|
|
521
543
|
]);
|
|
522
544
|
$contact->setRelation('user', $user);
|
|
523
545
|
$contact->fakeUser = $user;
|
|
546
|
+
$contact->exists = true;
|
|
524
547
|
|
|
525
548
|
expect($contact->syncWithUser())->toBeTrue()
|
|
526
549
|
->and($user->updates)->toBe([[
|
|
@@ -543,20 +566,40 @@ test('contact sync delete and user presence helpers use loaded user relations',
|
|
|
543
566
|
->and($unlinked->doesntHaveUser())->toBeTrue();
|
|
544
567
|
});
|
|
545
568
|
|
|
546
|
-
test('contact delete user
|
|
569
|
+
test('contact delete user leaves team member accounts alone', function () {
|
|
547
570
|
$user = new FleetOpsContactUnitUserFake();
|
|
548
571
|
$user->setRawAttributes([
|
|
549
572
|
'uuid' => 'user-uuid',
|
|
550
|
-
'type' => '
|
|
573
|
+
'type' => 'user',
|
|
551
574
|
], true);
|
|
552
575
|
|
|
553
|
-
$contact
|
|
554
|
-
$contact->
|
|
576
|
+
$contact = new FleetOpsContactUnitFake(['type' => 'customer']);
|
|
577
|
+
$contact->fakeUser = $user;
|
|
555
578
|
|
|
556
579
|
expect($contact->deleteUser())->toBeFalse()
|
|
557
580
|
->and($user->deleted)->toBeFalse();
|
|
558
581
|
});
|
|
559
582
|
|
|
583
|
+
test('contact delete user keeps a managed account still used by another organization', function () {
|
|
584
|
+
$connection = fleetopsContactUnitUseProfileTables();
|
|
585
|
+
$connection->table('drivers')->insert(['uuid' => 'driver-elsewhere', 'user_uuid' => 'user-uuid', 'company_uuid' => 'other-company']);
|
|
586
|
+
$connection->table('company_users')->insert([
|
|
587
|
+
['uuid' => 'cu-1', 'user_uuid' => 'user-uuid', 'company_uuid' => 'company-uuid'],
|
|
588
|
+
['uuid' => 'cu-2', 'user_uuid' => 'user-uuid', 'company_uuid' => 'other-company'],
|
|
589
|
+
]);
|
|
590
|
+
|
|
591
|
+
$user = new FleetOpsContactUnitUserFake();
|
|
592
|
+
$user->setRawAttributes(['uuid' => 'user-uuid', 'type' => 'driver'], true);
|
|
593
|
+
|
|
594
|
+
$contact = new FleetOpsContactUnitFake(['type' => 'contact', 'company_uuid' => 'company-uuid']);
|
|
595
|
+
$contact->fakeUser = $user;
|
|
596
|
+
|
|
597
|
+
// The account only leaves this organization
|
|
598
|
+
expect($contact->deleteUser())->toBeFalse()
|
|
599
|
+
->and($user->deleted)->toBeFalse()
|
|
600
|
+
->and($connection->table('company_users')->whereNull('deleted_at')->pluck('company_uuid')->all())->toBe(['other-company']);
|
|
601
|
+
});
|
|
602
|
+
|
|
560
603
|
test('contact real user helpers return loaded relations without database lookup', function () {
|
|
561
604
|
$user = new FleetOpsContactUnitUserFake();
|
|
562
605
|
$user->setRawAttributes([
|