@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,318 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Console\Commands;
4
+
5
+ use Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy;
6
+ use Illuminate\Console\Command;
7
+ use Illuminate\Support\Collection;
8
+ use Illuminate\Support\Facades\Cache;
9
+ use Illuminate\Support\Facades\DB;
10
+
11
+ /**
12
+ * Applies each company's telematics retention policy.
13
+ *
14
+ * Deletes are hard deletes issued in bounded batches so a large backlog never
15
+ * monopolizes the tables ingestion is writing to; a capped run picks up where
16
+ * it left off on the next schedule tick.
17
+ */
18
+ class PruneTelematicsData extends Command
19
+ {
20
+ public const TABLES = ['device_events', 'positions', 'telematic_deliveries', 'telematic_sync_runs'];
21
+
22
+ public const LOCK = 'fleetops:prune-telematics-data';
23
+
24
+ protected $signature = 'fleetops:prune-telematics-data
25
+ {--company= : Only prune data for this company (uuid or public id)}
26
+ {--orphans-only : Only prune data without an existing company or connection}
27
+ {--table=* : Only prune these tables (device_events, positions, telematic_deliveries, telematic_sync_runs)}
28
+ {--batch-size=1000 : Rows per delete or update statement}
29
+ {--max-batches=50 : Maximum batches per table per company per run}
30
+ {--dry-run : Report matching rows without deleting or compacting}
31
+ {--no-lock : Skip process locking}';
32
+
33
+ protected $description = 'Prune telematics device events, positions, deliveries and sync runs according to each company\'s retention policy.';
34
+
35
+ protected int $batchSize = 1000;
36
+ protected int $maxBatches = 50;
37
+ protected bool $dryRun = false;
38
+
39
+ public function handle(): int
40
+ {
41
+ $tables = $this->requestedTables();
42
+ if ($tables === null) {
43
+ $this->error('Unknown table. Valid tables: ' . implode(', ', self::TABLES) . '.');
44
+
45
+ return self::FAILURE;
46
+ }
47
+
48
+ $lock = null;
49
+ if (!$this->option('no-lock')) {
50
+ $lock = Cache::lock(self::LOCK, 840);
51
+ if (!$lock->get()) {
52
+ $this->warn('Another telematics prune run appears to be in progress.');
53
+
54
+ return self::SUCCESS;
55
+ }
56
+ }
57
+
58
+ try {
59
+ // Queue workers may have cached settings before an administrator saved a new policy.
60
+ RetentionPolicy::flush();
61
+ $this->batchSize = max(100, min(5000, (int) $this->option('batch-size') ?: 1000));
62
+ $this->maxBatches = max(1, (int) $this->option('max-batches') ?: 50);
63
+ $this->dryRun = (bool) $this->option('dry-run');
64
+
65
+ $only = ((string) $this->option('company')) ?: null;
66
+ $orphansOnly = (bool) $this->option('orphans-only');
67
+ if ($only && $orphansOnly) {
68
+ $this->error('Choose either --company or --orphans-only.');
69
+
70
+ return self::FAILURE;
71
+ }
72
+ $companies = $orphansOnly ? new Collection() : $this->companies($only);
73
+ if ($only && $companies->isEmpty()) {
74
+ $this->error(sprintf('Company [%s] was not found.', $only));
75
+
76
+ return self::FAILURE;
77
+ }
78
+
79
+ $totals = $this->emptyStats();
80
+ foreach ($companies as $company) {
81
+ $stats = $this->pruneCompany($company->uuid, $this->policyFor($company->uuid), $tables);
82
+ $totals = $this->mergeStats($totals, $stats);
83
+ $this->report($company->public_id ?: $company->uuid, $stats);
84
+ }
85
+ if (!$only) {
86
+ $stats = $this->pruneOrphans($this->policyFor(null), $tables);
87
+ $totals = $this->mergeStats($totals, $stats);
88
+ $this->report('orphaned', $stats);
89
+ }
90
+
91
+ $this->info(sprintf(
92
+ '%s: deleted=%d compacted=%d batches=%d%s',
93
+ $this->dryRun ? 'Telematics prune dry run' : 'Telematics prune complete',
94
+ $totals['deleted'],
95
+ $totals['compacted'],
96
+ $totals['batches'],
97
+ $totals['capped'] ? ' (capped; more rows remain for the next run)' : ''
98
+ ));
99
+
100
+ return self::SUCCESS;
101
+ } finally {
102
+ $lock?->release();
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @return string[]|null null when an unknown table was requested
108
+ */
109
+ protected function requestedTables(): ?array
110
+ {
111
+ $requested = array_values(array_filter((array) $this->option('table')));
112
+ if ($requested === []) {
113
+ return self::TABLES;
114
+ }
115
+
116
+ return array_diff($requested, self::TABLES) === [] ? array_values(array_unique($requested)) : null;
117
+ }
118
+
119
+ protected function companies(?string $only): Collection
120
+ {
121
+ $query = DB::table('companies')->select(['id', 'uuid', 'public_id'])->orderBy('id');
122
+ if ($only) {
123
+ $query->where(fn ($where) => $where->where('uuid', $only)->orWhere('public_id', $only));
124
+ }
125
+
126
+ return $query->get();
127
+ }
128
+
129
+ protected function policyFor(?string $companyUuid): RetentionPolicy
130
+ {
131
+ return RetentionPolicy::forCompany($companyUuid);
132
+ }
133
+
134
+ protected function pruneCompany(string $companyUuid, RetentionPolicy $policy, array $tables): array
135
+ {
136
+ $rows = fn ($query) => $query->where('company_uuid', $companyUuid);
137
+
138
+ $telematics = null;
139
+ if (array_intersect(['telematic_deliveries', 'telematic_sync_runs'], $tables) !== []) {
140
+ // Inbox tables carry no company; trashed connections still prune under their company's policy.
141
+ $telematics = DB::table('telematics')->where('company_uuid', $companyUuid)->pluck('uuid')->all();
142
+ }
143
+ $inbox = $telematics ? fn ($query) => $query->whereIn('telematic_uuid', $telematics) : null;
144
+
145
+ return $this->prune($rows, $inbox, $policy, $tables);
146
+ }
147
+
148
+ /**
149
+ * Rows whose company or connection no longer exists fall back to the system defaults.
150
+ */
151
+ protected function pruneOrphans(RetentionPolicy $policy, array $tables): array
152
+ {
153
+ $rows = fn ($query) => $query->where(function ($where) {
154
+ $where->whereNull('company_uuid')->orWhereNotIn('company_uuid', DB::table('companies')->select('uuid'));
155
+ });
156
+ $inbox = fn ($query) => $query->whereNotExists(function ($connections) use ($query) {
157
+ $connections->selectRaw('1')
158
+ ->from('telematics')
159
+ ->join('companies', 'companies.uuid', '=', 'telematics.company_uuid')
160
+ ->whereColumn('telematics.uuid', $query->from . '.telematic_uuid');
161
+ });
162
+
163
+ return $this->prune($rows, $inbox, $policy, $tables);
164
+ }
165
+
166
+ protected function prune(\Closure $rows, ?\Closure $inbox, RetentionPolicy $policy, array $tables): array
167
+ {
168
+ $stats = $this->emptyStats();
169
+ if (in_array('device_events', $tables, true)) {
170
+ $stats['tables']['device_events'] = $this->pruneDeviceEvents($rows, $policy);
171
+ }
172
+ if (in_array('positions', $tables, true)) {
173
+ $stats['tables']['positions'] = $this->prunePositions($rows, $policy);
174
+ }
175
+ if ($inbox && in_array('telematic_deliveries', $tables, true)) {
176
+ $stats['tables']['telematic_deliveries'] = $this->pruneDeliveries($inbox, $policy);
177
+ }
178
+ if ($inbox && in_array('telematic_sync_runs', $tables, true)) {
179
+ $stats['tables']['telematic_sync_runs'] = $this->pruneSyncRuns($inbox, $policy);
180
+ }
181
+ foreach ($stats['tables'] as $table) {
182
+ $stats['deleted'] += $table['deleted'];
183
+ $stats['compacted'] += $table['compacted'];
184
+ $stats['batches'] += $table['batches'];
185
+ $stats['capped'] = $stats['capped'] || $table['capped'];
186
+ }
187
+
188
+ return $stats;
189
+ }
190
+
191
+ protected function pruneDeviceEvents(\Closure $rows, RetentionPolicy $policy): array
192
+ {
193
+ $stats = $this->emptyTableStats();
194
+ $cutoff = $policy->cutoff('event_retention_days');
195
+ // Soft-deleted events only grow the table; purge them regardless of age.
196
+ $this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats);
197
+ if ($cutoff) {
198
+ $this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats);
199
+ }
200
+ if ($policy->compactsEvents() && ($compactAt = $policy->cutoff('event_compact_after_days'))) {
201
+ // Keep the event (type, severity, location, normalized data) but drop the raw provider blobs.
202
+ // Rows past the delete cutoff are left to the delete sweep rather than rewritten first.
203
+ $this->updateInBatches(
204
+ 'device_events',
205
+ 'id',
206
+ function ($query) use ($rows, $cutoff, $compactAt) {
207
+ $query = $rows($query)->where('created_at', '<', $compactAt->toDateTimeString())->where(fn ($where) => $where->whereNotNull('payload')->orWhereNotNull('meta'));
208
+
209
+ return $cutoff ? $query->where('created_at', '>=', $cutoff->toDateTimeString()) : $query;
210
+ },
211
+ ['payload' => null, 'meta' => null],
212
+ $stats
213
+ );
214
+ }
215
+
216
+ return $stats;
217
+ }
218
+
219
+ protected function prunePositions(\Closure $rows, RetentionPolicy $policy): array
220
+ {
221
+ $stats = $this->emptyTableStats();
222
+ $this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats);
223
+ if ($cutoff = $policy->cutoff('position_retention_days')) {
224
+ $this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats);
225
+ }
226
+
227
+ return $stats;
228
+ }
229
+
230
+ protected function pruneDeliveries(\Closure $inbox, RetentionPolicy $policy): array
231
+ {
232
+ $stats = $this->emptyTableStats();
233
+ foreach (['processed' => 'processed_retention_hours', 'quarantined' => 'quarantine_retention_days'] as $status => $key) {
234
+ if ($cutoff = $policy->cutoff($key)) {
235
+ $this->deleteInBatches('telematic_deliveries', 'uuid', fn ($query) => $inbox($query)->where('status', $status)->where('updated_at', '<', $cutoff->toDateTimeString()), $stats);
236
+ }
237
+ }
238
+
239
+ return $stats;
240
+ }
241
+
242
+ protected function pruneSyncRuns(\Closure $inbox, RetentionPolicy $policy): array
243
+ {
244
+ $stats = $this->emptyTableStats();
245
+ if ($cutoff = $policy->cutoff('sync_run_retention_days')) {
246
+ // In-flight runs are recovered by the drain command, never expired here.
247
+ $this->deleteInBatches('telematic_sync_runs', 'uuid', fn ($query) => $inbox($query)->whereNotIn('status', ['fetching', 'ingesting'])->where('updated_at', '<', $cutoff->toDateTimeString()), $stats);
248
+ }
249
+
250
+ return $stats;
251
+ }
252
+
253
+ protected function deleteInBatches(string $table, string $key, \Closure $scope, array &$stats): void
254
+ {
255
+ if ($this->dryRun) {
256
+ $stats['deleted'] += $scope(DB::table($table))->count();
257
+
258
+ return;
259
+ }
260
+ for ($batch = 0; $batch < $this->maxBatches; $batch++) {
261
+ $ids = $scope(DB::table($table))->orderBy($key)->limit($this->batchSize)->pluck($key);
262
+ if ($ids->isEmpty()) {
263
+ return;
264
+ }
265
+ $stats['batches']++;
266
+ $stats['deleted'] += DB::table($table)->whereIn($key, $ids->all())->delete();
267
+ }
268
+ $stats['capped'] = true;
269
+ }
270
+
271
+ protected function updateInBatches(string $table, string $key, \Closure $scope, array $values, array &$stats): void
272
+ {
273
+ if ($this->dryRun) {
274
+ $stats['compacted'] += $scope(DB::table($table))->count();
275
+
276
+ return;
277
+ }
278
+ for ($batch = 0; $batch < $this->maxBatches; $batch++) {
279
+ $ids = $scope(DB::table($table))->orderBy($key)->limit($this->batchSize)->pluck($key);
280
+ if ($ids->isEmpty()) {
281
+ return;
282
+ }
283
+ $stats['batches']++;
284
+ $stats['compacted'] += DB::table($table)->whereIn($key, $ids->all())->update($values);
285
+ }
286
+ $stats['capped'] = true;
287
+ }
288
+
289
+ protected function report(string $label, array $stats): void
290
+ {
291
+ foreach ($stats['tables'] as $table => $counts) {
292
+ if (!$counts['deleted'] && !$counts['compacted'] && !$counts['capped']) {
293
+ continue;
294
+ }
295
+ $this->info(sprintf('%s %s: deleted=%d compacted=%d batches=%d%s', $label, $table, $counts['deleted'], $counts['compacted'], $counts['batches'], $counts['capped'] ? ' capped' : ''));
296
+ }
297
+ }
298
+
299
+ protected function emptyTableStats(): array
300
+ {
301
+ return ['deleted' => 0, 'compacted' => 0, 'batches' => 0, 'capped' => false];
302
+ }
303
+
304
+ protected function emptyStats(): array
305
+ {
306
+ return ['deleted' => 0, 'compacted' => 0, 'batches' => 0, 'capped' => false, 'tables' => []];
307
+ }
308
+
309
+ protected function mergeStats(array $totals, array $stats): array
310
+ {
311
+ $totals['deleted'] += $stats['deleted'];
312
+ $totals['compacted'] += $stats['compacted'];
313
+ $totals['batches'] += $stats['batches'];
314
+ $totals['capped'] = $totals['capped'] || $stats['capped'];
315
+
316
+ return $totals;
317
+ }
318
+ }
@@ -0,0 +1,34 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Exceptions;
4
+
5
+ /**
6
+ * Thrown when a driver, contact or customer profile is given an email or phone
7
+ * that belongs to an account it cannot be linked to.
8
+ */
9
+ class ProfileIdentityConflictException extends \Exception
10
+ {
11
+ /**
12
+ * The profile field the conflict was found on (`email` or `phone`).
13
+ */
14
+ protected string $field;
15
+
16
+ public function __construct(string $message, string $field = 'email')
17
+ {
18
+ parent::__construct($message);
19
+ $this->field = $field;
20
+ }
21
+
22
+ public function getField(): string
23
+ {
24
+ return $this->field;
25
+ }
26
+
27
+ /**
28
+ * The conflict keyed by field, in the shape of a validation error bag.
29
+ */
30
+ public function getErrors(): array
31
+ {
32
+ return [$this->field => [$this->getMessage()]];
33
+ }
34
+ }
@@ -44,6 +44,8 @@ class CustomerController extends Controller
44
44
  {
45
45
  use \Fleetbase\FleetOps\Http\Controllers\Concerns\ResolvesReviewAccountBypass;
46
46
 
47
+ public const DEACTIVATED_LOGIN_MESSAGE = 'This customer login has been deactivated.';
48
+
47
49
  /* ============================================================
48
50
  | Public auth flows (API credential only, no Customer-Token)
49
51
  * ============================================================ */
@@ -345,6 +347,10 @@ class CustomerController extends Controller
345
347
  return response()->apiError('Authentication failed using credentials provided.', 401);
346
348
  }
347
349
 
350
+ if ($user->status === 'inactive') {
351
+ return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
352
+ }
353
+
348
354
  $sessionCompany = $this->sessionCompany();
349
355
  if (!$sessionCompany) {
350
356
  return response()->apiError('No company resolved from API credential.', 500);
@@ -381,6 +387,10 @@ class CustomerController extends Controller
381
387
  return response()->apiError('No customer with this phone number found.');
382
388
  }
383
389
 
390
+ if ($user->status === 'inactive') {
391
+ return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
392
+ }
393
+
384
394
  try {
385
395
  $this->generateSmsVerification($user, 'fleetops_customer_login', [
386
396
  'messageCallback' => fn ($verification) => 'Your ' . config('app.name') . ' verification code is ' . $verification->code,
@@ -423,6 +433,10 @@ class CustomerController extends Controller
423
433
  return response()->apiError('Unable to verify code.');
424
434
  }
425
435
 
436
+ if ($user->status === 'inactive') {
437
+ return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
438
+ }
439
+
426
440
  $verificationCode = $this->verificationCodeExists([
427
441
  'subject_uuid' => $user->uuid,
428
442
  'code' => $code,
@@ -6,6 +6,7 @@ use Fleetbase\FleetOps\Events\DriverLocationChanged;
6
6
  use Fleetbase\FleetOps\Events\GeofenceEntered;
7
7
  use Fleetbase\FleetOps\Events\GeofenceExited;
8
8
  use Fleetbase\FleetOps\Events\VehicleLocationChanged;
9
+ use Fleetbase\FleetOps\Exceptions\ProfileIdentityConflictException;
9
10
  use Fleetbase\FleetOps\Exceptions\PublicRelationNotFoundException;
10
11
  use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesFleetOpsApiResources;
11
12
  use Fleetbase\FleetOps\Http\Controllers\Api\v1\Concerns\ResolvesPublicExpansions;
@@ -22,6 +23,7 @@ use Fleetbase\FleetOps\Models\Vehicle;
22
23
  use Fleetbase\FleetOps\Models\Vendor;
23
24
  use Fleetbase\FleetOps\Support\GeofenceIntersectionService;
24
25
  use Fleetbase\FleetOps\Support\OSRM;
26
+ use Fleetbase\FleetOps\Support\ProfileAccountManager;
25
27
  use Fleetbase\FleetOps\Support\Utils;
26
28
  use Fleetbase\Http\Controllers\Controller;
27
29
  use Fleetbase\Http\Requests\SwitchOrganizationRequest;
@@ -47,6 +49,11 @@ class DriverController extends Controller
47
49
  use ResolvesFleetOpsApiResources;
48
50
  use ResolvesPublicExpansions;
49
51
 
52
+ /**
53
+ * Returned when a driver whose login was deactivated from the console tries to sign in.
54
+ */
55
+ public const DEACTIVATED_LOGIN_MESSAGE = 'This driver login has been deactivated.';
56
+
50
57
  /**
51
58
  * Public expansion name => Eloquent relation name.
52
59
  *
@@ -97,20 +104,14 @@ class DriverController extends Controller
97
104
  // Apply user infos
98
105
  $userDetails = $this->applyUserInfoFromRequest($request, $userDetails);
99
106
 
100
- // Set company_uuid before creating user
101
- $userDetails['company_uuid'] = $company->uuid;
102
-
103
- // create user account for driver
104
- $user = $this->createUser($userDetails);
105
-
106
- // Assign company — the early return above guarantees $company is set
107
- $user->assignCompany($company);
108
-
109
- // Set user type
110
- $user->setUserType('driver');
111
-
112
- // assign driver role
113
- $user->assignSingleRole('Driver');
107
+ // Resolve the driver's login account: a team member of the company with
108
+ // the email/phone is linked, otherwise a driver account is created and
109
+ // added to the company without an organization invite.
110
+ try {
111
+ $user = $this->resolveDriverAccount($company->uuid, $userDetails);
112
+ } catch (ProfileIdentityConflictException $exception) {
113
+ return $this->jsonResponse(['error' => $exception->getMessage()], 422);
114
+ }
114
115
 
115
116
  // set user id
116
117
  $input['user_uuid'] = $user->uuid;
@@ -193,10 +194,11 @@ class DriverController extends Controller
193
194
  // it. The driver's own record has no timezone column; the user's does.
194
195
  $userDetails = $request->only(['name', 'email', 'phone', 'timezone']);
195
196
 
196
- // update driver user details
197
- $driverUser = $driver->getUser();
198
- if ($driverUser) {
199
- $driverUser->update($userDetails);
197
+ // update the driver's login account through its proxy fields
198
+ try {
199
+ ProfileAccountManager::syncProxyFields($driver->getUser(), $userDetails);
200
+ } catch (ProfileIdentityConflictException $exception) {
201
+ return $this->jsonResponse(['error' => $exception->getMessage()], 422);
200
202
  }
201
203
 
202
204
  // latitude / longitude
@@ -519,10 +521,14 @@ class DriverController extends Controller
519
521
  )->whereHas('driver')->first();
520
522
 
521
523
  // Check password to authenticate driver
522
- if (!Hash::check($password, $user->password)) {
524
+ if (!$user || !is_string($user->password) || !Hash::check((string) $password, $user->password)) {
523
525
  return response()->apiError('Authentication failed using password provided.', 401);
524
526
  }
525
527
 
528
+ if (static::isLoginDeactivated($user)) {
529
+ return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
530
+ }
531
+
526
532
  // Get the user's company for this driver profile
527
533
  $company = static::getDriverCompanyFromUser($user);
528
534
 
@@ -561,6 +567,10 @@ class DriverController extends Controller
561
567
  return response()->apiError('No driver with this phone # found.');
562
568
  }
563
569
 
570
+ if (static::isLoginDeactivated($user)) {
571
+ return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
572
+ }
573
+
564
574
  // Get the user's company for this driver profile
565
575
  $company = static::getDriverCompanyFromUser($user);
566
576
 
@@ -627,6 +637,10 @@ class DriverController extends Controller
627
637
  return response()->apiError('Unable to verify code.');
628
638
  }
629
639
 
640
+ if (static::isLoginDeactivated($user)) {
641
+ return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
642
+ }
643
+
630
644
  // find and verify code
631
645
  $verificationCode = VerificationCode::where(['subject_uuid' => $user->uuid, 'code' => $code, 'for' => $for])->exists();
632
646
  if (!$verificationCode && !static::verificationBypassMatches($identity, $code)) {
@@ -975,26 +989,29 @@ class DriverController extends Controller
975
989
  return User::applyUserInfoFromRequest($request, $userDetails);
976
990
  }
977
991
 
978
- protected function createUser(array $userDetails): User
992
+ /**
993
+ * Resolve the login account for a driver created through the API.
994
+ *
995
+ * @throws ProfileIdentityConflictException
996
+ */
997
+ protected function resolveDriverAccount(string $companyUuid, array $userDetails): User
979
998
  {
980
- /*
981
- * `password` is guarded on User, so mass assignment drops it without a
982
- * word. The create endpoint has always accepted and validated one, so a
983
- * driver created through the API could never sign in with the password
984
- * their operator chose for them. Set it after the fact, where the
985
- * model's mutator hashes it.
986
- */
987
- $password = $userDetails['password'] ?? null;
988
- unset($userDetails['password']);
989
-
990
- $user = User::create($userDetails);
991
-
992
- if (is_string($password) && strlen($password)) {
993
- $user->password = $password;
994
- $user->save();
995
- }
999
+ return ProfileAccountManager::resolveForProfile(
1000
+ $companyUuid,
1001
+ 'driver',
1002
+ $userDetails['name'] ?? null,
1003
+ $userDetails['email'] ?? null,
1004
+ $userDetails['phone'] ?? null,
1005
+ array_merge(Arr::except($userDetails, ['name', 'email', 'phone']), ['status' => 'active'])
1006
+ );
1007
+ }
996
1008
 
997
- return $user;
1009
+ /**
1010
+ * Whether the driver's login was deactivated from the console.
1011
+ */
1012
+ public static function isLoginDeactivated(User $user): bool
1013
+ {
1014
+ return $user->status === 'inactive';
998
1015
  }
999
1016
 
1000
1017
  protected function getUuid(array|string $table, array $where, array $options = []): mixed
@@ -1313,7 +1330,7 @@ class DriverController extends Controller
1313
1330
  }
1314
1331
 
1315
1332
  $user = static::findDriverUserByIdentity($identity);
1316
- if (!$user) {
1333
+ if (!$user || static::isLoginDeactivated($user)) {
1317
1334
  return response()->json(['status' => 'ok']);
1318
1335
  }
1319
1336
 
@@ -1344,7 +1361,7 @@ class DriverController extends Controller
1344
1361
  }
1345
1362
 
1346
1363
  $user = static::findDriverUserByIdentity($identity);
1347
- if (!$user) {
1364
+ if (!$user || static::isLoginDeactivated($user)) {
1348
1365
  return response()->apiError('Invalid or expired reset code.', 422);
1349
1366
  }
1350
1367
 
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
4
4
 
5
5
  use Fleetbase\FleetOps\Http\Controllers\Internal\v1\OrchestrationController as InternalOrchestrationController;
6
+ use Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry;
6
7
  use Illuminate\Http\JsonResponse;
7
8
  use Illuminate\Http\Request;
8
9
 
@@ -14,6 +15,14 @@ use Illuminate\Http\Request;
14
15
  */
15
16
  class OrchestrationController extends InternalOrchestrationController
16
17
  {
18
+ public function __construct(OrchestrationEngineRegistry $registry)
19
+ {
20
+ // The fleetbase.api route group authenticates API credentials and establishes
21
+ // their company scope. Console IAM middleware belongs to the internal
22
+ // workbench, not to these organization-credential endpoints.
23
+ $this->registry = $registry;
24
+ }
25
+
17
26
  public function run(Request $request): JsonResponse
18
27
  {
19
28
  return $this->publicResponse(parent::run($request));
@@ -2,12 +2,33 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Controllers;
4
4
 
5
+ use Fleetbase\FleetOps\Traits\AuthorizesMethods;
5
6
  use Fleetbase\Http\Controllers\FleetbaseController;
7
+ use Illuminate\Database\Eloquent\Model;
6
8
 
7
9
  class FleetOpsController extends FleetbaseController
8
10
  {
11
+ use AuthorizesMethods;
12
+
9
13
  /**
10
14
  * The package namespace used to resolve from.
11
15
  */
12
16
  public string $namespace = '\\Fleetbase\\FleetOps';
17
+
18
+ /**
19
+ * Explicit permissions for custom actions, keyed by controller method, e.g.
20
+ * `['bulkDispatch' => 'dispatch order']`. Those methods also carry
21
+ * `#[SkipAuthorizationCheck]` so AuthorizationGuard does not demand the
22
+ * permission it would otherwise infer from the HTTP verb.
23
+ *
24
+ * @var array<string, string|string[]>
25
+ */
26
+ protected array $methodPermissions = [];
27
+
28
+ public function __construct(?Model $model = null, ?string $resource = null)
29
+ {
30
+ parent::__construct($model, $resource);
31
+
32
+ $this->authorizeMethods($this->methodPermissions);
33
+ }
13
34
  }
@@ -14,6 +14,7 @@ use Fleetbase\FleetOps\Support\Analytics\OperationsPulse;
14
14
  use Fleetbase\FleetOps\Support\Analytics\OrdersByStatus;
15
15
  use Fleetbase\FleetOps\Support\Analytics\RevenueTrend;
16
16
  use Fleetbase\FleetOps\Support\Analytics\TopDrivers;
17
+ use Fleetbase\FleetOps\Support\Authorization;
17
18
  use Fleetbase\Http\Controllers\Controller;
18
19
  use Illuminate\Http\Request;
19
20
 
@@ -28,6 +29,16 @@ use Illuminate\Http\Request;
28
29
  */
29
30
  class AnalyticsController extends Controller
30
31
  {
32
+ public function __construct()
33
+ {
34
+ // Not a resource controller, so AuthorizationGuard cannot resolve a permission for it.
35
+ $this->middleware(function ($request, $next) {
36
+ Authorization::authorize('view analytics');
37
+
38
+ return $next($request);
39
+ });
40
+ }
41
+
31
42
  public function operationsPulse(Request $request)
32
43
  {
33
44
  return $this->run($request, OperationsPulse::class);