@fleetbase/fleetops-engine 0.6.68 → 0.6.70
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 +44 -9
- package/addon/components/admin/telematics-settings.hbs +143 -0
- package/addon/components/admin/telematics-settings.js +145 -0
- 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/hub.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +37 -17
- package/addon/components/map/leaflet-live-map.hbs +4 -242
- package/addon/components/map/leaflet-live-map.js +2 -1
- package/addon/components/map/marker-card/driver.hbs +57 -0
- package/addon/components/map/marker-card/driver.js +3 -0
- package/addon/components/map/marker-card/vehicle.hbs +78 -0
- package/addon/components/map/marker-card/vehicle.js +3 -0
- 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/components/telematic/hub.hbs +1 -1
- package/addon/components/widget/live-fleet.hbs +8 -40
- package/addon/controllers/analytics/reports/index.js +5 -0
- package/addon/controllers/connectivity/devices/index.js +4 -0
- package/addon/controllers/connectivity/fuel-providers/index.js +6 -4
- package/addon/controllers/connectivity/sensors/index.js +4 -0
- package/addon/controllers/connectivity/telematics/index.js +4 -0
- package/addon/controllers/maintenance/equipment/index.js +11 -4
- package/addon/controllers/maintenance/inspection-forms/index.js +2 -2
- package/addon/controllers/maintenance/inspection-submissions/index.js +8 -2
- package/addon/controllers/maintenance/maintenances/index.js +4 -0
- package/addon/controllers/maintenance/parts/index.js +11 -4
- package/addon/controllers/maintenance/schedules/index.js +17 -4
- package/addon/controllers/maintenance/work-orders/index.js +11 -4
- package/addon/controllers/management/contacts/index.js +4 -0
- package/addon/controllers/management/drivers/index.js +8 -0
- package/addon/controllers/management/fleets/index.js +4 -0
- package/addon/controllers/management/fuel-reports/index.js +4 -0
- package/addon/controllers/management/fuel-transactions/index.js +9 -7
- package/addon/controllers/management/issues/index.js +4 -0
- package/addon/controllers/management/places/index.js +4 -0
- package/addon/controllers/management/vehicles/index.js +4 -0
- package/addon/controllers/management/vendors/index.js +4 -0
- package/addon/controllers/operations/orders/index/details.js +30 -1
- package/addon/controllers/operations/orders/index.js +7 -0
- package/addon/controllers/operations/service-rates/index.js +3 -0
- package/addon/controllers/settings/index.js +13 -0
- package/addon/controllers/settings/telematics.js +113 -0
- package/addon/extension.js +42 -8
- package/addon/routes/analytics/index.js +14 -1
- package/addon/routes/analytics/reports/index/details.js +1 -1
- package/addon/routes/analytics/reports/index/edit.js +1 -1
- package/addon/routes/analytics/reports/index/new.js +14 -1
- package/addon/routes/analytics/reports.js +14 -1
- package/addon/routes/connectivity/devices/index/new.js +14 -1
- package/addon/routes/connectivity/devices.js +14 -1
- package/addon/routes/connectivity/events.js +14 -1
- package/addon/routes/connectivity/fuel-providers/details/transactions.js +12 -0
- package/addon/routes/connectivity/fuel-providers/details.js +9 -0
- package/addon/routes/connectivity/fuel-providers/edit.js +9 -0
- package/addon/routes/connectivity/fuel-providers/new.js +14 -1
- package/addon/routes/connectivity/fuel-providers.js +14 -1
- package/addon/routes/connectivity/sensors/index/new.js +14 -1
- package/addon/routes/connectivity/sensors.js +14 -1
- package/addon/routes/connectivity/telematics/new.js +14 -1
- package/addon/routes/connectivity/telematics.js +14 -1
- package/addon/routes/connectivity/tracking.js +14 -1
- package/addon/routes/maintenance/equipment/index/details.js +2 -2
- package/addon/routes/maintenance/equipment/index/edit.js +2 -2
- package/addon/routes/maintenance/equipment/index/new.js +14 -1
- package/addon/routes/maintenance/equipment.js +14 -1
- package/addon/routes/maintenance/index.js +14 -1
- package/addon/routes/maintenance/inspection-forms/index/details.js +10 -1
- package/addon/routes/maintenance/inspection-forms/index/edit.js +10 -1
- package/addon/routes/maintenance/inspection-forms/index/new.js +14 -1
- package/addon/routes/maintenance/inspection-forms.js +14 -1
- package/addon/routes/maintenance/inspection-submissions/index/details.js +10 -1
- package/addon/routes/maintenance/inspection-submissions/index/edit.js +10 -1
- package/addon/routes/maintenance/inspection-submissions/index/new.js +14 -1
- package/addon/routes/maintenance/inspection-submissions.js +14 -1
- package/addon/routes/maintenance/maintenances/index/details.js +2 -2
- package/addon/routes/maintenance/maintenances/index/edit.js +2 -2
- package/addon/routes/maintenance/maintenances/index/new.js +14 -1
- package/addon/routes/maintenance/maintenances.js +14 -1
- package/addon/routes/maintenance/parts/index/details.js +2 -2
- package/addon/routes/maintenance/parts/index/edit.js +2 -2
- package/addon/routes/maintenance/parts/index/new.js +14 -1
- package/addon/routes/maintenance/parts.js +14 -1
- package/addon/routes/maintenance/schedules/index/details.js +2 -2
- package/addon/routes/maintenance/schedules/index/edit.js +2 -2
- package/addon/routes/maintenance/schedules/index/new.js +15 -1
- package/addon/routes/maintenance/schedules.js +15 -1
- package/addon/routes/maintenance/work-orders/index/details.js +2 -2
- package/addon/routes/maintenance/work-orders/index/edit.js +2 -2
- package/addon/routes/maintenance/work-orders/index/new.js +14 -1
- package/addon/routes/maintenance/work-orders.js +14 -1
- package/addon/routes/management/fuel-transactions/index/details.js +9 -0
- package/addon/routes/management/fuel-transactions.js +14 -1
- package/addon/routes/management/index.js +13 -0
- package/addon/routes/management/trailers.js +16 -0
- package/addon/routes/management/vendors/integrated/details.js +14 -1
- package/addon/routes/management/vendors/integrated/edit.js +14 -1
- package/addon/routes/management/vendors/integrated/new.js +14 -1
- package/addon/routes/management/vendors/integrated.js +14 -1
- package/addon/routes/operations/orders.js +40 -1
- package/addon/routes/operations/routes/index/details.js +14 -1
- package/addon/routes/operations/routes/index/new.js +13 -0
- package/addon/routes/operations/routes.js +14 -1
- package/addon/routes/operations/service-rates/index/new.js +11 -0
- package/addon/routes/operations/service-rates.js +14 -1
- package/addon/routes/settings/avatars.js +14 -1
- package/addon/routes/settings/custom-fields.js +14 -1
- package/addon/routes/settings/map.js +14 -1
- package/addon/routes/settings/notifications.js +11 -0
- package/addon/routes/settings/orchestrator.js +1 -1
- package/addon/routes/settings/payments/index.js +1 -1
- package/addon/routes/settings/routing.js +14 -1
- package/addon/routes/settings/scheduling.js +14 -1
- package/addon/routes/settings/telematics.js +21 -0
- package/addon/routes/virtual.js +11 -0
- package/addon/routes.js +1 -0
- package/addon/services/contact-actions.js +50 -89
- package/addon/services/driver-actions.js +98 -0
- package/addon/templates/settings/telematics.hbs +70 -0
- package/addon/utils/profile-login-actions.js +172 -0
- package/app/components/admin/telematics-settings.js +1 -0
- package/app/components/{modals/reset-customer-credentials.js → map/marker-card/driver.js} +1 -1
- package/app/components/map/marker-card/vehicle.js +1 -0
- package/app/components/modals/reset-profile-credentials.js +1 -0
- package/app/components/profile-identity-hint.js +1 -0
- package/app/controllers/settings/telematics.js +1 -0
- package/app/routes/settings/telematics.js +1 -0
- package/app/templates/settings/telematics.js +1 -0
- package/composer.json +2 -2
- package/docs/AFAQY.md +1 -1
- package/docs/TELEMATICS_QUEUES.md +1 -1
- package/docs/TELEMETRY_ARCHITECTURE.md +72 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -0
- package/server/config/telemetry.php +12 -0
- package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
- package/server/migrations/2026_09_23_000001_add_retention_index_to_device_events.php +40 -0
- package/server/migrations/2026_09_23_000002_add_retention_index_to_positions.php +40 -0
- package/server/migrations/2026_09_23_000003_add_retention_index_to_telematic_sync_runs.php +40 -0
- package/server/resources/views/mail/driver-credentials.blade.php +21 -0
- package/server/src/Auth/Schemas/FleetOps.php +104 -5
- package/server/src/Console/Commands/DrainTelematicInbox.php +3 -13
- package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
- package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +3 -3
- package/server/src/Console/Commands/PruneTelematicsData.php +318 -0
- 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/Api/v1/OrchestrationController.php +9 -0
- package/server/src/Http/Controllers/FleetOpsController.php +21 -0
- package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +16 -17
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +54 -4
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +13 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +204 -162
- package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
- package/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +13 -0
- package/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/FuelReportController.php +9 -3
- package/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +16 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/ManifestController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/MetricsController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/NavigatorController.php +42 -1
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +12 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +178 -27
- package/server/src/Http/Controllers/Internal/v1/PaymentController.php +13 -0
- package/server/src/Http/Controllers/Internal/v1/RadarController.php +19 -0
- package/server/src/Http/Controllers/Internal/v1/ServiceAreaController.php +9 -3
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +465 -14
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +21 -0
- package/server/src/Http/Controllers/Internal/v1/TrailerController.php +21 -0
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +21 -0
- package/server/src/Http/Controllers/Internal/v1/VendorController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +11 -0
- 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/DispatchTelematicsRetentionJobs.php +70 -0
- package/server/src/Jobs/PruneTelematicsDataJob.php +74 -0
- package/server/src/Listeners/NotifyDriverOnShiftChange.php +3 -3
- package/server/src/Mail/CustomerCredentialsMail.php +1 -1
- 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/Orchestration/Engines/VroomOrchestrationEngine.php +9 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +18 -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/Analytics/LiveFleet.php +18 -2
- package/server/src/Support/Authorization.php +103 -0
- package/server/src/Support/Database/TableIndexes.php +66 -0
- package/server/src/Support/OrderTracker.php +2 -2
- package/server/src/Support/ProfileAccountManager.php +405 -0
- package/server/src/Support/Reporting/FleetOpsReportSchema.php +476 -116
- package/server/src/Support/Telematics/Retention/RetentionPolicy.php +300 -0
- package/server/src/Support/Telematics/Retention/TelemetryActivity.php +40 -0
- package/server/src/Support/Telematics/TelematicService.php +12 -2
- package/server/src/Support/Telematics/Telemetry/Ingestor.php +4 -2
- package/server/src/Tracking/TrackingIntelligenceService.php +1 -1
- package/server/src/Tracking/TrackingOptions.php +2 -2
- package/server/src/Traits/AuthorizesMethods.php +37 -0
- package/server/src/routes.php +14 -1
- package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
- package/server/tests/AnalyticsRoutesTest.php +29 -2
- 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 +21 -1
- 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/Api/TelematicsRetentionIndexMigrationTest.php +115 -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/Internal/SettingControllerHelpersTest.php +71 -0
- package/server/tests/Feature/Http/Internal/TelematicsStorageDeadlinesTest.php +119 -0
- package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
- package/server/tests/InternalImportExportControllerContractsTest.php +5 -8
- package/server/tests/ModelAccessorContractsTest.php +82 -20
- package/server/tests/NavigatorControllerContractsTest.php +36 -4
- package/server/tests/NotificationAndMailContractsTest.php +31 -0
- package/server/tests/ObserverContractsTest.php +28 -3
- package/server/tests/OrchestratorConsumableApiTest.php +42 -0
- package/server/tests/ProviderContractsTest.php +10 -0
- package/server/tests/ReportSchemaContractsTest.php +120 -6
- package/server/tests/RequestContractsTest.php +19 -11
- package/server/tests/SettingControllerContractsTest.php +429 -18
- package/server/tests/SmallControllerContractsTest.php +8 -5
- package/server/tests/Support/PruneTelematicsDataProbe.php +55 -0
- package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
- package/server/tests/TelematicControllerContractsTest.php +22 -4
- package/server/tests/Unit/Console/Commands/ProcessOperationalAlertsCommandTest.php +2 -2
- package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +353 -0
- package/server/tests/Unit/Jobs/DispatchTelematicsRetentionJobsTest.php +114 -0
- package/server/tests/Unit/Jobs/PruneTelematicsDataJobTest.php +60 -0
- 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/Orchestration/VroomEngineTransportAndSettingsTest.php +20 -0
- 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/Analytics/LiveFleetTest.php +6 -0
- package/server/tests/Unit/Support/AuthorizationTest.php +142 -0
- package/server/tests/Unit/Support/MaintainableAndOperationalAlertsTest.php +12 -1
- package/server/tests/Unit/Support/MoreQuerySeamsTest.php +11 -2
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
- package/server/tests/Unit/Support/Telematics/Retention/RetentionPolicyTest.php +190 -0
- package/server/tests/Unit/Support/Telematics/Retention/TelemetryActivityTest.php +68 -0
- package/server/tests/Unit/Tracking/TrackingOptionsCompanySettingsTest.php +109 -0
- package/translations/en-us.yaml +85 -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
|
@@ -30,6 +30,8 @@ class FleetOpsApiDriverControllerProbe extends DriverController
|
|
|
30
30
|
public array $deviceCreates = [];
|
|
31
31
|
public array $companyCalls = [];
|
|
32
32
|
public array $createdUsers = [];
|
|
33
|
+
public array $resolvedAccounts = [];
|
|
34
|
+
public ?Throwable $accountConflict = null;
|
|
33
35
|
public array $uuidLookups = [];
|
|
34
36
|
public array $relationLookups = [];
|
|
35
37
|
public array $relationCompanyScopes = [];
|
|
@@ -65,11 +67,17 @@ class FleetOpsApiDriverControllerProbe extends DriverController
|
|
|
65
67
|
return $userDetails;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
protected function
|
|
70
|
+
protected function resolveDriverAccount(string $companyUuid, array $userDetails): User
|
|
69
71
|
{
|
|
70
|
-
$this->
|
|
72
|
+
$this->resolvedAccounts[] = $companyUuid;
|
|
73
|
+
$this->createdUsers[] = $userDetails;
|
|
74
|
+
|
|
75
|
+
if ($this->accountConflict) {
|
|
76
|
+
throw $this->accountConflict;
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
$this->user ??= new FleetOpsApiDriverUserFake();
|
|
72
|
-
$this->user->setRawAttributes(array_merge(['uuid' => 'user-uuid'], $userDetails), true);
|
|
80
|
+
$this->user->setRawAttributes(array_merge(['uuid' => 'user-uuid', 'type' => 'driver'], $userDetails), true);
|
|
73
81
|
|
|
74
82
|
return $this->user;
|
|
75
83
|
}
|
|
@@ -282,7 +290,17 @@ class FleetOpsApiDriverUserFake extends User
|
|
|
282
290
|
return true;
|
|
283
291
|
}
|
|
284
292
|
|
|
285
|
-
public function
|
|
293
|
+
public function save(array $options = []): bool
|
|
294
|
+
{
|
|
295
|
+
$this->updates[] = $this->getDirty();
|
|
296
|
+
$this->syncOriginal();
|
|
297
|
+
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Matches core-api 1.6.63+, where no role is granted unless one is given (a nullable
|
|
302
|
+
// role is also compatible with older core-api, whose parameter was a plain string).
|
|
303
|
+
public function assignCompany(Company $company, ?string $role = null): User
|
|
286
304
|
{
|
|
287
305
|
$this->assignedCompanies[] = $company->uuid;
|
|
288
306
|
|
|
@@ -317,6 +335,27 @@ class FleetOpsApiDriverUserFake extends User
|
|
|
317
335
|
}
|
|
318
336
|
}
|
|
319
337
|
|
|
338
|
+
/**
|
|
339
|
+
* ProfileAccountManager checks a changed email/phone against the users table.
|
|
340
|
+
*/
|
|
341
|
+
function fleetopsApiDriverContractsUsersTable(array $rows = []): void
|
|
342
|
+
{
|
|
343
|
+
$connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
|
|
344
|
+
$connection->getSchemaBuilder()->create('users', function ($table) {
|
|
345
|
+
$table->string('uuid')->nullable();
|
|
346
|
+
$table->string('type')->nullable();
|
|
347
|
+
$table->string('email')->nullable();
|
|
348
|
+
$table->string('phone')->nullable();
|
|
349
|
+
$table->timestamp('deleted_at')->nullable();
|
|
350
|
+
});
|
|
351
|
+
if ($rows) {
|
|
352
|
+
$connection->table('users')->insert($rows);
|
|
353
|
+
}
|
|
354
|
+
$resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
355
|
+
$resolver->setDefaultConnection('mysql');
|
|
356
|
+
Illuminate\Database\Eloquent\Model::setConnectionResolver($resolver);
|
|
357
|
+
}
|
|
358
|
+
|
|
320
359
|
class FleetOpsApiDriverVehicleFake extends Vehicle
|
|
321
360
|
{
|
|
322
361
|
public array $quietUpdates = [];
|
|
@@ -363,16 +402,15 @@ test('api driver controller creates drivers with user company assignment and rel
|
|
|
363
402
|
|
|
364
403
|
expect($response)->toBe(['resource' => 'driver', 'driver' => $controller->driver])
|
|
365
404
|
->and($controller->companyCalls)->toBe([['request', 'company_public']])
|
|
405
|
+
// The login account is resolved by ProfileAccountManager in the driver's company
|
|
406
|
+
->and($controller->resolvedAccounts)->toBe(['company-uuid'])
|
|
366
407
|
->and($controller->createdUsers[0])->toMatchArray([
|
|
367
|
-
'name'
|
|
368
|
-
'email'
|
|
369
|
-
'phone'
|
|
370
|
-
'
|
|
371
|
-
'applied'
|
|
408
|
+
'name' => 'Driver One',
|
|
409
|
+
'email' => 'driver@example.test',
|
|
410
|
+
'phone' => '+15551234567',
|
|
411
|
+
'password' => 'secret-password',
|
|
412
|
+
'applied' => true,
|
|
372
413
|
])
|
|
373
|
-
->and($controller->user->assignedCompanies)->toBe(['company-uuid'])
|
|
374
|
-
->and($controller->user->assignedTypes)->toBe(['driver'])
|
|
375
|
-
->and($controller->user->assignedRoles)->toBe(['Driver'])
|
|
376
414
|
->and($controller->createdDrivers[0])->toMatchArray([
|
|
377
415
|
'status' => 'available',
|
|
378
416
|
'vehicle_uuid' => 'vehicle-uuid',
|
|
@@ -407,8 +445,10 @@ test('api driver controller reports missing company before creating drivers', fu
|
|
|
407
445
|
});
|
|
408
446
|
|
|
409
447
|
test('api driver controller updates drivers user details assignments location and photo', function () {
|
|
448
|
+
fleetopsApiDriverContractsUsersTable();
|
|
449
|
+
|
|
410
450
|
$user = new FleetOpsApiDriverUserFake();
|
|
411
|
-
$user->setRawAttributes(['uuid' => 'user-uuid'], true);
|
|
451
|
+
$user->setRawAttributes(['uuid' => 'user-uuid', 'type' => 'driver'], true);
|
|
412
452
|
|
|
413
453
|
$driver = new FleetOpsApiDriverFake();
|
|
414
454
|
$driver->setRawAttributes([
|
|
@@ -645,7 +685,7 @@ test('api driver controller refuses to set a password through a general update',
|
|
|
645
685
|
* and now has its own endpoint that demands the current password.
|
|
646
686
|
*/
|
|
647
687
|
$user = new FleetOpsApiDriverUserFake();
|
|
648
|
-
$user->setRawAttributes(['uuid' => 'user-uuid'], true);
|
|
688
|
+
$user->setRawAttributes(['uuid' => 'user-uuid', 'type' => 'driver'], true);
|
|
649
689
|
$driver = new FleetOpsApiDriverFake();
|
|
650
690
|
$driver->setRawAttributes([
|
|
651
691
|
'uuid' => 'driver-uuid',
|
|
@@ -759,11 +799,9 @@ test('api driver controller creates an operational driver with no email or phone
|
|
|
759
799
|
'company_uuid' => 'company-uuid',
|
|
760
800
|
'status' => 'available',
|
|
761
801
|
])
|
|
762
|
-
// The Driver-to-User relationship
|
|
763
|
-
//
|
|
764
|
-
->and($controller->
|
|
765
|
-
->and($controller->user->assignedTypes)->toBe(['driver'])
|
|
766
|
-
->and($controller->user->assignedRoles)->toBe(['Driver']);
|
|
802
|
+
// The Driver-to-User relationship and organization membership are
|
|
803
|
+
// preserved for a credential-less driver.
|
|
804
|
+
->and($controller->resolvedAccounts)->toBe(['company-uuid']);
|
|
767
805
|
});
|
|
768
806
|
|
|
769
807
|
test('api driver controller creates a driver with only one contact method', function () {
|
|
@@ -873,7 +911,7 @@ test('api driver controller copies timezone through to the linked user account',
|
|
|
873
911
|
]));
|
|
874
912
|
|
|
875
913
|
$user = new FleetOpsApiDriverUserFake();
|
|
876
|
-
$user->setRawAttributes(['uuid' => 'user-uuid'], true);
|
|
914
|
+
$user->setRawAttributes(['uuid' => 'user-uuid', 'type' => 'driver'], true);
|
|
877
915
|
|
|
878
916
|
$driver = new FleetOpsApiDriverFake();
|
|
879
917
|
$driver->setRawAttributes(['uuid' => 'driver-uuid', 'public_id' => 'driver_public', 'user_uuid' => 'user-uuid'], true);
|
|
@@ -905,3 +943,47 @@ test('api driver controller only expands relationships the public contract allow
|
|
|
905
943
|
// name is dropped rather than reaching Eloquent, where it would be a 500.
|
|
906
944
|
expect($request->input('with'))->toBe(['vehicle', 'currentJob']);
|
|
907
945
|
});
|
|
946
|
+
|
|
947
|
+
test('api driver controller answers 422 when the email or phone belongs to another account', function () {
|
|
948
|
+
$create = new FleetOpsApiDriverControllerProbe();
|
|
949
|
+
$create->accountConflict = new Fleetbase\FleetOps\Exceptions\ProfileIdentityConflictException('This email is already in use by another account.');
|
|
950
|
+
|
|
951
|
+
expect($create->create(new CreateDriverRequest(['name' => 'Driver One', 'email' => 'taken@example.test'])))->toBe([
|
|
952
|
+
'json' => ['error' => 'This email is already in use by another account.'],
|
|
953
|
+
'status' => 422,
|
|
954
|
+
])
|
|
955
|
+
->and($create->createdDrivers)->toBe([]);
|
|
956
|
+
|
|
957
|
+
fleetopsApiDriverContractsUsersTable([['uuid' => 'other-user', 'type' => 'user', 'phone' => '+15550009999']]);
|
|
958
|
+
|
|
959
|
+
$user = new FleetOpsApiDriverUserFake();
|
|
960
|
+
$user->setRawAttributes(['uuid' => 'user-uuid', 'type' => 'driver', 'phone' => '+15550001111'], true);
|
|
961
|
+
$driver = new FleetOpsApiDriverFake();
|
|
962
|
+
$driver->setRawAttributes(['uuid' => 'driver-uuid', 'public_id' => 'driver_public', 'user_uuid' => 'user-uuid'], true);
|
|
963
|
+
$driver->userForTest = $user;
|
|
964
|
+
|
|
965
|
+
$update = new FleetOpsApiDriverControllerProbe();
|
|
966
|
+
$update->driver = $driver;
|
|
967
|
+
|
|
968
|
+
expect($update->update('driver_public', new UpdateDriverRequest(['phone' => '+1 555 000 9999'])))->toBe([
|
|
969
|
+
'json' => ['error' => 'This phone number is already in use by another account.'],
|
|
970
|
+
'status' => 422,
|
|
971
|
+
])
|
|
972
|
+
->and($user->updates)->toBe([])
|
|
973
|
+
->and($driver->updates)->toBe([]);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
test('api driver controller only renames a team member account linked to a driver', function () {
|
|
977
|
+
$staff = new FleetOpsApiDriverUserFake();
|
|
978
|
+
$staff->setRawAttributes(['uuid' => 'staff-uuid', 'type' => 'user', 'name' => 'Staff', 'email' => 'staff@example.test'], true);
|
|
979
|
+
$driver = new FleetOpsApiDriverFake();
|
|
980
|
+
$driver->setRawAttributes(['uuid' => 'driver-uuid', 'public_id' => 'driver_public', 'user_uuid' => 'staff-uuid'], true);
|
|
981
|
+
$driver->userForTest = $staff;
|
|
982
|
+
|
|
983
|
+
$update = new FleetOpsApiDriverControllerProbe();
|
|
984
|
+
$update->driver = $driver;
|
|
985
|
+
$update->update('driver_public', new UpdateDriverRequest(['name' => 'Staff Driver', 'email' => 'changed@example.test']));
|
|
986
|
+
|
|
987
|
+
expect($staff->updates)->toBe([['name' => 'Staff Driver']])
|
|
988
|
+
->and($staff->email)->toBe('staff@example.test');
|
|
989
|
+
});
|
|
@@ -884,7 +884,7 @@ class FleetOpsUserCommandFake
|
|
|
884
884
|
return true;
|
|
885
885
|
}
|
|
886
886
|
|
|
887
|
-
public function assignCompany(Company $company, string $role =
|
|
887
|
+
public function assignCompany(Company $company, ?string $role = null): self
|
|
888
888
|
{
|
|
889
889
|
$this->assigned[] = [$company->uuid, $role];
|
|
890
890
|
|
|
@@ -2208,8 +2208,8 @@ test('fix customer companies command creates or links users and assigns missing
|
|
|
2208
2208
|
['email', FleetOpsCustomerCommandFake::class],
|
|
2209
2209
|
['phone', FleetOpsCustomerCommandFake::class],
|
|
2210
2210
|
])
|
|
2211
|
-
->and($createdUser->assigned)->toBe([['company-created', '
|
|
2212
|
-
->and($existingUser->assigned)->toBe([['company-existing', '
|
|
2211
|
+
->and($createdUser->assigned)->toBe([['company-created', 'Fleet-Ops Customer']])
|
|
2212
|
+
->and($existingUser->assigned)->toBe([['company-existing', 'Fleet-Ops Customer']]);
|
|
2213
2213
|
});
|
|
2214
2214
|
|
|
2215
2215
|
test('fix driver companies command syncs assigned users and missing company assignments', function () {
|
|
@@ -2236,7 +2236,7 @@ test('fix driver companies command syncs assigned users and missing company assi
|
|
|
2236
2236
|
['email', Driver::class],
|
|
2237
2237
|
['phone', Driver::class],
|
|
2238
2238
|
])
|
|
2239
|
-
->and($user->assigned)->toBe([['driver-company', '
|
|
2239
|
+
->and($user->assigned)->toBe([['driver-company', 'Driver']])
|
|
2240
2240
|
->and($command->messages)->toContain(['line', 'Found driver Driver User (driver@example.test) which doesnt have correct company assignment.'])
|
|
2241
2241
|
->and($command->messages)->toContain(['line', 'Driver driver@example.test was assigned to company: Driver Company']);
|
|
2242
2242
|
});
|
|
@@ -254,7 +254,6 @@ class FleetOpsZoneControllerProbe extends ZoneController
|
|
|
254
254
|
class FleetOpsInternalContactControllerProbe extends InternalContactController
|
|
255
255
|
{
|
|
256
256
|
public array $installedPackages = [];
|
|
257
|
-
public array $resolvedUsers = [];
|
|
258
257
|
public array $sentCredentials = [];
|
|
259
258
|
public array $savedMetas = [];
|
|
260
259
|
public ?User $contactUser = null;
|
|
@@ -269,13 +268,6 @@ class FleetOpsInternalContactControllerProbe extends InternalContactController
|
|
|
269
268
|
return $reflection->invoke($this, ...$arguments);
|
|
270
269
|
}
|
|
271
270
|
|
|
272
|
-
protected function resolveUserUuid(string $user): string
|
|
273
|
-
{
|
|
274
|
-
$this->resolvedUsers[] = $user;
|
|
275
|
-
|
|
276
|
-
return 'resolved-' . $user;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
271
|
protected function installedFleetbaseExtensions(): array
|
|
280
272
|
{
|
|
281
273
|
return $this->installedPackages;
|
|
@@ -1607,7 +1599,7 @@ test('internal contact controller create update and after save hooks protect cus
|
|
|
1607
1599
|
],
|
|
1608
1600
|
],
|
|
1609
1601
|
]);
|
|
1610
|
-
$input = ['type' => 'contact', 'user' => ['id' => 'user_public']];
|
|
1602
|
+
$input = ['type' => 'contact', 'user' => ['id' => 'user_public'], 'user_uuid' => 'user-uuid'];
|
|
1611
1603
|
|
|
1612
1604
|
$controller->onBeforeCreate($request, $input);
|
|
1613
1605
|
|
|
@@ -1623,8 +1615,8 @@ test('internal contact controller create update and after save hooks protect cus
|
|
|
1623
1615
|
|
|
1624
1616
|
$controller->afterSave($request, $contact);
|
|
1625
1617
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1618
|
+
// The login account is managed by the profile, so a user can't be picked from the console
|
|
1619
|
+
expect($input)->toBe(['type' => 'contact'])
|
|
1628
1620
|
->and($contact->normalized)->toBeTrue()
|
|
1629
1621
|
->and($contact->syncedCustomFields)->toBe([
|
|
1630
1622
|
['key' => 'tier', 'value' => 'gold'],
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_once __DIR__ . '/../../Support/AfaqyTestCrypto.php';
|
|
4
4
|
require_once __DIR__ . '/../../Support/ExampleTelemetryProvider.php';
|
|
5
5
|
require_once __DIR__ . '/../../Support/TelemetryTestEnvironment.php';
|
|
6
|
+
require_once __DIR__ . '/../../Support/PruneTelematicsDataProbe.php';
|
|
6
7
|
|
|
7
8
|
use Fleetbase\FleetOps\Http\Controllers\TelematicPositionWebhookController;
|
|
8
9
|
use Fleetbase\FleetOps\Jobs\ProcessTelematicDelivery;
|
|
@@ -123,6 +124,12 @@ test('database ingestion deduplicates poll and push and never regresses device s
|
|
|
123
124
|
expect(DeviceEvent::withoutGlobalScopes()->count())->toBe(2);
|
|
124
125
|
expect(count($GLOBALS['afaqy_broadcasts']))->toBe(1);
|
|
125
126
|
expect($device->last_position->getLat())->toBe(24.0);
|
|
127
|
+
// The raw unit is stored once, in payload; meta carries only the normalized block.
|
|
128
|
+
$stored = DeviceEvent::withoutGlobalScopes()->orderBy('id')->first();
|
|
129
|
+
expect(data_get($stored->payload, '_id'))->toBe('unit-1')
|
|
130
|
+
->and($stored->meta)->not->toHaveKeys(['_id', 'last_update'])
|
|
131
|
+
->and(data_get($stored->meta, 'telemetry.position_at'))->toBe('2026-09-15T11:59:00.000000Z')
|
|
132
|
+
->and(data_get($stored->meta, 'telematic_uuid'))->toBe('integration-1');
|
|
126
133
|
});
|
|
127
134
|
|
|
128
135
|
test('webhook authenticates before durable acceptance and worker quarantines unknown shapes', function () {
|
|
@@ -282,7 +289,7 @@ test('five thousand units and a duplicate reconciliation sweep fit the local pro
|
|
|
282
289
|
expect($elapsed)->toBeLessThan(60.0);
|
|
283
290
|
})->skip(getenv('AFAQY_RUN_LOAD_TESTS') !== '1', 'Opt-in local processing benchmark; production latency requires deployment testing.');
|
|
284
291
|
|
|
285
|
-
test('inbox
|
|
292
|
+
test('inbox recovery leaves retention to the prune command, which expires processed deliveries by policy', function () {
|
|
286
293
|
$telematic = afaqyDbFixture();
|
|
287
294
|
$inbox = new Inbox();
|
|
288
295
|
$pending = $inbox->accept($telematic, afaqyDbUnit(), 'poll');
|
|
@@ -297,9 +304,17 @@ test('inbox maintenance retains replayable payloads and cleans expired rows duri
|
|
|
297
304
|
'metadata' => ['telemetry' => ['durable_ingestion' => true]],
|
|
298
305
|
]));
|
|
299
306
|
expect((new Fleetbase\FleetOps\Console\Commands\DrainTelematicInbox())->handle())->toBe(0);
|
|
307
|
+
// The drain only recovers; nothing is expired until the retention sweep runs.
|
|
308
|
+
expect(DB::table('telematic_deliveries')->count())->toBe(3);
|
|
309
|
+
|
|
310
|
+
$prune = new PruneTelematicsDataProbe();
|
|
311
|
+
$prune->options['company'] = 'company-1';
|
|
312
|
+
$prune->companyRows = collect([(object) ['id' => 1, 'uuid' => 'company-1', 'public_id' => 'company_1']]);
|
|
313
|
+
expect($prune->handle())->toBe(0);
|
|
300
314
|
expect(DB::table('telematic_deliveries')->where('uuid', $processed)->exists())->toBeFalse();
|
|
301
315
|
expect(DB::table('telematic_deliveries')->where('uuid', $quarantined)->exists())->toBeTrue();
|
|
302
316
|
expect(DB::table('telematic_deliveries')->where('uuid', $pending)->value('status'))->toBe('pending');
|
|
317
|
+
expect($prune->messages)->toContain(['info', 'company_1 telematic_deliveries: deleted=1 compacted=0 batches=1']);
|
|
303
318
|
});
|
|
304
319
|
|
|
305
320
|
test('sensor values use source time and preserve units through partial and older snapshots', function () {
|
|
@@ -596,6 +611,11 @@ test('inbox completion waits for pending deliveries and aggregates quarantined f
|
|
|
596
611
|
DB::table('telematic_deliveries')->where('uuid', $id)->update(['status' => 'quarantined', 'applied' => 2, 'failed' => 1]);
|
|
597
612
|
Inbox::finishRun('run-pending');
|
|
598
613
|
expect((array) DB::table('telematic_sync_runs')->first())->toMatchArray(['status' => 'partial', 'applied' => 2, 'failed' => 1]);
|
|
614
|
+
|
|
615
|
+
// A finished run whose connection was removed completes without touching connection meta
|
|
616
|
+
DB::table('telematic_sync_runs')->insert(['uuid' => 'run-orphan', 'telematic_uuid' => 'missing-connection', 'status' => 'ingesting', 'created_at' => now(), 'updated_at' => now()]);
|
|
617
|
+
Inbox::finishRun('run-orphan');
|
|
618
|
+
expect(DB::table('telematic_sync_runs')->where('uuid', 'run-orphan')->value('status'))->not->toBe('ingesting');
|
|
599
619
|
});
|
|
600
620
|
|
|
601
621
|
test('telemetry diagnostics report setup receipt backlog and degraded delivery states', function () {
|
|
@@ -230,6 +230,42 @@ test('password login rejects bad credentials and issues driver tokens', function
|
|
|
230
230
|
->and($connection->table('personal_access_tokens')->value('name'))->toBe('driver-1');
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
+
test('deactivated driver logins are refused on every sign-in path', function () {
|
|
234
|
+
$connection = fleetopsDriverAuthBoot();
|
|
235
|
+
$controller = new DriverController();
|
|
236
|
+
$connection->table('users')->where('uuid', 'user-1')->update(['status' => 'inactive']);
|
|
237
|
+
app('hash')->checks = true;
|
|
238
|
+
|
|
239
|
+
// An unknown identity fails authentication rather than erroring on a null user
|
|
240
|
+
$unknown = $controller->login(Request::create('/x', 'POST', ['identity' => 'ghost@example.test', 'password' => 'secret']));
|
|
241
|
+
expect($unknown->getStatusCode())->toBe(401);
|
|
242
|
+
|
|
243
|
+
$password = $controller->login(Request::create('/x', 'POST', ['identity' => 'driver@example.test', 'password' => 'secret']));
|
|
244
|
+
|
|
245
|
+
app()->instance('request', Request::create('/x', 'POST', ['phone' => '6591234567']));
|
|
246
|
+
$phone = $controller->loginWithPhone();
|
|
247
|
+
|
|
248
|
+
$connection->table('verification_codes')->insert(['uuid' => 'vc-1', 'subject_uuid' => 'user-1', 'code' => '424242', 'for' => 'driver_login']);
|
|
249
|
+
$code = $controller->verifyCode(Request::create('/x', 'POST', ['identity' => '6591234567', 'code' => '424242']));
|
|
250
|
+
|
|
251
|
+
foreach ([$password, $phone, $code] as $response) {
|
|
252
|
+
expect($response->getStatusCode())->toBe(403)
|
|
253
|
+
->and($response->getData(true)['error'])->toBe(DriverController::DEACTIVATED_LOGIN_MESSAGE);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Password recovery answers as if the identity were unknown
|
|
257
|
+
$forgot = $controller->forgotPassword(Request::create('/x', 'POST', ['identity' => 'driver@example.test']));
|
|
258
|
+
$reset = $controller->resetPassword(Request::create('/x', 'POST', ['identity' => 'driver@example.test', 'code' => '424242', 'password' => 'new-secret-123']));
|
|
259
|
+
|
|
260
|
+
expect($forgot->getData(true))->toBe(['status' => 'ok'])
|
|
261
|
+
->and($reset->getStatusCode())->toBe(422)
|
|
262
|
+
->and($reset->getData(true)['error'])->toBe('Invalid or expired reset code.');
|
|
263
|
+
|
|
264
|
+
expect(DriverController::isLoginDeactivated(User::where('uuid', 'user-1')->first()))->toBeTrue()
|
|
265
|
+
->and($connection->table('personal_access_tokens')->count())->toBe(0)
|
|
266
|
+
->and($connection->table('verification_codes')->count())->toBe(1);
|
|
267
|
+
});
|
|
268
|
+
|
|
233
269
|
test('phone login falls back to email verification and errors without channels', function () {
|
|
234
270
|
$connection = fleetopsDriverAuthBoot();
|
|
235
271
|
$controller = new DriverController();
|
|
@@ -106,7 +106,7 @@ function fleetopsDriverSessionHelpersBoot(): SQLiteConnection
|
|
|
106
106
|
$schema = $connection->getSchemaBuilder();
|
|
107
107
|
app()->instance('db.schema', $schema);
|
|
108
108
|
$tables = [
|
|
109
|
-
'users' => ['uuid', 'public_id', 'company_uuid', 'name', 'email', 'phone', 'status', 'type', 'username', 'password'],
|
|
109
|
+
'users' => ['uuid', 'public_id', 'company_uuid', 'name', 'email', 'phone', 'status', 'type', 'username', 'password', 'timezone', 'slug', 'meta'],
|
|
110
110
|
'drivers' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'user_uuid', 'vehicle_uuid', 'vendor_uuid', 'status', 'online', 'location', 'meta'],
|
|
111
111
|
'companies' => ['uuid', 'public_id', 'name', 'status', 'owner_uuid'],
|
|
112
112
|
'user_devices' => ['uuid', 'public_id', 'user_uuid', 'token', 'platform', 'status'],
|
|
@@ -189,22 +189,35 @@ test('user and driver persistence helpers write records', function () {
|
|
|
189
189
|
$userDetails = $probe->callHelper('applyUserInfoFromRequest', Request::create('/x', 'POST', []), ['name' => 'Applicant']);
|
|
190
190
|
expect($userDetails['name'])->toBe('Applicant');
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
// A company that isn't in the table skips the organization membership seam,
|
|
193
|
+
// which needs spatie roles; the managed account itself is still created.
|
|
194
|
+
$missingCompany = '33333333-3333-4333-8333-333333333333';
|
|
195
|
+
|
|
196
|
+
$user = $probe->callHelper('resolveDriverAccount', $missingCompany, ['name' => 'New User']);
|
|
193
197
|
expect($user)->toBeInstanceOf(User::class)
|
|
194
|
-
->and($connection->table('users')->where('name', 'New User')->count())->toBe(1)
|
|
198
|
+
->and($connection->table('users')->where('name', 'New User')->count())->toBe(1)
|
|
199
|
+
->and($connection->table('users')->where('name', 'New User')->value('type'))->toBe('driver')
|
|
200
|
+
->and($connection->table('users')->where('name', 'New User')->value('status'))->toBe('active');
|
|
195
201
|
|
|
196
202
|
/*
|
|
197
203
|
* `password` is guarded on User, so mass assignment drops it without a
|
|
198
204
|
* word. The create endpoint has always documented and validated one, so
|
|
199
|
-
* unless
|
|
200
|
-
*
|
|
205
|
+
* unless it is set explicitly a driver created through the API can never
|
|
206
|
+
* sign in with the password chosen for them.
|
|
201
207
|
*/
|
|
202
|
-
$withPassword = $probe->callHelper('
|
|
208
|
+
$withPassword = $probe->callHelper('resolveDriverAccount', $missingCompany, ['name' => 'Password User', 'password' => 'seeded-password']);
|
|
203
209
|
$stored = $connection->table('users')->where('name', 'Password User')->value('password');
|
|
204
210
|
expect(Hash::check('seeded-password', $withPassword->password))->toBeTrue()
|
|
205
211
|
->and($stored)->not->toBeNull()
|
|
206
212
|
->and($stored)->not->toBe('seeded-password');
|
|
207
213
|
|
|
214
|
+
// A team member of the company with the email is linked instead of duplicated
|
|
215
|
+
$connection->table('users')->insert(['uuid' => 'staff-1', 'company_uuid' => '22222222-2222-4222-8222-222222222222', 'name' => 'Staff', 'email' => 'staff@example.test', 'type' => 'user']);
|
|
216
|
+
$linked = $probe->callHelper('resolveDriverAccount', '22222222-2222-4222-8222-222222222222', ['name' => 'Staff Driver', 'email' => 'STAFF@example.test']);
|
|
217
|
+
expect($linked->uuid)->toBe('staff-1')
|
|
218
|
+
->and($connection->table('users')->where('email', 'staff@example.test')->count())->toBe(1)
|
|
219
|
+
->and($connection->table('users')->where('uuid', 'staff-1')->value('type'))->toBe('user');
|
|
220
|
+
|
|
208
221
|
$driver = $probe->callHelper('createDriver', ['company_uuid' => '22222222-2222-4222-8222-222222222222', 'user_uuid' => 'user-1']);
|
|
209
222
|
expect($driver)->toBeInstanceOf(Driver::class);
|
|
210
223
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\ConnectionResolver;
|
|
4
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
5
|
+
use Illuminate\Database\SQLiteConnection;
|
|
6
|
+
use Illuminate\Support\Facades\DB;
|
|
7
|
+
use Illuminate\Support\Facades\Schema;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The migration that converts profile-only `user` accounts into managed
|
|
11
|
+
* driver/customer accounts, run up and down against in-memory SQLite.
|
|
12
|
+
*
|
|
13
|
+
* An account is converted only when it clearly is a profile login and nothing
|
|
14
|
+
* else; anything that looks like a team member is left alone, and down()
|
|
15
|
+
* reverts only the rows up() converted.
|
|
16
|
+
*/
|
|
17
|
+
function fleetopsProfileConversionBoot(bool $withPolicies = true): SQLiteConnection
|
|
18
|
+
{
|
|
19
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
20
|
+
$resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
21
|
+
$resolver->setDefaultConnection('mysql');
|
|
22
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
23
|
+
|
|
24
|
+
app()->instance('db', new class($connection) {
|
|
25
|
+
public function __construct(public SQLiteConnection $c)
|
|
26
|
+
{
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public function connection($name = null): SQLiteConnection
|
|
30
|
+
{
|
|
31
|
+
return $this->c;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public function table($table, $as = null)
|
|
35
|
+
{
|
|
36
|
+
return $this->c->table($table, $as);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public function __call($method, $arguments)
|
|
40
|
+
{
|
|
41
|
+
return $this->c->{$method}(...$arguments);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
app()->instance('db.schema', $connection->getSchemaBuilder());
|
|
45
|
+
DB::clearResolvedInstance('db');
|
|
46
|
+
Schema::clearResolvedInstance('db.schema');
|
|
47
|
+
|
|
48
|
+
$schema = $connection->getSchemaBuilder();
|
|
49
|
+
$tables = [
|
|
50
|
+
'users' => ['uuid', 'type', 'meta'],
|
|
51
|
+
'drivers' => ['uuid', 'user_uuid'],
|
|
52
|
+
'contacts' => ['uuid', 'user_uuid', 'type'],
|
|
53
|
+
'companies' => ['uuid', 'owner_uuid'],
|
|
54
|
+
'company_users' => ['uuid', 'user_uuid', 'company_uuid'],
|
|
55
|
+
'roles' => ['name'],
|
|
56
|
+
'model_has_roles' => ['role_id', 'model_uuid'],
|
|
57
|
+
'model_has_permissions' => ['permission_id', 'model_uuid'],
|
|
58
|
+
];
|
|
59
|
+
if ($withPolicies) {
|
|
60
|
+
$tables['model_has_policies'] = ['policy_id', 'model_uuid'];
|
|
61
|
+
}
|
|
62
|
+
foreach ($tables as $table => $columns) {
|
|
63
|
+
$schema->create($table, function ($blueprint) use ($columns) {
|
|
64
|
+
$blueprint->increments('id');
|
|
65
|
+
foreach ($columns as $column) {
|
|
66
|
+
$blueprint->string($column)->nullable();
|
|
67
|
+
}
|
|
68
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
foreach (['Driver', 'Fleet-Ops Customer', 'Administrator'] as $role) {
|
|
73
|
+
$connection->table('roles')->insert(['name' => $role]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return $connection;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fleetopsProfileConversionMigration(): object
|
|
80
|
+
{
|
|
81
|
+
return require dirname(__DIR__, 4) . '/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Seed a user holding the given profile(s), roles on its company membership
|
|
86
|
+
* (or on the user itself), and optional direct permission or policy.
|
|
87
|
+
*/
|
|
88
|
+
function fleetopsProfileConversionUser(SQLiteConnection $connection, string $uuid, array $options = []): void
|
|
89
|
+
{
|
|
90
|
+
$connection->table('users')->insert(['uuid' => $uuid, 'type' => $options['type'] ?? 'user', 'meta' => $options['meta'] ?? null, 'deleted_at' => $options['deleted_at'] ?? null]);
|
|
91
|
+
$connection->table('company_users')->insert(['uuid' => 'cu-' . $uuid, 'user_uuid' => $uuid, 'company_uuid' => 'company-1']);
|
|
92
|
+
|
|
93
|
+
if ($options['driver'] ?? false) {
|
|
94
|
+
$connection->table('drivers')->insert(['uuid' => 'driver-' . $uuid, 'user_uuid' => $uuid]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ($options['customer'] ?? false) {
|
|
98
|
+
$connection->table('contacts')->insert(['uuid' => 'contact-' . $uuid, 'user_uuid' => $uuid, 'type' => 'customer']);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
foreach ($options['roles'] ?? [] as $role) {
|
|
102
|
+
$roleId = $connection->table('roles')->where('name', $role)->value('id');
|
|
103
|
+
$connection->table('model_has_roles')->insert(['role_id' => $roleId, 'model_uuid' => ($options['role_on_user'] ?? false) ? $uuid : 'cu-' . $uuid]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if ($options['permission'] ?? false) {
|
|
107
|
+
$connection->table('model_has_permissions')->insert(['permission_id' => 1, 'model_uuid' => 'cu-' . $uuid]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ($options['policy'] ?? false) {
|
|
111
|
+
$connection->table('model_has_policies')->insert(['policy_id' => 1, 'model_uuid' => $uuid]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fleetopsProfileConversionTypes(SQLiteConnection $connection): array
|
|
116
|
+
{
|
|
117
|
+
return $connection->table('users')->orderBy('uuid')->pluck('type', 'uuid')->all();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test('up converts profile-only accounts and leaves anything that looks like a team member', function () {
|
|
121
|
+
$connection = fleetopsProfileConversionBoot();
|
|
122
|
+
|
|
123
|
+
fleetopsProfileConversionUser($connection, 'a-driver', ['driver' => true, 'roles' => ['Driver'], 'meta' => json_encode(['source' => 'console'])]);
|
|
124
|
+
fleetopsProfileConversionUser($connection, 'b-customer', ['customer' => true, 'roles' => ['Fleet-Ops Customer'], 'role_on_user' => true]);
|
|
125
|
+
fleetopsProfileConversionUser($connection, 'c-owner', ['driver' => true, 'roles' => ['Driver']]);
|
|
126
|
+
$connection->table('companies')->insert(['uuid' => 'company-1', 'owner_uuid' => 'c-owner']);
|
|
127
|
+
fleetopsProfileConversionUser($connection, 'd-extra-role', ['driver' => true, 'roles' => ['Driver', 'Administrator']]);
|
|
128
|
+
fleetopsProfileConversionUser($connection, 'e-permission', ['driver' => true, 'roles' => ['Driver'], 'permission' => true]);
|
|
129
|
+
fleetopsProfileConversionUser($connection, 'f-policy', ['customer' => true, 'roles' => ['Fleet-Ops Customer'], 'policy' => true]);
|
|
130
|
+
fleetopsProfileConversionUser($connection, 'g-both-profiles', ['driver' => true, 'customer' => true, 'roles' => ['Driver']]);
|
|
131
|
+
fleetopsProfileConversionUser($connection, 'h-no-roles', ['driver' => true]);
|
|
132
|
+
fleetopsProfileConversionUser($connection, 'i-wrong-role', ['customer' => true, 'roles' => ['Driver']]);
|
|
133
|
+
fleetopsProfileConversionUser($connection, 'j-no-profile', ['roles' => ['Driver']]);
|
|
134
|
+
fleetopsProfileConversionUser($connection, 'k-admin', ['type' => 'admin', 'driver' => true, 'roles' => ['Driver']]);
|
|
135
|
+
fleetopsProfileConversionUser($connection, 'l-deleted', ['driver' => true, 'roles' => ['Driver'], 'deleted_at' => '2026-01-01 00:00:00']);
|
|
136
|
+
fleetopsProfileConversionUser($connection, 'm-bad-meta', ['driver' => true, 'roles' => ['Driver'], 'meta' => 'not-json']);
|
|
137
|
+
|
|
138
|
+
fleetopsProfileConversionMigration()->up();
|
|
139
|
+
|
|
140
|
+
expect(fleetopsProfileConversionTypes($connection))->toBe([
|
|
141
|
+
'a-driver' => 'driver',
|
|
142
|
+
'b-customer' => 'customer',
|
|
143
|
+
'c-owner' => 'user',
|
|
144
|
+
'd-extra-role' => 'user',
|
|
145
|
+
'e-permission' => 'user',
|
|
146
|
+
'f-policy' => 'user',
|
|
147
|
+
'g-both-profiles' => 'user',
|
|
148
|
+
'h-no-roles' => 'user',
|
|
149
|
+
'i-wrong-role' => 'user',
|
|
150
|
+
'j-no-profile' => 'user',
|
|
151
|
+
'k-admin' => 'admin',
|
|
152
|
+
'l-deleted' => 'user',
|
|
153
|
+
'm-bad-meta' => 'driver',
|
|
154
|
+
])
|
|
155
|
+
// The previous type is recorded next to any existing meta
|
|
156
|
+
->and(json_decode($connection->table('users')->where('uuid', 'a-driver')->value('meta'), true))->toBe(['source' => 'console', 'previous_type' => 'user'])
|
|
157
|
+
->and(json_decode($connection->table('users')->where('uuid', 'm-bad-meta')->value('meta'), true))->toBe(['previous_type' => 'user']);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('down reverts only the accounts up converted', function () {
|
|
161
|
+
$connection = fleetopsProfileConversionBoot();
|
|
162
|
+
|
|
163
|
+
fleetopsProfileConversionUser($connection, 'a-driver', ['driver' => true, 'roles' => ['Driver'], 'meta' => json_encode(['source' => 'console'])]);
|
|
164
|
+
fleetopsProfileConversionUser($connection, 'b-customer', ['customer' => true, 'roles' => ['Fleet-Ops Customer']]);
|
|
165
|
+
// Managed accounts created after the change carry no previous type
|
|
166
|
+
fleetopsProfileConversionUser($connection, 'c-native-driver', ['type' => 'driver', 'driver' => true, 'meta' => json_encode(['source' => 'app'])]);
|
|
167
|
+
// A previous type other than `user` is not this migration's
|
|
168
|
+
fleetopsProfileConversionUser($connection, 'd-other-previous', ['type' => 'customer', 'customer' => true, 'meta' => json_encode(['previous_type' => 'contact'])]);
|
|
169
|
+
|
|
170
|
+
$migration = fleetopsProfileConversionMigration();
|
|
171
|
+
$migration->up();
|
|
172
|
+
$migration->down();
|
|
173
|
+
|
|
174
|
+
expect(fleetopsProfileConversionTypes($connection))->toBe([
|
|
175
|
+
'a-driver' => 'user',
|
|
176
|
+
'b-customer' => 'user',
|
|
177
|
+
'c-native-driver' => 'driver',
|
|
178
|
+
'd-other-previous' => 'customer',
|
|
179
|
+
])
|
|
180
|
+
->and(json_decode($connection->table('users')->where('uuid', 'a-driver')->value('meta'), true))->toBe(['source' => 'console'])
|
|
181
|
+
->and(json_decode($connection->table('users')->where('uuid', 'b-customer')->value('meta'), true))->toBe([])
|
|
182
|
+
->and(json_decode($connection->table('users')->where('uuid', 'd-other-previous')->value('meta'), true))->toBe(['previous_type' => 'contact']);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('up and down do nothing when the tables they read are missing', function () {
|
|
186
|
+
$connection = fleetopsProfileConversionBoot(false);
|
|
187
|
+
fleetopsProfileConversionUser($connection, 'a-driver', ['driver' => true, 'roles' => ['Driver']]);
|
|
188
|
+
|
|
189
|
+
$migration = fleetopsProfileConversionMigration();
|
|
190
|
+
$migration->up();
|
|
191
|
+
|
|
192
|
+
expect(fleetopsProfileConversionTypes($connection))->toBe(['a-driver' => 'user']);
|
|
193
|
+
|
|
194
|
+
$connection->table('users')->where('uuid', 'a-driver')->update(['type' => 'driver', 'meta' => json_encode(['previous_type' => 'user'])]);
|
|
195
|
+
$connection->getSchemaBuilder()->drop('users');
|
|
196
|
+
|
|
197
|
+
// Without a users table there is nothing to revert
|
|
198
|
+
$migration->down();
|
|
199
|
+
|
|
200
|
+
expect($connection->getSchemaBuilder()->hasTable('users'))->toBeFalse();
|
|
201
|
+
});
|