@fleetbase/fleetops-engine 0.6.69 → 0.6.71
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 +21 -14
- package/addon/components/admin/telematics-settings.hbs +143 -0
- package/addon/components/admin/telematics-settings.js +145 -0
- 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/order/details/activity.hbs +8 -6
- package/addon/components/order/details/activity.js +9 -0
- package/addon/components/order/details.hbs +29 -0
- package/addon/components/order/details.js +26 -0
- package/addon/components/order/form/details.hbs +30 -4
- package/addon/components/order/form/details.js +46 -0
- package/addon/components/order/form/route.hbs +4 -2
- package/addon/components/order/form/route.js +11 -0
- package/addon/components/order/form.hbs +12 -0
- package/addon/components/order/form.js +21 -1
- package/addon/components/order/kanban.js +72 -0
- package/addon/components/order-config-manager/activity-flow.js +45 -10
- package/addon/components/order-config-manager/lifecycle.hbs +250 -0
- package/addon/components/order-config-manager/lifecycle.js +358 -0
- package/addon/components/order-config-manager.js +5 -1
- package/addon/components/telematic/hub.hbs +1 -1
- package/addon/components/widget/live-fleet.hbs +8 -40
- package/addon/controllers/analytics/reports/index.js +15 -1
- package/addon/controllers/connectivity/devices/index/details.js +1 -0
- package/addon/controllers/connectivity/devices/index.js +12 -2
- package/addon/controllers/connectivity/events/index.js +31 -1
- package/addon/controllers/connectivity/fuel-providers/index.js +6 -4
- package/addon/controllers/connectivity/sensors/index/details.js +1 -1
- package/addon/controllers/connectivity/sensors/index.js +41 -1
- package/addon/controllers/connectivity/telematics/index.js +4 -0
- package/addon/controllers/maintenance/equipment/index/details.js +2 -2
- package/addon/controllers/maintenance/equipment/index.js +36 -10
- package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
- package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
- package/addon/controllers/maintenance/inspection-submissions/index.js +56 -10
- package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
- package/addon/controllers/maintenance/maintenances/index.js +24 -1
- package/addon/controllers/maintenance/parts/index/details.js +2 -2
- package/addon/controllers/maintenance/parts/index.js +36 -9
- package/addon/controllers/maintenance/schedules/index.js +48 -6
- package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
- package/addon/controllers/maintenance/work-orders/index.js +47 -9
- package/addon/controllers/management/contacts/customers.js +13 -0
- package/addon/controllers/management/contacts/index.js +47 -1
- package/addon/controllers/management/drivers/index/details.js +1 -0
- package/addon/controllers/management/drivers/index.js +36 -2
- package/addon/controllers/management/fleets/index/details.js +1 -0
- package/addon/controllers/management/fleets/index.js +43 -1
- package/addon/controllers/management/fuel-reports/index.js +26 -2
- package/addon/controllers/management/fuel-transactions/index.js +45 -7
- package/addon/controllers/management/issues/index/details.js +1 -0
- package/addon/controllers/management/issues/index.js +26 -2
- package/addon/controllers/management/places/index.js +40 -1
- package/addon/controllers/management/trailers/index/details.js +1 -0
- package/addon/controllers/management/trailers/index.js +50 -2
- package/addon/controllers/management/vehicles/index/details.js +1 -0
- package/addon/controllers/management/vehicles/index.js +37 -2
- package/addon/controllers/management/vendors/index.js +30 -2
- package/addon/controllers/operations/orders/index/details.js +61 -5
- package/addon/controllers/operations/orders/index.js +66 -4
- package/addon/controllers/operations/service-rates/index.js +18 -1
- package/addon/controllers/settings/index.js +13 -0
- package/addon/controllers/settings/telematics.js +113 -0
- package/addon/extension.js +70 -6
- 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/index/details.js +1 -0
- 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 +1 -0
- package/addon/services/customer-actions.js +2 -1
- package/addon/services/device-actions.js +1 -0
- package/addon/services/device-event-actions.js +1 -0
- package/addon/services/driver-actions.js +4 -13
- package/addon/services/equipment-actions.js +1 -0
- package/addon/services/fleet-actions.js +1 -0
- package/addon/services/fuel-report-actions.js +1 -0
- package/addon/services/inspection-form-actions.js +1 -0
- package/addon/services/inspection-submission-actions.js +1 -0
- package/addon/services/integrated-vendor-actions.js +1 -0
- package/addon/services/issue-actions.js +1 -0
- package/addon/services/maintenance-actions.js +1 -0
- package/addon/services/maintenance-schedule-actions.js +2 -1
- package/addon/services/order-actions.js +7 -0
- package/addon/services/order-presentation.js +129 -0
- package/addon/services/part-actions.js +1 -0
- package/addon/services/place-actions.js +1 -0
- package/addon/services/sensor-actions.js +1 -0
- package/addon/services/service-area-actions.js +1 -0
- package/addon/services/service-rate-actions.js +1 -0
- package/addon/services/telematic-actions.js +1 -0
- package/addon/services/trailer-actions.js +4 -14
- package/addon/services/vehicle-actions.js +4 -13
- package/addon/services/vendor-actions.js +1 -0
- package/addon/services/work-order-actions.js +1 -0
- package/addon/services/zone-actions.js +1 -0
- package/addon/templates/analytics/reports/index/details.hbs +1 -0
- package/addon/templates/analytics/reports/index.hbs +1 -0
- package/addon/templates/connectivity/devices/index/details.hbs +1 -0
- package/addon/templates/connectivity/devices/index.hbs +1 -0
- package/addon/templates/connectivity/events/index.hbs +1 -0
- package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
- package/addon/templates/connectivity/sensors/index.hbs +1 -0
- package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
- package/addon/templates/maintenance/equipment/index.hbs +2 -0
- package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
- package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
- package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
- package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
- package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
- package/addon/templates/maintenance/maintenances/index.hbs +1 -0
- package/addon/templates/maintenance/parts/index/details.hbs +1 -0
- package/addon/templates/maintenance/parts/index.hbs +2 -0
- package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
- package/addon/templates/maintenance/schedules/index.hbs +2 -0
- package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
- package/addon/templates/maintenance/work-orders/index.hbs +1 -0
- package/addon/templates/management/contacts/customers/details.hbs +1 -0
- package/addon/templates/management/contacts/customers.hbs +1 -0
- package/addon/templates/management/contacts/index/details.hbs +1 -0
- package/addon/templates/management/contacts/index.hbs +1 -0
- package/addon/templates/management/contacts.hbs +1 -0
- package/addon/templates/management/drivers/index/details.hbs +1 -0
- package/addon/templates/management/drivers/index.hbs +2 -0
- package/addon/templates/management/fleets/index/details.hbs +1 -0
- package/addon/templates/management/fleets/index.hbs +1 -0
- package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
- package/addon/templates/management/fuel-reports/index.hbs +1 -0
- package/addon/templates/management/fuel-transactions/index.hbs +1 -0
- package/addon/templates/management/issues/index/details.hbs +1 -0
- package/addon/templates/management/issues/index.hbs +1 -0
- package/addon/templates/management/places/index/details.hbs +1 -0
- package/addon/templates/management/places/index.hbs +1 -0
- package/addon/templates/management/trailers/index/details.hbs +1 -0
- package/addon/templates/management/trailers/index.hbs +2 -0
- package/addon/templates/management/vehicles/index/details.hbs +1 -0
- package/addon/templates/management/vehicles/index.hbs +2 -0
- package/addon/templates/management/vendors/index/details.hbs +1 -0
- package/addon/templates/management/vendors/index.hbs +2 -0
- package/addon/templates/management/vendors/integrated.hbs +1 -0
- package/addon/templates/operations/orders/index/details.hbs +1 -0
- package/addon/templates/operations/orders/index.hbs +1 -0
- package/addon/templates/operations/service-rates/index/details.hbs +1 -0
- package/addon/templates/operations/service-rates/index.hbs +1 -0
- package/addon/templates/settings/telematics.hbs +70 -0
- package/addon/utils/normalize-order-config-flow.js +6 -1
- package/app/components/admin/telematics-settings.js +1 -0
- package/app/components/map/marker-card/driver.js +1 -0
- package/app/components/map/marker-card/vehicle.js +1 -0
- package/app/controllers/settings/telematics.js +1 -0
- package/app/routes/settings/telematics.js +1 -0
- package/app/services/order-presentation.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/server/config/telemetry.php +12 -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/src/Auth/Schemas/FleetOps.php +104 -5
- package/server/src/Console/Commands/DrainTelematicInbox.php +3 -13
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +3 -3
- package/server/src/Console/Commands/PruneTelematicsData.php +318 -0
- 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 +11 -0
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +14 -0
- 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 +22 -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 +56 -4
- 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/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/Models/Order.php +35 -3
- package/server/src/Models/OrderConfig.php +129 -0
- package/server/src/Models/TrackingNumber.php +8 -4
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +9 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +3 -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/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 +9 -1
- package/server/tests/AnalyticsRoutesTest.php +29 -2
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +16 -1
- package/server/tests/Feature/Http/Api/TelematicsRetentionIndexMigrationTest.php +115 -0
- package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
- package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
- package/server/tests/Feature/Http/Internal/SettingControllerHelpersTest.php +71 -0
- package/server/tests/Feature/Http/Internal/TelematicsStorageDeadlinesTest.php +119 -0
- package/server/tests/InternalImportExportControllerContractsTest.php +5 -8
- package/server/tests/NavigatorControllerContractsTest.php +36 -4
- package/server/tests/NotificationAndMailContractsTest.php +31 -0
- package/server/tests/ObserverContractsTest.php +1 -1
- package/server/tests/OrchestratorConsumableApiTest.php +42 -0
- package/server/tests/OrderConfigLifecycleTest.php +120 -0
- package/server/tests/ProviderContractsTest.php +10 -0
- package/server/tests/ReportSchemaContractsTest.php +120 -6
- 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/Unit/Console/Commands/ProcessOperationalAlertsCommandTest.php +2 -2
- package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +353 -0
- package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
- package/server/tests/Unit/Jobs/DispatchTelematicsRetentionJobsTest.php +114 -0
- package/server/tests/Unit/Jobs/PruneTelematicsDataJobTest.php +60 -0
- package/server/tests/Unit/Orchestration/VroomEngineTransportAndSettingsTest.php +20 -0
- 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/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 +108 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Support\Database\TableIndexes;
|
|
4
|
+
use Illuminate\Database\SQLiteConnection;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The retention sweep ranges over (company_uuid, created_at) and
|
|
9
|
+
* (telematic_uuid, updated_at); these migrations add those indexes once,
|
|
10
|
+
* skip equivalent existing ones, and roll back only what they created.
|
|
11
|
+
*/
|
|
12
|
+
function fleetopsRetentionIndexDatabase(): SQLiteConnection
|
|
13
|
+
{
|
|
14
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'), ':memory:', '', ['driver' => 'sqlite']);
|
|
15
|
+
Schema::swap($connection->getSchemaBuilder());
|
|
16
|
+
|
|
17
|
+
return $connection;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fleetopsRetentionIndexMigrations(): array
|
|
21
|
+
{
|
|
22
|
+
$migrations = [];
|
|
23
|
+
foreach (glob(dirname(__DIR__, 4) . '/migrations/2026_09_23_00000*_add_retention_index_to_*.php') as $path) {
|
|
24
|
+
$migrations[basename($path)] = fn () => require $path;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return $migrations;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('retention index migrations add composite indexes once and roll them back', function () {
|
|
31
|
+
fleetopsRetentionIndexDatabase();
|
|
32
|
+
Schema::create('device_events', function ($table) {
|
|
33
|
+
$table->increments('id');
|
|
34
|
+
$table->uuid('company_uuid')->nullable();
|
|
35
|
+
$table->timestamps();
|
|
36
|
+
});
|
|
37
|
+
Schema::create('positions', function ($table) {
|
|
38
|
+
$table->increments('id');
|
|
39
|
+
$table->uuid('company_uuid')->nullable();
|
|
40
|
+
$table->timestamps();
|
|
41
|
+
});
|
|
42
|
+
Schema::create('telematic_sync_runs', function ($table) {
|
|
43
|
+
$table->uuid('uuid')->primary();
|
|
44
|
+
$table->uuid('telematic_uuid')->index();
|
|
45
|
+
$table->timestamps();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
$expected = [
|
|
49
|
+
'device_events' => ['device_events_company_created_at_index', ['company_uuid', 'created_at']],
|
|
50
|
+
'positions' => ['positions_company_created_at_index', ['company_uuid', 'created_at']],
|
|
51
|
+
'telematic_sync_runs' => ['telematic_sync_runs_telematic_updated_at_index', ['telematic_uuid', 'updated_at']],
|
|
52
|
+
];
|
|
53
|
+
$migrations = fleetopsRetentionIndexMigrations();
|
|
54
|
+
expect($migrations)->toHaveCount(3);
|
|
55
|
+
foreach ($migrations as $load) {
|
|
56
|
+
$load()->up();
|
|
57
|
+
$load()->up();
|
|
58
|
+
}
|
|
59
|
+
foreach ($expected as $table => [$name, $columns]) {
|
|
60
|
+
expect(TableIndexes::for($table)[$name] ?? null)->toBe($columns)
|
|
61
|
+
->and(TableIndexes::covers($table, $columns))->toBeTrue()
|
|
62
|
+
->and(TableIndexes::covers($table, ['created_at']))->toBeFalse();
|
|
63
|
+
}
|
|
64
|
+
// The existing single-column index on telematic_uuid does not cover the composite.
|
|
65
|
+
expect(count(array_filter(TableIndexes::for('telematic_sync_runs'), fn ($columns) => $columns === ['telematic_uuid'])))->toBe(1);
|
|
66
|
+
|
|
67
|
+
foreach ($migrations as $load) {
|
|
68
|
+
$load()->down();
|
|
69
|
+
}
|
|
70
|
+
foreach ($expected as $table => [$name]) {
|
|
71
|
+
expect(TableIndexes::for($table))->not->toHaveKey($name);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('retention index migrations skip missing tables, missing columns and equivalent indexes', function () {
|
|
76
|
+
fleetopsRetentionIndexDatabase();
|
|
77
|
+
$migrations = fleetopsRetentionIndexMigrations();
|
|
78
|
+
|
|
79
|
+
// No tables at all: nothing to do either way.
|
|
80
|
+
foreach ($migrations as $load) {
|
|
81
|
+
$load()->up();
|
|
82
|
+
$load()->down();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Schema::create('device_events', fn ($table) => $table->increments('id'));
|
|
86
|
+
Schema::create('positions', function ($table) {
|
|
87
|
+
$table->increments('id');
|
|
88
|
+
$table->uuid('company_uuid')->nullable();
|
|
89
|
+
$table->timestamps();
|
|
90
|
+
$table->index(['company_uuid', 'created_at', 'id'], 'positions_existing_company_created');
|
|
91
|
+
});
|
|
92
|
+
Schema::create('telematic_sync_runs', function ($table) {
|
|
93
|
+
$table->uuid('uuid')->primary();
|
|
94
|
+
$table->uuid('telematic_uuid');
|
|
95
|
+
$table->timestamps();
|
|
96
|
+
$table->index(['telematic_uuid', 'updated_at'], 'telematic_sync_runs_telematic_updated_at_index');
|
|
97
|
+
});
|
|
98
|
+
Schema::table('telematic_sync_runs', fn ($table) => $table->index('updated_at', 'unrelated_updated_at'));
|
|
99
|
+
|
|
100
|
+
foreach ($migrations as $load) {
|
|
101
|
+
$load()->up();
|
|
102
|
+
}
|
|
103
|
+
expect(TableIndexes::for('device_events'))->toBe([])
|
|
104
|
+
->and(array_keys(TableIndexes::for('positions')))->toBe(['positions_existing_company_created'])
|
|
105
|
+
->and(TableIndexes::for('telematic_sync_runs'))->toHaveKeys(['telematic_sync_runs_telematic_updated_at_index', 'unrelated_updated_at']);
|
|
106
|
+
|
|
107
|
+
// Rolling back never drops an index the migration did not create, even under the reserved name when its shape differs.
|
|
108
|
+
Schema::table('telematic_sync_runs', fn ($table) => $table->dropIndex('telematic_sync_runs_telematic_updated_at_index'));
|
|
109
|
+
Schema::table('telematic_sync_runs', fn ($table) => $table->index('updated_at', 'telematic_sync_runs_telematic_updated_at_index'));
|
|
110
|
+
foreach ($migrations as $load) {
|
|
111
|
+
$load()->down();
|
|
112
|
+
}
|
|
113
|
+
expect(array_keys(TableIndexes::for('positions')))->toBe(['positions_existing_company_created'])
|
|
114
|
+
->and(TableIndexes::for('telematic_sync_runs')['telematic_sync_runs_telematic_updated_at_index'])->toBe(['updated_at']);
|
|
115
|
+
});
|
|
@@ -661,3 +661,75 @@ test('set destination and next activity guard missing orders and configs', funct
|
|
|
661
661
|
$noConfig = $controller->nextActivity('order_internal', Request::create('/x', 'GET'));
|
|
662
662
|
expect($noConfig->getData(true)['error'] ?? '')->toBe('No order config found for order.');
|
|
663
663
|
});
|
|
664
|
+
|
|
665
|
+
function fleetopsInternalActivityUseLifecycle(SQLiteConnection $connection, array $lifecycle = []): void
|
|
666
|
+
{
|
|
667
|
+
$activity = fn (string $code, array $children = []) => ['key' => $code, 'code' => $code, 'status' => ucfirst($code), 'details' => ucfirst($code) . ' loan', 'activities' => $children, 'logic' => [], 'events' => [], 'complete' => $code === 'completed'];
|
|
668
|
+
|
|
669
|
+
$connection->table('order_configs')->where('uuid', 'config-1')->update([
|
|
670
|
+
'flow' => json_encode([
|
|
671
|
+
'requested' => $activity('requested', ['assigned', 'cancelled']),
|
|
672
|
+
'assigned' => $activity('assigned', ['completed']),
|
|
673
|
+
'completed' => $activity('completed'),
|
|
674
|
+
'cancelled' => $activity('cancelled'),
|
|
675
|
+
]),
|
|
676
|
+
'meta' => json_encode(['lifecycle' => array_replace([
|
|
677
|
+
'initial' => 'requested',
|
|
678
|
+
'completed' => 'completed',
|
|
679
|
+
'canceled' => 'cancelled',
|
|
680
|
+
'dispatch' => false,
|
|
681
|
+
'strict_transitions' => true,
|
|
682
|
+
], $lifecycle)]),
|
|
683
|
+
]);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
test('strict transitions reject activities outside the configured flow and use the configured definition', function () {
|
|
687
|
+
$connection = fleetopsInternalActivityBoot();
|
|
688
|
+
$controller = new OrderController();
|
|
689
|
+
|
|
690
|
+
fleetopsInternalActivityUseLifecycle($connection);
|
|
691
|
+
fleetopsInternalActivitySeedOrder($connection, ['status' => 'requested']);
|
|
692
|
+
|
|
693
|
+
// Not a child of the current activity
|
|
694
|
+
$skipped = $controller->updateActivity('order_internal', Request::create('/x', 'POST', ['activity' => ['key' => 'completed', 'code' => 'completed', 'status' => 'Completed'], 'bypass_proof' => 1]));
|
|
695
|
+
expect($skipped->getStatusCode())->toBe(422)
|
|
696
|
+
->and($skipped->getData(true)['error'])->toContain('not a permitted next step');
|
|
697
|
+
|
|
698
|
+
// Not in the flow at all
|
|
699
|
+
$unknown = $controller->updateActivity('order_internal', Request::create('/x', 'POST', ['activity' => ['key' => 'started', 'code' => 'started', 'status' => 'Started'], 'bypass_proof' => 1]));
|
|
700
|
+
expect($unknown->getStatusCode())->toBe(422);
|
|
701
|
+
|
|
702
|
+
// A configured child is accepted, with the server-side definition replacing the client's
|
|
703
|
+
$assigned = $controller->updateActivity('order_internal', Request::create('/x', 'POST', ['activity' => ['key' => 'assigned', 'code' => 'assigned', 'status' => 'Tampered'], 'bypass_proof' => 1]));
|
|
704
|
+
expect($assigned)->toBeInstanceOf(Fleetbase\FleetOps\Http\Resources\v1\Order::class)
|
|
705
|
+
->and($connection->table('tracking_statuses')->where('code', 'ASSIGNED')->value('status'))->toBe('Assigned');
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('dispatch order refuses orders whose lifecycle disables dispatch', function () {
|
|
709
|
+
$connection = fleetopsInternalActivityBoot();
|
|
710
|
+
$controller = new OrderController();
|
|
711
|
+
|
|
712
|
+
fleetopsInternalActivityUseLifecycle($connection);
|
|
713
|
+
fleetopsInternalActivitySeedOrder($connection, ['status' => 'requested', 'driver_assigned_uuid' => 'driver-1']);
|
|
714
|
+
|
|
715
|
+
$response = $controller->dispatchOrder(Request::create('/x', 'POST', ['order' => 'order_internal']));
|
|
716
|
+
expect($response->getData(true)['error'] ?? '')->toContain('not dispatched')
|
|
717
|
+
->and($connection->table('orders')->value('dispatched'))->toBe(0);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('orders with a configured lifecycle start their tracking at the initial activity', function () {
|
|
721
|
+
$connection = fleetopsInternalActivityBoot();
|
|
722
|
+
|
|
723
|
+
// No config, or a config without a lifecycle, keeps the default created status
|
|
724
|
+
expect((new Fleetbase\FleetOps\Models\Order())->getInitialTrackingStatus())->toBeNull();
|
|
725
|
+
$order = new Fleetbase\FleetOps\Models\Order();
|
|
726
|
+
$order->order_config_uuid = 'config-1';
|
|
727
|
+
expect($order->getInitialTrackingStatus())->toBeNull();
|
|
728
|
+
|
|
729
|
+
fleetopsInternalActivityUseLifecycle($connection);
|
|
730
|
+
expect($order->getInitialTrackingStatus())->toBe(['code' => 'requested', 'status' => 'Requested', 'details' => 'Requested loan']);
|
|
731
|
+
|
|
732
|
+
// An initial activity missing from the flow falls back to the default
|
|
733
|
+
fleetopsInternalActivityUseLifecycle($connection, ['initial' => 'nowhere']);
|
|
734
|
+
expect($order->getInitialTrackingStatus())->toBeNull();
|
|
735
|
+
});
|
|
@@ -486,3 +486,29 @@ test('create record dispatches orders when the dispatch flag is not disabled', f
|
|
|
486
486
|
expect($result)->toBeArray()->toHaveKey('order')
|
|
487
487
|
->and($connection->table('orders')->value('dispatched'))->not->toBeNull();
|
|
488
488
|
});
|
|
489
|
+
|
|
490
|
+
test('create record starts configured lifecycles at their initial activity and never dispatches them', function () {
|
|
491
|
+
$connection = fleetopsInternalOrderCreateBoot();
|
|
492
|
+
$connection->table('order_configs')->where('uuid', 'config-1')->update([
|
|
493
|
+
'flow' => json_encode([
|
|
494
|
+
'requested' => ['key' => 'requested', 'code' => 'requested', 'status' => 'Requested', 'details' => 'Loan requested', 'activities' => ['completed']],
|
|
495
|
+
'completed' => ['key' => 'completed', 'code' => 'completed', 'status' => 'Completed', 'details' => 'Loan completed', 'activities' => []],
|
|
496
|
+
]),
|
|
497
|
+
'meta' => json_encode(['lifecycle' => ['initial' => 'requested', 'completed' => 'completed', 'dispatch' => false]]),
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
// Asks to be dispatched; the lifecycle overrides both the status and the dispatch flag
|
|
501
|
+
$result = (new OrderController())->createRecord(Request::create('/int/v1/orders', 'POST', [
|
|
502
|
+
'order' => [
|
|
503
|
+
'status' => 'created',
|
|
504
|
+
'dispatched' => true,
|
|
505
|
+
'adhoc' => true,
|
|
506
|
+
'payload' => ['type' => 'transport'],
|
|
507
|
+
],
|
|
508
|
+
]));
|
|
509
|
+
|
|
510
|
+
expect($result)->toBeArray()->toHaveKey('order')
|
|
511
|
+
->and($connection->table('orders')->value('status'))->toBe('requested')
|
|
512
|
+
->and((bool) $connection->table('orders')->value('dispatched'))->toBeFalse()
|
|
513
|
+
->and((bool) $connection->table('orders')->value('adhoc'))->toBeFalse();
|
|
514
|
+
});
|
|
@@ -131,3 +131,74 @@ test('setting helpers read notification and tracking registries', function () {
|
|
|
131
131
|
->and($helper('notificationsByPackage', 'fleet-ops'))->toBeArray()
|
|
132
132
|
->and($helper('trackingProviders'))->toBeArray();
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
test('history policy reconciliation clamps every organization and preserves inherited and internal settings', function () {
|
|
136
|
+
$connection = fleetopsSettingHelpersBoot();
|
|
137
|
+
$key = Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy::SETTING_KEY;
|
|
138
|
+
$controller = new class extends SettingController {
|
|
139
|
+
public ?Closure $beforeLock = null;
|
|
140
|
+
|
|
141
|
+
public function reconcile(array $defaults): void
|
|
142
|
+
{
|
|
143
|
+
$this->reconcileTelematicsCompanyPreferences($defaults);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public function lock(Closure $callback): mixed
|
|
147
|
+
{
|
|
148
|
+
return parent::withTelematicsPolicyLock($callback);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
protected function withTelematicsPolicyLock(Closure $callback): mixed
|
|
152
|
+
{
|
|
153
|
+
if ($this->beforeLock) {
|
|
154
|
+
($this->beforeLock)();
|
|
155
|
+
$this->beforeLock = null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return parent::withTelematicsPolicyLock($callback);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected function telematicsDefaults(): array
|
|
162
|
+
{
|
|
163
|
+
return ['max_event_retention_days' => 90, 'max_position_retention_days' => 360];
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
// The first write creates the lock anchor; subsequent writes reuse it.
|
|
167
|
+
expect($controller->lock(fn () => 'saved'))->toBe('saved');
|
|
168
|
+
expect($controller->lock(fn () => $connection->transactionLevel()))->toBe(1)
|
|
169
|
+
->and($connection->table('settings')->where('key', $key)->count())->toBe(1);
|
|
170
|
+
foreach ([
|
|
171
|
+
'a' => ['event_retention_days' => 0, 'position_retention_days' => 900, 'log_telemetry_activity' => true],
|
|
172
|
+
'b' => ['event_retention_days' => 30],
|
|
173
|
+
'c' => [],
|
|
174
|
+
] as $company => $preferences) {
|
|
175
|
+
$connection->table('settings')->insert(['key' => "company.$company.$key", 'value' => json_encode($preferences)]);
|
|
176
|
+
}
|
|
177
|
+
$controller->reconcile(['max_event_retention_days' => 0, 'max_position_retention_days' => 0]);
|
|
178
|
+
$read = fn ($company) => json_decode($connection->table('settings')->where('key', "company.$company.$key")->value('value'), true);
|
|
179
|
+
expect($read('a')['event_retention_days'])->toBe(0);
|
|
180
|
+
$controller->reconcile(['max_event_retention_days' => 90, 'max_position_retention_days' => 360]);
|
|
181
|
+
expect($read('a'))->toBe(['event_retention_days' => 90, 'position_retention_days' => 360, 'log_telemetry_activity' => true])
|
|
182
|
+
->and($read('b'))->toBe(['event_retention_days' => 30])->and($read('c'))->toBe([]);
|
|
183
|
+
// A tenant removed after the batch is read must not be recreated.
|
|
184
|
+
$controller->beforeLock = fn () => $connection->table('settings')->where('key', "company.a.$key")->delete();
|
|
185
|
+
$controller->reconcile(['max_event_retention_days' => 90]);
|
|
186
|
+
expect($connection->table('settings')->where('key', "company.a.$key")->exists())->toBeFalse();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('company order configuration keys exclude other tenants and deleted configurations', function () {
|
|
190
|
+
$connection = fleetopsSettingHelpersBoot();
|
|
191
|
+
$connection->getSchemaBuilder()->create('order_configs', function ($table) {
|
|
192
|
+
$table->increments('id');
|
|
193
|
+
$table->string('uuid');
|
|
194
|
+
$table->string('public_id')->nullable();
|
|
195
|
+
$table->string('company_uuid');
|
|
196
|
+
$table->softDeletes();
|
|
197
|
+
});
|
|
198
|
+
$connection->table('order_configs')->insert([
|
|
199
|
+
['uuid' => 'own', 'public_id' => 'own-public', 'company_uuid' => 'company-1', 'deleted_at' => null],
|
|
200
|
+
['uuid' => 'other', 'public_id' => 'other-public', 'company_uuid' => 'company-2', 'deleted_at' => null],
|
|
201
|
+
['uuid' => 'deleted', 'public_id' => null, 'company_uuid' => 'company-1', 'deleted_at' => '2026-01-01'],
|
|
202
|
+
]);
|
|
203
|
+
expect((new ReflectionMethod(SettingController::class, 'companyOrderConfigKeys'))->invoke(new SettingController()))->toBe(['own', 'own-public']);
|
|
204
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Controllers\Internal\v1\SettingController;
|
|
4
|
+
use Illuminate\Database\MySqlConnection;
|
|
5
|
+
use Illuminate\Database\QueryException;
|
|
6
|
+
use Illuminate\Support\Facades\DB;
|
|
7
|
+
|
|
8
|
+
class FleetOpsStorageDeadlineConnection extends MySqlConnection
|
|
9
|
+
{
|
|
10
|
+
public array $queries = [];
|
|
11
|
+
public array $responses = [];
|
|
12
|
+
public bool $maria = false;
|
|
13
|
+
|
|
14
|
+
public function getReadPdo()
|
|
15
|
+
{
|
|
16
|
+
return new class($this->maria) extends PDO {
|
|
17
|
+
public function __construct(private bool $maria)
|
|
18
|
+
{
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public function getAttribute(int $attribute): mixed
|
|
22
|
+
{
|
|
23
|
+
if ($attribute !== PDO::ATTR_SERVER_VERSION) {
|
|
24
|
+
throw new LogicException('Only the database server version should be read');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return $this->maria ? '10.11.8-MariaDB' : '8.0.40';
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public function select($query, $bindings = [], $useReadPdo = true)
|
|
33
|
+
{
|
|
34
|
+
$this->queries[] = [$query, $bindings];
|
|
35
|
+
$response = array_shift($this->responses) ?? [];
|
|
36
|
+
if ($response instanceof Throwable) {
|
|
37
|
+
throw $response;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return $response;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fleetopsStorageQueryError(int $code): QueryException
|
|
45
|
+
{
|
|
46
|
+
$error = new PDOException('Database query failed');
|
|
47
|
+
$error->errorInfo = ['HY000', $code, 'Database query failed'];
|
|
48
|
+
|
|
49
|
+
return new QueryException('mysql', 'select history', [], $error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeEach(function () {
|
|
53
|
+
$this->originalDb = app()->bound('db') ? app('db') : null;
|
|
54
|
+
$this->connection = new FleetOpsStorageDeadlineConnection(new PDO('sqlite::memory:'), '', '', ['driver' => 'mysql']);
|
|
55
|
+
$connection = $this->connection;
|
|
56
|
+
app()->instance('db', new class($connection) {
|
|
57
|
+
public function __construct(public $database)
|
|
58
|
+
{
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public function connection($name = null)
|
|
62
|
+
{
|
|
63
|
+
return $this->database;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public function __call($method, $args)
|
|
67
|
+
{
|
|
68
|
+
return $this->database->{$method}(...$args);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
DB::clearResolvedInstance('db');
|
|
72
|
+
$this->controller = new SettingController();
|
|
73
|
+
$this->invoke = fn ($method, ...$arguments) => (new ReflectionMethod(SettingController::class, $method))->invoke($this->controller, ...$arguments);
|
|
74
|
+
$this->scope = fn ($query) => $query->where('company_uuid', 'company-a');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(function () {
|
|
78
|
+
if ($this->originalDb) {
|
|
79
|
+
app()->instance('db', $this->originalDb);
|
|
80
|
+
} else {
|
|
81
|
+
app()->forgetInstance('db');
|
|
82
|
+
}
|
|
83
|
+
DB::clearResolvedInstance('db');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('storage scans enforce driver specific deadlines while preserving bindings', function (bool $maria) {
|
|
87
|
+
$this->connection->maria = $maria;
|
|
88
|
+
$this->connection->responses = [[(object) ['row_count' => '4', 'oldest' => '2026-01-01']], [(object) ['rows' => 4]]];
|
|
89
|
+
expect(($this->invoke)('tableUsage', 'device_events', $this->scope, 'created_at'))->toBe(['rows' => 4, 'oldest' => '2026-01-01']);
|
|
90
|
+
($this->invoke)('storageUsageSelect', 'select * from device_events where company_uuid = ?', ['company-a'], true);
|
|
91
|
+
expect($this->connection->queries[0][1])->toBe(['company-a'])
|
|
92
|
+
->and($this->connection->queries[0][0])->toStartWith($maria ? 'SET STATEMENT max_statement_time=1 FOR select' : 'SELECT /*+ MAX_EXECUTION_TIME(1000) */')
|
|
93
|
+
->and($this->connection->queries[1][0])->toStartWith($maria ? 'SET STATEMENT max_statement_time=1 FOR EXPLAIN select' : 'EXPLAIN SELECT /*+ MAX_EXECUTION_TIME(1000) */')
|
|
94
|
+
->and($this->connection->queries[1][1])->toBe(['company-a']);
|
|
95
|
+
})->with([false, true]);
|
|
96
|
+
|
|
97
|
+
test('timed out counts use a scoped query plan estimate instead of another history scan', function () {
|
|
98
|
+
$this->connection->responses = [fleetopsStorageQueryError(3024), [(object) ['rows' => 101, 'filtered' => 25]]];
|
|
99
|
+
expect(($this->invoke)('tableUsage', 'device_events', $this->scope, 'created_at'))->toBe(['rows' => 25, 'oldest' => null, 'rows_estimated' => true])
|
|
100
|
+
->and($this->connection->queries[1][0])->toStartWith('EXPLAIN SELECT')
|
|
101
|
+
->and($this->connection->queries[1][1])->toBe(['company-a']);
|
|
102
|
+
$this->connection->responses = [fleetopsStorageQueryError(1969), fleetopsStorageQueryError(1969)];
|
|
103
|
+
expect(($this->invoke)('tableUsage', 'device_events', $this->scope, 'created_at'))->toBe(['rows' => null, 'oldest' => null, 'rows_estimated' => true]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('storage queries suppress only timeout errors and retain real database failures', function () {
|
|
107
|
+
foreach (['tableUsage', 'tableOldest'] as $method) {
|
|
108
|
+
$this->connection->responses = [fleetopsStorageQueryError(1146)];
|
|
109
|
+
expect(fn () => ($this->invoke)($method, 'device_events', $this->scope, 'created_at'))->toThrow(QueryException::class);
|
|
110
|
+
}
|
|
111
|
+
$this->connection->responses = [fleetopsStorageQueryError(3024), fleetopsStorageQueryError(1146)];
|
|
112
|
+
expect(fn () => ($this->invoke)('tableUsage', 'device_events', $this->scope, 'created_at'))->toThrow(QueryException::class);
|
|
113
|
+
$this->connection->responses = [fleetopsStorageQueryError(3024)];
|
|
114
|
+
expect(($this->invoke)('tableOldest', 'device_events', $this->scope, 'created_at'))->toBeNull();
|
|
115
|
+
$this->connection->responses = [fleetopsStorageQueryError(1969)];
|
|
116
|
+
expect(($this->invoke)('storageTableStatistics', ['device_events']))->toBe([]);
|
|
117
|
+
$this->connection->responses = [fleetopsStorageQueryError(1146)];
|
|
118
|
+
expect(fn () => ($this->invoke)('storageTableStatistics', ['device_events']))->toThrow(QueryException::class);
|
|
119
|
+
});
|
|
@@ -269,14 +269,11 @@ test('internal fuel report controller syncs custom fields after save when provid
|
|
|
269
269
|
$controller = new FleetOpsInternalFuelReportControllerProbe();
|
|
270
270
|
$fuelReport = new FleetOpsInternalFuelReportAfterSaveFake();
|
|
271
271
|
|
|
272
|
-
$
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
],
|
|
278
|
-
]), $fuelReport);
|
|
279
|
-
$controller->afterSave(new Request(['fuel_report' => ['custom_field_values' => []]]), $fuelReport);
|
|
272
|
+
$input = ['custom_field_values' => [['key' => 'receipt_number', 'value' => 'R-100']]];
|
|
273
|
+
$controller->afterSave(new Request(['fuelReport' => $input]), $fuelReport, $input);
|
|
274
|
+
$controller->afterSave(new Request(), $fuelReport, ['custom_field_values' => []]);
|
|
275
|
+
$controller->afterSave(new Request(), $fuelReport, ['custom_field_values' => 'invalid']);
|
|
276
|
+
$controller->afterSave(new Request(), $fuelReport);
|
|
280
277
|
|
|
281
278
|
expect($fuelReport->synced)->toBe([
|
|
282
279
|
[
|
|
@@ -137,17 +137,24 @@ function fleetopsNavigatorCredential(string $key = 'flb_live_key'): ApiCredentia
|
|
|
137
137
|
return $credential;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function fleetopsNavigatorSignedQuery(): array
|
|
141
|
+
{
|
|
142
|
+
$expires = time() + 600;
|
|
143
|
+
|
|
144
|
+
return ['expires' => $expires, 'signature' => NavigatorController::linkSignature($expires)];
|
|
145
|
+
}
|
|
146
|
+
|
|
140
147
|
test('navigator controller builds android and ios app link redirects', function () {
|
|
141
148
|
$company = fleetopsNavigatorCompany();
|
|
142
149
|
$controller = new FleetOpsNavigatorControllerProbe();
|
|
143
150
|
$controller->adminUser = fleetopsNavigatorUser($company);
|
|
144
151
|
$controller->apiCredential = fleetopsNavigatorCredential('flb_live_navigator');
|
|
145
152
|
|
|
146
|
-
$android = $controller->linkApp(Request::create('/navigator/link-app', 'GET',
|
|
153
|
+
$android = $controller->linkApp(Request::create('/navigator/link-app', 'GET', fleetopsNavigatorSignedQuery(), [], [], [
|
|
147
154
|
'HTTP_USER_AGENT' => 'Mozilla/5.0 Android',
|
|
148
155
|
]))->getData(true);
|
|
149
156
|
|
|
150
|
-
$ios = $controller->linkApp(Request::create('/navigator/link-app', 'GET',
|
|
157
|
+
$ios = $controller->linkApp(Request::create('/navigator/link-app', 'GET', fleetopsNavigatorSignedQuery(), [], [], [
|
|
151
158
|
'HTTP_USER_AGENT' => 'Mozilla/5.0 iPhone',
|
|
152
159
|
]))->getData(true);
|
|
153
160
|
|
|
@@ -169,11 +176,32 @@ test('navigator controller returns missing organization error when no admin comp
|
|
|
169
176
|
$controller = new FleetOpsNavigatorControllerProbe();
|
|
170
177
|
$controller->adminUser = fleetopsNavigatorUser(null);
|
|
171
178
|
|
|
172
|
-
$response = $controller->linkApp(
|
|
179
|
+
$response = $controller->linkApp(Request::create('/navigator/link-app', 'GET', fleetopsNavigatorSignedQuery()));
|
|
173
180
|
|
|
174
181
|
expect($response->getData(true))->toBe(['error' => 'Organization for linking not found.']);
|
|
175
182
|
});
|
|
176
183
|
|
|
184
|
+
test('navigator controller refuses unsigned, tampered and expired app links before touching credentials', function () {
|
|
185
|
+
$company = fleetopsNavigatorCompany();
|
|
186
|
+
$controller = new FleetOpsNavigatorControllerProbe();
|
|
187
|
+
$controller->adminUser = fleetopsNavigatorUser($company);
|
|
188
|
+
$controller->apiCredential = fleetopsNavigatorCredential('flb_live_navigator');
|
|
189
|
+
|
|
190
|
+
$expired = time() - 60;
|
|
191
|
+
$requests = [
|
|
192
|
+
'unsigned' => Request::create('/navigator/link-app', 'GET'),
|
|
193
|
+
'tampered' => Request::create('/navigator/link-app', 'GET', ['signature' => 'forged'] + fleetopsNavigatorSignedQuery()),
|
|
194
|
+
'expired' => Request::create('/navigator/link-app', 'GET', ['expires' => $expired, 'signature' => NavigatorController::linkSignature($expired)]),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
foreach ($requests as $request) {
|
|
198
|
+
$response = $controller->linkApp($request);
|
|
199
|
+
expect($response->getData(true))->toBe(['error' => 'This Navigator link is invalid or has expired. Generate a new one from the console.']);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
expect($controller->credentialLookups)->toBe([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
177
205
|
test('navigator controller exposes link url settings and current organization token lookup branches', function () {
|
|
178
206
|
$controller = new FleetOpsNavigatorControllerProbe();
|
|
179
207
|
$controller->apiCredential = fleetopsNavigatorCredential('flb_test_key');
|
|
@@ -194,7 +222,11 @@ test('navigator controller exposes link url settings and current organization to
|
|
|
194
222
|
]);
|
|
195
223
|
$missing = $controller->getCurrentOrganization($secretRequest);
|
|
196
224
|
|
|
197
|
-
|
|
225
|
+
parse_str((string) parse_url($linkUrl['linkUrl'], PHP_URL_QUERY), $linkQuery);
|
|
226
|
+
|
|
227
|
+
expect($linkUrl['linkUrl'])->toStartWith('http://localhost/int/v1/fleet-ops/navigator/link-app?')
|
|
228
|
+
->and($linkQuery['signature'])->toBe(NavigatorController::linkSignature((int) $linkQuery['expires']))
|
|
229
|
+
->and((int) $linkQuery['expires'])->toBeGreaterThan(time())
|
|
198
230
|
->and($settings)->toBe(['enabled' => true, 'invite_code_required' => false])
|
|
199
231
|
->and($organization)->toBeInstanceOf(Organization::class)
|
|
200
232
|
->and($controller->credentialLookups)->toContain(
|
|
@@ -675,3 +675,34 @@ test('maintenance schedule reminder mail exposes schedule context', function ()
|
|
|
675
675
|
'offsetDays' => 7,
|
|
676
676
|
]);
|
|
677
677
|
});
|
|
678
|
+
|
|
679
|
+
test('customer credentials mail reads the portal slug from the customer company regardless of session', function (?string $sessionCompany) {
|
|
680
|
+
app('config')->set('fleetbase.console.host', 'console.fleetbase.test');
|
|
681
|
+
app('config')->set('fleetbase.console.secure', true);
|
|
682
|
+
app('config')->set('fleetbase.console.subdomain', null);
|
|
683
|
+
app('session.store')->put('company', $sessionCompany);
|
|
684
|
+
|
|
685
|
+
$connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
|
|
686
|
+
$resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
687
|
+
$resolver->setDefaultConnection('mysql');
|
|
688
|
+
Illuminate\Database\Eloquent\Model::setConnectionResolver($resolver);
|
|
689
|
+
$connection->getSchemaBuilder()->create('settings', function ($blueprint) {
|
|
690
|
+
$blueprint->increments('id');
|
|
691
|
+
$blueprint->string('key')->nullable();
|
|
692
|
+
$blueprint->text('value')->nullable();
|
|
693
|
+
$blueprint->timestamps();
|
|
694
|
+
});
|
|
695
|
+
$connection->table('settings')->insert([
|
|
696
|
+
['key' => 'company.company-portal.customer-portal-config', 'value' => json_encode(['accessUrlSlug' => 'acme-portal'])],
|
|
697
|
+
['key' => 'company.other-company.customer-portal-config', 'value' => json_encode(['accessUrlSlug' => 'other-portal'])],
|
|
698
|
+
]);
|
|
699
|
+
|
|
700
|
+
$customer = new FleetOpsNotificationContactFake();
|
|
701
|
+
$customer->userForTest = new User();
|
|
702
|
+
$customer->userForTest->setRawAttributes(['email' => 'customer@example.test'], true);
|
|
703
|
+
$customer->setRawAttributes(['company_uuid' => 'company-portal'], true);
|
|
704
|
+
|
|
705
|
+
$content = fleetOpsNotificationWithEnvironment(fn () => (new CustomerCredentialsMail('plain-secret', $customer))->content());
|
|
706
|
+
|
|
707
|
+
expect($content->with['customerPortalUrl'])->toBe('https://console.fleetbase.test/acme-portal');
|
|
708
|
+
})->with([null, 'other-company']);
|
|
@@ -454,7 +454,7 @@ class FleetOpsNotifyDriverOnShiftChangeProbe extends NotifyDriverOnShiftChange
|
|
|
454
454
|
return $this->schedule;
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
protected function getSchedulingSettings(): array
|
|
457
|
+
protected function getSchedulingSettings(?string $companyUuid): array
|
|
458
458
|
{
|
|
459
459
|
return $this->settings;
|
|
460
460
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
use Fleetbase\FleetOps\Http\Controllers\Api\v1\OrchestrationController;
|
|
4
4
|
use Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry;
|
|
5
|
+
use Illuminate\Http\Exceptions\HttpResponseException;
|
|
6
|
+
use Illuminate\Http\Request;
|
|
5
7
|
|
|
6
8
|
function sanitize_orchestrator_payload(array $payload): array
|
|
7
9
|
{
|
|
@@ -72,3 +74,43 @@ test('consumable orchestrator routes are registered under the public api group',
|
|
|
72
74
|
expect($routes)->toContain("\$router->post('run', 'OrchestrationController@run');");
|
|
73
75
|
expect($routes)->toContain("\$router->post('commit', 'OrchestrationController@commit');");
|
|
74
76
|
});
|
|
77
|
+
|
|
78
|
+
test('public orchestration uses API authentication without inheriting console IAM middleware', function () {
|
|
79
|
+
$registry = new OrchestrationEngineRegistry();
|
|
80
|
+
$public = new OrchestrationController($registry);
|
|
81
|
+
$internal = new Fleetbase\FleetOps\Http\Controllers\Internal\v1\OrchestrationController($registry);
|
|
82
|
+
|
|
83
|
+
expect($public->getMiddleware())->toBe([]);
|
|
84
|
+
|
|
85
|
+
// An API-key request has no console IAM role. The internal workbench must
|
|
86
|
+
// still refuse this actor; its middleware must not leak onto the API class.
|
|
87
|
+
app('session.store')->flush();
|
|
88
|
+
$request = Request::create('/int/v1/fleet-ops/orchestrator/run', 'POST');
|
|
89
|
+
app()->instance('request', $request);
|
|
90
|
+
foreach (['run', 'commit'] as $method) {
|
|
91
|
+
$guard = collect($internal->getMiddleware())->first(fn ($entry) => in_array($method, $entry['options']['only'], true));
|
|
92
|
+
expect($guard)->not->toBeNull();
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
$guard['middleware']($request, fn () => throw new RuntimeException('Unauthorized request reached the workbench'));
|
|
96
|
+
test()->fail('The internal workbench must require a permitted user');
|
|
97
|
+
} catch (HttpResponseException $error) {
|
|
98
|
+
expect($error->getResponse()->getStatusCode())->toBe(401);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('public orchestration resolves tenant scope from the authenticated session, never request input', function () {
|
|
104
|
+
$controller = new OrchestrationController(new OrchestrationEngineRegistry());
|
|
105
|
+
$company = new ReflectionMethod($controller, 'companyUuid');
|
|
106
|
+
$company->setAccessible(true);
|
|
107
|
+
$request = Request::create('/v1/orchestrator/run', 'POST', ['company_uuid' => 'other-company']);
|
|
108
|
+
app()->instance('request', $request);
|
|
109
|
+
|
|
110
|
+
session(['company' => 'credential-company']);
|
|
111
|
+
expect($company->invoke($controller))->toBe('credential-company');
|
|
112
|
+
|
|
113
|
+
session(['company' => 'next-credential-company']);
|
|
114
|
+
expect($company->invoke($controller))->toBe('next-credential-company');
|
|
115
|
+
app('session.store')->flush();
|
|
116
|
+
});
|