@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
|
@@ -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([
|
|
@@ -116,7 +116,7 @@ test('sensor model exposes relation builders and logging options', function () {
|
|
|
116
116
|
|
|
117
117
|
$sensor = new Sensor();
|
|
118
118
|
|
|
119
|
-
expect($sensor->getSlugOptions()->generateSlugFrom)->toBe(['name', '
|
|
119
|
+
expect($sensor->getSlugOptions()->generateSlugFrom)->toBe(['name', 'type'])
|
|
120
120
|
->and($sensor->getSlugOptions()->slugField)->toBe('slug')
|
|
121
121
|
->and($sensor->getActivitylogOptions()->logOnlyDirty)->toBeTrue()
|
|
122
122
|
->and($sensor->telematic())->toBeInstanceOf(BelongsTo::class)
|
|
@@ -222,7 +222,7 @@ test('sensor scopes write expected query constraints', function () {
|
|
|
222
222
|
->and($sensor->scopeActive($query))->toBe($query)
|
|
223
223
|
->and($sensor->scopeWithRecentReadings($query, 30))->toBe($query)
|
|
224
224
|
->and($sensor->scopeOutOfThreshold($query))->toBe($query)
|
|
225
|
-
->and($query->calls[0])->toBe(['where', ['
|
|
225
|
+
->and($query->calls[0])->toBe(['where', ['type', 'temperature']])
|
|
226
226
|
->and($query->calls[1])->toBe(['where', ['status', 'active']])
|
|
227
227
|
->and($query->calls[2][0])->toBe('where')
|
|
228
228
|
->and($query->calls[2][1][0])->toBe('last_reading_at')
|
|
@@ -176,45 +176,51 @@ test('customer creation forces the customer type and scopes orders', function ()
|
|
|
176
176
|
expect($customer->orders())->toBeInstanceOf(HasMany::class);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
test('contact observer
|
|
179
|
+
test('contact observer deletion releases the managed account and leaves team members alone', function () {
|
|
180
180
|
$connection = fleetopsSmallModelBoot();
|
|
181
|
-
|
|
181
|
+
config(['activitylog.enabled' => false]);
|
|
182
|
+
app()->bind(Illuminate\Contracts\Config\Repository::class, fn () => config());
|
|
183
|
+
$connection->getSchemaBuilder()->create('drivers', function ($blueprint) {
|
|
184
|
+
$blueprint->increments('id');
|
|
185
|
+
$blueprint->string('uuid')->nullable();
|
|
186
|
+
$blueprint->string('company_uuid')->nullable();
|
|
187
|
+
$blueprint->string('user_uuid')->nullable();
|
|
188
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
189
|
+
});
|
|
190
|
+
$connection->table('users')->insert([
|
|
191
|
+
['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'type' => 'contact'],
|
|
192
|
+
['uuid' => '22222222-2222-4222-8222-222222222222', 'company_uuid' => 'company-1', 'type' => 'user'],
|
|
193
|
+
]);
|
|
182
194
|
|
|
183
|
-
$observer
|
|
184
|
-
$reflection = new ReflectionClass($observer);
|
|
195
|
+
$observer = new ContactObserver();
|
|
185
196
|
|
|
186
197
|
$contact = new Fleetbase\FleetOps\Models\Contact();
|
|
187
|
-
$contact->setRawAttributes(['uuid' => 'contact-1', 'company_uuid' => 'company-1', 'user_uuid' => '
|
|
198
|
+
$contact->setRawAttributes(['uuid' => 'contact-1', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111', 'type' => 'contact'], true);
|
|
188
199
|
$contact->exists = true;
|
|
189
200
|
|
|
190
|
-
$
|
|
191
|
-
$
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
$phoneCheck = $reflection->getMethod('isPhoneUnavailable');
|
|
195
|
-
$phoneCheck->setAccessible(true);
|
|
196
|
-
expect($phoneCheck->invoke($observer, $contact))->toBeTrue();
|
|
201
|
+
$staffContact = new Fleetbase\FleetOps\Models\Contact();
|
|
202
|
+
$staffContact->setRawAttributes(['uuid' => 'contact-2', 'company_uuid' => 'company-1', 'user_uuid' => '22222222-2222-4222-8222-222222222222', 'type' => 'contact'], true);
|
|
203
|
+
$staffContact->exists = true;
|
|
197
204
|
|
|
198
|
-
$contact->email = 'free@example.test';
|
|
199
|
-
$contact->phone = '+6599999999';
|
|
200
|
-
expect($emailCheck->invoke($observer, $contact))->toBeFalse()
|
|
201
|
-
->and($phoneCheck->invoke($observer, $contact))->toBeFalse();
|
|
202
|
-
|
|
203
|
-
// Deletion removes the associated user account
|
|
204
205
|
$observer->deleted($contact);
|
|
205
|
-
|
|
206
|
+
$observer->deleted($staffContact);
|
|
207
|
+
|
|
208
|
+
// The managed account had no other profile, so it is deleted
|
|
209
|
+
expect($connection->table('users')->where('uuid', '11111111-1111-4111-8111-111111111111')->value('deleted_at'))->not->toBeNull()
|
|
210
|
+
->and($connection->table('users')->where('uuid', '22222222-2222-4222-8222-222222222222')->value('deleted_at'))->toBeNull();
|
|
206
211
|
});
|
|
207
212
|
|
|
208
213
|
test('contact observer rejects saves whose email or phone belongs to another account', function () {
|
|
209
214
|
$connection = fleetopsSmallModelBoot();
|
|
210
|
-
$connection->table('users')->insert([
|
|
215
|
+
$connection->table('users')->insert([
|
|
216
|
+
['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'type' => 'contact', 'email' => 'free@example.test', 'phone' => '+6599999999'],
|
|
217
|
+
['uuid' => '22222222-2222-4222-8222-222222222222', 'company_uuid' => 'company-1', 'type' => 'user', 'email' => 'taken@example.test', 'phone' => '+6512345678'],
|
|
218
|
+
]);
|
|
211
219
|
|
|
212
220
|
$observer = new ContactObserver();
|
|
213
221
|
|
|
214
|
-
// The availability checks are private, so the guards can only be reached
|
|
215
|
-
// through saving() with a contact that genuinely collides on a real table.
|
|
216
222
|
// hasUser() keys off a well-formed uuid, so a real one keeps saving() from
|
|
217
|
-
// detouring into account provisioning before it reaches the
|
|
223
|
+
// detouring into account provisioning before it reaches the sync.
|
|
218
224
|
$contact = new Fleetbase\FleetOps\Models\Contact();
|
|
219
225
|
$contact->setRawAttributes([
|
|
220
226
|
'uuid' => 'contact-collide',
|
|
@@ -226,23 +232,16 @@ test('contact observer rejects saves whose email or phone belongs to another acc
|
|
|
226
232
|
], true);
|
|
227
233
|
$contact->exists = true;
|
|
228
234
|
|
|
229
|
-
// wasChanged() reads the post-save change set, which setRawAttributes does
|
|
230
|
-
// not populate — syncChanges promotes the pending edit into it.
|
|
231
235
|
$contact->email = 'taken@example.test';
|
|
232
|
-
$contact->syncChanges();
|
|
233
236
|
|
|
234
237
|
expect(fn () => $observer->saving($contact))
|
|
235
|
-
->toThrow(
|
|
238
|
+
->toThrow(Fleetbase\FleetOps\Exceptions\ProfileIdentityConflictException::class, 'This email is already in use by another account.');
|
|
236
239
|
|
|
237
|
-
// The phone guard sits behind the email one, so it only surfaces once the
|
|
238
|
-
// email is back to an available value
|
|
239
|
-
$contact->syncOriginal();
|
|
240
240
|
$contact->email = 'free@example.test';
|
|
241
241
|
$contact->phone = '+6512345678';
|
|
242
|
-
$contact->syncChanges();
|
|
243
242
|
|
|
244
243
|
expect(fn () => $observer->saving($contact))
|
|
245
|
-
->toThrow(
|
|
244
|
+
->toThrow(Fleetbase\FleetOps\Exceptions\ProfileIdentityConflictException::class, 'This phone number is already in use by another account.');
|
|
246
245
|
});
|
|
247
246
|
|
|
248
247
|
test('tracking status request authorizes by session and rejects duplicates', function () {
|
|
@@ -98,11 +98,16 @@ test('driver observer seams unassign orders and resolve the driver user', functi
|
|
|
98
98
|
->and($connection->table('orders')->where('uuid', 'order-seam-1')->value('driver_assigned_uuid'))->toBeNull()
|
|
99
99
|
->and($connection->table('orders')->where('uuid', 'order-seam-2')->value('driver_assigned_uuid'))->toBe('driver-other');
|
|
100
100
|
|
|
101
|
-
// The linked account resolves
|
|
101
|
+
// The linked account resolves whatever its type: releaseForProfile decides
|
|
102
|
+
// whether a managed account is deleted and leaves a team member alone
|
|
102
103
|
expect(fleetopsObserverSeamInvoke(Fleetbase\FleetOps\Observers\DriverObserver::class, 'findDriverUser', [$driver])?->uuid)->toBe('user-seam-1');
|
|
103
104
|
|
|
105
|
+
$managedDriver = new Driver();
|
|
106
|
+
$managedDriver->setRawAttributes(['uuid' => 'driver-seam-2', 'user_uuid' => 'user-seam-2'], true);
|
|
107
|
+
expect(fleetopsObserverSeamInvoke(Fleetbase\FleetOps\Observers\DriverObserver::class, 'findDriverUser', [$managedDriver])?->uuid)->toBe('user-seam-2');
|
|
108
|
+
|
|
104
109
|
$driverWithoutUser = new Driver();
|
|
105
|
-
$driverWithoutUser->setRawAttributes(['uuid' => 'driver-seam-
|
|
110
|
+
$driverWithoutUser->setRawAttributes(['uuid' => 'driver-seam-3', 'user_uuid' => null], true);
|
|
106
111
|
expect(fleetopsObserverSeamInvoke(Fleetbase\FleetOps\Observers\DriverObserver::class, 'findDriverUser', [$driverWithoutUser]))->toBeNull();
|
|
107
112
|
});
|
|
108
113
|
|
|
@@ -251,3 +251,23 @@ test('connection settings fall back to defaults when the settings table is absen
|
|
|
251
251
|
expect($call('resolveVroomBaseUri'))->toBeString()
|
|
252
252
|
->and($call('resolveVroomApiKey'))->toBeIn([null, '']);
|
|
253
253
|
});
|
|
254
|
+
|
|
255
|
+
test('allocation resolves organization settings from the orders company regardless of session', function (?string $sessionCompany) {
|
|
256
|
+
$connection = fleetopsVroomBoot();
|
|
257
|
+
$connection->table('settings')->insert([
|
|
258
|
+
['key' => 'company.company-2.vroom', 'value' => json_encode(['api_host' => 'https://company-two-vroom.test'])],
|
|
259
|
+
['key' => 'company.company-1.vroom', 'value' => json_encode(['api_host' => 'https://company-one-vroom.test'])],
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
$order = fleetopsVroomOrder('order_vroom_queued', fleetopsVroomPlace('p-9', new Point(1.31, 103.81)), null);
|
|
263
|
+
$order->setAttribute('company_uuid', 'company-2');
|
|
264
|
+
$vehicle = fleetopsVroomVehicle('vehicle_vroom_queued', new Point(1.20, 103.70));
|
|
265
|
+
|
|
266
|
+
Http::fake(['*' => Http::response(['routes' => [], 'unassigned' => [], 'summary' => []], 200)]);
|
|
267
|
+
|
|
268
|
+
// Background allocation must not depend on an absent or unrelated session.
|
|
269
|
+
session(['company' => $sessionCompany]);
|
|
270
|
+
(new VroomOrchestrationEngine())->allocate(collect([$order]), collect([$vehicle]));
|
|
271
|
+
|
|
272
|
+
Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://company-two-vroom.test'));
|
|
273
|
+
})->with([null, 'company-1']);
|
|
@@ -4,6 +4,10 @@ if (!class_exists('Fleetbase\Ai\Support\AiCapabilityRegistry', false)) {
|
|
|
4
4
|
eval('namespace Fleetbase\Ai\Support; class AiCapabilityRegistry { public array $registered = []; public function register($capability) { $this->registered[] = $capability; return $this; } }');
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
if (!class_exists('Fleetbase\Ai\Support\Commands\AiCommandRegistry', false)) {
|
|
8
|
+
eval('namespace Fleetbase\Ai\Support\Commands; class AiCommandRegistry { public array $registered = []; public function registerMany(array $commands) { $this->registered = array_merge($this->registered, $commands); return $this; } }');
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
if (!class_exists('Fleetbase\Ai\Support\AiQueryRegistry', false)) {
|
|
8
12
|
eval('namespace Fleetbase\Ai\Support; class AiQueryRegistry { public array $registered = []; public function register($resource = null) { $this->registered[] = $resource; return $this; } }');
|
|
9
13
|
}
|
|
@@ -44,9 +48,18 @@ test('ai capability registration wires query resources and capabilities', functi
|
|
|
44
48
|
$capabilityRegistry = app(Fleetbase\Ai\Support\AiCapabilityRegistry::class);
|
|
45
49
|
|
|
46
50
|
fwrite(STDERR, "\nDBG ai: " . json_encode([is_object($queryRegistry) ? get_class($queryRegistry) : gettype($queryRegistry), is_object($capabilityRegistry) ? get_class($capabilityRegistry) : gettype($capabilityRegistry), is_object($capabilityRegistry) ? count($capabilityRegistry->registered) : null]) . "\n");
|
|
47
|
-
expect($capabilityRegistry->registered)->toHaveCount(
|
|
51
|
+
expect($capabilityRegistry->registered)->toHaveCount(13)
|
|
48
52
|
->and(collect($capabilityRegistry->registered)->map(fn ($capability) => get_class($capability)))
|
|
49
|
-
->toContain(
|
|
53
|
+
->toContain(
|
|
54
|
+
Fleetbase\FleetOps\Support\Ai\Capabilities\SearchResourcesCapability::class,
|
|
55
|
+
Fleetbase\FleetOps\Support\Ai\Tools\SearchResourcesTool::class,
|
|
56
|
+
Fleetbase\FleetOps\Support\Ai\Tools\CreateOrderTool::class,
|
|
57
|
+
Fleetbase\FleetOps\Support\Ai\Tools\OptimizeOrderRouteTool::class,
|
|
58
|
+
Fleetbase\FleetOps\Support\Ai\Tools\ImportOrdersTool::class,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
$commandRegistry = app(Fleetbase\Ai\Support\Commands\AiCommandRegistry::class);
|
|
62
|
+
expect(collect($commandRegistry->registered)->pluck('id'))->toContain('fleet-ops.orders.create', 'fleet-ops.settings.map.open');
|
|
50
63
|
|
|
51
64
|
// The query resources helper registers the fleet-ops queryables directly
|
|
52
65
|
$freshQueryRegistry = new Fleetbase\Ai\Support\AiQueryRegistry();
|
|
@@ -117,4 +117,46 @@ test('search capability queries scope companies with eager relations', function
|
|
|
117
117
|
->and($helper('vehicleSearchQuery')->count())->toBe(0)
|
|
118
118
|
->and($helper('driverSearchQuery')->count())->toBe(3)
|
|
119
119
|
->and($helper('genericSearchQuery', Fleetbase\FleetOps\Models\Contact::class)->count())->toBe(0);
|
|
120
|
+
|
|
121
|
+
foreach ([Fleetbase\FleetOps\Models\WorkOrder::class, Fleetbase\FleetOps\Models\Maintenance::class, Fleetbase\FleetOps\Models\Device::class, Fleetbase\FleetOps\Models\Sensor::class, Fleetbase\FleetOps\Models\Telematic::class] as $modelClass) {
|
|
122
|
+
expect($helper('genericSearchQuery', $modelClass))->toBeInstanceOf(Illuminate\Database\Eloquent\Builder::class);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('asset status scoped queries resolve a list permission for every supported model', function () {
|
|
127
|
+
fleetopsAssetStatusCapabilityBoot();
|
|
128
|
+
|
|
129
|
+
$capability = (new ReflectionClass(AssetStatusCapability::class))->newInstanceWithoutConstructor();
|
|
130
|
+
$scoped = new ReflectionMethod(AssetStatusCapability::class, 'scopedQuery');
|
|
131
|
+
$scoped->setAccessible(true);
|
|
132
|
+
|
|
133
|
+
foreach ([Fleetbase\FleetOps\Models\Vehicle::class, Fleetbase\FleetOps\Models\Device::class, Fleetbase\FleetOps\Models\Sensor::class, Fleetbase\FleetOps\Models\Telematic::class, Fleetbase\FleetOps\Models\Contact::class] as $modelClass) {
|
|
134
|
+
expect($scoped->invoke($capability, $modelClass))->toBeInstanceOf(Illuminate\Database\Eloquent\Builder::class);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('capability queries apply IAM directives for the list permission when the directive macro is available', function () {
|
|
139
|
+
fleetopsAssetStatusCapabilityBoot();
|
|
140
|
+
|
|
141
|
+
if (!Illuminate\Database\Eloquent\Builder::hasGlobalMacro('applyDirectivesForPermissions')) {
|
|
142
|
+
Illuminate\Database\Eloquent\Builder::macro('applyDirectivesForPermissions', function (string|array $names = []) {
|
|
143
|
+
$GLOBALS['fleetopsAppliedDirectivePermissions'][] = $names;
|
|
144
|
+
|
|
145
|
+
return $this;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
$GLOBALS['fleetopsAppliedDirectivePermissions'] = [];
|
|
150
|
+
|
|
151
|
+
$capability = (new ReflectionClass(AssetStatusCapability::class))->newInstanceWithoutConstructor();
|
|
152
|
+
$scope = new ReflectionMethod(AssetStatusCapability::class, 'scopeToPermission');
|
|
153
|
+
$scope->setAccessible(true);
|
|
154
|
+
$plain = new stdClass();
|
|
155
|
+
|
|
156
|
+
expect($scope->invoke($capability, $plain, 'fleet-ops list driver'))->toBe($plain);
|
|
157
|
+
|
|
158
|
+
$builder = Fleetbase\FleetOps\Models\Driver::query();
|
|
159
|
+
|
|
160
|
+
expect($scope->invoke($capability, $builder, 'fleet-ops list driver'))->toBe($builder)
|
|
161
|
+
->and($GLOBALS['fleetopsAppliedDirectivePermissions'])->toContain('fleet-ops list driver');
|
|
120
162
|
});
|
|
@@ -123,6 +123,11 @@ class FleetOpsOrderInsightsCapabilityFake extends OrderInsightsCapability
|
|
|
123
123
|
return $this->allowed && $permission === 'fleet-ops see order';
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
protected function currency(): string
|
|
127
|
+
{
|
|
128
|
+
return 'USD';
|
|
129
|
+
}
|
|
130
|
+
|
|
126
131
|
protected function orderQuery(?string $companyUuid): mixed
|
|
127
132
|
{
|
|
128
133
|
$this->companies[] = $companyUuid;
|
|
@@ -183,6 +188,7 @@ test('order insights resolve returns bounded aggregate order metrics', function
|
|
|
183
188
|
'authorized' => true,
|
|
184
189
|
'metric' => 'orders',
|
|
185
190
|
'amount_threshold' => 125.50,
|
|
191
|
+
'amount_currency' => 'USD',
|
|
186
192
|
'count' => 7,
|
|
187
193
|
'counts_by_status' => ['completed' => 5, 'active' => 2],
|
|
188
194
|
'sample_order_ids' => ['order_1', 'order_2'],
|
|
@@ -196,7 +202,7 @@ test('order insights resolve returns bounded aggregate order metrics', function
|
|
|
196
202
|
->and($capability->companies)->toBe(['company-123'])
|
|
197
203
|
->and($capability->query->recordedCalls())->toContain(
|
|
198
204
|
['whereBetween', 'created_at', [$start, $end]],
|
|
199
|
-
['whereHas', 'transaction', [['where', 'amount', '>',
|
|
205
|
+
['whereHas', 'transaction', [['where', 'amount', '>', 12550.0]]],
|
|
200
206
|
['count'],
|
|
201
207
|
['selectRaw', 'status, count(*) as aggregate'],
|
|
202
208
|
['groupBy', 'status'],
|
|
@@ -224,3 +230,15 @@ test('order insights resolve omits optional filters when prompt has no date or a
|
|
|
224
230
|
->and($capability->query->recordedCalls())->not->toContain(['whereBetween'])
|
|
225
231
|
->and($capability->query->recordedCalls())->not->toContain(['whereHas']);
|
|
226
232
|
});
|
|
233
|
+
|
|
234
|
+
test('order insights converts major-unit thresholds using the currency exponent', function () {
|
|
235
|
+
$capability = new FleetOpsOrderInsightsCapabilityFake();
|
|
236
|
+
$method = new ReflectionMethod(OrderInsightsCapability::class, 'toMinorUnits');
|
|
237
|
+
$method->setAccessible(true);
|
|
238
|
+
|
|
239
|
+
expect($method->invoke($capability, 500.0, 'USD'))->toBe(50000)
|
|
240
|
+
->and($method->invoke($capability, 125.5, 'usd'))->toBe(12550)
|
|
241
|
+
->and($method->invoke($capability, 1000.0, 'JPY'))->toBe(1000)
|
|
242
|
+
->and($method->invoke($capability, 1.5, 'BHD'))->toBe(1500)
|
|
243
|
+
->and($method->invoke($capability, 10.0, 'NOT-A-CURRENCY'))->toBe(1000);
|
|
244
|
+
});
|