@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
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Console\Commands\PruneTelematicsData;
|
|
4
|
+
use Illuminate\Database\ConnectionResolver;
|
|
5
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
6
|
+
use Illuminate\Database\SQLiteConnection;
|
|
7
|
+
use Illuminate\Support\Carbon;
|
|
8
|
+
use Illuminate\Support\Facades\DB;
|
|
9
|
+
use Illuminate\Support\Facades\Schema;
|
|
10
|
+
|
|
11
|
+
require_once __DIR__ . '/../../Support/PruneTelematicsDataProbe.php';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* fleetops:prune-telematics-data against an in-memory SQLite fixture: per-company
|
|
15
|
+
* policies, compaction, inbox tables resolved through connections, orphans,
|
|
16
|
+
* bounded batches, dry runs, filters and locking.
|
|
17
|
+
*/
|
|
18
|
+
function fleetopsPruneBoot(): SQLiteConnection
|
|
19
|
+
{
|
|
20
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'), '', '', ['name' => 'mysql', 'driver' => 'sqlite']);
|
|
21
|
+
$resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
22
|
+
$resolver->setDefaultConnection('mysql');
|
|
23
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
24
|
+
app()->instance('db', new class($connection) {
|
|
25
|
+
public function __construct(public SQLiteConnection $c)
|
|
26
|
+
{
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public function connection($name = null): SQLiteConnection
|
|
30
|
+
{
|
|
31
|
+
return $this->c;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public function __call($method, $arguments)
|
|
35
|
+
{
|
|
36
|
+
return $this->c->{$method}(...$arguments);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
app()->instance('db.schema', $connection->getSchemaBuilder());
|
|
40
|
+
DB::clearResolvedInstance('db');
|
|
41
|
+
Schema::clearResolvedInstance('db.schema');
|
|
42
|
+
|
|
43
|
+
foreach ([
|
|
44
|
+
'companies' => ['uuid', 'public_id', 'name'],
|
|
45
|
+
'telematics' => ['uuid', 'public_id', 'company_uuid', 'provider', 'status'],
|
|
46
|
+
'devices' => ['uuid', 'company_uuid', 'telematic_uuid', 'device_id'],
|
|
47
|
+
'device_events' => ['uuid', 'company_uuid', 'device_uuid', 'event_type', 'payload', 'meta', 'data'],
|
|
48
|
+
'positions' => ['uuid', 'company_uuid', 'subject_uuid', 'subject_type'],
|
|
49
|
+
] as $table => $columns) {
|
|
50
|
+
Schema::create($table, function ($blueprint) use ($columns) {
|
|
51
|
+
$blueprint->increments('id');
|
|
52
|
+
foreach ($columns as $column) {
|
|
53
|
+
$blueprint->text($column)->nullable();
|
|
54
|
+
}
|
|
55
|
+
$blueprint->timestamps();
|
|
56
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
(require __DIR__ . '/../../../migrations/2026_09_15_000001_create_telematic_telemetry_tables.php')->up();
|
|
60
|
+
foreach (glob(__DIR__ . '/../../../migrations/2026_09_23_00000*_add_retention_index_to_*.php') as $migration) {
|
|
61
|
+
(require $migration)->up();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Carbon::setTestNow('2026-09-23 12:00:00 UTC');
|
|
65
|
+
DB::table('companies')->insert([
|
|
66
|
+
['id' => 1, 'uuid' => 'company-1', 'public_id' => 'company_one'],
|
|
67
|
+
['id' => 2, 'uuid' => 'company-2', 'public_id' => 'company_two'],
|
|
68
|
+
]);
|
|
69
|
+
DB::table('telematics')->insert([
|
|
70
|
+
['uuid' => 'tm-1', 'company_uuid' => 'company-1', 'provider' => 'afaqy', 'status' => 'active', 'deleted_at' => null],
|
|
71
|
+
['uuid' => 'tm-1-trashed', 'company_uuid' => 'company-1', 'provider' => 'afaqy', 'status' => 'disabled', 'deleted_at' => '2026-09-01 00:00:00'],
|
|
72
|
+
['uuid' => 'tm-2', 'company_uuid' => 'company-2', 'provider' => 'safee', 'status' => 'active', 'deleted_at' => null],
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
return $connection;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fleetopsPruneEvent(string $uuid, ?string $company, int $daysOld, array $extra = []): array
|
|
79
|
+
{
|
|
80
|
+
$at = Carbon::now()->subDays($daysOld)->toDateTimeString();
|
|
81
|
+
|
|
82
|
+
return array_merge([
|
|
83
|
+
'uuid' => $uuid, 'company_uuid' => $company, 'event_type' => 'telemetry_update',
|
|
84
|
+
'payload' => '{"raw":true}', 'meta' => '{"speed":1}', 'data' => '{"speed":1}',
|
|
85
|
+
'created_at' => $at, 'updated_at' => $at, 'deleted_at' => null,
|
|
86
|
+
], $extra);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fleetopsPruneDelivery(string $uuid, string $telematic, string $status, int $hoursOld): array
|
|
90
|
+
{
|
|
91
|
+
$at = Carbon::now()->subHours($hoursOld)->toDateTimeString();
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
'uuid' => $uuid, 'telematic_uuid' => $telematic, 'source' => 'poll', 'status' => $status,
|
|
95
|
+
'payload_hash' => str_repeat('a', 64), 'payload' => 'x', 'received_at' => $at, 'available_at' => $at,
|
|
96
|
+
'created_at' => $at, 'updated_at' => $at,
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fleetopsPruneRun(string $uuid, string $telematic, string $status, int $daysOld): array
|
|
101
|
+
{
|
|
102
|
+
$at = Carbon::now()->subDays($daysOld)->toDateTimeString();
|
|
103
|
+
|
|
104
|
+
return ['uuid' => $uuid, 'telematic_uuid' => $telematic, 'status' => $status, 'created_at' => $at, 'updated_at' => $at];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
afterEach(fn () => Carbon::setTestNow());
|
|
108
|
+
|
|
109
|
+
test('orphan cleanup reaches missing connections and connections whose organization no longer exists', function () {
|
|
110
|
+
fleetopsPruneBoot();
|
|
111
|
+
DB::table('telematics')->insert(['uuid' => 'tm-orphan-company', 'company_uuid' => 'company-gone', 'provider' => 'afaqy', 'status' => 'disabled']);
|
|
112
|
+
DB::table('device_events')->insert([
|
|
113
|
+
fleetopsPruneEvent('e-unowned', null, 40),
|
|
114
|
+
fleetopsPruneEvent('e-owned', 'company-1', 40),
|
|
115
|
+
]);
|
|
116
|
+
DB::table('telematic_deliveries')->insert([
|
|
117
|
+
fleetopsPruneDelivery('d-missing-connection', 'tm-gone', 'processed', 48),
|
|
118
|
+
fleetopsPruneDelivery('d-missing-company', 'tm-orphan-company', 'processed', 48),
|
|
119
|
+
fleetopsPruneDelivery('d-active-company', 'tm-1', 'processed', 48),
|
|
120
|
+
fleetopsPruneDelivery('d-orphan-pending', 'tm-gone', 'pending', 48),
|
|
121
|
+
]);
|
|
122
|
+
DB::table('telematic_sync_runs')->insert([
|
|
123
|
+
fleetopsPruneRun('r-missing-connection', 'tm-gone', 'completed', 10),
|
|
124
|
+
fleetopsPruneRun('r-missing-company', 'tm-orphan-company', 'completed', 10),
|
|
125
|
+
fleetopsPruneRun('r-active-company', 'tm-1', 'completed', 10),
|
|
126
|
+
fleetopsPruneRun('r-orphan-in-flight', 'tm-gone', 'ingesting', 10),
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
$command = new PruneTelematicsDataProbe();
|
|
130
|
+
$command->options['orphans-only'] = true;
|
|
131
|
+
expect($command->handle())->toBe(PruneTelematicsData::SUCCESS)
|
|
132
|
+
->and(DB::table('device_events')->pluck('uuid')->all())->toBe(['e-owned'])
|
|
133
|
+
->and(DB::table('telematic_deliveries')->orderBy('uuid')->pluck('uuid')->all())->toBe(['d-active-company', 'd-orphan-pending'])
|
|
134
|
+
->and(DB::table('telematic_sync_runs')->orderBy('uuid')->pluck('uuid')->all())->toBe(['r-active-company', 'r-orphan-in-flight']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('prune applies each company policy to events, positions and the inbox, and system defaults to orphans', function () {
|
|
138
|
+
fleetopsPruneBoot();
|
|
139
|
+
DB::table('device_events')->insert([
|
|
140
|
+
fleetopsPruneEvent('e-old', 'company-1', 40),
|
|
141
|
+
fleetopsPruneEvent('e-compact', 'company-1', 10),
|
|
142
|
+
fleetopsPruneEvent('e-compacted-already', 'company-1', 10, ['payload' => null, 'meta' => null]),
|
|
143
|
+
fleetopsPruneEvent('e-fresh', 'company-1', 1),
|
|
144
|
+
fleetopsPruneEvent('e-trashed', 'company-1', 1, ['deleted_at' => Carbon::now()->toDateTimeString()]),
|
|
145
|
+
fleetopsPruneEvent('e-forever', 'company-2', 400),
|
|
146
|
+
fleetopsPruneEvent('e-orphan-null', null, 40),
|
|
147
|
+
fleetopsPruneEvent('e-orphan-gone', 'company-gone', 40),
|
|
148
|
+
fleetopsPruneEvent('e-orphan-fresh', 'company-gone', 1),
|
|
149
|
+
]);
|
|
150
|
+
DB::table('positions')->insert([
|
|
151
|
+
['uuid' => 'p-old', 'company_uuid' => 'company-1', 'created_at' => Carbon::now()->subDays(100)->toDateTimeString(), 'deleted_at' => null],
|
|
152
|
+
['uuid' => 'p-fresh', 'company_uuid' => 'company-1', 'created_at' => Carbon::now()->subDays(5)->toDateTimeString(), 'deleted_at' => null],
|
|
153
|
+
['uuid' => 'p-trashed', 'company_uuid' => 'company-1', 'created_at' => Carbon::now()->toDateTimeString(), 'deleted_at' => Carbon::now()->toDateTimeString()],
|
|
154
|
+
['uuid' => 'p-forever', 'company_uuid' => 'company-2', 'created_at' => Carbon::now()->subDays(1000)->toDateTimeString(), 'deleted_at' => null],
|
|
155
|
+
]);
|
|
156
|
+
DB::table('telematic_deliveries')->insert([
|
|
157
|
+
fleetopsPruneDelivery('d-processed-old', 'tm-1', 'processed', 30),
|
|
158
|
+
fleetopsPruneDelivery('d-processed-trashed-connection', 'tm-1-trashed', 'processed', 30),
|
|
159
|
+
fleetopsPruneDelivery('d-processed-fresh', 'tm-1', 'processed', 2),
|
|
160
|
+
fleetopsPruneDelivery('d-pending-old', 'tm-1', 'pending', 200),
|
|
161
|
+
fleetopsPruneDelivery('d-quarantined-old', 'tm-1', 'quarantined', 24 * 8),
|
|
162
|
+
fleetopsPruneDelivery('d-quarantined-fresh', 'tm-1', 'quarantined', 24 * 2),
|
|
163
|
+
fleetopsPruneDelivery('d-company-2-long-hours', 'tm-2', 'processed', 30),
|
|
164
|
+
fleetopsPruneDelivery('d-orphan', 'tm-gone', 'processed', 30),
|
|
165
|
+
]);
|
|
166
|
+
DB::table('telematic_sync_runs')->insert([
|
|
167
|
+
fleetopsPruneRun('r-old', 'tm-1', 'completed', 10),
|
|
168
|
+
fleetopsPruneRun('r-fresh', 'tm-1', 'completed', 2),
|
|
169
|
+
fleetopsPruneRun('r-fetching-old', 'tm-1', 'fetching', 10),
|
|
170
|
+
fleetopsPruneRun('r-ingesting-old', 'tm-1', 'ingesting', 10),
|
|
171
|
+
fleetopsPruneRun('r-orphan', 'tm-gone', 'partial', 10),
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
$command = new PruneTelematicsDataProbe();
|
|
175
|
+
$command->policies = [
|
|
176
|
+
'company-1' => ['event_retention_days' => 30, 'event_compact_after_days' => 7, 'position_retention_days' => 90, 'processed_retention_hours' => 24, 'quarantine_retention_days' => 7, 'sync_run_retention_days' => 7],
|
|
177
|
+
'company-2' => ['event_retention_days' => 0, 'event_compact_after_days' => 0, 'position_retention_days' => 0, 'processed_retention_hours' => 48],
|
|
178
|
+
'' => ['event_retention_days' => 30, 'processed_retention_hours' => 24, 'sync_run_retention_days' => 7],
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
expect($command->handle())->toBe(0);
|
|
182
|
+
|
|
183
|
+
expect(DB::table('device_events')->orderBy('id')->pluck('uuid')->all())->toBe(['e-compact', 'e-compacted-already', 'e-fresh', 'e-forever', 'e-orphan-fresh']);
|
|
184
|
+
$compacted = DB::table('device_events')->where('uuid', 'e-compact')->first();
|
|
185
|
+
expect($compacted->payload)->toBeNull()->and($compacted->meta)->toBeNull()->and($compacted->data)->toBe('{"speed":1}');
|
|
186
|
+
expect(DB::table('device_events')->where('uuid', 'e-fresh')->value('payload'))->toBe('{"raw":true}');
|
|
187
|
+
expect(DB::table('positions')->orderBy('id')->pluck('uuid')->all())->toBe(['p-fresh', 'p-forever']);
|
|
188
|
+
expect(DB::table('telematic_deliveries')->orderBy('uuid')->pluck('uuid')->all())->toBe(['d-company-2-long-hours', 'd-pending-old', 'd-processed-fresh', 'd-quarantined-fresh']);
|
|
189
|
+
expect(DB::table('telematic_sync_runs')->orderBy('uuid')->pluck('uuid')->all())->toBe(['r-fetching-old', 'r-fresh', 'r-ingesting-old']);
|
|
190
|
+
|
|
191
|
+
expect($command->messages)->toContain(
|
|
192
|
+
['info', 'company_one device_events: deleted=2 compacted=1 batches=3'],
|
|
193
|
+
['info', 'company_one positions: deleted=2 compacted=0 batches=2'],
|
|
194
|
+
['info', 'company_one telematic_deliveries: deleted=3 compacted=0 batches=2'],
|
|
195
|
+
['info', 'company_one telematic_sync_runs: deleted=1 compacted=0 batches=1'],
|
|
196
|
+
['info', 'orphaned device_events: deleted=2 compacted=0 batches=1'],
|
|
197
|
+
['info', 'orphaned telematic_deliveries: deleted=1 compacted=0 batches=1'],
|
|
198
|
+
['info', 'orphaned telematic_sync_runs: deleted=1 compacted=0 batches=1'],
|
|
199
|
+
['info', 'Telematics prune complete: deleted=12 compacted=1 batches=11'],
|
|
200
|
+
);
|
|
201
|
+
// Company two kept everything, so it reports nothing.
|
|
202
|
+
expect(array_filter($command->messages, fn ($message) => str_starts_with($message[1], 'company_two')))->toBe([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('prune bounds each sweep by batch count and reports a capped run', function () {
|
|
206
|
+
fleetopsPruneBoot();
|
|
207
|
+
$rows = [];
|
|
208
|
+
for ($i = 0; $i < 120; $i++) {
|
|
209
|
+
$rows[] = fleetopsPruneEvent('e-' . $i, 'company-1', 60);
|
|
210
|
+
}
|
|
211
|
+
DB::table('device_events')->insert($rows);
|
|
212
|
+
|
|
213
|
+
$command = new PruneTelematicsDataProbe();
|
|
214
|
+
$command->options['company'] = 'company_one';
|
|
215
|
+
$command->options['table'] = ['device_events'];
|
|
216
|
+
$command->options['batch-size'] = 10; // clamped up to the 100 row floor
|
|
217
|
+
$command->options['max-batches'] = 1;
|
|
218
|
+
$command->policies = ['company-1' => ['event_retention_days' => 30, 'event_compact_after_days' => 0]];
|
|
219
|
+
|
|
220
|
+
expect($command->handle())->toBe(0)
|
|
221
|
+
->and(DB::table('device_events')->count())->toBe(20)
|
|
222
|
+
->and($command->messages)->toContain(
|
|
223
|
+
['info', 'company_one device_events: deleted=100 compacted=0 batches=1 capped'],
|
|
224
|
+
['info', 'Telematics prune complete: deleted=100 compacted=0 batches=1 (capped; more rows remain for the next run)'],
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// The next run finishes the backlog; an exact multiple of the batch size is still reported as capped.
|
|
228
|
+
$command->messages = [];
|
|
229
|
+
expect($command->handle())->toBe(0)->and(DB::table('device_events')->count())->toBe(0);
|
|
230
|
+
$command->options['max-batches'] = 50;
|
|
231
|
+
$command->messages = [];
|
|
232
|
+
expect($command->handle())->toBe(0)->and($command->messages)->toContain(['info', 'Telematics prune complete: deleted=0 compacted=0 batches=0']);
|
|
233
|
+
|
|
234
|
+
// Compaction is bounded the same way.
|
|
235
|
+
$rows = [];
|
|
236
|
+
for ($i = 0; $i < 120; $i++) {
|
|
237
|
+
$rows[] = fleetopsPruneEvent('c-' . $i, 'company-1', 10);
|
|
238
|
+
}
|
|
239
|
+
DB::table('device_events')->insert($rows);
|
|
240
|
+
$command->options['max-batches'] = 1;
|
|
241
|
+
$command->policies = ['company-1' => ['event_retention_days' => 30, 'event_compact_after_days' => 7]];
|
|
242
|
+
$command->messages = [];
|
|
243
|
+
expect($command->handle())->toBe(0)
|
|
244
|
+
->and(DB::table('device_events')->whereNull('payload')->count())->toBe(100)
|
|
245
|
+
->and($command->messages)->toContain(['info', 'company_one device_events: deleted=0 compacted=100 batches=1 capped']);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('prune dry runs count without touching rows and honour table and company filters', function () {
|
|
249
|
+
fleetopsPruneBoot();
|
|
250
|
+
DB::table('device_events')->insert([
|
|
251
|
+
fleetopsPruneEvent('e-old-1', 'company-1', 40),
|
|
252
|
+
fleetopsPruneEvent('e-compact-1', 'company-1', 10),
|
|
253
|
+
fleetopsPruneEvent('e-old-2', 'company-2', 40),
|
|
254
|
+
]);
|
|
255
|
+
DB::table('positions')->insert([['uuid' => 'p-old', 'company_uuid' => 'company-1', 'created_at' => Carbon::now()->subDays(100)->toDateTimeString()]]);
|
|
256
|
+
|
|
257
|
+
$command = new PruneTelematicsDataProbe();
|
|
258
|
+
$command->options['dry-run'] = true;
|
|
259
|
+
$command->options['company'] = 'company-1';
|
|
260
|
+
$command->options['table'] = ['device_events', 'positions', 'device_events'];
|
|
261
|
+
$command->policies = ['company-1' => ['event_retention_days' => 30, 'event_compact_after_days' => 7, 'position_retention_days' => 90]];
|
|
262
|
+
|
|
263
|
+
expect($command->handle())->toBe(0)
|
|
264
|
+
->and(DB::table('device_events')->count())->toBe(3)
|
|
265
|
+
->and(DB::table('positions')->count())->toBe(1)
|
|
266
|
+
->and($command->messages)->toContain(
|
|
267
|
+
['info', 'company_one device_events: deleted=1 compacted=1 batches=0'],
|
|
268
|
+
['info', 'company_one positions: deleted=1 compacted=0 batches=0'],
|
|
269
|
+
['info', 'Telematics prune dry run: deleted=2 compacted=1 batches=0'],
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Only the requested table is swept when not a dry run.
|
|
273
|
+
$command->options['dry-run'] = false;
|
|
274
|
+
$command->options['table'] = ['positions'];
|
|
275
|
+
$command->messages = [];
|
|
276
|
+
expect($command->handle())->toBe(0)
|
|
277
|
+
->and(DB::table('device_events')->count())->toBe(3)
|
|
278
|
+
->and(DB::table('positions')->count())->toBe(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('prune rejects unknown tables and companies', function () {
|
|
282
|
+
fleetopsPruneBoot();
|
|
283
|
+
$command = new PruneTelematicsDataProbe();
|
|
284
|
+
$command->options['table'] = ['device_events', 'orders'];
|
|
285
|
+
expect($command->handle())->toBe(PruneTelematicsData::FAILURE)
|
|
286
|
+
->and($command->messages)->toContain(['error', 'Unknown table. Valid tables: device_events, positions, telematic_deliveries, telematic_sync_runs.']);
|
|
287
|
+
|
|
288
|
+
$command = new PruneTelematicsDataProbe();
|
|
289
|
+
$command->options['company'] = 'company-missing';
|
|
290
|
+
expect($command->handle())->toBe(PruneTelematicsData::FAILURE)
|
|
291
|
+
->and($command->messages)->toContain(['error', 'Company [company-missing] was not found.']);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('prune takes a bounded lock and yields to a run already in progress', function () {
|
|
295
|
+
fleetopsPruneBoot();
|
|
296
|
+
$originalCache = Illuminate\Support\Facades\Cache::getFacadeRoot();
|
|
297
|
+
try {
|
|
298
|
+
$store = new class extends Illuminate\Cache\ArrayStore {
|
|
299
|
+
public array $requestedLocks = [];
|
|
300
|
+
|
|
301
|
+
public function lock($name, $seconds = 0, $owner = null)
|
|
302
|
+
{
|
|
303
|
+
$this->requestedLocks[] = [$name, $seconds];
|
|
304
|
+
|
|
305
|
+
return parent::lock($name, $seconds, $owner);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
Illuminate\Support\Facades\Cache::swap(new Illuminate\Cache\Repository($store));
|
|
309
|
+
|
|
310
|
+
$command = new PruneTelematicsDataProbe();
|
|
311
|
+
$command->options['no-lock'] = false;
|
|
312
|
+
expect($command->handle())->toBe(0)
|
|
313
|
+
->and($store->requestedLocks)->toBe([[PruneTelematicsData::LOCK, 840]])
|
|
314
|
+
->and($store->locks)->toBe([]);
|
|
315
|
+
|
|
316
|
+
expect($store->lock(PruneTelematicsData::LOCK, 840)->get())->toBeTrue();
|
|
317
|
+
$command->messages = [];
|
|
318
|
+
expect($command->handle())->toBe(0)
|
|
319
|
+
->and($command->messages)->toBe([['warn', 'Another telematics prune run appears to be in progress.']]);
|
|
320
|
+
} finally {
|
|
321
|
+
Illuminate\Support\Facades\Cache::swap($originalCache);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('prune resolves the live company policy when none is injected', function () {
|
|
326
|
+
fleetopsPruneBoot();
|
|
327
|
+
Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy::flush();
|
|
328
|
+
Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy::$settingsResolver = fn () => ['event_retention_days' => 5];
|
|
329
|
+
try {
|
|
330
|
+
DB::table('device_events')->insert([fleetopsPruneEvent('e-old', 'company-1', 6)]);
|
|
331
|
+
$command = new class extends PruneTelematicsDataProbe {
|
|
332
|
+
protected function policyFor(?string $companyUuid): Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy
|
|
333
|
+
{
|
|
334
|
+
return PruneTelematicsData::policyFor($companyUuid);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
expect($command->handle())->toBe(0)->and(DB::table('device_events')->count())->toBe(0);
|
|
338
|
+
} finally {
|
|
339
|
+
Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy::flush();
|
|
340
|
+
Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy::$settingsResolver = null;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('prune rejects conflicting company and orphan scopes without deleting history', function () {
|
|
345
|
+
fleetopsPruneBoot();
|
|
346
|
+
DB::table('device_events')->insert([fleetopsPruneEvent('keep', 'company-1', 400)]);
|
|
347
|
+
$command = new PruneTelematicsDataProbe();
|
|
348
|
+
$command->options['company'] = 'company-1';
|
|
349
|
+
$command->options['orphans-only'] = true;
|
|
350
|
+
expect($command->handle())->toBe(PruneTelematicsData::FAILURE)
|
|
351
|
+
->and($command->messages)->toContain(['error', 'Choose either --company or --orphans-only.'])
|
|
352
|
+
->and(DB::table('device_events')->count())->toBe(1);
|
|
353
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Jobs\DispatchTelematicsRetentionJobs;
|
|
4
|
+
use Fleetbase\FleetOps\Jobs\PruneTelematicsDataJob;
|
|
5
|
+
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
|
6
|
+
use Illuminate\Support\Collection;
|
|
7
|
+
|
|
8
|
+
class FleetOpsTelematicsRetentionDispatchProbe extends DispatchTelematicsRetentionJobs
|
|
9
|
+
{
|
|
10
|
+
public array $dispatched = [];
|
|
11
|
+
public array $companyRows = [];
|
|
12
|
+
|
|
13
|
+
protected function companies(): Collection
|
|
14
|
+
{
|
|
15
|
+
return collect($this->companyRows);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected function queueJob(ShouldBeUnique $job): void
|
|
19
|
+
{
|
|
20
|
+
$this->dispatched[] = $job;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('system cleanup fans out a bounded page and continues from its last company', function () {
|
|
25
|
+
$job = new FleetOpsTelematicsRetentionDispatchProbe(200, 25);
|
|
26
|
+
$job->companyRows = array_map(fn ($id) => (object) ['id' => $id, 'uuid' => 'company-' . $id], range(201, 300));
|
|
27
|
+
$job->handle();
|
|
28
|
+
|
|
29
|
+
$prunes = array_slice($job->dispatched, 0, 100);
|
|
30
|
+
expect($prunes)->toHaveCount(100)
|
|
31
|
+
->and(array_map(fn ($prune) => $prune->companyUuid, $prunes))->toBe(array_map(fn ($id) => 'company-' . $id, range(201, 300)));
|
|
32
|
+
foreach ($prunes as $prune) {
|
|
33
|
+
expect($prune)->toBeInstanceOf(PruneTelematicsDataJob::class)
|
|
34
|
+
->and($prune->maxBatches)->toBe(25)
|
|
35
|
+
->and($prune->orphansOnly)->toBeFalse();
|
|
36
|
+
}
|
|
37
|
+
$nextPage = $job->dispatched[100];
|
|
38
|
+
expect($nextPage)->toBeInstanceOf(DispatchTelematicsRetentionJobs::class)
|
|
39
|
+
->and($nextPage->afterCompanyId)->toBe(300)
|
|
40
|
+
->and($nextPage->maxBatches)->toBe(25)
|
|
41
|
+
->and($nextPage->uniqueId())->not->toBe($job->uniqueId())
|
|
42
|
+
->and($nextPage->queue)->toBe(config('telematics.telemetry.ingestion_queue', 'default'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('the final system cleanup page queues an orphan pass even when there are no companies', function () {
|
|
46
|
+
foreach ([[], [(object) ['id' => 1001, 'uuid' => 'company-1001']]] as $companies) {
|
|
47
|
+
$job = new FleetOpsTelematicsRetentionDispatchProbe(1000);
|
|
48
|
+
$job->companyRows = $companies;
|
|
49
|
+
$job->handle();
|
|
50
|
+
|
|
51
|
+
expect($job->dispatched)->toHaveCount(count($companies) + 1);
|
|
52
|
+
$orphans = $job->dispatched[count($companies)];
|
|
53
|
+
expect($orphans)->toBeInstanceOf(PruneTelematicsDataJob::class)
|
|
54
|
+
->and($orphans->companyUuid)->toBeNull()
|
|
55
|
+
->and($orphans->orphansOnly)->toBeTrue();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('system cleanup reads all companies in bounded id order and dispatches through the queue', function () {
|
|
60
|
+
$originalDb = app()->bound('db') ? app('db') : null;
|
|
61
|
+
$originalCache = Illuminate\Support\Facades\Cache::getFacadeRoot();
|
|
62
|
+
$originalConfig = config('cache', []);
|
|
63
|
+
$contract = Illuminate\Contracts\Bus\Dispatcher::class;
|
|
64
|
+
$originalDispatcher = app()->bound($contract) ? app($contract) : null;
|
|
65
|
+
$connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
|
|
66
|
+
$connection->getSchemaBuilder()->create('companies', function ($table) {
|
|
67
|
+
$table->integer('id');
|
|
68
|
+
$table->string('uuid');
|
|
69
|
+
});
|
|
70
|
+
foreach (array_reverse(range(1, 103)) as $id) {
|
|
71
|
+
$connection->table('companies')->insert(['id' => $id, 'uuid' => 'tenant-' . $id]);
|
|
72
|
+
}
|
|
73
|
+
app()->instance('db', $connection);
|
|
74
|
+
Illuminate\Support\Facades\DB::clearResolvedInstance('db');
|
|
75
|
+
config(['cache.default' => 'array', 'cache.stores.array' => ['driver' => 'array']]);
|
|
76
|
+
Illuminate\Support\Facades\Cache::swap(new Illuminate\Cache\CacheManager(app()));
|
|
77
|
+
$dispatcher = new class(app()) extends Illuminate\Bus\Dispatcher {
|
|
78
|
+
public array $jobs = [];
|
|
79
|
+
|
|
80
|
+
public function dispatch($command)
|
|
81
|
+
{
|
|
82
|
+
$this->jobs[] = $command;
|
|
83
|
+
|
|
84
|
+
return $command;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
app()->instance($contract, $dispatcher);
|
|
88
|
+
try {
|
|
89
|
+
(new DispatchTelematicsRetentionJobs(1, 7))->handle();
|
|
90
|
+
expect($dispatcher->jobs)->toHaveCount(101)
|
|
91
|
+
->and($dispatcher->jobs[0]->companyUuid)->toBe('tenant-2')
|
|
92
|
+
->and($dispatcher->jobs[99]->companyUuid)->toBe('tenant-101')
|
|
93
|
+
->and($dispatcher->jobs[100]->afterCompanyId)->toBe(101);
|
|
94
|
+
$dispatcher->jobs[100]->handle();
|
|
95
|
+
expect($dispatcher->jobs)->toHaveCount(104)
|
|
96
|
+
->and($dispatcher->jobs[101]->companyUuid)->toBe('tenant-102')
|
|
97
|
+
->and($dispatcher->jobs[102]->companyUuid)->toBe('tenant-103')
|
|
98
|
+
->and($dispatcher->jobs[103]->orphansOnly)->toBeTrue();
|
|
99
|
+
} finally {
|
|
100
|
+
if ($originalDb) {
|
|
101
|
+
app()->instance('db', $originalDb);
|
|
102
|
+
} else {
|
|
103
|
+
app()->forgetInstance('db');
|
|
104
|
+
}
|
|
105
|
+
Illuminate\Support\Facades\DB::clearResolvedInstance('db');
|
|
106
|
+
Illuminate\Support\Facades\Cache::swap($originalCache);
|
|
107
|
+
config(['cache' => $originalConfig]);
|
|
108
|
+
if ($originalDispatcher) {
|
|
109
|
+
app()->instance($contract, $originalDispatcher);
|
|
110
|
+
} else {
|
|
111
|
+
app()->forgetInstance($contract);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Console\Commands\PruneTelematicsData;
|
|
4
|
+
use Fleetbase\FleetOps\Jobs\PruneTelematicsDataJob;
|
|
5
|
+
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
|
6
|
+
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
7
|
+
use Symfony\Component\Console\Input\InputInterface;
|
|
8
|
+
use Symfony\Component\Console\Output\OutputInterface;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The on-demand cleanup job is unique per company and runs the prune command
|
|
12
|
+
* directly, without the console kernel's command registry.
|
|
13
|
+
*/
|
|
14
|
+
class FleetOpsPruneCommandRunRecorder extends PruneTelematicsData
|
|
15
|
+
{
|
|
16
|
+
public array $runs = [];
|
|
17
|
+
public int $exitCode = 0;
|
|
18
|
+
|
|
19
|
+
public function run(InputInterface $input, OutputInterface $output): int
|
|
20
|
+
{
|
|
21
|
+
$this->runs[] = (string) $input;
|
|
22
|
+
|
|
23
|
+
return $this->exitCode;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('prune job is unique per company and runs the command for that company without the process lock', function () {
|
|
28
|
+
$recorder = new FleetOpsPruneCommandRunRecorder();
|
|
29
|
+
app()->instance(PruneTelematicsData::class, $recorder);
|
|
30
|
+
try {
|
|
31
|
+
$job = new PruneTelematicsDataJob('company-1');
|
|
32
|
+
expect($job)->toBeInstanceOf(ShouldQueue::class)->toBeInstanceOf(ShouldBeUnique::class)
|
|
33
|
+
->and($job->uniqueId())->toBe('company-1')
|
|
34
|
+
->and($job->uniqueFor)->toBe(900)
|
|
35
|
+
->and($job->tries)->toBe(1)
|
|
36
|
+
->and($job->timeout)->toBeLessThan(90)
|
|
37
|
+
->and($job->failOnTimeout)->toBeTrue()
|
|
38
|
+
->and($job->queue)->toBe(config('telematics.telemetry.ingestion_queue', 'default'))
|
|
39
|
+
->and($job->parameters())->toBe(['--no-lock' => true, '--max-batches' => 200, '--company' => 'company-1']);
|
|
40
|
+
$job->handle();
|
|
41
|
+
|
|
42
|
+
(new PruneTelematicsDataJob(null, 25))->handle();
|
|
43
|
+
$orphans = new PruneTelematicsDataJob(null, 25, true);
|
|
44
|
+
$orphans->handle();
|
|
45
|
+
expect((new PruneTelematicsDataJob())->uniqueId())->toBe('all')
|
|
46
|
+
->and($orphans->uniqueId())->toBe('orphans')
|
|
47
|
+
->and($orphans->parameters())->toBe(['--no-lock' => true, '--max-batches' => 25, '--orphans-only' => true])
|
|
48
|
+
->and($recorder->runs)->toBe([
|
|
49
|
+
'--no-lock=1 --max-batches=200 --company=company-1',
|
|
50
|
+
'--no-lock=1 --max-batches=25',
|
|
51
|
+
'--no-lock=1 --max-batches=25 --orphans-only=1',
|
|
52
|
+
])
|
|
53
|
+
->and($recorder->getLaravel())->toBe(app());
|
|
54
|
+
|
|
55
|
+
$recorder->exitCode = PruneTelematicsData::FAILURE;
|
|
56
|
+
expect(fn () => $job->handle())->toThrow(RuntimeException::class, 'Telematics cleanup failed with exit code 1.');
|
|
57
|
+
} finally {
|
|
58
|
+
app()->forgetInstance(PruneTelematicsData::class);
|
|
59
|
+
}
|
|
60
|
+
});
|