@fleetbase/fleetops-engine 0.6.69 → 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 +41 -8
- 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/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 +4 -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 +38 -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.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/templates/settings/telematics.hbs +70 -0
- 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/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 +1 -1
- 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 +22 -0
- 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/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/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/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/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 +48 -0
|
@@ -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
|
+
});
|
|
@@ -251,3 +251,23 @@ test('connection settings fall back to defaults when the settings table is absen
|
|
|
251
251
|
expect($call('resolveVroomBaseUri'))->toBeString()
|
|
252
252
|
->and($call('resolveVroomApiKey'))->toBeIn([null, '']);
|
|
253
253
|
});
|
|
254
|
+
|
|
255
|
+
test('allocation resolves organization settings from the orders company regardless of session', function (?string $sessionCompany) {
|
|
256
|
+
$connection = fleetopsVroomBoot();
|
|
257
|
+
$connection->table('settings')->insert([
|
|
258
|
+
['key' => 'company.company-2.vroom', 'value' => json_encode(['api_host' => 'https://company-two-vroom.test'])],
|
|
259
|
+
['key' => 'company.company-1.vroom', 'value' => json_encode(['api_host' => 'https://company-one-vroom.test'])],
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
$order = fleetopsVroomOrder('order_vroom_queued', fleetopsVroomPlace('p-9', new Point(1.31, 103.81)), null);
|
|
263
|
+
$order->setAttribute('company_uuid', 'company-2');
|
|
264
|
+
$vehicle = fleetopsVroomVehicle('vehicle_vroom_queued', new Point(1.20, 103.70));
|
|
265
|
+
|
|
266
|
+
Http::fake(['*' => Http::response(['routes' => [], 'unassigned' => [], 'summary' => []], 200)]);
|
|
267
|
+
|
|
268
|
+
// Background allocation must not depend on an absent or unrelated session.
|
|
269
|
+
session(['company' => $sessionCompany]);
|
|
270
|
+
(new VroomOrchestrationEngine())->allocate(collect([$order]), collect([$vehicle]));
|
|
271
|
+
|
|
272
|
+
Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://company-two-vroom.test'));
|
|
273
|
+
})->with([null, 'company-1']);
|
|
@@ -38,6 +38,12 @@ function fleetopsLiveFleetUseInMemoryConnection(): SQLiteConnection
|
|
|
38
38
|
$connection->statement('create table drivers (uuid varchar(64) primary key, public_id varchar(64) null, user_uuid varchar(64) null, company_uuid varchar(64) null, vehicle_uuid varchar(64) null, current_job_uuid varchar(64) null, avatar_url varchar(255) null, location blob null, heading numeric null, online integer null, last_location_update_at datetime null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
39
39
|
$connection->statement('create table vehicles (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, driver_uuid varchar(64) null, photo_uuid varchar(64) null, avatar_url varchar(255) null, name varchar(255) null, year integer null, make varchar(255) null, model varchar(255) null, trim varchar(255) null, plate_number varchar(64) null, location blob null, heading numeric null, online integer null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
40
40
|
$connection->statement('create table orders (uuid varchar(64) primary key, public_id varchar(64) null, internal_id varchar(64) null, company_uuid varchar(64) null, driver_assigned_uuid varchar(64) null, status varchar(64) null, tracking_number_uuid varchar(64) null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
41
|
+
// Tables the markers' index-resource cards read: devices, coupled trailers and positions.
|
|
42
|
+
$connection->statement('create table devices (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, attachable_uuid varchar(64) null, attachable_type varchar(255) null, name varchar(255) null, device_id varchar(64) null, serial_number varchar(64) null, imei varchar(64) null, provider varchar(64) null, status varchar(64) null, online integer null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
43
|
+
$connection->statement('create table asset_connections (uuid varchar(64) primary key, company_uuid varchar(64) null, connector_uuid varchar(64) null, connector_type varchar(255) null, connected_uuid varchar(64) null, connected_type varchar(255) null, connected_at datetime null, disconnected_at datetime null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
44
|
+
$connection->statement('create table assets (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, asset_class varchar(64) null, name varchar(255) null, code varchar(64) null, type varchar(64) null, status varchar(64) null, plate_number varchar(64) null, online integer null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
45
|
+
$connection->statement('create table positions (uuid varchar(64) primary key, company_uuid varchar(64) null, subject_uuid varchar(64) null, subject_type varchar(255) null, order_uuid varchar(64) null, speed numeric null, heading numeric null, coordinates blob null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
46
|
+
$connection->statement('create table tracking_numbers (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, owner_uuid varchar(64) null, owner_type varchar(255) null, status_uuid varchar(64) null, tracking_number varchar(64) null, region varchar(64) null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
41
47
|
$connection->statement('create table files (uuid varchar(64) primary key, type varchar(64) null, url varchar(255) null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
42
48
|
|
|
43
49
|
$resolver = new ConnectionResolver([
|