@fleetbase/fleetops-engine 0.6.68 → 0.6.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/RELEASE.md +44 -9
  2. package/addon/components/admin/telematics-settings.hbs +143 -0
  3. package/addon/components/admin/telematics-settings.js +145 -0
  4. package/addon/components/contact/form.hbs +13 -2
  5. package/addon/components/customer/details.hbs +17 -17
  6. package/addon/components/customer/form.hbs +13 -2
  7. package/addon/components/customer/form.js +0 -56
  8. package/addon/components/driver/details.hbs +13 -19
  9. package/addon/components/driver/form.hbs +20 -35
  10. package/addon/components/driver/form.js +0 -58
  11. package/addon/components/fuel-integration/hub.hbs +1 -1
  12. package/addon/components/layout/fleet-ops-sidebar.js +37 -17
  13. package/addon/components/map/leaflet-live-map.hbs +4 -242
  14. package/addon/components/map/leaflet-live-map.js +2 -1
  15. package/addon/components/map/marker-card/driver.hbs +57 -0
  16. package/addon/components/map/marker-card/driver.js +3 -0
  17. package/addon/components/map/marker-card/vehicle.hbs +78 -0
  18. package/addon/components/map/marker-card/vehicle.js +3 -0
  19. package/addon/components/modals/driver-form.hbs +2 -2
  20. package/addon/components/modals/reset-profile-credentials.hbs +23 -0
  21. package/addon/components/modals/reset-profile-credentials.js +70 -0
  22. package/addon/components/profile-identity-hint.hbs +13 -0
  23. package/addon/components/profile-identity-hint.js +49 -0
  24. package/addon/components/telematic/hub.hbs +1 -1
  25. package/addon/components/widget/live-fleet.hbs +8 -40
  26. package/addon/controllers/analytics/reports/index.js +5 -0
  27. package/addon/controllers/connectivity/devices/index.js +4 -0
  28. package/addon/controllers/connectivity/fuel-providers/index.js +6 -4
  29. package/addon/controllers/connectivity/sensors/index.js +4 -0
  30. package/addon/controllers/connectivity/telematics/index.js +4 -0
  31. package/addon/controllers/maintenance/equipment/index.js +11 -4
  32. package/addon/controllers/maintenance/inspection-forms/index.js +2 -2
  33. package/addon/controllers/maintenance/inspection-submissions/index.js +8 -2
  34. package/addon/controllers/maintenance/maintenances/index.js +4 -0
  35. package/addon/controllers/maintenance/parts/index.js +11 -4
  36. package/addon/controllers/maintenance/schedules/index.js +17 -4
  37. package/addon/controllers/maintenance/work-orders/index.js +11 -4
  38. package/addon/controllers/management/contacts/index.js +4 -0
  39. package/addon/controllers/management/drivers/index.js +8 -0
  40. package/addon/controllers/management/fleets/index.js +4 -0
  41. package/addon/controllers/management/fuel-reports/index.js +4 -0
  42. package/addon/controllers/management/fuel-transactions/index.js +9 -7
  43. package/addon/controllers/management/issues/index.js +4 -0
  44. package/addon/controllers/management/places/index.js +4 -0
  45. package/addon/controllers/management/vehicles/index.js +4 -0
  46. package/addon/controllers/management/vendors/index.js +4 -0
  47. package/addon/controllers/operations/orders/index/details.js +30 -1
  48. package/addon/controllers/operations/orders/index.js +7 -0
  49. package/addon/controllers/operations/service-rates/index.js +3 -0
  50. package/addon/controllers/settings/index.js +13 -0
  51. package/addon/controllers/settings/telematics.js +113 -0
  52. package/addon/extension.js +42 -8
  53. package/addon/routes/analytics/index.js +14 -1
  54. package/addon/routes/analytics/reports/index/details.js +1 -1
  55. package/addon/routes/analytics/reports/index/edit.js +1 -1
  56. package/addon/routes/analytics/reports/index/new.js +14 -1
  57. package/addon/routes/analytics/reports.js +14 -1
  58. package/addon/routes/connectivity/devices/index/new.js +14 -1
  59. package/addon/routes/connectivity/devices.js +14 -1
  60. package/addon/routes/connectivity/events.js +14 -1
  61. package/addon/routes/connectivity/fuel-providers/details/transactions.js +12 -0
  62. package/addon/routes/connectivity/fuel-providers/details.js +9 -0
  63. package/addon/routes/connectivity/fuel-providers/edit.js +9 -0
  64. package/addon/routes/connectivity/fuel-providers/new.js +14 -1
  65. package/addon/routes/connectivity/fuel-providers.js +14 -1
  66. package/addon/routes/connectivity/sensors/index/new.js +14 -1
  67. package/addon/routes/connectivity/sensors.js +14 -1
  68. package/addon/routes/connectivity/telematics/new.js +14 -1
  69. package/addon/routes/connectivity/telematics.js +14 -1
  70. package/addon/routes/connectivity/tracking.js +14 -1
  71. package/addon/routes/maintenance/equipment/index/details.js +2 -2
  72. package/addon/routes/maintenance/equipment/index/edit.js +2 -2
  73. package/addon/routes/maintenance/equipment/index/new.js +14 -1
  74. package/addon/routes/maintenance/equipment.js +14 -1
  75. package/addon/routes/maintenance/index.js +14 -1
  76. package/addon/routes/maintenance/inspection-forms/index/details.js +10 -1
  77. package/addon/routes/maintenance/inspection-forms/index/edit.js +10 -1
  78. package/addon/routes/maintenance/inspection-forms/index/new.js +14 -1
  79. package/addon/routes/maintenance/inspection-forms.js +14 -1
  80. package/addon/routes/maintenance/inspection-submissions/index/details.js +10 -1
  81. package/addon/routes/maintenance/inspection-submissions/index/edit.js +10 -1
  82. package/addon/routes/maintenance/inspection-submissions/index/new.js +14 -1
  83. package/addon/routes/maintenance/inspection-submissions.js +14 -1
  84. package/addon/routes/maintenance/maintenances/index/details.js +2 -2
  85. package/addon/routes/maintenance/maintenances/index/edit.js +2 -2
  86. package/addon/routes/maintenance/maintenances/index/new.js +14 -1
  87. package/addon/routes/maintenance/maintenances.js +14 -1
  88. package/addon/routes/maintenance/parts/index/details.js +2 -2
  89. package/addon/routes/maintenance/parts/index/edit.js +2 -2
  90. package/addon/routes/maintenance/parts/index/new.js +14 -1
  91. package/addon/routes/maintenance/parts.js +14 -1
  92. package/addon/routes/maintenance/schedules/index/details.js +2 -2
  93. package/addon/routes/maintenance/schedules/index/edit.js +2 -2
  94. package/addon/routes/maintenance/schedules/index/new.js +15 -1
  95. package/addon/routes/maintenance/schedules.js +15 -1
  96. package/addon/routes/maintenance/work-orders/index/details.js +2 -2
  97. package/addon/routes/maintenance/work-orders/index/edit.js +2 -2
  98. package/addon/routes/maintenance/work-orders/index/new.js +14 -1
  99. package/addon/routes/maintenance/work-orders.js +14 -1
  100. package/addon/routes/management/fuel-transactions/index/details.js +9 -0
  101. package/addon/routes/management/fuel-transactions.js +14 -1
  102. package/addon/routes/management/index.js +13 -0
  103. package/addon/routes/management/trailers.js +16 -0
  104. package/addon/routes/management/vendors/integrated/details.js +14 -1
  105. package/addon/routes/management/vendors/integrated/edit.js +14 -1
  106. package/addon/routes/management/vendors/integrated/new.js +14 -1
  107. package/addon/routes/management/vendors/integrated.js +14 -1
  108. package/addon/routes/operations/orders.js +40 -1
  109. package/addon/routes/operations/routes/index/details.js +14 -1
  110. package/addon/routes/operations/routes/index/new.js +13 -0
  111. package/addon/routes/operations/routes.js +14 -1
  112. package/addon/routes/operations/service-rates/index/new.js +11 -0
  113. package/addon/routes/operations/service-rates.js +14 -1
  114. package/addon/routes/settings/avatars.js +14 -1
  115. package/addon/routes/settings/custom-fields.js +14 -1
  116. package/addon/routes/settings/map.js +14 -1
  117. package/addon/routes/settings/notifications.js +11 -0
  118. package/addon/routes/settings/orchestrator.js +1 -1
  119. package/addon/routes/settings/payments/index.js +1 -1
  120. package/addon/routes/settings/routing.js +14 -1
  121. package/addon/routes/settings/scheduling.js +14 -1
  122. package/addon/routes/settings/telematics.js +21 -0
  123. package/addon/routes/virtual.js +11 -0
  124. package/addon/routes.js +1 -0
  125. package/addon/services/contact-actions.js +50 -89
  126. package/addon/services/driver-actions.js +98 -0
  127. package/addon/templates/settings/telematics.hbs +70 -0
  128. package/addon/utils/profile-login-actions.js +172 -0
  129. package/app/components/admin/telematics-settings.js +1 -0
  130. package/app/components/{modals/reset-customer-credentials.js → map/marker-card/driver.js} +1 -1
  131. package/app/components/map/marker-card/vehicle.js +1 -0
  132. package/app/components/modals/reset-profile-credentials.js +1 -0
  133. package/app/components/profile-identity-hint.js +1 -0
  134. package/app/controllers/settings/telematics.js +1 -0
  135. package/app/routes/settings/telematics.js +1 -0
  136. package/app/templates/settings/telematics.js +1 -0
  137. package/composer.json +2 -2
  138. package/docs/AFAQY.md +1 -1
  139. package/docs/TELEMATICS_QUEUES.md +1 -1
  140. package/docs/TELEMETRY_ARCHITECTURE.md +72 -1
  141. package/extension.json +1 -1
  142. package/package.json +3 -3
  143. package/scripts/pest-bootstrap.php +12 -0
  144. package/server/config/telemetry.php +12 -0
  145. package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
  146. package/server/migrations/2026_09_23_000001_add_retention_index_to_device_events.php +40 -0
  147. package/server/migrations/2026_09_23_000002_add_retention_index_to_positions.php +40 -0
  148. package/server/migrations/2026_09_23_000003_add_retention_index_to_telematic_sync_runs.php +40 -0
  149. package/server/resources/views/mail/driver-credentials.blade.php +21 -0
  150. package/server/src/Auth/Schemas/FleetOps.php +104 -5
  151. package/server/src/Console/Commands/DrainTelematicInbox.php +3 -13
  152. package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
  153. package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
  154. package/server/src/Console/Commands/ProcessOperationalAlerts.php +3 -3
  155. package/server/src/Console/Commands/PruneTelematicsData.php +318 -0
  156. package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
  157. package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
  158. package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
  159. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +9 -0
  160. package/server/src/Http/Controllers/FleetOpsController.php +21 -0
  161. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +11 -0
  162. package/server/src/Http/Controllers/Internal/v1/ContactController.php +16 -17
  163. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +54 -4
  164. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +13 -0
  165. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +11 -0
  166. package/server/src/Http/Controllers/Internal/v1/DriverController.php +204 -162
  167. package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
  168. package/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +13 -0
  169. package/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +15 -0
  170. package/server/src/Http/Controllers/Internal/v1/FuelReportController.php +9 -3
  171. package/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +15 -0
  172. package/server/src/Http/Controllers/Internal/v1/LiveController.php +16 -0
  173. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +15 -0
  174. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +15 -0
  175. package/server/src/Http/Controllers/Internal/v1/ManifestController.php +15 -0
  176. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +11 -0
  177. package/server/src/Http/Controllers/Internal/v1/NavigatorController.php +42 -1
  178. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +12 -0
  179. package/server/src/Http/Controllers/Internal/v1/OrderController.php +178 -27
  180. package/server/src/Http/Controllers/Internal/v1/PaymentController.php +13 -0
  181. package/server/src/Http/Controllers/Internal/v1/RadarController.php +19 -0
  182. package/server/src/Http/Controllers/Internal/v1/ServiceAreaController.php +9 -3
  183. package/server/src/Http/Controllers/Internal/v1/SettingController.php +465 -14
  184. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +21 -0
  185. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +21 -0
  186. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +21 -0
  187. package/server/src/Http/Controllers/Internal/v1/VendorController.php +17 -0
  188. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +11 -0
  189. package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
  190. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
  191. package/server/src/Http/Resources/v1/Contact.php +3 -0
  192. package/server/src/Http/Resources/v1/Driver.php +3 -0
  193. package/server/src/Jobs/DispatchTelematicsRetentionJobs.php +70 -0
  194. package/server/src/Jobs/PruneTelematicsDataJob.php +74 -0
  195. package/server/src/Listeners/NotifyDriverOnShiftChange.php +3 -3
  196. package/server/src/Mail/CustomerCredentialsMail.php +1 -1
  197. package/server/src/Mail/DriverCredentialsMail.php +55 -0
  198. package/server/src/Models/Contact.php +55 -90
  199. package/server/src/Models/Sensor.php +3 -5
  200. package/server/src/Observers/ContactObserver.php +2 -22
  201. package/server/src/Observers/DriverObserver.php +5 -6
  202. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +9 -1
  203. package/server/src/Providers/FleetOpsServiceProvider.php +18 -0
  204. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
  205. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
  206. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
  207. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
  208. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
  209. package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
  210. package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
  211. package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
  212. package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
  213. package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
  214. package/server/src/Support/Analytics/LiveFleet.php +18 -2
  215. package/server/src/Support/Authorization.php +103 -0
  216. package/server/src/Support/Database/TableIndexes.php +66 -0
  217. package/server/src/Support/OrderTracker.php +2 -2
  218. package/server/src/Support/ProfileAccountManager.php +405 -0
  219. package/server/src/Support/Reporting/FleetOpsReportSchema.php +476 -116
  220. package/server/src/Support/Telematics/Retention/RetentionPolicy.php +300 -0
  221. package/server/src/Support/Telematics/Retention/TelemetryActivity.php +40 -0
  222. package/server/src/Support/Telematics/TelematicService.php +12 -2
  223. package/server/src/Support/Telematics/Telemetry/Ingestor.php +4 -2
  224. package/server/src/Tracking/TrackingIntelligenceService.php +1 -1
  225. package/server/src/Tracking/TrackingOptions.php +2 -2
  226. package/server/src/Traits/AuthorizesMethods.php +37 -0
  227. package/server/src/routes.php +14 -1
  228. package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
  229. package/server/tests/AnalyticsRoutesTest.php +29 -2
  230. package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
  231. package/server/tests/ApiDriverControllerContractsTest.php +102 -20
  232. package/server/tests/CommandContractsTest.php +4 -4
  233. package/server/tests/ControllerHelperContractsTest.php +3 -11
  234. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +21 -1
  235. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
  236. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
  237. package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
  238. package/server/tests/Feature/Http/Api/TelematicsRetentionIndexMigrationTest.php +115 -0
  239. package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
  240. package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
  241. package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
  242. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
  243. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
  244. package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
  245. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
  246. package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
  247. package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
  248. package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
  249. package/server/tests/Feature/Http/Internal/SettingControllerHelpersTest.php +71 -0
  250. package/server/tests/Feature/Http/Internal/TelematicsStorageDeadlinesTest.php +119 -0
  251. package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
  252. package/server/tests/InternalImportExportControllerContractsTest.php +5 -8
  253. package/server/tests/ModelAccessorContractsTest.php +82 -20
  254. package/server/tests/NavigatorControllerContractsTest.php +36 -4
  255. package/server/tests/NotificationAndMailContractsTest.php +31 -0
  256. package/server/tests/ObserverContractsTest.php +28 -3
  257. package/server/tests/OrchestratorConsumableApiTest.php +42 -0
  258. package/server/tests/ProviderContractsTest.php +10 -0
  259. package/server/tests/ReportSchemaContractsTest.php +120 -6
  260. package/server/tests/RequestContractsTest.php +19 -11
  261. package/server/tests/SettingControllerContractsTest.php +429 -18
  262. package/server/tests/SmallControllerContractsTest.php +8 -5
  263. package/server/tests/Support/PruneTelematicsDataProbe.php +55 -0
  264. package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
  265. package/server/tests/TelematicControllerContractsTest.php +22 -4
  266. package/server/tests/Unit/Console/Commands/ProcessOperationalAlertsCommandTest.php +2 -2
  267. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +353 -0
  268. package/server/tests/Unit/Jobs/DispatchTelematicsRetentionJobsTest.php +114 -0
  269. package/server/tests/Unit/Jobs/PruneTelematicsDataJobTest.php +60 -0
  270. package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
  271. package/server/tests/Unit/Models/ContactTest.php +48 -5
  272. package/server/tests/Unit/Models/SensorTest.php +2 -2
  273. package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
  274. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
  275. package/server/tests/Unit/Orchestration/VroomEngineTransportAndSettingsTest.php +20 -0
  276. package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
  277. package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
  278. package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
  279. package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
  280. package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
  281. package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
  282. package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
  283. package/server/tests/Unit/Support/Analytics/LiveFleetTest.php +6 -0
  284. package/server/tests/Unit/Support/AuthorizationTest.php +142 -0
  285. package/server/tests/Unit/Support/MaintainableAndOperationalAlertsTest.php +12 -1
  286. package/server/tests/Unit/Support/MoreQuerySeamsTest.php +11 -2
  287. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
  288. package/server/tests/Unit/Support/Telematics/Retention/RetentionPolicyTest.php +190 -0
  289. package/server/tests/Unit/Support/Telematics/Retention/TelemetryActivityTest.php +68 -0
  290. package/server/tests/Unit/Tracking/TrackingOptionsCompanySettingsTest.php +109 -0
  291. package/translations/en-us.yaml +85 -2
  292. package/translations/uk-ua.yaml +3475 -0
  293. package/addon/components/modals/reset-customer-credentials.hbs +0 -18
  294. package/addon/components/modals/reset-customer-credentials.js +0 -55
@@ -0,0 +1,70 @@
1
+ <Layout::Section::Header @title={{t "settings.telematics.telematics-settings"}}>
2
+ <Button
3
+ @type="primary"
4
+ @size="sm"
5
+ @icon="save"
6
+ @text={{t "common.save-changes"}}
7
+ @onClick={{perform this.saveSettings}}
8
+ @disabled={{or this.saveSettings.isRunning this.getSettings.isRunning (not this.settingsLoaded)}}
9
+ @isLoading={{or this.saveSettings.isRunning this.getSettings.isRunning}}
10
+ @permission="fleet-ops update telematics-settings"
11
+ />
12
+ </Layout::Section::Header>
13
+ <Layout::Section::Body class="overflow-y-scroll-i h-full">
14
+ <div class="container mx-auto">
15
+ <div class="max-w-3xl my-10 mx-auto space-y-6">
16
+ <InfoBlock @type="info" @icon="circle-info">
17
+ {{t "settings.telematics.intro"}}
18
+ </InfoBlock>
19
+
20
+ <ContentPanel @title={{t "settings.telematics.device-events"}} @open={{true}} @wrapperClass="bordered-classic">
21
+ <InputGroup>
22
+ <Toggle
23
+ @label={{t "settings.telematics.use-system-default"}}
24
+ @isToggled={{this.useDefaultEventRetention}}
25
+ @onToggle={{this.setUseDefaultEventRetention}}
26
+ @disabled={{not this.settingsLoaded}}
27
+ />
28
+ </InputGroup>
29
+ <InputGroup @name={{t "settings.telematics.event-retention-days"}} @helpText={{t "settings.telematics.event-retention-days-help"}}>
30
+ <Input
31
+ @type="number"
32
+ @value={{this.eventRetentionDays}}
33
+ min={{this.eventRetentionMinimum}}
34
+ max={{this.eventRetentionMaximum}}
35
+ disabled={{or this.useDefaultEventRetention (not this.settingsLoaded)}}
36
+ class="w-full form-input"
37
+ />
38
+ </InputGroup>
39
+ {{#if this.settingsLoaded}}
40
+ <p class="text-xs text-gray-500 dark:text-gray-400">{{this.eventRetentionPolicyHelp}}</p>
41
+ {{/if}}
42
+ </ContentPanel>
43
+
44
+ <ContentPanel @title={{t "settings.telematics.positions"}} @open={{true}} @wrapperClass="bordered-classic">
45
+ <InputGroup>
46
+ <Toggle
47
+ @label={{t "settings.telematics.use-system-default"}}
48
+ @isToggled={{this.useDefaultPositionRetention}}
49
+ @onToggle={{this.setUseDefaultPositionRetention}}
50
+ @disabled={{not this.settingsLoaded}}
51
+ />
52
+ </InputGroup>
53
+ <InputGroup @name={{t "settings.telematics.position-retention-days"}} @helpText={{t "settings.telematics.position-retention-days-help"}}>
54
+ <Input
55
+ @type="number"
56
+ @value={{this.positionRetentionDays}}
57
+ min={{this.positionRetentionMinimum}}
58
+ max={{this.positionRetentionMaximum}}
59
+ disabled={{or this.useDefaultPositionRetention (not this.settingsLoaded)}}
60
+ class="w-full form-input"
61
+ />
62
+ </InputGroup>
63
+ {{#if this.settingsLoaded}}
64
+ <p class="text-xs text-gray-500 dark:text-gray-400">{{this.positionRetentionPolicyHelp}}</p>
65
+ {{/if}}
66
+ </ContentPanel>
67
+ </div>
68
+ </div>
69
+ <Spacer @height="200px" />
70
+ </Layout::Section::Body>
@@ -0,0 +1,172 @@
1
+ import { get } from '@ember/object';
2
+
3
+ /**
4
+ * Shared helpers for the login account behind a driver, contact or customer
5
+ * profile. The account itself is created, updated and removed by the server
6
+ * from the profile's name/email/phone; the console only offers the account
7
+ * actions (reset password, send credentials, deactivate/reactivate login).
8
+ *
9
+ * A profile owned by a staff member (`is_staff_linked`) has its login
10
+ * managed from IAM, so no account actions are offered for it here.
11
+ */
12
+
13
+ export const INTERNAL_NAMESPACE = 'int/v1';
14
+ export const INACTIVE_LOGIN_STATUSES = ['inactive', 'disabled', 'suspended'];
15
+
16
+ function read(subject, key) {
17
+ if (!subject) {
18
+ return undefined;
19
+ }
20
+
21
+ try {
22
+ return get(subject, key);
23
+ } catch {
24
+ return subject[key];
25
+ }
26
+ }
27
+
28
+ /**
29
+ * The linked user record when it is already loaded, without triggering a
30
+ * fetch for an async relationship.
31
+ */
32
+ export function loadedUser(profile) {
33
+ if (typeof profile?.belongsTo === 'function') {
34
+ try {
35
+ return profile.belongsTo('user').value();
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ const user = read(profile, 'user');
42
+ if (!user) {
43
+ return null;
44
+ }
45
+
46
+ return typeof user.then === 'function' ? (read(user, 'content') ?? null) : user;
47
+ }
48
+
49
+ export function isStaffLinked(profile) {
50
+ return Boolean(read(profile, 'is_staff_linked'));
51
+ }
52
+
53
+ export function hasLinkedUser(profile) {
54
+ if (!profile) {
55
+ return false;
56
+ }
57
+
58
+ if (read(profile, 'user_uuid') || read(profile, 'login_status')) {
59
+ return true;
60
+ }
61
+
62
+ const user = loadedUser(profile);
63
+ return Boolean(user && (read(user, 'id') || read(user, 'uuid')));
64
+ }
65
+
66
+ /**
67
+ * True when the profile has a login account that is managed from Fleet-Ops,
68
+ * i.e. a linked user which is not a staff member's account.
69
+ */
70
+ export function hasManagedLogin(profile) {
71
+ return hasLinkedUser(profile) && !isStaffLinked(profile);
72
+ }
73
+
74
+ /**
75
+ * The login status, preferring the server-provided `login_status` over the
76
+ * embedded user's status.
77
+ */
78
+ export function linkedUserStatus(profile) {
79
+ const loginStatus = read(profile, 'login_status');
80
+ if (loginStatus) {
81
+ return loginStatus;
82
+ }
83
+
84
+ const user = loadedUser(profile);
85
+ return (user ? (read(user, 'status') ?? read(user, 'session_status')) : null) ?? 'active';
86
+ }
87
+
88
+ export function hasInactiveLogin(profile) {
89
+ return INACTIVE_LOGIN_STATUSES.includes(linkedUserStatus(profile));
90
+ }
91
+
92
+ export function withoutIdentityFields(payload = {}) {
93
+ const attributes = { ...payload };
94
+
95
+ delete attributes.id;
96
+ delete attributes.uuid;
97
+ delete attributes.public_id;
98
+
99
+ return attributes;
100
+ }
101
+
102
+ /**
103
+ * Applies the profile payload returned by an account action endpoint
104
+ * (`{ driver: {...} }`, `{ customer: {...} }` or `{ contact: {...} }`).
105
+ */
106
+ export function updateProfileFromResponse(profile, response = {}) {
107
+ const payload = response?.driver ?? response?.customer ?? response?.contact;
108
+
109
+ if (!payload || typeof profile?.setProperties !== 'function') {
110
+ return;
111
+ }
112
+
113
+ const attributes = {};
114
+ if ('user_uuid' in payload) {
115
+ attributes.user_uuid = payload.user_uuid;
116
+ }
117
+ if ('is_staff_linked' in payload) {
118
+ attributes.is_staff_linked = Boolean(payload.is_staff_linked);
119
+ }
120
+ if ('login_status' in payload) {
121
+ attributes.login_status = payload.login_status;
122
+ } else if (payload.user && 'status' in payload.user) {
123
+ attributes.login_status = payload.user.status;
124
+ }
125
+
126
+ profile.setProperties(attributes);
127
+
128
+ if (payload.user) {
129
+ const user = loadedUser(profile);
130
+
131
+ if (typeof user?.setProperties === 'function') {
132
+ user.setProperties(withoutIdentityFields(payload.user));
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Opens a confirm modal which POSTs to an account action endpoint and applies
139
+ * the response to the profile.
140
+ *
141
+ * @param {Object} services { modalsManager, fetch, notifications }
142
+ * @param {Object} profile the driver or contact record
143
+ * @param {Object} options { title, body, acceptButtonText, acceptButtonScheme, endpoint, payload, successMessage, onSuccess }
144
+ */
145
+ export function confirmLoginAction({ modalsManager, fetch, notifications }, profile, options = {}) {
146
+ const { title, body, acceptButtonText, acceptButtonScheme, endpoint, payload = {}, successMessage, onSuccess } = options;
147
+
148
+ return modalsManager.confirm({
149
+ title,
150
+ body,
151
+ acceptButtonText,
152
+ acceptButtonScheme,
153
+ confirm: async (modal) => {
154
+ modal.startLoading();
155
+
156
+ try {
157
+ const response = await fetch.post(endpoint, payload, { namespace: INTERNAL_NAMESPACE });
158
+ updateProfileFromResponse(profile, response);
159
+ notifications.success(successMessage);
160
+
161
+ if (typeof onSuccess === 'function') {
162
+ onSuccess(response);
163
+ }
164
+
165
+ modal.done();
166
+ } catch (error) {
167
+ notifications.serverError(error);
168
+ modal.stopLoading();
169
+ }
170
+ },
171
+ });
172
+ }
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/admin/telematics-settings';
@@ -1 +1 @@
1
- export { default } from '@fleetbase/fleetops-engine/components/modals/reset-customer-credentials';
1
+ export { default } from '@fleetbase/fleetops-engine/components/map/marker-card/driver';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/map/marker-card/vehicle';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/modals/reset-profile-credentials';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/profile-identity-hint';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/controllers/settings/telematics';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/routes/settings/telematics';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/templates/settings/telematics';
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.68",
3
+ "version": "0.6.70",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
@@ -25,7 +25,7 @@
25
25
  "barryvdh/laravel-dompdf": "^3.1",
26
26
  "brick/geo": "0.7.2",
27
27
  "cknow/laravel-money": "^7.1",
28
- "fleetbase/core-api": "*",
28
+ "fleetbase/core-api": ">=1.6.65",
29
29
  "geocoder-php/google-maps-places-provider": "^1.4",
30
30
  "giggsey/libphonenumber-for-php": "^8.13",
31
31
  "league/geotools": "^1.1.0",
package/docs/AFAQY.md CHANGED
@@ -57,7 +57,7 @@ Existing provider interfaces and vehicle/trailer event names remain compatible.
57
57
 
58
58
  The public receiver remains `POST webhooks/telematics/afaqy?telematic=...&key=...` under the application's API prefix. A new `device.telemetry_updated` broadcast identifies the device; an open device panel reloads its authorized resource and refetches after socket reconnection. Company broadcast identity comes from the persisted asset, not a worker session. Map movement uses the existing vehicle/trailer events, rejects older source timestamps, and reloads visible telemetry assets after socket reconnection with at most five concurrent requests.
59
59
 
60
- Configure shared queues, retention, limits, and freshness under `telematics.telemetry` using `server/config/telemetry.php`. `TELEMATICS_BATCH_SIZE` controls units per ingestion delivery (default 100); `TELEMATICS_REQUEST_TIMEOUT_SECONDS` controls the bounded polling request timeout (default 45 seconds). AFAQY's adapter switches and optional queue overrides remain under `telematics.afaqy` in `server/config/afaqy.php`. Defaults: two MiB per webhook, 10,000 pending deliveries per connection, processed payload retention 24 hours, quarantine retention seven days, stale-position thresholds 120 seconds with ignition on and 600 seconds with ignition off/unknown. Each device's metadata records the effective threshold. Connection status categories remain unchanged.
60
+ Configure shared queues, retention, limits, and freshness under `telematics.telemetry` using `server/config/telemetry.php`. `TELEMATICS_BATCH_SIZE` controls units per ingestion delivery (default 100); `TELEMATICS_REQUEST_TIMEOUT_SECONDS` controls the bounded polling request timeout (default 45 seconds). AFAQY's adapter switches and optional queue overrides remain under `telematics.afaqy` in `server/config/afaqy.php`. Defaults: two MiB per webhook, 10,000 pending deliveries per connection, stale-position thresholds 120 seconds with ignition on and 600 seconds with ignition off/unknown. Retention (processed payloads 24 hours, quarantined deliveries seven days, device events 30 days, positions 90 days) is applied by `fleetops:prune-telematics-data`. Administrators configure retention policy, logging, system storage diagnostics and system cleanup in Admin → Fleet-Ops Config → Telematics. Organizations choose only event and position history preferences within those limits in Fleet-Ops → Settings → Telematics; config values are the fallback. See `docs/TELEMETRY_ARCHITECTURE.md` § Retention. Each device's metadata records the effective threshold. Connection status categories remain unchanged.
61
61
 
62
62
  ## Incident verification
63
63
 
@@ -14,7 +14,7 @@ nothing changes: every job and broadcast stays on the same queue as before.
14
14
  | Setting | Default | Work routed |
15
15
  | --- | --- | --- |
16
16
  | `TELEMATICS_POLL_QUEUE` | `default` | Scheduled and manual polls (`PollTelematicTelemetry`), legacy device discovery (`SyncTelematicDevicesJob`), and queued connection tests (`TestTelematicConnectionJob`). |
17
- | `TELEMATICS_INGESTION_QUEUE` | `default` | Applying polled or pushed positions (`ProcessTelematicDelivery`), including jobs re-queued by `fleetops:drain-telematic-inbox` and manual replays. |
17
+ | `TELEMATICS_INGESTION_QUEUE` | `default` | Applying polled or pushed positions (`ProcessTelematicDelivery`), including jobs re-queued by `fleetops:drain-telematic-inbox` and manual replays, and on-demand retention runs (`PruneTelematicsDataJob`, queued by "Run system cleanup" in Admin → Fleet-Ops Config → Telematics). |
18
18
  | `TELEMATICS_BROADCAST_QUEUE` | unset | Live-map broadcasts created by telematics ingestion: `DeviceTelemetryUpdated`, and `VehicleLocationChanged` / `TrailerLocationChanged` when a device is attached. When unset, broadcasts use the queue connection's default queue. |
19
19
 
20
20
  `TELEMATICS_BROADCAST_QUEUE` affects only broadcasts that telematics ingestion
@@ -48,11 +48,82 @@ persistence. The performance requirement did not justify those dependencies.
48
48
  ## Tradeoffs
49
49
 
50
50
  Three shared tables separate run-level diagnostics, high-volume delivery retention,
51
- and connection-level secrets with different lifetimes. Provider-specific protocol
51
+ and connection-level secrets with different lifetimes (see Retention below). Provider-specific protocol
52
52
  classes remain appropriate. Capability opt-in avoids silently changing every
53
53
  existing integration's ingestion or webhook behavior. Production throughput and
54
54
  real AFAQY payload verification remain separate deployment gates.
55
55
 
56
+ ## Retention
57
+
58
+ Every poll writes one `device_events` row, one `positions` row and a delivery
59
+ envelope per batch for each reporting device. Without a ceiling those tables grow
60
+ until the disk fills, so retention is enforced by `fleetops:prune-telematics-data`
61
+ (scheduled every fifteen minutes, `withoutOverlapping(14)`) using a per-company
62
+ policy resolved by `Support\Telematics\Retention\RetentionPolicy`:
63
+
64
+ | Layer | Source | Where it is edited |
65
+ |---|---|---|
66
+ | Package config | `server/config/telemetry.php` | deployment |
67
+ | System policy | setting `fleet-ops.telematics-settings` | Admin console → Fleet-Ops Config → Telematics |
68
+ | Company history preferences | `company.<uuid>.fleet-ops.telematics-settings` | Fleet-Ops → Settings → Telematics |
69
+
70
+ Defaults: device events 30 days, raw payload/meta stripped ("compacted") after
71
+ 7 days, positions 90 days, processed deliveries 24 hours, quarantined deliveries
72
+ 7 days, sync runs 7 days. Organizations can choose only device event and position
73
+ history retention, or inherit the system defaults. Administrators own raw payload,
74
+ delivery, sync-run retention and telemetry activity logging.
75
+
76
+ `max_event_retention_days` and `max_position_retention_days` constrain every
77
+ organization's history, including inherited defaults. A maximum of `0` allows
78
+ unlimited history; a history preference of `0` keeps records forever only while
79
+ that maximum is unset. Setting a finite maximum replaces saved unlimited or longer
80
+ preferences with that maximum. Missing preferences continue to inherit. Values
81
+ are also clamped to `RetentionPolicy::LIMITS`.
82
+
83
+ Admin storage diagnostics cover the entire system, including organizations the
84
+ administrator does not belong to. System cleanup queues work for every
85
+ organization plus orphaned records, with each organization's effective policy
86
+ applied independently. Storage sizes include table data and indexes; database row
87
+ estimates are labeled. Cleanup is asynchronous, so refreshing usage does not mean
88
+ all queued work has finished.
89
+
90
+ Manual system cleanup requires an asynchronous queue connection and a worker for
91
+ `telematics.telemetry.ingestion_queue`. A synchronous or disabled queue is rejected
92
+ instead of running every organization's cleanup inside the HTTP request.
93
+
94
+ Deploy the frontend and backend together and reload Octane and queue workers after
95
+ updating the backend. In linked development checkouts, Octane's watch paths must
96
+ include `packages/fleetops/server`; watching only the host API directory leaves
97
+ previously loaded controllers and policies running.
98
+
99
+ Rules the sweep follows:
100
+
101
+ - Hard deletes only. The models soft-delete, so `delete()` would grow the table;
102
+ rows already soft-deleted are purged regardless of age.
103
+ - Bounded work: at most `--max-batches` (default 50) statements of `--batch-size`
104
+ (default 1000) rows per table per company per run. A capped run is normal; the
105
+ next tick continues. Operators can catch up faster off-peak with
106
+ `php artisan fleetops:prune-telematics-data --max-batches=500 --no-lock`.
107
+ - Compaction keeps the event (type, severity, location, normalized `data`) and
108
+ nulls `payload` and `meta`, which hold the bulk of each row.
109
+ - `telematic_deliveries` and `telematic_sync_runs` carry no company; they are
110
+ resolved through the company's connections (including trashed ones). In-flight
111
+ sync runs (`fetching`, `ingesting`) are recovered by the drain, never expired.
112
+ - Rows whose company or connection no longer exists are swept with the system
113
+ defaults.
114
+ - Migrations add `(company_uuid, created_at)` on `device_events` and `positions`
115
+ and `(telematic_uuid, updated_at)` on `telematic_sync_runs`; on multi-GB tables
116
+ run them in a maintenance window.
117
+ - Deleting rows frees space inside InnoDB files, not on disk. Run
118
+ `OPTIMIZE TABLE device_events` (requires `innodb_file_per_table`) off-peak to
119
+ return it to the operating system.
120
+
121
+ Two write-amplification fixes accompany the sweep: `device_events.meta` stores only
122
+ the normalized block (the raw unit lives once, in `payload`), and telemetry-driven
123
+ saves run inside `activity()->withoutLogs()` unless the system administrator enables
124
+ "Log telemetry activity". `DrainTelematicInbox` now only recovers deliveries and
125
+ finishes interrupted runs.
126
+
56
127
  ## Adding an adapter
57
128
 
58
129
  Implement `TelemetryProviderInterface` alongside the existing provider contract.
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Fleet-Ops",
3
- "version": "0.6.68",
3
+ "version": "0.6.70",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/fleetops",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/fleetops-engine",
3
- "version": "0.6.68",
3
+ "version": "0.6.70",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -45,8 +45,8 @@
45
45
  "dependencies": {
46
46
  "@babel/core": "^7.23.2",
47
47
  "@fleetbase/ember-core": "^0.3.24",
48
- "@fleetbase/ember-ui": "^0.4.1",
49
- "@fleetbase/fleetops-data": "^0.2.1",
48
+ "@fleetbase/ember-ui": "^0.4.3",
49
+ "@fleetbase/fleetops-data": "^0.2.2",
50
50
  "@fleetbase/leaflet-routing-machine": "^3.2.17",
51
51
  "@fortawesome/ember-fontawesome": "^2.0.0",
52
52
  "@fortawesome/fontawesome-svg-core": "6.4.0",
@@ -382,6 +382,18 @@ if (!interface_exists('Fleetbase\Ai\Contracts\AIActionCapabilityInterface')) {
382
382
  eval('namespace Fleetbase\Ai\Contracts; interface AIActionCapabilityInterface {}');
383
383
  }
384
384
 
385
+ if (!interface_exists('Fleetbase\Ai\Contracts\AIToolCapabilityInterface')) {
386
+ eval('namespace Fleetbase\Ai\Contracts; interface AIToolCapabilityInterface {}');
387
+ }
388
+
389
+ if (!class_exists('Fleetbase\Ai\Support\AiAudience')) {
390
+ eval('namespace Fleetbase\Ai\Support; class AiAudience { public function __construct(public bool $isSystemAdmin = false, public array $granted = []) {} public function can(string $permission): bool { return $this->isSystemAdmin || in_array($permission, $this->granted, true); } public function canAll(array $permissions): bool { foreach ($permissions as $permission) { if (!$this->can($permission)) { return false; } } return true; } }');
391
+ }
392
+
393
+ if (!class_exists('Fleetbase\Ai\Support\AiToolContext')) {
394
+ eval('namespace Fleetbase\Ai\Support; class AiToolContext { public array $actionPreviews = []; public array $uiActions = []; public function __construct(public $task, public $audience) {} public function addActionPreview($capability, array $preview): array { $preview = array_merge(["preview_id" => "preview-" . (count($this->actionPreviews) + 1), "key" => $capability->key()], $preview); $this->actionPreviews[] = $preview; return $preview; } }');
395
+ }
396
+
385
397
  if (!class_exists('Fleetbase\Ai\Models\AiTask')) {
386
398
  eval('namespace Fleetbase\Ai\Models; class AiTask { public function __construct(array $attributes = []) { foreach ($attributes as $key => $value) { $this->{$key} = $value; } } }');
387
399
  }
@@ -13,8 +13,20 @@ return [
13
13
  'batch_size' => env('TELEMATICS_BATCH_SIZE', 100),
14
14
  'max_payload_bytes' => 2097152,
15
15
  'max_pending_deliveries' => 10000,
16
+ // Administrators control infrastructure retention and activity logging.
17
+ // Companies may choose event/position history retention within the maximums.
18
+ // A maximum of 0 imposes no cap and permits unlimited history (retention 0).
16
19
  'processed_retention_hours' => 24,
17
20
  'quarantine_retention_days' => 7,
21
+ 'sync_run_retention_days' => 7,
22
+ 'event_retention_days' => 30,
23
+ 'event_compact_after_days' => 7,
24
+ 'position_retention_days' => 90,
25
+ 'max_event_retention_days' => 0,
26
+ 'max_position_retention_days' => 0,
27
+ 'log_telemetry_activity' => false,
28
+ 'prune_batch_size' => 1000,
29
+ 'prune_max_batches' => 50,
18
30
  'stale_engine_on_seconds' => 120,
19
31
  'stale_engine_off_seconds' => 600,
20
32
  ];
@@ -0,0 +1,132 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Support\Facades\DB;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ /*
8
+ * Driver and customer login accounts are now managed by their FleetOps
9
+ * profile (user type `driver` or `customer`) instead of in IAM.
10
+ *
11
+ * Before this change the driver form created plain `user` accounts holding only
12
+ * the Driver role, so these accounts show up in IAM and can sign in to the
13
+ * console. This converts them to managed accounts. An account is converted only
14
+ * when it is clearly a profile login and nothing else: it has a driver or
15
+ * customer profile, every organization role it holds is the profile's role, it
16
+ * has no direct permissions or policies, and it owns no organization. Anything
17
+ * else is left as a team member.
18
+ *
19
+ * The previous type is kept in `meta.previous_type` so down() can revert it.
20
+ */
21
+ return new class extends Migration {
22
+ private const PROFILE_ROLES = [
23
+ 'driver' => 'Driver',
24
+ 'customer' => 'Fleet-Ops Customer',
25
+ ];
26
+
27
+ public function up(): void
28
+ {
29
+ if (!$this->hasRequiredTables()) {
30
+ return;
31
+ }
32
+
33
+ DB::table('users')
34
+ ->select(['uuid', 'meta'])
35
+ ->where('type', 'user')
36
+ ->whereNull('deleted_at')
37
+ ->where(function ($query) {
38
+ $query->whereExists(function ($query) {
39
+ $query->selectRaw(1)->from('drivers')->whereColumn('drivers.user_uuid', 'users.uuid')->whereNull('drivers.deleted_at');
40
+ })->orWhereExists(function ($query) {
41
+ $query->selectRaw(1)->from('contacts')->whereColumn('contacts.user_uuid', 'users.uuid')->where('contacts.type', 'customer')->whereNull('contacts.deleted_at');
42
+ });
43
+ })
44
+ ->whereNotExists(function ($query) {
45
+ $query->selectRaw(1)->from('companies')->whereColumn('companies.owner_uuid', 'users.uuid');
46
+ })
47
+ ->orderBy('uuid')
48
+ ->chunk(500, function ($users) {
49
+ foreach ($users as $user) {
50
+ $type = $this->managedTypeFor($user->uuid);
51
+ if (!$type) {
52
+ continue;
53
+ }
54
+
55
+ $meta = json_decode($user->meta ?? '', true);
56
+ $meta = is_array($meta) ? $meta : [];
57
+ $meta['previous_type'] = 'user';
58
+
59
+ DB::table('users')->where('uuid', $user->uuid)->update(['type' => $type, 'meta' => json_encode($meta)]);
60
+ }
61
+ });
62
+ }
63
+
64
+ public function down(): void
65
+ {
66
+ if (!Schema::hasTable('users')) {
67
+ return;
68
+ }
69
+
70
+ DB::table('users')
71
+ ->select(['uuid', 'meta'])
72
+ ->whereIn('type', array_keys(self::PROFILE_ROLES))
73
+ ->where('meta', 'like', '%"previous_type"%')
74
+ ->orderBy('uuid')
75
+ ->chunk(500, function ($users) {
76
+ foreach ($users as $user) {
77
+ $meta = json_decode($user->meta ?? '', true);
78
+ if (!is_array($meta) || ($meta['previous_type'] ?? null) !== 'user') {
79
+ continue;
80
+ }
81
+
82
+ unset($meta['previous_type']);
83
+ DB::table('users')->where('uuid', $user->uuid)->update(['type' => 'user', 'meta' => json_encode($meta)]);
84
+ }
85
+ });
86
+ }
87
+
88
+ /**
89
+ * The managed type for the account, or null when it should stay a team member.
90
+ */
91
+ private function managedTypeFor(string $userUuid): ?string
92
+ {
93
+ $hasDriver = DB::table('drivers')->where('user_uuid', $userUuid)->whereNull('deleted_at')->exists();
94
+ $hasCustomer = DB::table('contacts')->where('user_uuid', $userUuid)->where('type', 'customer')->whereNull('deleted_at')->exists();
95
+
96
+ // An account holding both kinds of profile is a person the organization
97
+ // relates to in two ways; leave it for an administrator to decide.
98
+ if ($hasDriver === $hasCustomer) {
99
+ return null;
100
+ }
101
+
102
+ $type = $hasDriver ? 'driver' : 'customer';
103
+ $companyUserUuids = DB::table('company_users')->where('user_uuid', $userUuid)->whereNull('deleted_at')->pluck('uuid');
104
+ $modelUuids = $companyUserUuids->push($userUuid)->all();
105
+
106
+ $roles = DB::table('model_has_roles')
107
+ ->join('roles', 'roles.id', '=', 'model_has_roles.role_id')
108
+ ->whereIn('model_has_roles.model_uuid', $modelUuids)
109
+ ->pluck('roles.name')
110
+ ->unique();
111
+
112
+ if ($roles->isEmpty() || $roles->contains(fn ($role) => $role !== self::PROFILE_ROLES[$type])) {
113
+ return null;
114
+ }
115
+
116
+ $hasPermissions = DB::table('model_has_permissions')->whereIn('model_uuid', $modelUuids)->exists();
117
+ $hasPolicies = DB::table('model_has_policies')->whereIn('model_uuid', $modelUuids)->exists();
118
+
119
+ return $hasPermissions || $hasPolicies ? null : $type;
120
+ }
121
+
122
+ private function hasRequiredTables(): bool
123
+ {
124
+ foreach (['users', 'drivers', 'contacts', 'companies', 'company_users', 'roles', 'model_has_roles', 'model_has_permissions', 'model_has_policies'] as $table) {
125
+ if (!Schema::hasTable($table)) {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ return true;
131
+ }
132
+ };
@@ -0,0 +1,40 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Support\Database\TableIndexes;
4
+ use Illuminate\Database\Migrations\Migration;
5
+ use Illuminate\Database\Schema\Blueprint;
6
+ use Illuminate\Support\Facades\Schema;
7
+
8
+ /**
9
+ * Supports the bounded retention sweep in fleetops:prune-telematics-data,
10
+ * which ranges over these columns for every company on every run.
11
+ */
12
+ return new class extends Migration {
13
+ private const TABLE = 'device_events';
14
+ private const INDEX = 'device_events_company_created_at_index';
15
+ private const COLUMNS = ['company_uuid', 'created_at'];
16
+
17
+ public function up(): void
18
+ {
19
+ if (!Schema::hasTable(self::TABLE) || !Schema::hasColumns(self::TABLE, self::COLUMNS)) {
20
+ return;
21
+ }
22
+
23
+ if (TableIndexes::covers(self::TABLE, self::COLUMNS)) {
24
+ return;
25
+ }
26
+
27
+ Schema::table(self::TABLE, fn (Blueprint $table) => $table->index(self::COLUMNS, self::INDEX));
28
+ }
29
+
30
+ public function down(): void
31
+ {
32
+ if (!Schema::hasTable(self::TABLE)) {
33
+ return;
34
+ }
35
+
36
+ if ((TableIndexes::for(self::TABLE)[self::INDEX] ?? null) === self::COLUMNS) {
37
+ Schema::table(self::TABLE, fn (Blueprint $table) => $table->dropIndex(self::INDEX));
38
+ }
39
+ }
40
+ };