@fleetbase/fleetops-engine 0.6.69 → 0.6.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/RELEASE.md +21 -14
  2. package/addon/components/admin/telematics-settings.hbs +143 -0
  3. package/addon/components/admin/telematics-settings.js +145 -0
  4. package/addon/components/fuel-integration/hub.hbs +1 -1
  5. package/addon/components/layout/fleet-ops-sidebar.js +37 -17
  6. package/addon/components/map/leaflet-live-map.hbs +4 -242
  7. package/addon/components/map/leaflet-live-map.js +2 -1
  8. package/addon/components/map/marker-card/driver.hbs +57 -0
  9. package/addon/components/map/marker-card/driver.js +3 -0
  10. package/addon/components/map/marker-card/vehicle.hbs +78 -0
  11. package/addon/components/map/marker-card/vehicle.js +3 -0
  12. package/addon/components/order/details/activity.hbs +8 -6
  13. package/addon/components/order/details/activity.js +9 -0
  14. package/addon/components/order/details.hbs +29 -0
  15. package/addon/components/order/details.js +26 -0
  16. package/addon/components/order/form/details.hbs +30 -4
  17. package/addon/components/order/form/details.js +46 -0
  18. package/addon/components/order/form/route.hbs +4 -2
  19. package/addon/components/order/form/route.js +11 -0
  20. package/addon/components/order/form.hbs +12 -0
  21. package/addon/components/order/form.js +21 -1
  22. package/addon/components/order/kanban.js +72 -0
  23. package/addon/components/order-config-manager/activity-flow.js +45 -10
  24. package/addon/components/order-config-manager/lifecycle.hbs +250 -0
  25. package/addon/components/order-config-manager/lifecycle.js +358 -0
  26. package/addon/components/order-config-manager.js +5 -1
  27. package/addon/components/telematic/hub.hbs +1 -1
  28. package/addon/components/widget/live-fleet.hbs +8 -40
  29. package/addon/controllers/analytics/reports/index.js +15 -1
  30. package/addon/controllers/connectivity/devices/index/details.js +1 -0
  31. package/addon/controllers/connectivity/devices/index.js +12 -2
  32. package/addon/controllers/connectivity/events/index.js +31 -1
  33. package/addon/controllers/connectivity/fuel-providers/index.js +6 -4
  34. package/addon/controllers/connectivity/sensors/index/details.js +1 -1
  35. package/addon/controllers/connectivity/sensors/index.js +41 -1
  36. package/addon/controllers/connectivity/telematics/index.js +4 -0
  37. package/addon/controllers/maintenance/equipment/index/details.js +2 -2
  38. package/addon/controllers/maintenance/equipment/index.js +36 -10
  39. package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
  40. package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
  41. package/addon/controllers/maintenance/inspection-submissions/index.js +56 -10
  42. package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
  43. package/addon/controllers/maintenance/maintenances/index.js +24 -1
  44. package/addon/controllers/maintenance/parts/index/details.js +2 -2
  45. package/addon/controllers/maintenance/parts/index.js +36 -9
  46. package/addon/controllers/maintenance/schedules/index.js +48 -6
  47. package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
  48. package/addon/controllers/maintenance/work-orders/index.js +47 -9
  49. package/addon/controllers/management/contacts/customers.js +13 -0
  50. package/addon/controllers/management/contacts/index.js +47 -1
  51. package/addon/controllers/management/drivers/index/details.js +1 -0
  52. package/addon/controllers/management/drivers/index.js +36 -2
  53. package/addon/controllers/management/fleets/index/details.js +1 -0
  54. package/addon/controllers/management/fleets/index.js +43 -1
  55. package/addon/controllers/management/fuel-reports/index.js +26 -2
  56. package/addon/controllers/management/fuel-transactions/index.js +45 -7
  57. package/addon/controllers/management/issues/index/details.js +1 -0
  58. package/addon/controllers/management/issues/index.js +26 -2
  59. package/addon/controllers/management/places/index.js +40 -1
  60. package/addon/controllers/management/trailers/index/details.js +1 -0
  61. package/addon/controllers/management/trailers/index.js +50 -2
  62. package/addon/controllers/management/vehicles/index/details.js +1 -0
  63. package/addon/controllers/management/vehicles/index.js +37 -2
  64. package/addon/controllers/management/vendors/index.js +30 -2
  65. package/addon/controllers/operations/orders/index/details.js +61 -5
  66. package/addon/controllers/operations/orders/index.js +66 -4
  67. package/addon/controllers/operations/service-rates/index.js +18 -1
  68. package/addon/controllers/settings/index.js +13 -0
  69. package/addon/controllers/settings/telematics.js +113 -0
  70. package/addon/extension.js +70 -6
  71. package/addon/routes/analytics/index.js +14 -1
  72. package/addon/routes/analytics/reports/index/details.js +1 -1
  73. package/addon/routes/analytics/reports/index/edit.js +1 -1
  74. package/addon/routes/analytics/reports/index/new.js +14 -1
  75. package/addon/routes/analytics/reports.js +14 -1
  76. package/addon/routes/connectivity/devices/index/new.js +14 -1
  77. package/addon/routes/connectivity/devices.js +14 -1
  78. package/addon/routes/connectivity/events.js +14 -1
  79. package/addon/routes/connectivity/fuel-providers/details/transactions.js +12 -0
  80. package/addon/routes/connectivity/fuel-providers/details.js +9 -0
  81. package/addon/routes/connectivity/fuel-providers/edit.js +9 -0
  82. package/addon/routes/connectivity/fuel-providers/new.js +14 -1
  83. package/addon/routes/connectivity/fuel-providers.js +14 -1
  84. package/addon/routes/connectivity/sensors/index/new.js +14 -1
  85. package/addon/routes/connectivity/sensors.js +14 -1
  86. package/addon/routes/connectivity/telematics/new.js +14 -1
  87. package/addon/routes/connectivity/telematics.js +14 -1
  88. package/addon/routes/connectivity/tracking.js +14 -1
  89. package/addon/routes/maintenance/equipment/index/details.js +2 -2
  90. package/addon/routes/maintenance/equipment/index/edit.js +2 -2
  91. package/addon/routes/maintenance/equipment/index/new.js +14 -1
  92. package/addon/routes/maintenance/equipment.js +14 -1
  93. package/addon/routes/maintenance/index.js +14 -1
  94. package/addon/routes/maintenance/inspection-forms/index/details.js +10 -1
  95. package/addon/routes/maintenance/inspection-forms/index/edit.js +10 -1
  96. package/addon/routes/maintenance/inspection-forms/index/new.js +14 -1
  97. package/addon/routes/maintenance/inspection-forms.js +14 -1
  98. package/addon/routes/maintenance/inspection-submissions/index/details.js +10 -1
  99. package/addon/routes/maintenance/inspection-submissions/index/edit.js +10 -1
  100. package/addon/routes/maintenance/inspection-submissions/index/new.js +14 -1
  101. package/addon/routes/maintenance/inspection-submissions.js +14 -1
  102. package/addon/routes/maintenance/maintenances/index/details.js +2 -2
  103. package/addon/routes/maintenance/maintenances/index/edit.js +2 -2
  104. package/addon/routes/maintenance/maintenances/index/new.js +14 -1
  105. package/addon/routes/maintenance/maintenances.js +14 -1
  106. package/addon/routes/maintenance/parts/index/details.js +2 -2
  107. package/addon/routes/maintenance/parts/index/edit.js +2 -2
  108. package/addon/routes/maintenance/parts/index/new.js +14 -1
  109. package/addon/routes/maintenance/parts.js +14 -1
  110. package/addon/routes/maintenance/schedules/index/details.js +2 -2
  111. package/addon/routes/maintenance/schedules/index/edit.js +2 -2
  112. package/addon/routes/maintenance/schedules/index/new.js +15 -1
  113. package/addon/routes/maintenance/schedules.js +15 -1
  114. package/addon/routes/maintenance/work-orders/index/details.js +2 -2
  115. package/addon/routes/maintenance/work-orders/index/edit.js +2 -2
  116. package/addon/routes/maintenance/work-orders/index/new.js +14 -1
  117. package/addon/routes/maintenance/work-orders.js +14 -1
  118. package/addon/routes/management/fuel-transactions/index/details.js +9 -0
  119. package/addon/routes/management/fuel-transactions.js +14 -1
  120. package/addon/routes/management/index.js +13 -0
  121. package/addon/routes/management/trailers.js +16 -0
  122. package/addon/routes/management/vendors/integrated/details.js +14 -1
  123. package/addon/routes/management/vendors/integrated/edit.js +14 -1
  124. package/addon/routes/management/vendors/integrated/new.js +14 -1
  125. package/addon/routes/management/vendors/integrated.js +14 -1
  126. package/addon/routes/operations/orders/index/details.js +1 -0
  127. package/addon/routes/operations/orders.js +40 -1
  128. package/addon/routes/operations/routes/index/details.js +14 -1
  129. package/addon/routes/operations/routes/index/new.js +13 -0
  130. package/addon/routes/operations/routes.js +14 -1
  131. package/addon/routes/operations/service-rates/index/new.js +11 -0
  132. package/addon/routes/operations/service-rates.js +14 -1
  133. package/addon/routes/settings/avatars.js +14 -1
  134. package/addon/routes/settings/custom-fields.js +14 -1
  135. package/addon/routes/settings/map.js +14 -1
  136. package/addon/routes/settings/notifications.js +11 -0
  137. package/addon/routes/settings/orchestrator.js +1 -1
  138. package/addon/routes/settings/payments/index.js +1 -1
  139. package/addon/routes/settings/routing.js +14 -1
  140. package/addon/routes/settings/scheduling.js +14 -1
  141. package/addon/routes/settings/telematics.js +21 -0
  142. package/addon/routes/virtual.js +11 -0
  143. package/addon/routes.js +1 -0
  144. package/addon/services/contact-actions.js +1 -0
  145. package/addon/services/customer-actions.js +2 -1
  146. package/addon/services/device-actions.js +1 -0
  147. package/addon/services/device-event-actions.js +1 -0
  148. package/addon/services/driver-actions.js +4 -13
  149. package/addon/services/equipment-actions.js +1 -0
  150. package/addon/services/fleet-actions.js +1 -0
  151. package/addon/services/fuel-report-actions.js +1 -0
  152. package/addon/services/inspection-form-actions.js +1 -0
  153. package/addon/services/inspection-submission-actions.js +1 -0
  154. package/addon/services/integrated-vendor-actions.js +1 -0
  155. package/addon/services/issue-actions.js +1 -0
  156. package/addon/services/maintenance-actions.js +1 -0
  157. package/addon/services/maintenance-schedule-actions.js +2 -1
  158. package/addon/services/order-actions.js +7 -0
  159. package/addon/services/order-presentation.js +129 -0
  160. package/addon/services/part-actions.js +1 -0
  161. package/addon/services/place-actions.js +1 -0
  162. package/addon/services/sensor-actions.js +1 -0
  163. package/addon/services/service-area-actions.js +1 -0
  164. package/addon/services/service-rate-actions.js +1 -0
  165. package/addon/services/telematic-actions.js +1 -0
  166. package/addon/services/trailer-actions.js +4 -14
  167. package/addon/services/vehicle-actions.js +4 -13
  168. package/addon/services/vendor-actions.js +1 -0
  169. package/addon/services/work-order-actions.js +1 -0
  170. package/addon/services/zone-actions.js +1 -0
  171. package/addon/templates/analytics/reports/index/details.hbs +1 -0
  172. package/addon/templates/analytics/reports/index.hbs +1 -0
  173. package/addon/templates/connectivity/devices/index/details.hbs +1 -0
  174. package/addon/templates/connectivity/devices/index.hbs +1 -0
  175. package/addon/templates/connectivity/events/index.hbs +1 -0
  176. package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
  177. package/addon/templates/connectivity/sensors/index.hbs +1 -0
  178. package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
  179. package/addon/templates/maintenance/equipment/index.hbs +2 -0
  180. package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
  181. package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
  182. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
  183. package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
  184. package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
  185. package/addon/templates/maintenance/maintenances/index.hbs +1 -0
  186. package/addon/templates/maintenance/parts/index/details.hbs +1 -0
  187. package/addon/templates/maintenance/parts/index.hbs +2 -0
  188. package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
  189. package/addon/templates/maintenance/schedules/index.hbs +2 -0
  190. package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
  191. package/addon/templates/maintenance/work-orders/index.hbs +1 -0
  192. package/addon/templates/management/contacts/customers/details.hbs +1 -0
  193. package/addon/templates/management/contacts/customers.hbs +1 -0
  194. package/addon/templates/management/contacts/index/details.hbs +1 -0
  195. package/addon/templates/management/contacts/index.hbs +1 -0
  196. package/addon/templates/management/contacts.hbs +1 -0
  197. package/addon/templates/management/drivers/index/details.hbs +1 -0
  198. package/addon/templates/management/drivers/index.hbs +2 -0
  199. package/addon/templates/management/fleets/index/details.hbs +1 -0
  200. package/addon/templates/management/fleets/index.hbs +1 -0
  201. package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
  202. package/addon/templates/management/fuel-reports/index.hbs +1 -0
  203. package/addon/templates/management/fuel-transactions/index.hbs +1 -0
  204. package/addon/templates/management/issues/index/details.hbs +1 -0
  205. package/addon/templates/management/issues/index.hbs +1 -0
  206. package/addon/templates/management/places/index/details.hbs +1 -0
  207. package/addon/templates/management/places/index.hbs +1 -0
  208. package/addon/templates/management/trailers/index/details.hbs +1 -0
  209. package/addon/templates/management/trailers/index.hbs +2 -0
  210. package/addon/templates/management/vehicles/index/details.hbs +1 -0
  211. package/addon/templates/management/vehicles/index.hbs +2 -0
  212. package/addon/templates/management/vendors/index/details.hbs +1 -0
  213. package/addon/templates/management/vendors/index.hbs +2 -0
  214. package/addon/templates/management/vendors/integrated.hbs +1 -0
  215. package/addon/templates/operations/orders/index/details.hbs +1 -0
  216. package/addon/templates/operations/orders/index.hbs +1 -0
  217. package/addon/templates/operations/service-rates/index/details.hbs +1 -0
  218. package/addon/templates/operations/service-rates/index.hbs +1 -0
  219. package/addon/templates/settings/telematics.hbs +70 -0
  220. package/addon/utils/normalize-order-config-flow.js +6 -1
  221. package/app/components/admin/telematics-settings.js +1 -0
  222. package/app/components/map/marker-card/driver.js +1 -0
  223. package/app/components/map/marker-card/vehicle.js +1 -0
  224. package/app/controllers/settings/telematics.js +1 -0
  225. package/app/routes/settings/telematics.js +1 -0
  226. package/app/services/order-presentation.js +1 -0
  227. package/app/templates/settings/telematics.js +1 -0
  228. package/composer.json +2 -2
  229. package/docs/AFAQY.md +1 -1
  230. package/docs/TELEMATICS_QUEUES.md +1 -1
  231. package/docs/TELEMETRY_ARCHITECTURE.md +72 -1
  232. package/extension.json +1 -1
  233. package/package.json +3 -3
  234. package/server/config/telemetry.php +12 -0
  235. package/server/migrations/2026_09_23_000001_add_retention_index_to_device_events.php +40 -0
  236. package/server/migrations/2026_09_23_000002_add_retention_index_to_positions.php +40 -0
  237. package/server/migrations/2026_09_23_000003_add_retention_index_to_telematic_sync_runs.php +40 -0
  238. package/server/src/Auth/Schemas/FleetOps.php +104 -5
  239. package/server/src/Console/Commands/DrainTelematicInbox.php +3 -13
  240. package/server/src/Console/Commands/ProcessOperationalAlerts.php +3 -3
  241. package/server/src/Console/Commands/PruneTelematicsData.php +318 -0
  242. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +9 -0
  243. package/server/src/Http/Controllers/FleetOpsController.php +21 -0
  244. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +11 -0
  245. package/server/src/Http/Controllers/Internal/v1/ContactController.php +11 -0
  246. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +14 -0
  247. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +13 -0
  248. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +11 -0
  249. package/server/src/Http/Controllers/Internal/v1/DriverController.php +22 -0
  250. package/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +13 -0
  251. package/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +15 -0
  252. package/server/src/Http/Controllers/Internal/v1/FuelReportController.php +9 -3
  253. package/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +15 -0
  254. package/server/src/Http/Controllers/Internal/v1/LiveController.php +16 -0
  255. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +15 -0
  256. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +15 -0
  257. package/server/src/Http/Controllers/Internal/v1/ManifestController.php +15 -0
  258. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +11 -0
  259. package/server/src/Http/Controllers/Internal/v1/NavigatorController.php +42 -1
  260. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +12 -0
  261. package/server/src/Http/Controllers/Internal/v1/OrderController.php +56 -4
  262. package/server/src/Http/Controllers/Internal/v1/PaymentController.php +13 -0
  263. package/server/src/Http/Controllers/Internal/v1/RadarController.php +19 -0
  264. package/server/src/Http/Controllers/Internal/v1/ServiceAreaController.php +9 -3
  265. package/server/src/Http/Controllers/Internal/v1/SettingController.php +465 -14
  266. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +21 -0
  267. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +21 -0
  268. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +21 -0
  269. package/server/src/Http/Controllers/Internal/v1/VendorController.php +17 -0
  270. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +11 -0
  271. package/server/src/Jobs/DispatchTelematicsRetentionJobs.php +70 -0
  272. package/server/src/Jobs/PruneTelematicsDataJob.php +74 -0
  273. package/server/src/Listeners/NotifyDriverOnShiftChange.php +3 -3
  274. package/server/src/Mail/CustomerCredentialsMail.php +1 -1
  275. package/server/src/Models/Order.php +35 -3
  276. package/server/src/Models/OrderConfig.php +129 -0
  277. package/server/src/Models/TrackingNumber.php +8 -4
  278. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +9 -1
  279. package/server/src/Providers/FleetOpsServiceProvider.php +3 -0
  280. package/server/src/Support/Analytics/LiveFleet.php +18 -2
  281. package/server/src/Support/Authorization.php +103 -0
  282. package/server/src/Support/Database/TableIndexes.php +66 -0
  283. package/server/src/Support/OrderTracker.php +2 -2
  284. package/server/src/Support/Reporting/FleetOpsReportSchema.php +476 -116
  285. package/server/src/Support/Telematics/Retention/RetentionPolicy.php +300 -0
  286. package/server/src/Support/Telematics/Retention/TelemetryActivity.php +40 -0
  287. package/server/src/Support/Telematics/TelematicService.php +12 -2
  288. package/server/src/Support/Telematics/Telemetry/Ingestor.php +4 -2
  289. package/server/src/Tracking/TrackingIntelligenceService.php +1 -1
  290. package/server/src/Tracking/TrackingOptions.php +2 -2
  291. package/server/src/Traits/AuthorizesMethods.php +37 -0
  292. package/server/src/routes.php +9 -1
  293. package/server/tests/AnalyticsRoutesTest.php +29 -2
  294. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +16 -1
  295. package/server/tests/Feature/Http/Api/TelematicsRetentionIndexMigrationTest.php +115 -0
  296. package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
  297. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
  298. package/server/tests/Feature/Http/Internal/SettingControllerHelpersTest.php +71 -0
  299. package/server/tests/Feature/Http/Internal/TelematicsStorageDeadlinesTest.php +119 -0
  300. package/server/tests/InternalImportExportControllerContractsTest.php +5 -8
  301. package/server/tests/NavigatorControllerContractsTest.php +36 -4
  302. package/server/tests/NotificationAndMailContractsTest.php +31 -0
  303. package/server/tests/ObserverContractsTest.php +1 -1
  304. package/server/tests/OrchestratorConsumableApiTest.php +42 -0
  305. package/server/tests/OrderConfigLifecycleTest.php +120 -0
  306. package/server/tests/ProviderContractsTest.php +10 -0
  307. package/server/tests/ReportSchemaContractsTest.php +120 -6
  308. package/server/tests/SettingControllerContractsTest.php +429 -18
  309. package/server/tests/SmallControllerContractsTest.php +8 -5
  310. package/server/tests/Support/PruneTelematicsDataProbe.php +55 -0
  311. package/server/tests/Unit/Console/Commands/ProcessOperationalAlertsCommandTest.php +2 -2
  312. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +353 -0
  313. package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
  314. package/server/tests/Unit/Jobs/DispatchTelematicsRetentionJobsTest.php +114 -0
  315. package/server/tests/Unit/Jobs/PruneTelematicsDataJobTest.php +60 -0
  316. package/server/tests/Unit/Orchestration/VroomEngineTransportAndSettingsTest.php +20 -0
  317. package/server/tests/Unit/Support/Analytics/LiveFleetTest.php +6 -0
  318. package/server/tests/Unit/Support/AuthorizationTest.php +142 -0
  319. package/server/tests/Unit/Support/MaintainableAndOperationalAlertsTest.php +12 -1
  320. package/server/tests/Unit/Support/MoreQuerySeamsTest.php +11 -2
  321. package/server/tests/Unit/Support/Telematics/Retention/RetentionPolicyTest.php +190 -0
  322. package/server/tests/Unit/Support/Telematics/Retention/TelemetryActivityTest.php +68 -0
  323. package/server/tests/Unit/Tracking/TrackingOptionsCompanySettingsTest.php +109 -0
  324. package/translations/en-us.yaml +108 -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
+ });
@@ -6,7 +6,8 @@ use Illuminate\Support\Str;
6
6
 
7
7
  class FleetOpsOrderDispatchFailedUnitProbe extends OrderDispatchFailed
8
8
  {
9
- public function getEventData(): array
9
+ // core-api 1.6.68 adds an optional $channel parameter; the literal keeps this probe loadable on earlier versions too.
10
+ public function getEventData(string $channel = 'webhook'): array
10
11
  {
11
12
  return [];
12
13
  }
@@ -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([