@fleetbase/fleetops-engine 0.6.69 → 0.6.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/RELEASE.md +21 -14
  2. package/addon/components/admin/telematics-settings.hbs +143 -0
  3. package/addon/components/admin/telematics-settings.js +145 -0
  4. package/addon/components/fuel-integration/hub.hbs +1 -1
  5. package/addon/components/layout/fleet-ops-sidebar.js +37 -17
  6. package/addon/components/map/leaflet-live-map.hbs +4 -242
  7. package/addon/components/map/leaflet-live-map.js +2 -1
  8. package/addon/components/map/marker-card/driver.hbs +57 -0
  9. package/addon/components/map/marker-card/driver.js +3 -0
  10. package/addon/components/map/marker-card/vehicle.hbs +78 -0
  11. package/addon/components/map/marker-card/vehicle.js +3 -0
  12. package/addon/components/order/details/activity.hbs +8 -6
  13. package/addon/components/order/details/activity.js +9 -0
  14. package/addon/components/order/details.hbs +29 -0
  15. package/addon/components/order/details.js +26 -0
  16. package/addon/components/order/form/details.hbs +30 -4
  17. package/addon/components/order/form/details.js +46 -0
  18. package/addon/components/order/form/route.hbs +4 -2
  19. package/addon/components/order/form/route.js +11 -0
  20. package/addon/components/order/form.hbs +12 -0
  21. package/addon/components/order/form.js +21 -1
  22. package/addon/components/order/kanban.js +72 -0
  23. package/addon/components/order-config-manager/activity-flow.js +45 -10
  24. package/addon/components/order-config-manager/lifecycle.hbs +250 -0
  25. package/addon/components/order-config-manager/lifecycle.js +358 -0
  26. package/addon/components/order-config-manager.js +5 -1
  27. package/addon/components/telematic/hub.hbs +1 -1
  28. package/addon/components/widget/live-fleet.hbs +8 -40
  29. package/addon/controllers/analytics/reports/index.js +15 -1
  30. package/addon/controllers/connectivity/devices/index/details.js +1 -0
  31. package/addon/controllers/connectivity/devices/index.js +12 -2
  32. package/addon/controllers/connectivity/events/index.js +31 -1
  33. package/addon/controllers/connectivity/fuel-providers/index.js +6 -4
  34. package/addon/controllers/connectivity/sensors/index/details.js +1 -1
  35. package/addon/controllers/connectivity/sensors/index.js +41 -1
  36. package/addon/controllers/connectivity/telematics/index.js +4 -0
  37. package/addon/controllers/maintenance/equipment/index/details.js +2 -2
  38. package/addon/controllers/maintenance/equipment/index.js +36 -10
  39. package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
  40. package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
  41. package/addon/controllers/maintenance/inspection-submissions/index.js +56 -10
  42. package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
  43. package/addon/controllers/maintenance/maintenances/index.js +24 -1
  44. package/addon/controllers/maintenance/parts/index/details.js +2 -2
  45. package/addon/controllers/maintenance/parts/index.js +36 -9
  46. package/addon/controllers/maintenance/schedules/index.js +48 -6
  47. package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
  48. package/addon/controllers/maintenance/work-orders/index.js +47 -9
  49. package/addon/controllers/management/contacts/customers.js +13 -0
  50. package/addon/controllers/management/contacts/index.js +47 -1
  51. package/addon/controllers/management/drivers/index/details.js +1 -0
  52. package/addon/controllers/management/drivers/index.js +36 -2
  53. package/addon/controllers/management/fleets/index/details.js +1 -0
  54. package/addon/controllers/management/fleets/index.js +43 -1
  55. package/addon/controllers/management/fuel-reports/index.js +26 -2
  56. package/addon/controllers/management/fuel-transactions/index.js +45 -7
  57. package/addon/controllers/management/issues/index/details.js +1 -0
  58. package/addon/controllers/management/issues/index.js +26 -2
  59. package/addon/controllers/management/places/index.js +40 -1
  60. package/addon/controllers/management/trailers/index/details.js +1 -0
  61. package/addon/controllers/management/trailers/index.js +50 -2
  62. package/addon/controllers/management/vehicles/index/details.js +1 -0
  63. package/addon/controllers/management/vehicles/index.js +37 -2
  64. package/addon/controllers/management/vendors/index.js +30 -2
  65. package/addon/controllers/operations/orders/index/details.js +61 -5
  66. package/addon/controllers/operations/orders/index.js +66 -4
  67. package/addon/controllers/operations/service-rates/index.js +18 -1
  68. package/addon/controllers/settings/index.js +13 -0
  69. package/addon/controllers/settings/telematics.js +113 -0
  70. package/addon/extension.js +70 -6
  71. package/addon/routes/analytics/index.js +14 -1
  72. package/addon/routes/analytics/reports/index/details.js +1 -1
  73. package/addon/routes/analytics/reports/index/edit.js +1 -1
  74. package/addon/routes/analytics/reports/index/new.js +14 -1
  75. package/addon/routes/analytics/reports.js +14 -1
  76. package/addon/routes/connectivity/devices/index/new.js +14 -1
  77. package/addon/routes/connectivity/devices.js +14 -1
  78. package/addon/routes/connectivity/events.js +14 -1
  79. package/addon/routes/connectivity/fuel-providers/details/transactions.js +12 -0
  80. package/addon/routes/connectivity/fuel-providers/details.js +9 -0
  81. package/addon/routes/connectivity/fuel-providers/edit.js +9 -0
  82. package/addon/routes/connectivity/fuel-providers/new.js +14 -1
  83. package/addon/routes/connectivity/fuel-providers.js +14 -1
  84. package/addon/routes/connectivity/sensors/index/new.js +14 -1
  85. package/addon/routes/connectivity/sensors.js +14 -1
  86. package/addon/routes/connectivity/telematics/new.js +14 -1
  87. package/addon/routes/connectivity/telematics.js +14 -1
  88. package/addon/routes/connectivity/tracking.js +14 -1
  89. package/addon/routes/maintenance/equipment/index/details.js +2 -2
  90. package/addon/routes/maintenance/equipment/index/edit.js +2 -2
  91. package/addon/routes/maintenance/equipment/index/new.js +14 -1
  92. package/addon/routes/maintenance/equipment.js +14 -1
  93. package/addon/routes/maintenance/index.js +14 -1
  94. package/addon/routes/maintenance/inspection-forms/index/details.js +10 -1
  95. package/addon/routes/maintenance/inspection-forms/index/edit.js +10 -1
  96. package/addon/routes/maintenance/inspection-forms/index/new.js +14 -1
  97. package/addon/routes/maintenance/inspection-forms.js +14 -1
  98. package/addon/routes/maintenance/inspection-submissions/index/details.js +10 -1
  99. package/addon/routes/maintenance/inspection-submissions/index/edit.js +10 -1
  100. package/addon/routes/maintenance/inspection-submissions/index/new.js +14 -1
  101. package/addon/routes/maintenance/inspection-submissions.js +14 -1
  102. package/addon/routes/maintenance/maintenances/index/details.js +2 -2
  103. package/addon/routes/maintenance/maintenances/index/edit.js +2 -2
  104. package/addon/routes/maintenance/maintenances/index/new.js +14 -1
  105. package/addon/routes/maintenance/maintenances.js +14 -1
  106. package/addon/routes/maintenance/parts/index/details.js +2 -2
  107. package/addon/routes/maintenance/parts/index/edit.js +2 -2
  108. package/addon/routes/maintenance/parts/index/new.js +14 -1
  109. package/addon/routes/maintenance/parts.js +14 -1
  110. package/addon/routes/maintenance/schedules/index/details.js +2 -2
  111. package/addon/routes/maintenance/schedules/index/edit.js +2 -2
  112. package/addon/routes/maintenance/schedules/index/new.js +15 -1
  113. package/addon/routes/maintenance/schedules.js +15 -1
  114. package/addon/routes/maintenance/work-orders/index/details.js +2 -2
  115. package/addon/routes/maintenance/work-orders/index/edit.js +2 -2
  116. package/addon/routes/maintenance/work-orders/index/new.js +14 -1
  117. package/addon/routes/maintenance/work-orders.js +14 -1
  118. package/addon/routes/management/fuel-transactions/index/details.js +9 -0
  119. package/addon/routes/management/fuel-transactions.js +14 -1
  120. package/addon/routes/management/index.js +13 -0
  121. package/addon/routes/management/trailers.js +16 -0
  122. package/addon/routes/management/vendors/integrated/details.js +14 -1
  123. package/addon/routes/management/vendors/integrated/edit.js +14 -1
  124. package/addon/routes/management/vendors/integrated/new.js +14 -1
  125. package/addon/routes/management/vendors/integrated.js +14 -1
  126. package/addon/routes/operations/orders/index/details.js +1 -0
  127. package/addon/routes/operations/orders.js +40 -1
  128. package/addon/routes/operations/routes/index/details.js +14 -1
  129. package/addon/routes/operations/routes/index/new.js +13 -0
  130. package/addon/routes/operations/routes.js +14 -1
  131. package/addon/routes/operations/service-rates/index/new.js +11 -0
  132. package/addon/routes/operations/service-rates.js +14 -1
  133. package/addon/routes/settings/avatars.js +14 -1
  134. package/addon/routes/settings/custom-fields.js +14 -1
  135. package/addon/routes/settings/map.js +14 -1
  136. package/addon/routes/settings/notifications.js +11 -0
  137. package/addon/routes/settings/orchestrator.js +1 -1
  138. package/addon/routes/settings/payments/index.js +1 -1
  139. package/addon/routes/settings/routing.js +14 -1
  140. package/addon/routes/settings/scheduling.js +14 -1
  141. package/addon/routes/settings/telematics.js +21 -0
  142. package/addon/routes/virtual.js +11 -0
  143. package/addon/routes.js +1 -0
  144. package/addon/services/contact-actions.js +1 -0
  145. package/addon/services/customer-actions.js +2 -1
  146. package/addon/services/device-actions.js +1 -0
  147. package/addon/services/device-event-actions.js +1 -0
  148. package/addon/services/driver-actions.js +4 -13
  149. package/addon/services/equipment-actions.js +1 -0
  150. package/addon/services/fleet-actions.js +1 -0
  151. package/addon/services/fuel-report-actions.js +1 -0
  152. package/addon/services/inspection-form-actions.js +1 -0
  153. package/addon/services/inspection-submission-actions.js +1 -0
  154. package/addon/services/integrated-vendor-actions.js +1 -0
  155. package/addon/services/issue-actions.js +1 -0
  156. package/addon/services/maintenance-actions.js +1 -0
  157. package/addon/services/maintenance-schedule-actions.js +2 -1
  158. package/addon/services/order-actions.js +7 -0
  159. package/addon/services/order-presentation.js +129 -0
  160. package/addon/services/part-actions.js +1 -0
  161. package/addon/services/place-actions.js +1 -0
  162. package/addon/services/sensor-actions.js +1 -0
  163. package/addon/services/service-area-actions.js +1 -0
  164. package/addon/services/service-rate-actions.js +1 -0
  165. package/addon/services/telematic-actions.js +1 -0
  166. package/addon/services/trailer-actions.js +4 -14
  167. package/addon/services/vehicle-actions.js +4 -13
  168. package/addon/services/vendor-actions.js +1 -0
  169. package/addon/services/work-order-actions.js +1 -0
  170. package/addon/services/zone-actions.js +1 -0
  171. package/addon/templates/analytics/reports/index/details.hbs +1 -0
  172. package/addon/templates/analytics/reports/index.hbs +1 -0
  173. package/addon/templates/connectivity/devices/index/details.hbs +1 -0
  174. package/addon/templates/connectivity/devices/index.hbs +1 -0
  175. package/addon/templates/connectivity/events/index.hbs +1 -0
  176. package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
  177. package/addon/templates/connectivity/sensors/index.hbs +1 -0
  178. package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
  179. package/addon/templates/maintenance/equipment/index.hbs +2 -0
  180. package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
  181. package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
  182. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
  183. package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
  184. package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
  185. package/addon/templates/maintenance/maintenances/index.hbs +1 -0
  186. package/addon/templates/maintenance/parts/index/details.hbs +1 -0
  187. package/addon/templates/maintenance/parts/index.hbs +2 -0
  188. package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
  189. package/addon/templates/maintenance/schedules/index.hbs +2 -0
  190. package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
  191. package/addon/templates/maintenance/work-orders/index.hbs +1 -0
  192. package/addon/templates/management/contacts/customers/details.hbs +1 -0
  193. package/addon/templates/management/contacts/customers.hbs +1 -0
  194. package/addon/templates/management/contacts/index/details.hbs +1 -0
  195. package/addon/templates/management/contacts/index.hbs +1 -0
  196. package/addon/templates/management/contacts.hbs +1 -0
  197. package/addon/templates/management/drivers/index/details.hbs +1 -0
  198. package/addon/templates/management/drivers/index.hbs +2 -0
  199. package/addon/templates/management/fleets/index/details.hbs +1 -0
  200. package/addon/templates/management/fleets/index.hbs +1 -0
  201. package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
  202. package/addon/templates/management/fuel-reports/index.hbs +1 -0
  203. package/addon/templates/management/fuel-transactions/index.hbs +1 -0
  204. package/addon/templates/management/issues/index/details.hbs +1 -0
  205. package/addon/templates/management/issues/index.hbs +1 -0
  206. package/addon/templates/management/places/index/details.hbs +1 -0
  207. package/addon/templates/management/places/index.hbs +1 -0
  208. package/addon/templates/management/trailers/index/details.hbs +1 -0
  209. package/addon/templates/management/trailers/index.hbs +2 -0
  210. package/addon/templates/management/vehicles/index/details.hbs +1 -0
  211. package/addon/templates/management/vehicles/index.hbs +2 -0
  212. package/addon/templates/management/vendors/index/details.hbs +1 -0
  213. package/addon/templates/management/vendors/index.hbs +2 -0
  214. package/addon/templates/management/vendors/integrated.hbs +1 -0
  215. package/addon/templates/operations/orders/index/details.hbs +1 -0
  216. package/addon/templates/operations/orders/index.hbs +1 -0
  217. package/addon/templates/operations/service-rates/index/details.hbs +1 -0
  218. package/addon/templates/operations/service-rates/index.hbs +1 -0
  219. package/addon/templates/settings/telematics.hbs +70 -0
  220. package/addon/utils/normalize-order-config-flow.js +6 -1
  221. package/app/components/admin/telematics-settings.js +1 -0
  222. package/app/components/map/marker-card/driver.js +1 -0
  223. package/app/components/map/marker-card/vehicle.js +1 -0
  224. package/app/controllers/settings/telematics.js +1 -0
  225. package/app/routes/settings/telematics.js +1 -0
  226. package/app/services/order-presentation.js +1 -0
  227. package/app/templates/settings/telematics.js +1 -0
  228. package/composer.json +2 -2
  229. package/docs/AFAQY.md +1 -1
  230. package/docs/TELEMATICS_QUEUES.md +1 -1
  231. package/docs/TELEMETRY_ARCHITECTURE.md +72 -1
  232. package/extension.json +1 -1
  233. package/package.json +3 -3
  234. package/server/config/telemetry.php +12 -0
  235. package/server/migrations/2026_09_23_000001_add_retention_index_to_device_events.php +40 -0
  236. package/server/migrations/2026_09_23_000002_add_retention_index_to_positions.php +40 -0
  237. package/server/migrations/2026_09_23_000003_add_retention_index_to_telematic_sync_runs.php +40 -0
  238. package/server/src/Auth/Schemas/FleetOps.php +104 -5
  239. package/server/src/Console/Commands/DrainTelematicInbox.php +3 -13
  240. package/server/src/Console/Commands/ProcessOperationalAlerts.php +3 -3
  241. package/server/src/Console/Commands/PruneTelematicsData.php +318 -0
  242. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +9 -0
  243. package/server/src/Http/Controllers/FleetOpsController.php +21 -0
  244. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +11 -0
  245. package/server/src/Http/Controllers/Internal/v1/ContactController.php +11 -0
  246. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +14 -0
  247. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +13 -0
  248. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +11 -0
  249. package/server/src/Http/Controllers/Internal/v1/DriverController.php +22 -0
  250. package/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +13 -0
  251. package/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +15 -0
  252. package/server/src/Http/Controllers/Internal/v1/FuelReportController.php +9 -3
  253. package/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +15 -0
  254. package/server/src/Http/Controllers/Internal/v1/LiveController.php +16 -0
  255. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +15 -0
  256. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +15 -0
  257. package/server/src/Http/Controllers/Internal/v1/ManifestController.php +15 -0
  258. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +11 -0
  259. package/server/src/Http/Controllers/Internal/v1/NavigatorController.php +42 -1
  260. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +12 -0
  261. package/server/src/Http/Controllers/Internal/v1/OrderController.php +56 -4
  262. package/server/src/Http/Controllers/Internal/v1/PaymentController.php +13 -0
  263. package/server/src/Http/Controllers/Internal/v1/RadarController.php +19 -0
  264. package/server/src/Http/Controllers/Internal/v1/ServiceAreaController.php +9 -3
  265. package/server/src/Http/Controllers/Internal/v1/SettingController.php +465 -14
  266. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +21 -0
  267. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +21 -0
  268. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +21 -0
  269. package/server/src/Http/Controllers/Internal/v1/VendorController.php +17 -0
  270. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +11 -0
  271. package/server/src/Jobs/DispatchTelematicsRetentionJobs.php +70 -0
  272. package/server/src/Jobs/PruneTelematicsDataJob.php +74 -0
  273. package/server/src/Listeners/NotifyDriverOnShiftChange.php +3 -3
  274. package/server/src/Mail/CustomerCredentialsMail.php +1 -1
  275. package/server/src/Models/Order.php +35 -3
  276. package/server/src/Models/OrderConfig.php +129 -0
  277. package/server/src/Models/TrackingNumber.php +8 -4
  278. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +9 -1
  279. package/server/src/Providers/FleetOpsServiceProvider.php +3 -0
  280. package/server/src/Support/Analytics/LiveFleet.php +18 -2
  281. package/server/src/Support/Authorization.php +103 -0
  282. package/server/src/Support/Database/TableIndexes.php +66 -0
  283. package/server/src/Support/OrderTracker.php +2 -2
  284. package/server/src/Support/Reporting/FleetOpsReportSchema.php +476 -116
  285. package/server/src/Support/Telematics/Retention/RetentionPolicy.php +300 -0
  286. package/server/src/Support/Telematics/Retention/TelemetryActivity.php +40 -0
  287. package/server/src/Support/Telematics/TelematicService.php +12 -2
  288. package/server/src/Support/Telematics/Telemetry/Ingestor.php +4 -2
  289. package/server/src/Tracking/TrackingIntelligenceService.php +1 -1
  290. package/server/src/Tracking/TrackingOptions.php +2 -2
  291. package/server/src/Traits/AuthorizesMethods.php +37 -0
  292. package/server/src/routes.php +9 -1
  293. package/server/tests/AnalyticsRoutesTest.php +29 -2
  294. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +16 -1
  295. package/server/tests/Feature/Http/Api/TelematicsRetentionIndexMigrationTest.php +115 -0
  296. package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
  297. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
  298. package/server/tests/Feature/Http/Internal/SettingControllerHelpersTest.php +71 -0
  299. package/server/tests/Feature/Http/Internal/TelematicsStorageDeadlinesTest.php +119 -0
  300. package/server/tests/InternalImportExportControllerContractsTest.php +5 -8
  301. package/server/tests/NavigatorControllerContractsTest.php +36 -4
  302. package/server/tests/NotificationAndMailContractsTest.php +31 -0
  303. package/server/tests/ObserverContractsTest.php +1 -1
  304. package/server/tests/OrchestratorConsumableApiTest.php +42 -0
  305. package/server/tests/OrderConfigLifecycleTest.php +120 -0
  306. package/server/tests/ProviderContractsTest.php +10 -0
  307. package/server/tests/ReportSchemaContractsTest.php +120 -6
  308. package/server/tests/SettingControllerContractsTest.php +429 -18
  309. package/server/tests/SmallControllerContractsTest.php +8 -5
  310. package/server/tests/Support/PruneTelematicsDataProbe.php +55 -0
  311. package/server/tests/Unit/Console/Commands/ProcessOperationalAlertsCommandTest.php +2 -2
  312. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +353 -0
  313. package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
  314. package/server/tests/Unit/Jobs/DispatchTelematicsRetentionJobsTest.php +114 -0
  315. package/server/tests/Unit/Jobs/PruneTelematicsDataJobTest.php +60 -0
  316. package/server/tests/Unit/Orchestration/VroomEngineTransportAndSettingsTest.php +20 -0
  317. package/server/tests/Unit/Support/Analytics/LiveFleetTest.php +6 -0
  318. package/server/tests/Unit/Support/AuthorizationTest.php +142 -0
  319. package/server/tests/Unit/Support/MaintainableAndOperationalAlertsTest.php +12 -1
  320. package/server/tests/Unit/Support/MoreQuerySeamsTest.php +11 -2
  321. package/server/tests/Unit/Support/Telematics/Retention/RetentionPolicyTest.php +190 -0
  322. package/server/tests/Unit/Support/Telematics/Retention/TelemetryActivityTest.php +68 -0
  323. package/server/tests/Unit/Tracking/TrackingOptionsCompanySettingsTest.php +109 -0
  324. package/translations/en-us.yaml +108 -0
@@ -0,0 +1,142 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Support\Authorization;
4
+ use Fleetbase\FleetOps\Traits\AuthorizesMethods;
5
+ use Fleetbase\Models\User;
6
+ use Illuminate\Database\ConnectionResolver;
7
+ use Illuminate\Database\Eloquent\Model;
8
+ use Illuminate\Database\SQLiteConnection;
9
+ use Illuminate\Http\Exceptions\HttpResponseException;
10
+ use Illuminate\Http\Request;
11
+ use Illuminate\Routing\Controller;
12
+
13
+ if (!function_exists('Fleetbase\\Support\\auth')) {
14
+ eval('namespace Fleetbase\\Support; function auth() { return \\app("fleetops.authorization.test-auth"); }');
15
+ }
16
+
17
+ if (!function_exists('Fleetbase\\Support\\session')) {
18
+ eval('namespace Fleetbase\\Support; function session($key = null, $default = null) { return \\session($key, $default); }');
19
+ }
20
+
21
+ class FleetOpsAuthorizationUser extends User
22
+ {
23
+ public array $grantedPermissions = [];
24
+ public bool $permissionStoreUnavailable = false;
25
+
26
+ public function hasPermissionTo($permission, $guardName = null): bool
27
+ {
28
+ if ($this->permissionStoreUnavailable) {
29
+ throw new RuntimeException('Permission store unavailable');
30
+ }
31
+
32
+ return in_array($permission->name, $this->grantedPermissions, true);
33
+ }
34
+ }
35
+
36
+ beforeEach(function () {
37
+ $connection = new SQLiteConnection(new PDO('sqlite::memory:'));
38
+ $resolver = new ConnectionResolver(['mysql' => $connection]);
39
+ $resolver->setDefaultConnection('mysql');
40
+ Model::setConnectionResolver($resolver);
41
+ config()->set('auth.defaults.guard', 'sanctum');
42
+ config()->set('permission.table_names.permissions', 'permissions');
43
+ $connection->getSchemaBuilder()->create('permissions', function ($table) {
44
+ $table->string('id')->primary();
45
+ $table->string('name');
46
+ $table->string('guard_name')->default('sanctum');
47
+ });
48
+ $connection->table('permissions')->insert([
49
+ ['id' => 'optimize', 'name' => 'fleet-ops optimize order'],
50
+ ['id' => 'dispatch', 'name' => 'fleet-ops dispatch order'],
51
+ ['id' => 'resource', 'name' => 'fleet-ops * order'],
52
+ ['id' => 'service', 'name' => 'fleet-ops *'],
53
+ ]);
54
+ session(['user' => null]);
55
+ $this->user = new FleetOpsAuthorizationUser();
56
+ $this->user->setRawAttributes(['type' => 'user'], true);
57
+ $this->auth = new class($this->user) {
58
+ public bool $unavailable = false;
59
+
60
+ public function __construct(public ?User $currentUser)
61
+ {
62
+ }
63
+
64
+ public function user(): ?User
65
+ {
66
+ if ($this->unavailable) {
67
+ throw new RuntimeException('Session unavailable');
68
+ }
69
+
70
+ return $this->currentUser;
71
+ }
72
+ };
73
+ app()->instance('fleetops.authorization.test-auth', $this->auth);
74
+ });
75
+
76
+ test('operation authorization accepts exact and wildcard grants', function (string $grant) {
77
+ $this->user->grantedPermissions = [$grant];
78
+
79
+ expect(Authorization::canAny('optimize order'))->toBeTrue();
80
+ Authorization::authorize('optimize order');
81
+ })->with(['fleet-ops optimize order', 'fleet-ops * order', 'fleet-ops *']);
82
+
83
+ test('operation authorization accepts any requested permission and rejects missing grants', function () {
84
+ $this->user->grantedPermissions = ['fleet-ops dispatch order'];
85
+
86
+ expect(Authorization::canAny('optimize order'))->toBeFalse()
87
+ ->and(Authorization::canAny('optimize order', 'dispatch order'))->toBeTrue()
88
+ ->and(Authorization::canAny('list unseeded-resource'))->toBeTrue();
89
+ expect(fn () => Authorization::authorize('optimize order'))->toThrow(HttpResponseException::class);
90
+
91
+ $this->user->permissionStoreUnavailable = true;
92
+ expect(Authorization::canAny('dispatch order'))->toBeFalse();
93
+ });
94
+
95
+ test('platform administrators pass operations while system settings reject ordinary users', function () {
96
+ expect(fn () => Authorization::authorizeAdmin())->toThrow(HttpResponseException::class);
97
+ $this->user->setAttribute('type', 'admin');
98
+ expect(Authorization::canAny('optimize order'))->toBeTrue();
99
+ Authorization::authorizeAdmin();
100
+ });
101
+
102
+ test('authorization fails closed when the authenticated user is missing or cannot be read', function () {
103
+ $this->auth->currentUser = null;
104
+ expect(Authorization::canAny('optimize order'))->toBeFalse();
105
+ expect(fn () => Authorization::authorizeAdmin())->toThrow(HttpResponseException::class);
106
+ $this->auth->unavailable = true;
107
+ expect(Authorization::canAny('optimize order'))->toBeFalse();
108
+ });
109
+
110
+ test('controller middleware applies the mapped operation and administrator checks', function () {
111
+ $controller = new class extends Controller {
112
+ use AuthorizesMethods;
113
+
114
+ public function __construct()
115
+ {
116
+ $this->authorizeMethods(['optimize' => ['optimize order', 'dispatch order'], 'settings' => 'admin']);
117
+ }
118
+ };
119
+ $middleware = $controller->getMiddleware();
120
+ $request = Request::create('/');
121
+ $this->user->grantedPermissions = ['fleet-ops dispatch order'];
122
+
123
+ expect($middleware[0]['options']['only'])->toBe(['optimize'])
124
+ ->and($middleware[0]['middleware']($request, fn ($nextRequest) => $nextRequest))->toBe($request);
125
+ expect(fn () => $middleware[1]['middleware']($request, fn () => 'allowed'))->toThrow(HttpResponseException::class);
126
+ $this->user->setAttribute('type', 'admin');
127
+ expect($middleware[1]['middleware']($request, fn () => 'allowed'))->toBe('allowed');
128
+ });
129
+
130
+ test('analytics controllers enforce authorization before invoking their next middleware', function (string $class) {
131
+ $connection = Model::getConnectionResolver()->connection();
132
+ $connection->table('permissions')->insert(['id' => 'analytics', 'name' => 'fleet-ops view analytics']);
133
+ $controller = new $class();
134
+ $middleware = $controller->getMiddleware()[0]['middleware'];
135
+ $request = Request::create('/analytics');
136
+ expect(fn () => $middleware($request, fn () => 'private analytics'))->toThrow(HttpResponseException::class);
137
+ $this->user->grantedPermissions = ['fleet-ops view analytics'];
138
+ expect($middleware($request, fn ($forwarded) => $forwarded))->toBe($request);
139
+ })->with([
140
+ Fleetbase\FleetOps\Http\Controllers\Internal\v1\AnalyticsController::class,
141
+ Fleetbase\FleetOps\Http\Controllers\Internal\v1\MetricsController::class,
142
+ ]);
@@ -184,10 +184,21 @@ test('operational alert helpers query orders positions and settings', function (
184
184
  $order = Order::where('uuid', 'order-1')->withoutGlobalScopes()->first();
185
185
  expect($probe->callHelper('latestPositionForOrder', $order))->toBeInstanceOf(Position::class);
186
186
 
187
- $settings = $probe->callHelper('alertSettings');
187
+ $settings = $probe->callHelper('alertSettings', $order);
188
188
  expect($settings['late_departures']['enabled'])->toBeTrue()
189
189
  ->and($settings['route_deviations']['distance_threshold_meters'])->toBe(500)
190
190
  ->and($settings['prolonged_stoppages']['duration_threshold_minutes'])->toBe(30);
191
+
192
+ // Settings resolve from the order's company without a company session
193
+ $connection->table('settings')->insert([
194
+ 'key' => 'company.company-1.tracking',
195
+ 'value' => json_encode(['alerts' => ['late_departures' => ['enabled' => false], 'route_deviations' => ['distance_threshold_meters' => 750]]]),
196
+ ]);
197
+ session(['company' => null]);
198
+ $settings = $probe->callHelper('alertSettings', $order);
199
+ session(['company' => 'company-1']);
200
+ expect($settings['late_departures']['enabled'])->toBeFalse()
201
+ ->and($settings['route_deviations']['distance_threshold_meters'])->toBe(750);
191
202
  });
192
203
 
193
204
  test('route point collection normalizes pairs and measures distances', function () {
@@ -384,13 +384,22 @@ test('index driver resource counts assigned orders and names the current one', f
384
384
  });
385
385
 
386
386
  test('shift change listener reads company scheduling settings', function () {
387
- fleetopsMoreSeamBoot();
387
+ $connection = fleetopsMoreSeamBoot();
388
388
 
389
389
  $class = Fleetbase\FleetOps\Listeners\NotifyDriverOnShiftChange::class;
390
390
  $listener = (new ReflectionClass($class))->newInstanceWithoutConstructor();
391
391
 
392
392
  // With nothing stored the lookup falls back to the empty default
393
- expect(fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings'))->toBe([]);
393
+ expect(fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings', ['company-seam-2']))->toBe([]);
394
+
395
+ // Queued listeners have no company session, so the schedule's company is used
396
+ $connection->table('settings')->insert(['key' => 'company.company-seam-3.fleet-ops.scheduling-settings', 'value' => json_encode(['notify_drivers_on_shift_change' => true])]);
397
+ session(['company' => null]);
398
+ $settings = fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings', ['company-seam-3']);
399
+ session(['company' => 'company-seam-2']);
400
+
401
+ expect($settings)->toBe(['notify_drivers_on_shift_change' => true])
402
+ ->and(fleetopsMoreSeamInvoke($listener, $class, 'getSchedulingSettings', [null]))->toBe([]);
394
403
  });
395
404
 
396
405
  test('order insights capability builds its own company-scoped order query', function () {
@@ -0,0 +1,190 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy;
4
+ use Illuminate\Support\Carbon;
5
+
6
+ /**
7
+ * RetentionPolicy combines administrator policy with allowed company history
8
+ * preferences, enforces maximums, and caches policies for long-lived workers.
9
+ */
10
+ function fleetopsRetentionResolver(array $system, array $company, ?array &$calls = null): void
11
+ {
12
+ $calls = [];
13
+ RetentionPolicy::$settingsResolver = function (string $scope, string $key, mixed $default, ?string $companyUuid) use ($system, $company, &$calls) {
14
+ $calls[] = [$scope, $key, $companyUuid];
15
+ expect($key)->toBe(RetentionPolicy::SETTING_KEY);
16
+
17
+ return $scope === 'system' ? $system : ($company[$companyUuid] ?? $default);
18
+ };
19
+ }
20
+
21
+ beforeEach(function () {
22
+ RetentionPolicy::flush();
23
+ RetentionPolicy::$settingsResolver = null;
24
+ config(['telematics.telemetry' => ['event_retention_days' => 45, 'processed_retention_hours' => 48, 'log_telemetry_activity' => true, 'poll_queue' => 'default']]);
25
+ });
26
+
27
+ afterEach(function () {
28
+ RetentionPolicy::flush();
29
+ RetentionPolicy::$settingsResolver = null;
30
+ Carbon::setTestNow();
31
+ config(['telematics.telemetry' => []]);
32
+ });
33
+
34
+ test('defaults come from package config, overridden by the admin setting, with everything clamped', function () {
35
+ fleetopsRetentionResolver([], []);
36
+ expect(RetentionPolicy::defaults())->toBe([
37
+ 'event_retention_days' => 45,
38
+ 'event_compact_after_days' => 7,
39
+ 'position_retention_days' => 90,
40
+ 'processed_retention_hours' => 48,
41
+ 'quarantine_retention_days' => 7,
42
+ 'sync_run_retention_days' => 7,
43
+ 'max_event_retention_days' => 0,
44
+ 'max_position_retention_days' => 0,
45
+ 'log_telemetry_activity' => true,
46
+ ]);
47
+
48
+ RetentionPolicy::flush();
49
+ fleetopsRetentionResolver([
50
+ 'event_retention_days' => 99999,
51
+ 'event_compact_after_days' => '',
52
+ 'position_retention_days' => 0,
53
+ 'quarantine_retention_days' => -5,
54
+ 'processed_retention_hours' => null,
55
+ 'log_telemetry_activity' => 'no',
56
+ 'ignored' => 'value',
57
+ ], []);
58
+ $defaults = RetentionPolicy::defaults();
59
+ expect($defaults['event_retention_days'])->toBe(3650)
60
+ ->and($defaults['event_compact_after_days'])->toBe(7)
61
+ ->and($defaults['position_retention_days'])->toBe(0)
62
+ ->and($defaults['quarantine_retention_days'])->toBe(1)
63
+ ->and($defaults['processed_retention_hours'])->toBe(48)
64
+ ->and($defaults['log_telemetry_activity'])->toBeFalse()
65
+ ->and($defaults)->not->toHaveKey('ignored')
66
+ ->and(RetentionPolicy::keys())->toBe(array_keys(RetentionPolicy::FALLBACKS));
67
+ });
68
+
69
+ test('company policies layer over the defaults and are cached until flushed', function () {
70
+ fleetopsRetentionResolver(['position_retention_days' => 30], ['company-1' => ['event_retention_days' => 10, 'log_telemetry_activity' => false]], $calls);
71
+
72
+ $policy = RetentionPolicy::forCompany('company-1');
73
+ expect($policy->get('event_retention_days'))->toBe(10)
74
+ ->and($policy->get('position_retention_days'))->toBe(30)
75
+ ->and($policy->get('processed_retention_hours'))->toBe(48)
76
+ ->and($policy->logsTelemetryActivity())->toBeTrue()
77
+ ->and($policy->toArray())->toHaveKeys(RetentionPolicy::keys())
78
+ ->and(RetentionPolicy::forCompany('company-1'))->toBe($policy)
79
+ ->and(RetentionPolicy::forCompany(null)->toArray())->toBe(RetentionPolicy::defaults())
80
+ ->and(RetentionPolicy::forCompany('company-2')->get('event_retention_days'))->toBe(45)
81
+ ->and(array_column($calls, 0))->toBe(['system', 'company', 'company']);
82
+
83
+ RetentionPolicy::flush('company-1');
84
+ RetentionPolicy::forCompany('company-1');
85
+ RetentionPolicy::forCompany('company-2');
86
+ expect(count($calls))->toBe(4);
87
+
88
+ RetentionPolicy::flush();
89
+ RetentionPolicy::forCompany('company-2');
90
+ expect(array_column($calls, 0))->toBe(['system', 'company', 'company', 'company', 'system', 'company']);
91
+ });
92
+
93
+ test('company history respects admin maximums and cannot override infrastructure or logging', function () {
94
+ fleetopsRetentionResolver([
95
+ 'event_retention_days' => 0,
96
+ 'position_retention_days' => 90,
97
+ 'max_event_retention_days' => 30,
98
+ 'max_position_retention_days' => 45,
99
+ 'log_telemetry_activity' => false,
100
+ ], [
101
+ 'company-1' => [
102
+ 'event_retention_days' => 0,
103
+ 'position_retention_days' => 120,
104
+ 'max_event_retention_days' => 0,
105
+ 'max_position_retention_days' => 0,
106
+ 'event_compact_after_days' => 0,
107
+ 'processed_retention_hours' => 0,
108
+ 'quarantine_retention_days' => 0,
109
+ 'sync_run_retention_days' => 0,
110
+ 'log_telemetry_activity' => true,
111
+ ],
112
+ 'company-2' => ['event_retention_days' => 5, 'position_retention_days' => 15],
113
+ ]);
114
+
115
+ $policy = RetentionPolicy::forCompany('company-1');
116
+ expect($policy->get('event_retention_days'))->toBe(30)
117
+ ->and($policy->get('position_retention_days'))->toBe(45)
118
+ ->and($policy->get('event_compact_after_days'))->toBe(7)
119
+ ->and($policy->get('processed_retention_hours'))->toBe(48)
120
+ ->and($policy->get('quarantine_retention_days'))->toBe(7)
121
+ ->and($policy->get('sync_run_retention_days'))->toBe(7)
122
+ ->and($policy->logsTelemetryActivity())->toBeFalse()
123
+ ->and(RetentionPolicy::forCompany('company-2')->get('event_retention_days'))->toBe(5)
124
+ ->and(RetentionPolicy::forCompany('company-2')->get('position_retention_days'))->toBe(15)
125
+ ->and(RetentionPolicy::forCompany('inherited')->get('event_retention_days'))->toBe(30)
126
+ ->and(RetentionPolicy::forCompany('inherited')->get('position_retention_days'))->toBe(45)
127
+ ->and(RetentionPolicy::forCompany(null)->get('event_retention_days'))->toBe(30);
128
+ });
129
+
130
+ test('unconfigured maximums preserve unlimited history and null preferences inherit defaults', function () {
131
+ fleetopsRetentionResolver([], ['company-1' => ['event_retention_days' => 0, 'position_retention_days' => null]]);
132
+
133
+ expect(RetentionPolicy::forCompany('company-1')->get('event_retention_days'))->toBe(0)
134
+ ->and(RetentionPolicy::forCompany('company-1')->get('position_retention_days'))->toBe(90)
135
+ ->and(RetentionPolicy::companyPreferences(['event_retention_days' => 'invalid', 'position_retention_days' => false]))->toBe([])
136
+ ->and(RetentionPolicy::companyPreferences(['event_retention_days' => null, 'position_retention_days' => '', 'log_telemetry_activity' => true]))->toBe([]);
137
+ });
138
+
139
+ test('constrained preferences keep zero explicit when allowed and replace it when an administrator imposes a maximum', function () {
140
+ $preferences = ['event_retention_days' => 0, 'position_retention_days' => null];
141
+ expect(RetentionPolicy::constrainCompanyPreferences($preferences, RetentionPolicy::FALLBACKS))->toBe(['event_retention_days' => 0]);
142
+
143
+ $defaults = array_replace(RetentionPolicy::FALLBACKS, ['max_event_retention_days' => 90, 'max_position_retention_days' => 360]);
144
+ $bounded = RetentionPolicy::constrainCompanyPreferences($preferences, $defaults);
145
+ expect($bounded)->toBe(['event_retention_days' => 90])
146
+ ->and(RetentionPolicy::constrainCompanyPreferences(['event_retention_days' => 30, 'position_retention_days' => 720], $defaults))->toBe(['event_retention_days' => 30, 'position_retention_days' => 360])
147
+ ->and(RetentionPolicy::constrainCompanyPreferences([], $defaults))->toBe([])
148
+ ->and(RetentionPolicy::constrainCompanyPreferences($bounded, RetentionPolicy::FALLBACKS))->toBe(['event_retention_days' => 90]);
149
+ });
150
+
151
+ test('cutoffs, compaction and deletion rules follow the clamped values', function () {
152
+ Carbon::setTestNow('2026-09-23 12:00:00 UTC');
153
+
154
+ $policy = RetentionPolicy::fromArray(['event_retention_days' => 10, 'event_compact_after_days' => 10, 'processed_retention_hours' => 6, 'position_retention_days' => 0]);
155
+ expect($policy->deletesEvents())->toBeTrue()
156
+ ->and($policy->compactsEvents())->toBeFalse()
157
+ ->and($policy->cutoff('processed_retention_hours')?->toDateTimeString())->toBe('2026-09-23 06:00:00')
158
+ ->and($policy->cutoff('event_retention_days')?->toDateTimeString())->toBe('2026-09-13 12:00:00')
159
+ ->and($policy->cutoff('position_retention_days'))->toBeNull()
160
+ ->and($policy->cutoff('unknown'))->toBeNull()
161
+ ->and($policy->get('unknown'))->toBeNull();
162
+
163
+ expect(RetentionPolicy::fromArray(['event_retention_days' => 10, 'event_compact_after_days' => 3])->compactsEvents())->toBeTrue()
164
+ ->and(RetentionPolicy::fromArray(['event_retention_days' => 0, 'event_compact_after_days' => 5])->compactsEvents())->toBeTrue()
165
+ ->and(RetentionPolicy::fromArray(['event_retention_days' => 0, 'event_compact_after_days' => 5])->deletesEvents())->toBeFalse()
166
+ ->and(RetentionPolicy::fromArray(['event_compact_after_days' => 0])->compactsEvents())->toBeFalse()
167
+ ->and(RetentionPolicy::fromArray([], ['event_retention_days' => 12])->get('event_retention_days'))->toBe(12)
168
+ ->and(RetentionPolicy::normalize(['log_telemetry_activity' => '1'], [])['log_telemetry_activity'])->toBeTrue();
169
+ });
170
+
171
+ test('a failing or malformed settings store falls back to the layer below', function () {
172
+ RetentionPolicy::$settingsResolver = fn () => throw new RuntimeException('settings unavailable');
173
+ expect(RetentionPolicy::forCompany('company-1')->get('event_retention_days'))->toBe(45);
174
+
175
+ RetentionPolicy::flush();
176
+ RetentionPolicy::$settingsResolver = fn () => 'not-an-array';
177
+ expect(RetentionPolicy::defaults()['processed_retention_hours'])->toBe(48);
178
+ });
179
+
180
+ test('company lookups scope the session for the setting store and restore it afterwards', function () {
181
+ // Without a resolver the policy reaches Setting, which has no database here and throws.
182
+ app('session')->put('company', 'previous-company');
183
+ expect(RetentionPolicy::forCompany('company-9')->get('event_retention_days'))->toBe(45)
184
+ ->and(app('session')->get('company'))->toBe('previous-company');
185
+
186
+ app('session')->forget('company');
187
+ RetentionPolicy::flush();
188
+ expect(RetentionPolicy::forCompany('company-9')->get('processed_retention_hours'))->toBe(48)
189
+ ->and(app('session')->has('company'))->toBeFalse();
190
+ });
@@ -0,0 +1,68 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy;
4
+ use Fleetbase\FleetOps\Support\Telematics\Retention\TelemetryActivity;
5
+ use Spatie\Activitylog\ActivityLogger;
6
+
7
+ /**
8
+ * Telemetry writes bypass the activity log unless an administrator enabled it, and
9
+ * still run when no activity logger is available.
10
+ */
11
+ function fleetopsTelemetryActivityLogger(): ActivityLogger
12
+ {
13
+ $logger = new class extends ActivityLogger {
14
+ public int $suppressed = 0;
15
+
16
+ public function __construct()
17
+ {
18
+ }
19
+
20
+ public function withoutLogs(Closure $callback): mixed
21
+ {
22
+ $this->suppressed++;
23
+
24
+ return $callback();
25
+ }
26
+ };
27
+ app()->instance(ActivityLogger::class, $logger);
28
+
29
+ return $logger;
30
+ }
31
+
32
+ beforeEach(function () {
33
+ RetentionPolicy::flush();
34
+ config(['telematics.telemetry' => []]);
35
+ });
36
+
37
+ afterEach(function () {
38
+ RetentionPolicy::flush();
39
+ RetentionPolicy::$settingsResolver = null;
40
+ app()->forgetInstance(ActivityLogger::class);
41
+ });
42
+
43
+ test('telemetry activity follows the system policy and ignores legacy company overrides', function () {
44
+ RetentionPolicy::$settingsResolver = fn (string $scope, string $key, mixed $default, ?string $company) => $company === 'verbose' ? ['log_telemetry_activity' => true] : [];
45
+ $logger = fleetopsTelemetryActivityLogger();
46
+
47
+ expect(TelemetryActivity::run('quiet', fn () => 'saved'))->toBe('saved')
48
+ ->and($logger->suppressed)->toBe(1)
49
+ ->and(TelemetryActivity::run('verbose', fn () => 'saved'))->toBe('saved')
50
+ ->and($logger->suppressed)->toBe(2)
51
+ ->and(TelemetryActivity::run(null, fn () => 'system'))->toBe('system')
52
+ ->and($logger->suppressed)->toBe(3);
53
+
54
+ RetentionPolicy::flush();
55
+ RetentionPolicy::$settingsResolver = fn (string $scope) => ['log_telemetry_activity' => $scope === 'system'];
56
+
57
+ expect(TelemetryActivity::run('quiet', fn () => 'logged'))->toBe('logged')
58
+ ->and(TelemetryActivity::run('verbose', fn () => 'logged'))->toBe('logged')
59
+ ->and(TelemetryActivity::run(null, fn () => 'system'))->toBe('system')
60
+ ->and($logger->suppressed)->toBe(3);
61
+ });
62
+
63
+ test('telemetry writes still run when the activity logger cannot be resolved', function () {
64
+ RetentionPolicy::$settingsResolver = fn () => [];
65
+ app()->forgetInstance(ActivityLogger::class);
66
+
67
+ expect(TelemetryActivity::run('company-1', fn () => 'unlogged'))->toBe('unlogged');
68
+ });
@@ -0,0 +1,109 @@
1
+ <?php
2
+
3
+ if (!function_exists('Fleetbase\Models\session')) {
4
+ eval('namespace Fleetbase\Models; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } public function missing($k) { return \session($k) === null; } }; } return \session($key, $default); }');
5
+ }
6
+
7
+ use Fleetbase\FleetOps\Models\Order;
8
+ use Fleetbase\FleetOps\Support\OrderTracker;
9
+ use Fleetbase\FleetOps\Tracking\TrackingIntelligenceService;
10
+ use Fleetbase\FleetOps\Tracking\TrackingOptions;
11
+ use Illuminate\Database\ConnectionResolver;
12
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
13
+ use Illuminate\Database\SQLiteConnection;
14
+
15
+ /**
16
+ * Covers company tracking settings resolution for TrackingOptions: the
17
+ * order's company is used when there is no company session (queued jobs,
18
+ * console commands), and the session company remains the fallback.
19
+ */
20
+ function fleetopsTrackingOptionsBoot(): SQLiteConnection
21
+ {
22
+ $connection = new SQLiteConnection(new PDO('sqlite::memory:'));
23
+ $resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
24
+ $resolver->setDefaultConnection('mysql');
25
+ EloquentModel::setConnectionResolver($resolver);
26
+
27
+ $connection->getSchemaBuilder()->create('settings', function ($blueprint) {
28
+ $blueprint->increments('id');
29
+ $blueprint->string('key')->nullable();
30
+ $blueprint->text('value')->nullable();
31
+ $blueprint->timestamps();
32
+ });
33
+
34
+ $connection->table('settings')->insert([
35
+ ['key' => 'company.company-a.tracking', 'value' => json_encode(['provider' => 'osrm', 'cache_ttl_seconds' => 15])],
36
+ ['key' => 'company.company-b.tracking', 'value' => json_encode(['provider' => 'calculated', 'cache_ttl_seconds' => 45])],
37
+ ]);
38
+
39
+ session(['company' => null]);
40
+
41
+ return $connection;
42
+ }
43
+
44
+ test('tracking options read the given company settings regardless of session', function (?string $sessionCompany) {
45
+ fleetopsTrackingOptionsBoot();
46
+ session(['company' => $sessionCompany]);
47
+
48
+ $options = TrackingOptions::fromArray([], 'company-a');
49
+
50
+ expect($options->provider)->toBe('osrm')
51
+ ->and($options->cacheTtlSeconds)->toBe(15);
52
+
53
+ // Explicit options still win over company settings
54
+ expect(TrackingOptions::fromArray(['provider' => 'google'], 'company-a')->provider)->toBe('google');
55
+ })->with([null, 'company-b']);
56
+
57
+ test('tracking options fall back to the session company when no company is given', function () {
58
+ fleetopsTrackingOptionsBoot();
59
+ session(['company' => 'company-b']);
60
+
61
+ $options = TrackingOptions::fromArray([]);
62
+ session(['company' => null]);
63
+
64
+ expect($options->provider)->toBe('calculated')
65
+ ->and($options->cacheTtlSeconds)->toBe(45);
66
+ });
67
+
68
+ test('order tracker builds options from the order company', function () {
69
+ fleetopsTrackingOptionsBoot();
70
+
71
+ $service = new class extends TrackingIntelligenceService {
72
+ public ?TrackingOptions $received = null;
73
+
74
+ public function __construct()
75
+ {
76
+ }
77
+
78
+ public function track(Order $order, array|TrackingOptions $options = []): array
79
+ {
80
+ $this->received = $options;
81
+
82
+ return [];
83
+ }
84
+ };
85
+ app()->instance(TrackingIntelligenceService::class, $service);
86
+
87
+ $order = new Order();
88
+ $order->setRawAttributes(['uuid' => 'order-tracking-options', 'company_uuid' => 'company-a'], true);
89
+
90
+ (new OrderTracker($order))->toArray();
91
+ app()->forgetInstance(TrackingIntelligenceService::class);
92
+
93
+ expect($service->received)->toBeInstanceOf(TrackingOptions::class)
94
+ ->and($service->received->provider)->toBe('osrm');
95
+ });
96
+
97
+ test('tracking remains available with configured defaults when the settings database is unavailable', function () {
98
+ $connection = fleetopsTrackingOptionsBoot();
99
+ $connection->getSchemaBuilder()->drop('settings');
100
+ $original = config('fleetops.tracking', []);
101
+ try {
102
+ config(['fleetops.tracking' => ['provider' => 'calculated', 'cache_ttl_seconds' => 17]]);
103
+ $options = TrackingOptions::fromArray([], 'uncached-unavailable-company');
104
+ expect($options->provider)->toBe('calculated')->and($options->cacheTtlSeconds)->toBe(17);
105
+ expect(TrackingOptions::fromArray(['provider' => 'osrm'], 'uncached-unavailable-company')->provider)->toBe('osrm');
106
+ } finally {
107
+ config(['fleetops.tracking' => $original]);
108
+ }
109
+ });