@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,300 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Telematics\Retention;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Models\Setting;
|
|
6
|
+
use Illuminate\Support\Carbon;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves how long a company keeps telematics data.
|
|
10
|
+
*
|
|
11
|
+
* Administrators control retention policy and infrastructure settings. Companies
|
|
12
|
+
* may override event and position history retention within the system maximums.
|
|
13
|
+
* Zero means "keep forever" only when the administrator has not set a maximum.
|
|
14
|
+
*/
|
|
15
|
+
final class RetentionPolicy
|
|
16
|
+
{
|
|
17
|
+
public const SETTING_KEY = 'fleet-ops.telematics-settings';
|
|
18
|
+
|
|
19
|
+
/** Cached policies are refreshed after this many seconds so saved settings reach long-lived workers. */
|
|
20
|
+
public const CACHE_TTL_SECONDS = 60;
|
|
21
|
+
|
|
22
|
+
public const HISTORY_KEYS = ['event_retention_days', 'position_retention_days'];
|
|
23
|
+
|
|
24
|
+
public const MAXIMUMS = [
|
|
25
|
+
'event_retention_days' => 'max_event_retention_days',
|
|
26
|
+
'position_retention_days' => 'max_position_retention_days',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** @var array<string, array{0: int, 1: int}> */
|
|
30
|
+
public const LIMITS = [
|
|
31
|
+
'event_retention_days' => [1, 3650],
|
|
32
|
+
'event_compact_after_days' => [1, 3650],
|
|
33
|
+
'position_retention_days' => [1, 3650],
|
|
34
|
+
'processed_retention_hours' => [1, 720],
|
|
35
|
+
'quarantine_retention_days' => [1, 365],
|
|
36
|
+
'sync_run_retention_days' => [1, 365],
|
|
37
|
+
'max_event_retention_days' => [1, 3650],
|
|
38
|
+
'max_position_retention_days' => [1, 3650],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
public const BOOLEANS = ['log_telemetry_activity'];
|
|
42
|
+
|
|
43
|
+
/** Used when neither config nor settings provide a value. */
|
|
44
|
+
public const FALLBACKS = [
|
|
45
|
+
'event_retention_days' => 30,
|
|
46
|
+
'event_compact_after_days' => 7,
|
|
47
|
+
'position_retention_days' => 90,
|
|
48
|
+
'processed_retention_hours' => 24,
|
|
49
|
+
'quarantine_retention_days' => 7,
|
|
50
|
+
'sync_run_retention_days' => 7,
|
|
51
|
+
// A separate, opt-in cap preserves existing history when upgrading.
|
|
52
|
+
'max_event_retention_days' => 0,
|
|
53
|
+
'max_position_retention_days' => 0,
|
|
54
|
+
'log_telemetry_activity' => false,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Test seam: fn (string $scope, string $key, mixed $default, ?string $companyUuid): mixed
|
|
59
|
+
* where $scope is `system` or `company`.
|
|
60
|
+
*/
|
|
61
|
+
public static ?\Closure $settingsResolver = null;
|
|
62
|
+
|
|
63
|
+
/** @var array<string, array{policy: self, expires: int}> */
|
|
64
|
+
private static array $companyCache = [];
|
|
65
|
+
|
|
66
|
+
/** @var array{values: array, expires: int}|null */
|
|
67
|
+
private static ?array $defaultsCache = null;
|
|
68
|
+
|
|
69
|
+
private function __construct(private array $values)
|
|
70
|
+
{
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @return string[] */
|
|
74
|
+
public static function keys(): array
|
|
75
|
+
{
|
|
76
|
+
return array_keys(self::FALLBACKS);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* System-wide defaults: package config overridden by the admin setting.
|
|
81
|
+
*/
|
|
82
|
+
public static function defaults(): array
|
|
83
|
+
{
|
|
84
|
+
if (self::$defaultsCache && self::$defaultsCache['expires'] > time()) {
|
|
85
|
+
return self::$defaultsCache['values'];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$config = array_intersect_key((array) config('telematics.telemetry', []), self::FALLBACKS);
|
|
89
|
+
$base = self::normalize($config, self::FALLBACKS);
|
|
90
|
+
$values = self::normalize(self::resolve('system', null), $base);
|
|
91
|
+
|
|
92
|
+
self::$defaultsCache = ['values' => $values, 'expires' => time() + self::CACHE_TTL_SECONDS];
|
|
93
|
+
|
|
94
|
+
return $values;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public static function forCompany(?string $companyUuid): self
|
|
98
|
+
{
|
|
99
|
+
if (!$companyUuid) {
|
|
100
|
+
return self::fromCompanyPreferences([]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
$cached = self::$companyCache[$companyUuid] ?? null;
|
|
104
|
+
if ($cached && $cached['expires'] > time()) {
|
|
105
|
+
return $cached['policy'];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
$defaults = self::defaults();
|
|
109
|
+
$policy = self::fromCompanyPreferences(self::resolve('company', $companyUuid), $defaults);
|
|
110
|
+
|
|
111
|
+
// A policy must not extend the lifetime of system defaults it inherited.
|
|
112
|
+
self::$companyCache[$companyUuid] = ['policy' => $policy, 'expires' => self::$defaultsCache['expires']];
|
|
113
|
+
|
|
114
|
+
return $policy;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public static function fromArray(array $values, ?array $base = null): self
|
|
118
|
+
{
|
|
119
|
+
return new self(self::applyMaximums(self::normalize($values, $base ?? self::FALLBACKS)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Keep only explicit, valid customer history preferences. Missing or null
|
|
124
|
+
* values remain absent so later changes to the system defaults are inherited.
|
|
125
|
+
*/
|
|
126
|
+
public static function companyPreferences(array $input): array
|
|
127
|
+
{
|
|
128
|
+
$preferences = [];
|
|
129
|
+
foreach (self::HISTORY_KEYS as $key) {
|
|
130
|
+
$value = $input[$key] ?? null;
|
|
131
|
+
if ($value === null || $value === '' || is_bool($value) || filter_var($value, FILTER_VALIDATE_INT) === false) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[$min, $max] = self::LIMITS[$key];
|
|
136
|
+
$value = (int) $value;
|
|
137
|
+
$preferences[$key] = $value === 0 ? 0 : max($min, min($max, $value));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return $preferences;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Legacy company overrides for ingestion, payloads and logging are ignored.
|
|
145
|
+
* Apply caps after inheritance so unlimited or overly long defaults cannot
|
|
146
|
+
* bypass an administrator's maximum either.
|
|
147
|
+
*/
|
|
148
|
+
public static function fromCompanyPreferences(array $preferences, ?array $defaults = null): self
|
|
149
|
+
{
|
|
150
|
+
return self::fromArray(self::companyPreferences($preferences), $defaults ?? self::defaults());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Return the explicit history choices after applying the administrator's
|
|
155
|
+
* maximums. Inherited values stay absent, and an allowed zero stays explicit.
|
|
156
|
+
*/
|
|
157
|
+
public static function constrainCompanyPreferences(array $preferences, ?array $defaults = null): array
|
|
158
|
+
{
|
|
159
|
+
$preferences = self::companyPreferences($preferences);
|
|
160
|
+
$effective = self::fromCompanyPreferences($preferences, $defaults)->toArray();
|
|
161
|
+
|
|
162
|
+
return array_intersect_key($effective, $preferences);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Clamp and cast user input, filling gaps from `$base`.
|
|
167
|
+
*/
|
|
168
|
+
public static function normalize(array $input, array $base): array
|
|
169
|
+
{
|
|
170
|
+
$values = [];
|
|
171
|
+
foreach (self::LIMITS as $key => [$min, $max]) {
|
|
172
|
+
$value = (int) self::pick($input, $base, $key);
|
|
173
|
+
$values[$key] = $value === 0 ? 0 : max($min, min($max, $value));
|
|
174
|
+
}
|
|
175
|
+
foreach (self::BOOLEANS as $key) {
|
|
176
|
+
$values[$key] = filter_var(self::pick($input, $base, $key), FILTER_VALIDATE_BOOLEAN);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return $values;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public static function flush(?string $companyUuid = null): void
|
|
183
|
+
{
|
|
184
|
+
if ($companyUuid === null) {
|
|
185
|
+
self::$companyCache = [];
|
|
186
|
+
self::$defaultsCache = null;
|
|
187
|
+
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
unset(self::$companyCache[$companyUuid]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The point in time before which rows governed by `$key` are expired, or null when retention is off.
|
|
196
|
+
*/
|
|
197
|
+
public function cutoff(string $key): ?Carbon
|
|
198
|
+
{
|
|
199
|
+
$value = (int) ($this->values[$key] ?? 0);
|
|
200
|
+
if ($value <= 0) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return $key === 'processed_retention_hours' ? Carbon::now()->subHours($value) : Carbon::now()->subDays($value);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public function deletesEvents(): bool
|
|
208
|
+
{
|
|
209
|
+
return $this->values['event_retention_days'] > 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Compaction strips raw payloads from events that are kept but no longer need the provider blob.
|
|
214
|
+
* It is pointless when events are deleted before, or at the same age as, they would be compacted.
|
|
215
|
+
*/
|
|
216
|
+
public function compactsEvents(): bool
|
|
217
|
+
{
|
|
218
|
+
$compactAfter = $this->values['event_compact_after_days'];
|
|
219
|
+
if ($compactAfter <= 0) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return !$this->deletesEvents() || $compactAfter < $this->values['event_retention_days'];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public function logsTelemetryActivity(): bool
|
|
227
|
+
{
|
|
228
|
+
return (bool) $this->values['log_telemetry_activity'];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public function get(string $key): mixed
|
|
232
|
+
{
|
|
233
|
+
return $this->values[$key] ?? null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public function toArray(): array
|
|
237
|
+
{
|
|
238
|
+
return $this->values;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private static function applyMaximums(array $values): array
|
|
242
|
+
{
|
|
243
|
+
foreach (self::MAXIMUMS as $key => $maximumKey) {
|
|
244
|
+
$maximum = $values[$maximumKey];
|
|
245
|
+
if ($maximum > 0 && ($values[$key] === 0 || $values[$key] > $maximum)) {
|
|
246
|
+
$values[$key] = $maximum;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return $values;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private static function pick(array $input, array $base, string $key): mixed
|
|
254
|
+
{
|
|
255
|
+
if (array_key_exists($key, $input) && $input[$key] !== null && $input[$key] !== '') {
|
|
256
|
+
return $input[$key];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return $base[$key] ?? self::FALLBACKS[$key];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Read the stored setting for a scope. Any failure (no settings table, no session store)
|
|
264
|
+
* falls back to the layer below so ingestion never depends on the settings store.
|
|
265
|
+
*/
|
|
266
|
+
private static function resolve(string $scope, ?string $companyUuid): array
|
|
267
|
+
{
|
|
268
|
+
try {
|
|
269
|
+
if (self::$settingsResolver) {
|
|
270
|
+
$value = (self::$settingsResolver)($scope, self::SETTING_KEY, [], $companyUuid);
|
|
271
|
+
} elseif ($scope === 'system') {
|
|
272
|
+
$value = Setting::lookup(self::SETTING_KEY, []);
|
|
273
|
+
} else {
|
|
274
|
+
$value = self::lookupCompanySetting($companyUuid);
|
|
275
|
+
}
|
|
276
|
+
} catch (\Throwable) {
|
|
277
|
+
$value = [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return is_array($value) ? $value : [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* `Setting::lookupCompany` reads the company from the session, which queue workers and
|
|
285
|
+
* console commands do not have; scope it for the lookup and restore whatever was there.
|
|
286
|
+
*/
|
|
287
|
+
private static function lookupCompanySetting(string $companyUuid): mixed
|
|
288
|
+
{
|
|
289
|
+
$session = app('session');
|
|
290
|
+
$had = $session->has('company');
|
|
291
|
+
$previous = $session->get('company');
|
|
292
|
+
$session->put('company', $companyUuid);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
return Setting::lookupCompany(self::SETTING_KEY, []);
|
|
296
|
+
} finally {
|
|
297
|
+
$had ? $session->put('company', $previous) : $session->forget('company');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Telematics\Retention;
|
|
4
|
+
|
|
5
|
+
use Spatie\Activitylog\ActivityLogger;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Runs telemetry-driven writes with activity logging suppressed unless the
|
|
9
|
+
* system administrator enabled it. Every poll saves the device, event, position and vehicle,
|
|
10
|
+
* which otherwise writes several activity rows per device per minute.
|
|
11
|
+
*/
|
|
12
|
+
final class TelemetryActivity
|
|
13
|
+
{
|
|
14
|
+
public static function run(?string $companyUuid, \Closure $callback): mixed
|
|
15
|
+
{
|
|
16
|
+
if (RetentionPolicy::forCompany($companyUuid)->logsTelemetryActivity()) {
|
|
17
|
+
return $callback();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
$logger = self::logger();
|
|
21
|
+
if (!$logger) {
|
|
22
|
+
return $callback();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return $logger->withoutLogs($callback);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The logger is optional: an environment without the activity log package
|
|
30
|
+
* bound (package tests, minimal consoles) must still ingest telemetry.
|
|
31
|
+
*/
|
|
32
|
+
private static function logger(): ?ActivityLogger
|
|
33
|
+
{
|
|
34
|
+
try {
|
|
35
|
+
return app(ActivityLogger::class);
|
|
36
|
+
} catch (\Throwable) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -239,6 +239,12 @@ class TelematicService
|
|
|
239
239
|
if ($provider instanceof \Fleetbase\FleetOps\Contracts\TelemetryProviderInterface) {
|
|
240
240
|
return app(Telemetry\Ingestor::class)->ingest($telematic, $provider, $payload, $this);
|
|
241
241
|
}
|
|
242
|
+
|
|
243
|
+
return Retention\TelemetryActivity::run($telematic->company_uuid, fn () => $this->ingestLegacySnapshot($telematic, $provider, $payload));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
protected function ingestLegacySnapshot(Telematic $telematic, TelematicProviderInterface $provider, array $payload): array
|
|
247
|
+
{
|
|
242
248
|
$device = $this->linkDevice($telematic, $provider->normalizeDevice($payload));
|
|
243
249
|
|
|
244
250
|
$event = null;
|
|
@@ -299,7 +305,9 @@ class TelematicService
|
|
|
299
305
|
], fn ($value) => $value !== null);
|
|
300
306
|
$event->payload = $eventData['payload'] ?? $eventData['meta'] ?? $eventData;
|
|
301
307
|
$event->_key = $eventKey;
|
|
302
|
-
|
|
308
|
+
// Providers hand over the raw unit in `meta`; it already lives in `payload`.
|
|
309
|
+
// Persist only the normalized block so each event is stored once, not twice.
|
|
310
|
+
$event->meta = array_filter([
|
|
303
311
|
'telematic_uuid' => $telematic->uuid,
|
|
304
312
|
'telematic_id' => $telematic->public_id,
|
|
305
313
|
'provider_event_id' => $event->ident,
|
|
@@ -310,7 +318,9 @@ class TelematicService
|
|
|
310
318
|
'odometer' => $eventData['odometer'] ?? null,
|
|
311
319
|
'ignition' => $eventData['ignition'] ?? null,
|
|
312
320
|
'fuel_level' => $eventData['fuel_level'] ?? null,
|
|
313
|
-
|
|
321
|
+
'telemetry' => data_get($eventData, 'meta.telemetry'),
|
|
322
|
+
'provider_status' => data_get($eventData, 'meta.provider_status'),
|
|
323
|
+
], fn ($value) => $value !== null);
|
|
314
324
|
|
|
315
325
|
$location = $this->normalizeLocation($eventData['location'] ?? null);
|
|
316
326
|
if ($location) {
|
|
@@ -7,6 +7,7 @@ use Fleetbase\FleetOps\Events\DeviceTelemetryUpdated;
|
|
|
7
7
|
use Fleetbase\FleetOps\Models\Device;
|
|
8
8
|
use Fleetbase\FleetOps\Models\DeviceEvent;
|
|
9
9
|
use Fleetbase\FleetOps\Models\Telematic;
|
|
10
|
+
use Fleetbase\FleetOps\Support\Telematics\Retention\TelemetryActivity;
|
|
10
11
|
use Fleetbase\FleetOps\Support\Telematics\TelematicService;
|
|
11
12
|
use Illuminate\Support\Carbon;
|
|
12
13
|
use Illuminate\Support\Facades\Cache;
|
|
@@ -32,7 +33,8 @@ class Ingestor
|
|
|
32
33
|
}
|
|
33
34
|
$key = 'telemetry:device:' . hash('sha256', $telematic->uuid . '|' . $id);
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
// Telemetry saves (device, event, position, vehicle, sensors) only reach the activity log when the company opts in.
|
|
37
|
+
return Cache::lock($key, 60)->block(5, fn () => TelemetryActivity::run($telematic->company_uuid, fn () => DB::transaction(function () use ($telematic, $sample, $options, $service, $receivedAt, $source, $normalized, $event, $id) {
|
|
36
38
|
$device = Device::withoutGlobalScopes()->where('company_uuid', $telematic->company_uuid)->where('telematic_uuid', $telematic->uuid)->where('device_id', $id)->lockForUpdate()->first();
|
|
37
39
|
$current = data_get($device?->meta, 'telemetry.position_at') ?? data_get($device?->meta, 'last_update.occurred_at');
|
|
38
40
|
$valid = Sample::validPosition($event);
|
|
@@ -107,6 +109,6 @@ class Ingestor
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
return ['device' => $device, 'event' => $stored, 'events' => $stored ? [$stored] : [], 'sensors' => $sensors, 'duplicate' => $duplicate, 'invalid_position' => !$valid];
|
|
110
|
-
}, 3));
|
|
112
|
+
}, 3)));
|
|
111
113
|
}
|
|
112
114
|
}
|
|
@@ -18,7 +18,7 @@ class TrackingIntelligenceService
|
|
|
18
18
|
|
|
19
19
|
public function track(Order $order, array|TrackingOptions $options = []): array
|
|
20
20
|
{
|
|
21
|
-
$options = $options instanceof TrackingOptions ? $options : TrackingOptions::fromArray($options);
|
|
21
|
+
$options = $options instanceof TrackingOptions ? $options : TrackingOptions::fromArray($options, $order->company_uuid);
|
|
22
22
|
$context = $this->contextBuilder->build($order, $options);
|
|
23
23
|
$cacheKey = $this->cacheKey($context, $options);
|
|
24
24
|
|
|
@@ -19,7 +19,7 @@ class TrackingOptions
|
|
|
19
19
|
) {
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
public static function fromArray(array $options = []): self
|
|
22
|
+
public static function fromArray(array $options = [], ?string $companyUuid = null): self
|
|
23
23
|
{
|
|
24
24
|
try {
|
|
25
25
|
$systemSettings = Setting::lookup('fleet-ops.tracking-settings', []);
|
|
@@ -30,7 +30,7 @@ class TrackingOptions
|
|
|
30
30
|
$config = array_merge(config('fleetops.tracking', []), is_array($systemSettings) ? $systemSettings : []);
|
|
31
31
|
$companySettings = [];
|
|
32
32
|
try {
|
|
33
|
-
$companySettings = Setting::
|
|
33
|
+
$companySettings = Setting::lookupForCompany($companyUuid ?? session('company'), 'tracking', []);
|
|
34
34
|
} catch (\Throwable) {
|
|
35
35
|
$companySettings = [];
|
|
36
36
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Traits;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Support\Authorization;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registers controller middleware that enforces an explicit permission per method.
|
|
9
|
+
*
|
|
10
|
+
* Used for endpoints AuthorizationGuard cannot resolve on its own: methods on
|
|
11
|
+
* resource controllers whose names do not match a schema action (these also carry
|
|
12
|
+
* `#[SkipAuthorizationCheck]`) and every method of controllers that are not
|
|
13
|
+
* resource controllers.
|
|
14
|
+
*
|
|
15
|
+
* Map values are one permission in short form (`dispatch order`), a list of
|
|
16
|
+
* permissions of which the user needs any one, or `admin` for system administrators.
|
|
17
|
+
*/
|
|
18
|
+
trait AuthorizesMethods
|
|
19
|
+
{
|
|
20
|
+
/**
|
|
21
|
+
* @param array<string, string|string[]> $map method name => permission(s)
|
|
22
|
+
*/
|
|
23
|
+
protected function authorizeMethods(array $map): void
|
|
24
|
+
{
|
|
25
|
+
foreach ($map as $method => $permissions) {
|
|
26
|
+
$this->middleware(function ($request, $next) use ($permissions) {
|
|
27
|
+
if ($permissions === 'admin') {
|
|
28
|
+
Authorization::authorizeAdmin();
|
|
29
|
+
} else {
|
|
30
|
+
Authorization::authorize(...(array) $permissions);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return $next($request);
|
|
34
|
+
})->only($method);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/server/src/routes.php
CHANGED
|
@@ -405,8 +405,10 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
405
405
|
$router->group(
|
|
406
406
|
['prefix' => 'v1/fleet-ops/navigator', 'namespace' => 'v1'],
|
|
407
407
|
function ($router) {
|
|
408
|
-
|
|
408
|
+
// Opened from the Navigator app's QR scan without a session;
|
|
409
|
+
// NavigatorController::linkApp() requires a valid signature.
|
|
409
410
|
$router->get('link-app', 'NavigatorController@linkApp');
|
|
411
|
+
$router->get('get-link-app', 'NavigatorController@getLinkAppUrl')->middleware(['fleetbase.protected']);
|
|
410
412
|
}
|
|
411
413
|
);
|
|
412
414
|
|
|
@@ -814,6 +816,12 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
814
816
|
$router->post('orchestrator-settings', 'SettingController@saveOrchestratorSettings');
|
|
815
817
|
$router->get('orchestrator-card-fields', 'SettingController@getOrchestratorCardFields');
|
|
816
818
|
$router->post('orchestrator-card-fields', 'SettingController@saveOrchestratorCardFields');
|
|
819
|
+
$router->get('telematics-settings', 'SettingController@getTelematicsSettings');
|
|
820
|
+
$router->post('telematics-settings', 'SettingController@saveTelematicsSettings');
|
|
821
|
+
$router->get('admin-telematics-settings', 'SettingController@getAdminTelematicsSettings');
|
|
822
|
+
$router->post('admin-telematics-settings', 'SettingController@saveAdminTelematicsSettings');
|
|
823
|
+
$router->get('telematics-storage-usage', 'SettingController@getTelematicsStorageUsage');
|
|
824
|
+
$router->post('telematics-retention/run', 'SettingController@runTelematicsRetention');
|
|
817
825
|
}
|
|
818
826
|
);
|
|
819
827
|
$router->group(
|
|
@@ -17,6 +17,9 @@ use Fleetbase\FleetOps\Support\Analytics\RevenueTrend;
|
|
|
17
17
|
use Fleetbase\FleetOps\Support\Analytics\TopDrivers;
|
|
18
18
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
19
19
|
use Fleetbase\Models\Company;
|
|
20
|
+
use Illuminate\Database\ConnectionResolver;
|
|
21
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
22
|
+
use Illuminate\Database\SQLiteConnection;
|
|
20
23
|
use Illuminate\Http\Request;
|
|
21
24
|
use Illuminate\Support\Carbon;
|
|
22
25
|
|
|
@@ -273,6 +276,16 @@ test('analytics base applies company currency and explicit date ranges', functio
|
|
|
273
276
|
});
|
|
274
277
|
|
|
275
278
|
test('live fleet analytics serializes driver and vehicle map payloads', function () {
|
|
279
|
+
// The markers' cards look up the current order and last known position.
|
|
280
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
281
|
+
$connection->statement('create table orders (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, driver_assigned_uuid varchar(64) null, vehicle_assigned_uuid varchar(64) null, tracking_number_uuid varchar(64) null, status varchar(64) null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
282
|
+
$connection->statement('create table drivers (uuid varchar(64) primary key, public_id varchar(64) null, company_uuid varchar(64) null, user_uuid varchar(64) null, vehicle_uuid varchar(64) null, current_job_uuid varchar(64) null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
283
|
+
$connection->statement('create table users (uuid varchar(64) primary key, company_uuid varchar(64) null, name varchar(255) null, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
284
|
+
$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, deleted_at datetime null, created_at datetime null, updated_at datetime null)');
|
|
285
|
+
$resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
286
|
+
$resolver->setDefaultConnection('mysql');
|
|
287
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
288
|
+
|
|
276
289
|
$analytics = new LiveFleet();
|
|
277
290
|
|
|
278
291
|
$driver = new TestFleetOpsLiveFleetDriver();
|
|
@@ -308,7 +321,21 @@ test('live fleet analytics serializes driver and vehicle map payloads', function
|
|
|
308
321
|
$driverPayload->setAccessible(true);
|
|
309
322
|
$vehiclePayload->setAccessible(true);
|
|
310
323
|
|
|
311
|
-
|
|
324
|
+
$driverResult = $driverPayload->invoke($analytics, $driver);
|
|
325
|
+
$vehicleResult = $vehiclePayload->invoke($analytics, $vehicle);
|
|
326
|
+
|
|
327
|
+
// Each marker carries the live map's card: the same index resource the live endpoints return.
|
|
328
|
+
expect($driverResult['card'])->toBeArray()
|
|
329
|
+
->and($driverResult['card'])->toHaveKeys(['name', 'status', 'phone', 'email', 'online', 'meta'])
|
|
330
|
+
->and($driverResult['card']['meta'])->toHaveKeys(['status_label', 'speed_label', 'heading_label', 'location_coordinates', 'current_order_reference'])
|
|
331
|
+
->and($driverResult['card']['name'])->toBe('Ada Driver')
|
|
332
|
+
->and($vehicleResult['card'])->toHaveKeys(['display_name', 'driver_name', 'plate_number', 'online', 'meta'])
|
|
333
|
+
->and($vehicleResult['card']['display_name'])->toBe('Van 7')
|
|
334
|
+
->and($vehicleResult['card']['meta'])->toHaveKeys(['status_label', 'speed_label', 'heading_label', 'location_coordinates', 'current_order_reference']);
|
|
335
|
+
|
|
336
|
+
unset($driverResult['card'], $vehicleResult['card']);
|
|
337
|
+
|
|
338
|
+
expect($driverResult)->toBe([
|
|
312
339
|
'uuid' => 'driver-uuid',
|
|
313
340
|
'public_id' => 'driver-public',
|
|
314
341
|
'name' => 'Ada Driver',
|
|
@@ -320,7 +347,7 @@ test('live fleet analytics serializes driver and vehicle map payloads', function
|
|
|
320
347
|
'lat' => 1.30,
|
|
321
348
|
'lng' => 103.80,
|
|
322
349
|
'updated_at' => '2026-01-01 10:00:00',
|
|
323
|
-
])->and($
|
|
350
|
+
])->and($vehicleResult)->toBe([
|
|
324
351
|
'uuid' => 'vehicle-uuid',
|
|
325
352
|
'public_id' => 'vehicle-public',
|
|
326
353
|
'name' => 'Van 7',
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_once __DIR__ . '/../../Support/AfaqyTestCrypto.php';
|
|
4
4
|
require_once __DIR__ . '/../../Support/ExampleTelemetryProvider.php';
|
|
5
5
|
require_once __DIR__ . '/../../Support/TelemetryTestEnvironment.php';
|
|
6
|
+
require_once __DIR__ . '/../../Support/PruneTelematicsDataProbe.php';
|
|
6
7
|
|
|
7
8
|
use Fleetbase\FleetOps\Http\Controllers\TelematicPositionWebhookController;
|
|
8
9
|
use Fleetbase\FleetOps\Jobs\ProcessTelematicDelivery;
|
|
@@ -123,6 +124,12 @@ test('database ingestion deduplicates poll and push and never regresses device s
|
|
|
123
124
|
expect(DeviceEvent::withoutGlobalScopes()->count())->toBe(2);
|
|
124
125
|
expect(count($GLOBALS['afaqy_broadcasts']))->toBe(1);
|
|
125
126
|
expect($device->last_position->getLat())->toBe(24.0);
|
|
127
|
+
// The raw unit is stored once, in payload; meta carries only the normalized block.
|
|
128
|
+
$stored = DeviceEvent::withoutGlobalScopes()->orderBy('id')->first();
|
|
129
|
+
expect(data_get($stored->payload, '_id'))->toBe('unit-1')
|
|
130
|
+
->and($stored->meta)->not->toHaveKeys(['_id', 'last_update'])
|
|
131
|
+
->and(data_get($stored->meta, 'telemetry.position_at'))->toBe('2026-09-15T11:59:00.000000Z')
|
|
132
|
+
->and(data_get($stored->meta, 'telematic_uuid'))->toBe('integration-1');
|
|
126
133
|
});
|
|
127
134
|
|
|
128
135
|
test('webhook authenticates before durable acceptance and worker quarantines unknown shapes', function () {
|
|
@@ -282,7 +289,7 @@ test('five thousand units and a duplicate reconciliation sweep fit the local pro
|
|
|
282
289
|
expect($elapsed)->toBeLessThan(60.0);
|
|
283
290
|
})->skip(getenv('AFAQY_RUN_LOAD_TESTS') !== '1', 'Opt-in local processing benchmark; production latency requires deployment testing.');
|
|
284
291
|
|
|
285
|
-
test('inbox
|
|
292
|
+
test('inbox recovery leaves retention to the prune command, which expires processed deliveries by policy', function () {
|
|
286
293
|
$telematic = afaqyDbFixture();
|
|
287
294
|
$inbox = new Inbox();
|
|
288
295
|
$pending = $inbox->accept($telematic, afaqyDbUnit(), 'poll');
|
|
@@ -297,9 +304,17 @@ test('inbox maintenance retains replayable payloads and cleans expired rows duri
|
|
|
297
304
|
'metadata' => ['telemetry' => ['durable_ingestion' => true]],
|
|
298
305
|
]));
|
|
299
306
|
expect((new Fleetbase\FleetOps\Console\Commands\DrainTelematicInbox())->handle())->toBe(0);
|
|
307
|
+
// The drain only recovers; nothing is expired until the retention sweep runs.
|
|
308
|
+
expect(DB::table('telematic_deliveries')->count())->toBe(3);
|
|
309
|
+
|
|
310
|
+
$prune = new PruneTelematicsDataProbe();
|
|
311
|
+
$prune->options['company'] = 'company-1';
|
|
312
|
+
$prune->companyRows = collect([(object) ['id' => 1, 'uuid' => 'company-1', 'public_id' => 'company_1']]);
|
|
313
|
+
expect($prune->handle())->toBe(0);
|
|
300
314
|
expect(DB::table('telematic_deliveries')->where('uuid', $processed)->exists())->toBeFalse();
|
|
301
315
|
expect(DB::table('telematic_deliveries')->where('uuid', $quarantined)->exists())->toBeTrue();
|
|
302
316
|
expect(DB::table('telematic_deliveries')->where('uuid', $pending)->value('status'))->toBe('pending');
|
|
317
|
+
expect($prune->messages)->toContain(['info', 'company_1 telematic_deliveries: deleted=1 compacted=0 batches=1']);
|
|
303
318
|
});
|
|
304
319
|
|
|
305
320
|
test('sensor values use source time and preserve units through partial and older snapshots', function () {
|