@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
|
@@ -67,6 +67,17 @@ class FleetOpsSearchResourcesCapabilityProbe extends SearchResourcesCapability
|
|
|
67
67
|
public array $queries = [];
|
|
68
68
|
public array $appliedLikes = [];
|
|
69
69
|
|
|
70
|
+
public array $failing = [];
|
|
71
|
+
|
|
72
|
+
public function exposeSearchTerms(string $prompt): array
|
|
73
|
+
{
|
|
74
|
+
return $this->searchTerms($prompt);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected function reportSearchFailure(string $resource, Throwable $e): void
|
|
78
|
+
{
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
public function exposePromptMatches(string $prompt): bool
|
|
71
82
|
{
|
|
72
83
|
return $this->matchesPrompt($prompt);
|
|
@@ -103,6 +114,10 @@ class FleetOpsSearchResourcesCapabilityProbe extends SearchResourcesCapability
|
|
|
103
114
|
|
|
104
115
|
protected function genericSearchQuery(string $modelClass)
|
|
105
116
|
{
|
|
117
|
+
if ($modelClass === Sensor::class && in_array('sensors', $this->failing, true)) {
|
|
118
|
+
throw new RuntimeException("SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sensor_type'");
|
|
119
|
+
}
|
|
120
|
+
|
|
106
121
|
return match ($modelClass) {
|
|
107
122
|
WorkOrder::class => $this->queries['work_orders'],
|
|
108
123
|
Maintenance::class => $this->queries['maintenances'],
|
|
@@ -197,7 +212,7 @@ test('search resources resolve returns authorized resource summaries across all
|
|
|
197
212
|
$result = $capability->resolve(new AiTask(['prompt' => 'Find order ORDER-1 vehicle VEH-1 driver DRV-1 device DEV-1 sensor SNS-1 telematic TEL-1']));
|
|
198
213
|
|
|
199
214
|
expect($capability->exposePromptMatches('look up work order WO-1'))->toBeTrue()
|
|
200
|
-
->and($result['query_terms'])->toBe(['ORDER-1', 'VEH-1', 'DRV-1', '
|
|
215
|
+
->and($result['query_terms'])->toBe(['ORDER-1', 'VEH-1', 'DRV-1', 'DEV-1', 'SNS-1', 'TEL-1'])
|
|
201
216
|
->and($result['results'])->toHaveKeys(['orders', 'vehicles', 'drivers', 'work_orders', 'maintenances', 'devices', 'sensors', 'telematics'])
|
|
202
217
|
->and($result['results']['orders'][0])->toMatchArray([
|
|
203
218
|
'id' => 'order_public',
|
|
@@ -235,3 +250,72 @@ test('search resources resolve returns authorized resource summaries across all
|
|
|
235
250
|
->and($capability->queries['orders']->calls)->toContain(['limit', 5], ['get'])
|
|
236
251
|
->and($capability->appliedLikes)->not->toBeEmpty();
|
|
237
252
|
});
|
|
253
|
+
|
|
254
|
+
function fleetopsSearchResourcesEmptyQueries(): array
|
|
255
|
+
{
|
|
256
|
+
return collect(['orders', 'vehicles', 'drivers', 'work_orders', 'maintenances', 'devices', 'sensors', 'telematics'])
|
|
257
|
+
->mapWithKeys(fn ($key) => [$key => new FleetOpsSearchResourcesQueryFake()])
|
|
258
|
+
->all();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
test('search terms ignore ordinary words from real production prompts', function () {
|
|
262
|
+
$capability = new FleetOpsSearchResourcesCapabilityProbe();
|
|
263
|
+
|
|
264
|
+
expect($capability->exposeSearchTerms('i want turn this into a platform where i add cisterns and on a different page BUSINESS leave they orders'))->toBe([])
|
|
265
|
+
->and($capability->exposeSearchTerms('Create a few dummy orders so we can test with it'))->toBe([])
|
|
266
|
+
->and($capability->exposeSearchTerms('download order import template'))->toBe([])
|
|
267
|
+
->and($capability->exposeSearchTerms('config driver app ยังไง'))->toBe([])
|
|
268
|
+
->and($capability->exposeSearchTerms('como activar un conductor'))->toBe([]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('search terms keep record references such as public ids, plates, emails, quoted and capitalized names', function () {
|
|
272
|
+
$capability = new FleetOpsSearchResourcesCapabilityProbe();
|
|
273
|
+
|
|
274
|
+
expect($capability->exposeSearchTerms('status of order order_yhkejdnzgz'))->toBe(['order_yhkejdnzgz'])
|
|
275
|
+
->and($capability->exposeSearchTerms('vehicle plate SBA1234Z'))->toBe(['SBA1234Z'])
|
|
276
|
+
->and($capability->exposeSearchTerms('driver with email ada@example.com'))->toBe(['ada@example.com'])
|
|
277
|
+
->and($capability->exposeSearchTerms('where is driver Jane Doe right now'))->toBe(['Jane', 'Doe'])
|
|
278
|
+
->and($capability->exposeSearchTerms('find "north depot" vehicles'))->toBe(['north depot']);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('search resources skips querying when the prompt has no record reference', function () {
|
|
282
|
+
$capability = new FleetOpsSearchResourcesCapabilityProbe();
|
|
283
|
+
$capability->allowedPermissions = $capability->permissions();
|
|
284
|
+
$capability->queries = fleetopsSearchResourcesEmptyQueries();
|
|
285
|
+
|
|
286
|
+
$result = $capability->resolve(new AiTask(['prompt' => 'how do I create a new order']));
|
|
287
|
+
|
|
288
|
+
expect($result['query_terms'])->toBe([])
|
|
289
|
+
->and($result['results'])->toBe([])
|
|
290
|
+
->and($capability->appliedLikes)->toBe([]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('search resources never searches the dropped sensor_type column or status/uuid columns', function () {
|
|
294
|
+
$capability = new FleetOpsSearchResourcesCapabilityProbe();
|
|
295
|
+
$capability->allowedPermissions = $capability->permissions();
|
|
296
|
+
$capability->queries = fleetopsSearchResourcesEmptyQueries();
|
|
297
|
+
|
|
298
|
+
$capability->resolve(new AiTask(['prompt' => 'find SNS-1']));
|
|
299
|
+
|
|
300
|
+
$columns = collect($capability->appliedLikes)->flatMap(fn ($like) => $like[0])->unique()->all();
|
|
301
|
+
|
|
302
|
+
expect($columns)->not->toContain('sensor_type')
|
|
303
|
+
->and($columns)->not->toContain('status')
|
|
304
|
+
->and($columns)->not->toContain('uuid')
|
|
305
|
+
->and($columns)->not->toContain('type');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('a failing resource search does not discard the other results or leak the error', function () {
|
|
309
|
+
$vehicle = fleetopsSearchResourcesModel(Vehicle::class, ['public_id' => 'vehicle_public', 'uuid' => 'vehicle-uuid', 'name' => 'Van 12']);
|
|
310
|
+
|
|
311
|
+
$capability = new FleetOpsSearchResourcesCapabilityProbe();
|
|
312
|
+
$capability->allowedPermissions = $capability->permissions();
|
|
313
|
+
$capability->failing = ['sensors'];
|
|
314
|
+
$capability->queries = array_merge(fleetopsSearchResourcesEmptyQueries(), ['vehicles' => new FleetOpsSearchResourcesQueryFake([$vehicle])]);
|
|
315
|
+
|
|
316
|
+
$result = $capability->resolve(new AiTask(['prompt' => 'find VEH-12']));
|
|
317
|
+
|
|
318
|
+
expect($result['results'])->toHaveKey('vehicles')
|
|
319
|
+
->and($result['unavailable_search'])->toBe(['sensors'])
|
|
320
|
+
->and(json_encode($result))->not->toContain('SQLSTATE');
|
|
321
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Support\Ai\FleetOpsAiConsoleCommands;
|
|
4
|
+
|
|
5
|
+
test('fleet-ops console commands are unique, complete, and only navigate or call allowlisted services', function () {
|
|
6
|
+
$commands = collect(FleetOpsAiConsoleCommands::all());
|
|
7
|
+
|
|
8
|
+
expect($commands->pluck('id')->duplicates())->toBeEmpty()
|
|
9
|
+
->and($commands->every(fn ($command) => filled($command['label']) && filled($command['breadcrumb']) && filled($command['description']) && !empty($command['steps'])))->toBeTrue()
|
|
10
|
+
->and($commands->every(fn ($command) => collect($command['steps'])->every(fn ($step) => $step['type'] === 'navigate'
|
|
11
|
+
? str_starts_with($step['route'], 'console.fleet-ops.')
|
|
12
|
+
: $step['type'] === 'service' && $step['engine'] === FleetOpsAiConsoleCommands::ENGINE && preg_match('/^[a-zA-Z]+$/', $step['method']))))->toBeTrue()
|
|
13
|
+
->and($commands->every(fn ($command) => ($command['audience'] ?? 'end_user') === 'end_user'))->toBeTrue();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('fleet-ops console commands cover create, import, view, and settings', function () {
|
|
17
|
+
$commands = collect(FleetOpsAiConsoleCommands::all())->keyBy('id');
|
|
18
|
+
|
|
19
|
+
expect($commands['fleet-ops.orders.create']['steps'])->toBe([['type' => 'navigate', 'route' => 'console.fleet-ops.operations.orders.index.new']])
|
|
20
|
+
->and($commands['fleet-ops.orders.create']['permissions'])->toBe(['fleet-ops create order'])
|
|
21
|
+
->and($commands['fleet-ops.orders.import']['steps'][1])->toBe(['type' => 'service', 'engine' => '@fleetbase/fleetops-engine', 'service' => 'order-actions', 'method' => 'importOrders'])
|
|
22
|
+
->and($commands['fleet-ops.customers.import']['steps'][1]['service'])->toBe('customer-actions')
|
|
23
|
+
->and($commands['fleet-ops.customers.import']['permissions'])->toBe(['fleet-ops import contact'])
|
|
24
|
+
->and($commands)->not->toHaveKey('fleet-ops.fleets.import')
|
|
25
|
+
->and($commands['fleet-ops.drivers.view']['steps'][0]['models'])->toBe(['public_id'])
|
|
26
|
+
->and($commands['fleet-ops.drivers.view']['params'])->toHaveKey('public_id')
|
|
27
|
+
->and($commands['fleet-ops.settings.map.open']['steps'][0]['route'])->toBe('console.fleet-ops.settings.map')
|
|
28
|
+
->and($commands['fleet-ops.settings.map.open']['permissions'])->toBe(['fleet-ops view map-settings'])
|
|
29
|
+
->and($commands['fleet-ops.settings.map.open']['keywords'])->toContain('google maps', 'view maps')
|
|
30
|
+
->and($commands['fleet-ops.settings.scheduling.open']['permissions'])->toBe([]);
|
|
31
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\Ai\Models\AiTask;
|
|
4
|
+
use Fleetbase\Ai\Support\AiAudience;
|
|
5
|
+
use Fleetbase\Ai\Support\AiToolContext;
|
|
6
|
+
use Fleetbase\FleetOps\Models\OrderConfig;
|
|
7
|
+
use Fleetbase\FleetOps\Support\Ai\Tools\CreateOrderTool;
|
|
8
|
+
use Fleetbase\FleetOps\Support\Ai\Tools\SearchResourcesTool;
|
|
9
|
+
|
|
10
|
+
class FleetOpsCreateOrderToolProbe extends CreateOrderTool
|
|
11
|
+
{
|
|
12
|
+
public array $resolvedPlaces = [];
|
|
13
|
+
|
|
14
|
+
public function exposeBuildDraft(AiTask $task, array $input = []): array
|
|
15
|
+
{
|
|
16
|
+
return $this->buildDraft($task, $input);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected function can(string $permission): bool
|
|
20
|
+
{
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected function draftFromPrompt(string $prompt): array
|
|
25
|
+
{
|
|
26
|
+
return array_filter([
|
|
27
|
+
'order_config_uuid' => 'order-config-uuid',
|
|
28
|
+
'type' => 'transport',
|
|
29
|
+
'payload' => $prompt !== '' ? ['pickup_query' => 'Parsed from prompt'] : [],
|
|
30
|
+
'dispatched' => false,
|
|
31
|
+
], fn ($value) => $value !== []) + ['payload' => []];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected function resolvePlace(?string $query): ?array
|
|
35
|
+
{
|
|
36
|
+
return $this->resolvedPlaces[$query] ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected function resolveOrderConfig(array $draft): ?OrderConfig
|
|
40
|
+
{
|
|
41
|
+
$config = new OrderConfig();
|
|
42
|
+
$config->setRawAttributes(['uuid' => 'order-config-uuid', 'key' => 'transport', 'name' => 'Transport'], true);
|
|
43
|
+
|
|
44
|
+
return $config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected function resolveDriver(array $draft): ?Fleetbase\FleetOps\Models\Driver
|
|
48
|
+
{
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected function resolveVehicle(array $draft): ?Fleetbase\FleetOps\Models\Vehicle
|
|
53
|
+
{
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class FleetOpsSearchResourcesToolProbe extends SearchResourcesTool
|
|
59
|
+
{
|
|
60
|
+
public array $searched = [];
|
|
61
|
+
|
|
62
|
+
protected function searchAll(array $terms, ?array $types = null): array
|
|
63
|
+
{
|
|
64
|
+
$this->searched[] = [$terms, $types];
|
|
65
|
+
|
|
66
|
+
return ['query_terms' => $terms, 'results' => []];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test('create order tool prepares one draft preview per call from structured fields without parsing the prompt', function () {
|
|
71
|
+
$tool = new FleetOpsCreateOrderToolProbe();
|
|
72
|
+
$task = new AiTask(['prompt' => 'Create some dummy orders with made up data', 'metadata' => []]);
|
|
73
|
+
$context = new AiToolContext($task, new AiAudience(false, ['fleet-ops create order']));
|
|
74
|
+
|
|
75
|
+
$first = $tool->invoke($task, [
|
|
76
|
+
'pickup' => 'Kabelweg 57, Amsterdam',
|
|
77
|
+
'dropoff' => 'Danzigerkade 15, Amsterdam',
|
|
78
|
+
'scheduled_at' => '2026-09-16T09:30:00+02:00',
|
|
79
|
+
'driver' => ' ',
|
|
80
|
+
'notes' => 'Test order',
|
|
81
|
+
], $context);
|
|
82
|
+
$second = $tool->invoke($task, ['pickup' => 'Diemerhof 32, Diemen', 'dropoff' => 'Herikerbergweg 238, Amsterdam', 'dispatch' => true], $context);
|
|
83
|
+
|
|
84
|
+
expect($first['preview_id'])->toBe('preview-1')
|
|
85
|
+
->and($second['preview_id'])->toBe('preview-2')
|
|
86
|
+
->and($first['status'])->toBe('draft_shown_to_user')
|
|
87
|
+
->and($first['message'])->toContain('does not exist until the user reviews the card')
|
|
88
|
+
->and($context->actionPreviews)->toHaveCount(2)
|
|
89
|
+
->and($context->actionPreviews[0]['draft']['payload']['pickup_query'])->toBe('Kabelweg 57, Amsterdam')
|
|
90
|
+
->and($context->actionPreviews[0]['draft']['scheduled_at'])->toBe('2026-09-16T09:30:00+02:00')
|
|
91
|
+
->and($context->actionPreviews[0]['draft']['notes'])->toBe('Test order')
|
|
92
|
+
->and($context->actionPreviews[0]['draft'])->not->toHaveKey('driver_query')
|
|
93
|
+
->and($context->actionPreviews[0]['draft']['dispatched'])->toBeFalse()
|
|
94
|
+
->and($context->actionPreviews[1]['draft']['payload']['pickup_query'])->toBe('Diemerhof 32, Diemen')
|
|
95
|
+
->and($context->actionPreviews[1]['draft']['dispatched'])->toBeTrue()
|
|
96
|
+
->and(json_encode($context->actionPreviews))->not->toContain('Parsed from prompt')
|
|
97
|
+
->and($tool->toolName())->toBe('propose_create_order')
|
|
98
|
+
->and($tool->key())->toBe('fleet-ops.create_order')
|
|
99
|
+
->and($tool->toolDescription())->toContain('NOT created by this tool')
|
|
100
|
+
->and($tool->toolParameters()['required'])->toBe(['pickup', 'dropoff'])
|
|
101
|
+
->and($tool->availableFor($context))->toBeTrue()
|
|
102
|
+
->and($tool->availableFor(new AiToolContext($task, new AiAudience(false, []))))->toBeFalse();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('create order drafts refresh from the edited preview and still parse prompts in keyword mode', function () {
|
|
106
|
+
$tool = new FleetOpsCreateOrderToolProbe();
|
|
107
|
+
$task = new AiTask(['prompt' => 'create order', 'metadata' => ['action_previews' => [['draft' => ['notes' => 'first preview']]]]]);
|
|
108
|
+
|
|
109
|
+
$refreshed = $tool->exposeBuildDraft($task, ['existing_draft' => ['notes' => 'second preview'], 'draft' => ['dispatched' => true]]);
|
|
110
|
+
$keyword = $tool->exposeBuildDraft($task, ['dispatched' => false]);
|
|
111
|
+
|
|
112
|
+
expect($refreshed['notes'])->toBe('second preview')
|
|
113
|
+
->and($refreshed['dispatched'])->toBeTrue()
|
|
114
|
+
->and($refreshed['payload']['pickup_query'])->toBe('Parsed from prompt')
|
|
115
|
+
->and($keyword['notes'])->toBe('first preview')
|
|
116
|
+
->and($keyword)->not->toHaveKey('existing_draft');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('search tool searches the requested phrase and identifiers for the chosen record types', function () {
|
|
120
|
+
$tool = new FleetOpsSearchResourcesToolProbe();
|
|
121
|
+
$task = new AiTask(['prompt' => 'where is Jane']);
|
|
122
|
+
$context = new AiToolContext($task, new AiAudience(false, ['fleet-ops see driver']));
|
|
123
|
+
|
|
124
|
+
$tool->invoke($task, ['query' => 'Jane Doe', 'types' => ['drivers', 'pets']], $context);
|
|
125
|
+
$tool->invoke($task, ['query' => 'order order_yhkejdnzgz'], $context);
|
|
126
|
+
|
|
127
|
+
expect($tool->searched[0])->toBe([['Jane Doe', 'Doe'], ['drivers']])
|
|
128
|
+
->and($tool->searched[1])->toBe([['order order_yhkejdnzgz', 'order_yhkejdnzgz'], null])
|
|
129
|
+
->and($tool->invoke($task, ['query' => ' x '], $context))->toBe(['error' => 'Provide an identifier or name to search for.'])
|
|
130
|
+
->and($tool->toolName())->toBe('fleetops_search')
|
|
131
|
+
->and($tool->key())->toBe('fleet-ops.search_resources')
|
|
132
|
+
->and($tool->toolDescription())->toContain('particular record')
|
|
133
|
+
->and($tool->toolParameters()['properties']['types']['items']['enum'])->toBe(SearchResourcesTool::TYPES)
|
|
134
|
+
->and($tool->availableFor($context))->toBeTrue()
|
|
135
|
+
->and($tool->availableFor(new AiToolContext($task, new AiAudience(false, []))))->toBeFalse();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('search tool narrows the real search to the requested types', function () {
|
|
139
|
+
$tool = new class extends SearchResourcesTool {
|
|
140
|
+
public array $called = [];
|
|
141
|
+
|
|
142
|
+
protected function orders(array $terms): array
|
|
143
|
+
{
|
|
144
|
+
$this->called[] = 'orders';
|
|
145
|
+
|
|
146
|
+
return [['id' => 'order_1']];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected function drivers(array $terms): array
|
|
150
|
+
{
|
|
151
|
+
$this->called[] = 'drivers';
|
|
152
|
+
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
$result = $tool->invoke(new AiTask(['prompt' => '']), ['query' => 'ORDER-1', 'types' => ['orders']], new AiToolContext(new AiTask(), new AiAudience(true)));
|
|
158
|
+
|
|
159
|
+
expect($tool->called)->toBe(['orders'])
|
|
160
|
+
->and($result['results'])->toBe(['orders' => [['id' => 'order_1']]]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
class FleetOpsOptimizeOrderRouteToolProbe extends Fleetbase\FleetOps\Support\Ai\Tools\OptimizeOrderRouteTool
|
|
164
|
+
{
|
|
165
|
+
public array $previewedPrompts = [];
|
|
166
|
+
|
|
167
|
+
public function preview(AiTask $task, array $input = []): array
|
|
168
|
+
{
|
|
169
|
+
$this->previewedPrompts[] = $task->prompt;
|
|
170
|
+
|
|
171
|
+
return [
|
|
172
|
+
'action' => $this->key(),
|
|
173
|
+
'ready' => true,
|
|
174
|
+
'message' => 'Fleetbase AI prepared an optimized waypoint sequence. Review it before applying.',
|
|
175
|
+
'missing_fields' => [],
|
|
176
|
+
'fields' => [['label' => 'Order', 'value' => 'order_abc']],
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
test('optimize route tool proposes a reviewable preview for the order the model named', function () {
|
|
182
|
+
$tool = new FleetOpsOptimizeOrderRouteToolProbe();
|
|
183
|
+
$task = new AiTask(['prompt' => 'can you optimise the route please', 'metadata' => []]);
|
|
184
|
+
$context = new AiToolContext($task, new AiAudience(false, ['fleet-ops optimize order', 'fleet-ops update-route-for order']));
|
|
185
|
+
|
|
186
|
+
$result = $tool->invoke($task, ['order' => 'order_abc'], $context);
|
|
187
|
+
|
|
188
|
+
expect($result['preview_id'])->toBe('preview-1')
|
|
189
|
+
->and($result['ready'])->toBeTrue()
|
|
190
|
+
->and($result['status'])->toBe('proposal_shown_to_user')
|
|
191
|
+
->and($result['fields'])->toBe([['label' => 'Order', 'value' => 'order_abc']])
|
|
192
|
+
->and($result['missing_fields'])->toBe([])
|
|
193
|
+
// The capability finds the order in the prompt text, so the tool hands it the id the model gave.
|
|
194
|
+
->and($tool->previewedPrompts)->toBe(['order_abc'])
|
|
195
|
+
->and($task->prompt)->toBe('can you optimise the route please')
|
|
196
|
+
->and($context->actionPreviews)->toHaveCount(1)
|
|
197
|
+
->and($tool->toolName())->toBe('propose_optimize_order_route')
|
|
198
|
+
->and($tool->key())->toBe('fleet-ops.optimize_order_route')
|
|
199
|
+
->and($tool->toolDescription())->toContain('NOT changed by this tool')
|
|
200
|
+
->and($tool->toolParameters()['required'])->toBe(['order'])
|
|
201
|
+
->and($tool->availableFor($context))->toBeTrue()
|
|
202
|
+
->and($tool->availableFor(new AiToolContext($task, new AiAudience(false, ['fleet-ops optimize order']))))->toBeFalse();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('optimize route tool refuses to build a preview without an order', function () {
|
|
206
|
+
$tool = new FleetOpsOptimizeOrderRouteToolProbe();
|
|
207
|
+
$task = new AiTask(['prompt' => 'optimise a route', 'metadata' => []]);
|
|
208
|
+
$context = new AiToolContext($task, new AiAudience(false, ['fleet-ops optimize order', 'fleet-ops update-route-for order']));
|
|
209
|
+
|
|
210
|
+
expect($tool->invoke($task, ['order' => ' '], $context))->toBe([
|
|
211
|
+
'error' => 'invalid_arguments',
|
|
212
|
+
'message' => 'An order id is required to optimize a route.',
|
|
213
|
+
])
|
|
214
|
+
->and($tool->invoke($task, [], $context)['error'])->toBe('invalid_arguments')
|
|
215
|
+
->and($context->actionPreviews)->toBe([])
|
|
216
|
+
->and($tool->previewedPrompts)->toBe([]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
class FleetOpsImportOrdersToolProbe extends Fleetbase\FleetOps\Support\Ai\Tools\ImportOrdersTool
|
|
220
|
+
{
|
|
221
|
+
protected function can(string $permission): bool
|
|
222
|
+
{
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
test('import tool reports what an import needs and never claims to have imported anything', function () {
|
|
228
|
+
$tool = new FleetOpsImportOrdersToolProbe();
|
|
229
|
+
$task = new AiTask(['prompt' => 'can I import orders from a spreadsheet', 'metadata' => []]);
|
|
230
|
+
$context = new AiToolContext($task, new AiAudience(false, ['fleet-ops import order']));
|
|
231
|
+
|
|
232
|
+
$result = $tool->invoke($task, [], $context);
|
|
233
|
+
|
|
234
|
+
expect($result['accepted_sources'])->toBe(['xlsx', 'csv'])
|
|
235
|
+
->and($result['minimum_columns'])->toContain('pickup address or pickup place')
|
|
236
|
+
->and($result['can_import_here'])->toBeFalse()
|
|
237
|
+
->and($result['message'])->toContain('will not process uploaded spreadsheets')
|
|
238
|
+
->and($context->actionPreviews)->toBe([])
|
|
239
|
+
->and($tool->toolName())->toBe('fleetops_import_requirements')
|
|
240
|
+
->and($tool->key())->toBe('fleet-ops.import_orders_preview')
|
|
241
|
+
->and($tool->toolDescription())->toContain('does NOT import anything')
|
|
242
|
+
->and($tool->toolParameters()['properties'])->toBeInstanceOf(stdClass::class)
|
|
243
|
+
->and(json_encode($tool->toolParameters()['properties']))->toBe('{}')
|
|
244
|
+
->and($tool->availableFor($context))->toBeTrue()
|
|
245
|
+
->and($tool->availableFor(new AiToolContext($task, new AiAudience(false, []))))->toBeFalse();
|
|
246
|
+
});
|
|
@@ -96,7 +96,7 @@ test('permission checks for non admin users delegate to the gate', function () {
|
|
|
96
96
|
->and(fn () => fleetopsAiPermissionsHelper('canAll')->invoke($capability, ['fleet-ops list order']))->toThrow(TypeError::class);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
test('search terms extract identifiers and fall back to the raw prompt', function () {
|
|
99
|
+
test('search terms extract identifiers and never fall back to the raw prompt', function () {
|
|
100
100
|
fleetopsAiPermissionsBoot();
|
|
101
101
|
$capability = new OperationalQueryCapability();
|
|
102
102
|
$searchTerms = fleetopsAiPermissionsHelper('searchTerms');
|
|
@@ -105,8 +105,8 @@ test('search terms extract identifiers and fall back to the raw prompt', functio
|
|
|
105
105
|
expect($terms)->toContain('TRK-12345', 'atlas99')
|
|
106
106
|
->and($terms)->not->toContain('find', 'order');
|
|
107
107
|
|
|
108
|
-
//
|
|
109
|
-
expect($searchTerms->invoke($capability, 'find order'))->toBe([
|
|
108
|
+
// Prompts without a record reference produce no search terms, so nothing is searched
|
|
109
|
+
expect($searchTerms->invoke($capability, 'find order'))->toBe([]);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
test('where like any matches across columns and terms', function () {
|
|
@@ -38,6 +38,12 @@ function fleetopsLiveFleetUseInMemoryConnection(): SQLiteConnection
|
|
|
38
38
|
$connection->statement('create table drivers (uuid varchar(64) primary key, public_id varchar(64) null, user_uuid varchar(64) null, company_uuid varchar(64) null, vehicle_uuid varchar(64) null, current_job_uuid varchar(64) null, avatar_url varchar(255) null, location blob null, heading numeric null, online integer null, last_location_update_at datetime null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
39
39
|
$connection->statement('create table vehicles (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, driver_uuid varchar(64) null, photo_uuid varchar(64) null, avatar_url varchar(255) null, name varchar(255) null, year integer null, make varchar(255) null, model varchar(255) null, trim varchar(255) null, plate_number varchar(64) null, location blob null, heading numeric null, online integer null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
40
40
|
$connection->statement('create table orders (uuid varchar(64) primary key, public_id varchar(64) null, internal_id varchar(64) null, company_uuid varchar(64) null, driver_assigned_uuid varchar(64) null, status varchar(64) null, tracking_number_uuid varchar(64) null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
41
|
+
// Tables the markers' index-resource cards read: devices, coupled trailers and positions.
|
|
42
|
+
$connection->statement('create table devices (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, attachable_uuid varchar(64) null, attachable_type varchar(255) null, name varchar(255) null, device_id varchar(64) null, serial_number varchar(64) null, imei varchar(64) null, provider varchar(64) null, status varchar(64) null, online integer null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
43
|
+
$connection->statement('create table asset_connections (uuid varchar(64) primary key, company_uuid varchar(64) null, connector_uuid varchar(64) null, connector_type varchar(255) null, connected_uuid varchar(64) null, connected_type varchar(255) null, connected_at datetime null, disconnected_at datetime null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
44
|
+
$connection->statement('create table assets (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, asset_class varchar(64) null, name varchar(255) null, code varchar(64) null, type varchar(64) null, status varchar(64) null, plate_number varchar(64) null, online integer null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
45
|
+
$connection->statement('create table positions (uuid varchar(64) primary key, company_uuid varchar(64) null, subject_uuid varchar(64) null, subject_type varchar(255) null, order_uuid varchar(64) null, speed numeric null, heading numeric null, coordinates blob null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
46
|
+
$connection->statement('create table tracking_numbers (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, owner_uuid varchar(64) null, owner_type varchar(255) null, status_uuid varchar(64) null, tracking_number varchar(64) null, region varchar(64) null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
41
47
|
$connection->statement('create table files (uuid varchar(64) primary key, type varchar(64) null, url varchar(255) null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
42
48
|
|
|
43
49
|
$resolver = new ConnectionResolver([
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Support\Authorization;
|
|
4
|
+
use Fleetbase\FleetOps\Traits\AuthorizesMethods;
|
|
5
|
+
use Fleetbase\Models\User;
|
|
6
|
+
use Illuminate\Database\ConnectionResolver;
|
|
7
|
+
use Illuminate\Database\Eloquent\Model;
|
|
8
|
+
use Illuminate\Database\SQLiteConnection;
|
|
9
|
+
use Illuminate\Http\Exceptions\HttpResponseException;
|
|
10
|
+
use Illuminate\Http\Request;
|
|
11
|
+
use Illuminate\Routing\Controller;
|
|
12
|
+
|
|
13
|
+
if (!function_exists('Fleetbase\\Support\\auth')) {
|
|
14
|
+
eval('namespace Fleetbase\\Support; function auth() { return \\app("fleetops.authorization.test-auth"); }');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!function_exists('Fleetbase\\Support\\session')) {
|
|
18
|
+
eval('namespace Fleetbase\\Support; function session($key = null, $default = null) { return \\session($key, $default); }');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class FleetOpsAuthorizationUser extends User
|
|
22
|
+
{
|
|
23
|
+
public array $grantedPermissions = [];
|
|
24
|
+
public bool $permissionStoreUnavailable = false;
|
|
25
|
+
|
|
26
|
+
public function hasPermissionTo($permission, $guardName = null): bool
|
|
27
|
+
{
|
|
28
|
+
if ($this->permissionStoreUnavailable) {
|
|
29
|
+
throw new RuntimeException('Permission store unavailable');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return in_array($permission->name, $this->grantedPermissions, true);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
beforeEach(function () {
|
|
37
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
38
|
+
$resolver = new ConnectionResolver(['mysql' => $connection]);
|
|
39
|
+
$resolver->setDefaultConnection('mysql');
|
|
40
|
+
Model::setConnectionResolver($resolver);
|
|
41
|
+
config()->set('auth.defaults.guard', 'sanctum');
|
|
42
|
+
config()->set('permission.table_names.permissions', 'permissions');
|
|
43
|
+
$connection->getSchemaBuilder()->create('permissions', function ($table) {
|
|
44
|
+
$table->string('id')->primary();
|
|
45
|
+
$table->string('name');
|
|
46
|
+
$table->string('guard_name')->default('sanctum');
|
|
47
|
+
});
|
|
48
|
+
$connection->table('permissions')->insert([
|
|
49
|
+
['id' => 'optimize', 'name' => 'fleet-ops optimize order'],
|
|
50
|
+
['id' => 'dispatch', 'name' => 'fleet-ops dispatch order'],
|
|
51
|
+
['id' => 'resource', 'name' => 'fleet-ops * order'],
|
|
52
|
+
['id' => 'service', 'name' => 'fleet-ops *'],
|
|
53
|
+
]);
|
|
54
|
+
session(['user' => null]);
|
|
55
|
+
$this->user = new FleetOpsAuthorizationUser();
|
|
56
|
+
$this->user->setRawAttributes(['type' => 'user'], true);
|
|
57
|
+
$this->auth = new class($this->user) {
|
|
58
|
+
public bool $unavailable = false;
|
|
59
|
+
|
|
60
|
+
public function __construct(public ?User $currentUser)
|
|
61
|
+
{
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public function user(): ?User
|
|
65
|
+
{
|
|
66
|
+
if ($this->unavailable) {
|
|
67
|
+
throw new RuntimeException('Session unavailable');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return $this->currentUser;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
app()->instance('fleetops.authorization.test-auth', $this->auth);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('operation authorization accepts exact and wildcard grants', function (string $grant) {
|
|
77
|
+
$this->user->grantedPermissions = [$grant];
|
|
78
|
+
|
|
79
|
+
expect(Authorization::canAny('optimize order'))->toBeTrue();
|
|
80
|
+
Authorization::authorize('optimize order');
|
|
81
|
+
})->with(['fleet-ops optimize order', 'fleet-ops * order', 'fleet-ops *']);
|
|
82
|
+
|
|
83
|
+
test('operation authorization accepts any requested permission and rejects missing grants', function () {
|
|
84
|
+
$this->user->grantedPermissions = ['fleet-ops dispatch order'];
|
|
85
|
+
|
|
86
|
+
expect(Authorization::canAny('optimize order'))->toBeFalse()
|
|
87
|
+
->and(Authorization::canAny('optimize order', 'dispatch order'))->toBeTrue()
|
|
88
|
+
->and(Authorization::canAny('list unseeded-resource'))->toBeTrue();
|
|
89
|
+
expect(fn () => Authorization::authorize('optimize order'))->toThrow(HttpResponseException::class);
|
|
90
|
+
|
|
91
|
+
$this->user->permissionStoreUnavailable = true;
|
|
92
|
+
expect(Authorization::canAny('dispatch order'))->toBeFalse();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('platform administrators pass operations while system settings reject ordinary users', function () {
|
|
96
|
+
expect(fn () => Authorization::authorizeAdmin())->toThrow(HttpResponseException::class);
|
|
97
|
+
$this->user->setAttribute('type', 'admin');
|
|
98
|
+
expect(Authorization::canAny('optimize order'))->toBeTrue();
|
|
99
|
+
Authorization::authorizeAdmin();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('authorization fails closed when the authenticated user is missing or cannot be read', function () {
|
|
103
|
+
$this->auth->currentUser = null;
|
|
104
|
+
expect(Authorization::canAny('optimize order'))->toBeFalse();
|
|
105
|
+
expect(fn () => Authorization::authorizeAdmin())->toThrow(HttpResponseException::class);
|
|
106
|
+
$this->auth->unavailable = true;
|
|
107
|
+
expect(Authorization::canAny('optimize order'))->toBeFalse();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('controller middleware applies the mapped operation and administrator checks', function () {
|
|
111
|
+
$controller = new class extends Controller {
|
|
112
|
+
use AuthorizesMethods;
|
|
113
|
+
|
|
114
|
+
public function __construct()
|
|
115
|
+
{
|
|
116
|
+
$this->authorizeMethods(['optimize' => ['optimize order', 'dispatch order'], 'settings' => 'admin']);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
$middleware = $controller->getMiddleware();
|
|
120
|
+
$request = Request::create('/');
|
|
121
|
+
$this->user->grantedPermissions = ['fleet-ops dispatch order'];
|
|
122
|
+
|
|
123
|
+
expect($middleware[0]['options']['only'])->toBe(['optimize'])
|
|
124
|
+
->and($middleware[0]['middleware']($request, fn ($nextRequest) => $nextRequest))->toBe($request);
|
|
125
|
+
expect(fn () => $middleware[1]['middleware']($request, fn () => 'allowed'))->toThrow(HttpResponseException::class);
|
|
126
|
+
$this->user->setAttribute('type', 'admin');
|
|
127
|
+
expect($middleware[1]['middleware']($request, fn () => 'allowed'))->toBe('allowed');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('analytics controllers enforce authorization before invoking their next middleware', function (string $class) {
|
|
131
|
+
$connection = Model::getConnectionResolver()->connection();
|
|
132
|
+
$connection->table('permissions')->insert(['id' => 'analytics', 'name' => 'fleet-ops view analytics']);
|
|
133
|
+
$controller = new $class();
|
|
134
|
+
$middleware = $controller->getMiddleware()[0]['middleware'];
|
|
135
|
+
$request = Request::create('/analytics');
|
|
136
|
+
expect(fn () => $middleware($request, fn () => 'private analytics'))->toThrow(HttpResponseException::class);
|
|
137
|
+
$this->user->grantedPermissions = ['fleet-ops view analytics'];
|
|
138
|
+
expect($middleware($request, fn ($forwarded) => $forwarded))->toBe($request);
|
|
139
|
+
})->with([
|
|
140
|
+
Fleetbase\FleetOps\Http\Controllers\Internal\v1\AnalyticsController::class,
|
|
141
|
+
Fleetbase\FleetOps\Http\Controllers\Internal\v1\MetricsController::class,
|
|
142
|
+
]);
|
|
@@ -184,10 +184,21 @@ test('operational alert helpers query orders positions and settings', function (
|
|
|
184
184
|
$order = Order::where('uuid', 'order-1')->withoutGlobalScopes()->first();
|
|
185
185
|
expect($probe->callHelper('latestPositionForOrder', $order))->toBeInstanceOf(Position::class);
|
|
186
186
|
|
|
187
|
-
$settings = $probe->callHelper('alertSettings');
|
|
187
|
+
$settings = $probe->callHelper('alertSettings', $order);
|
|
188
188
|
expect($settings['late_departures']['enabled'])->toBeTrue()
|
|
189
189
|
->and($settings['route_deviations']['distance_threshold_meters'])->toBe(500)
|
|
190
190
|
->and($settings['prolonged_stoppages']['duration_threshold_minutes'])->toBe(30);
|
|
191
|
+
|
|
192
|
+
// Settings resolve from the order's company without a company session
|
|
193
|
+
$connection->table('settings')->insert([
|
|
194
|
+
'key' => 'company.company-1.tracking',
|
|
195
|
+
'value' => json_encode(['alerts' => ['late_departures' => ['enabled' => false], 'route_deviations' => ['distance_threshold_meters' => 750]]]),
|
|
196
|
+
]);
|
|
197
|
+
session(['company' => null]);
|
|
198
|
+
$settings = $probe->callHelper('alertSettings', $order);
|
|
199
|
+
session(['company' => 'company-1']);
|
|
200
|
+
expect($settings['late_departures']['enabled'])->toBeFalse()
|
|
201
|
+
->and($settings['route_deviations']['distance_threshold_meters'])->toBe(750);
|
|
191
202
|
});
|
|
192
203
|
|
|
193
204
|
test('route point collection normalizes pairs and measures distances', function () {
|
|
@@ -384,13 +384,22 @@ test('index driver resource counts assigned orders and names the current one', f
|
|
|
384
384
|
});
|
|
385
385
|
|
|
386
386
|
test('shift change listener reads company scheduling settings', function () {
|
|
387
|
-
fleetopsMoreSeamBoot();
|
|
387
|
+
$connection = fleetopsMoreSeamBoot();
|
|
388
388
|
|
|
389
389
|
$class = Fleetbase\FleetOps\Listeners\NotifyDriverOnShiftChange::class;
|
|
390
390
|
$listener = (new ReflectionClass($class))->newInstanceWithoutConstructor();
|
|
391
391
|
|
|
392
392
|
// With nothing stored the lookup falls back to the empty default
|
|
393
|
-
expect(fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings'))->toBe([]);
|
|
393
|
+
expect(fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings', ['company-seam-2']))->toBe([]);
|
|
394
|
+
|
|
395
|
+
// Queued listeners have no company session, so the schedule's company is used
|
|
396
|
+
$connection->table('settings')->insert(['key' => 'company.company-seam-3.fleet-ops.scheduling-settings', 'value' => json_encode(['notify_drivers_on_shift_change' => true])]);
|
|
397
|
+
session(['company' => null]);
|
|
398
|
+
$settings = fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings', ['company-seam-3']);
|
|
399
|
+
session(['company' => 'company-seam-2']);
|
|
400
|
+
|
|
401
|
+
expect($settings)->toBe(['notify_drivers_on_shift_change' => true])
|
|
402
|
+
->and(fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings', [null]))->toBe([]);
|
|
394
403
|
});
|
|
395
404
|
|
|
396
405
|
test('order insights capability builds its own company-scoped order query', function () {
|