@fleetbase/fleetops-engine 0.6.70 → 0.6.72

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 (510) hide show
  1. package/RELEASE.md +18 -47
  2. package/addon/components/admin/tracking-page-settings.hbs +56 -0
  3. package/addon/components/admin/tracking-page-settings.js +67 -0
  4. package/addon/components/device/form.hbs +2 -2
  5. package/addon/components/driver/details.hbs +1 -1
  6. package/addon/components/driver/form.hbs +1 -1
  7. package/addon/components/equipment/form.hbs +1 -1
  8. package/addon/components/layout/fleet-ops-sidebar.js +11 -3
  9. package/addon/components/maintenance/form.hbs +3 -3
  10. package/addon/components/map/google-live-map.js +2 -2
  11. package/addon/components/map/leaflet-live-map.js +45 -32
  12. package/addon/components/orchestrator/phase-builder.js +2 -1
  13. package/addon/components/orchestrator-workbench.hbs +7 -1
  14. package/addon/components/orchestrator-workbench.js +16 -2
  15. package/addon/components/order/details/activity.hbs +8 -6
  16. package/addon/components/order/details/activity.js +9 -0
  17. package/addon/components/order/details.hbs +29 -0
  18. package/addon/components/order/details.js +26 -0
  19. package/addon/components/order/form/details.hbs +30 -4
  20. package/addon/components/order/form/details.js +46 -0
  21. package/addon/components/order/form/route.hbs +5 -3
  22. package/addon/components/order/form/route.js +24 -1
  23. package/addon/components/order/form.hbs +12 -0
  24. package/addon/components/order/form.js +21 -1
  25. package/addon/components/order/kanban.js +72 -0
  26. package/addon/components/order-config-manager/activity-flow.js +45 -10
  27. package/addon/components/order-config-manager/lifecycle.hbs +250 -0
  28. package/addon/components/order-config-manager/lifecycle.js +358 -0
  29. package/addon/components/order-config-manager.js +5 -1
  30. package/addon/components/service-rate/details.hbs +1 -1
  31. package/addon/components/service-rate/form.hbs +1 -1
  32. package/addon/components/tracking-page/full-view.hbs +276 -0
  33. package/addon/components/tracking-page/full-view.js +474 -0
  34. package/addon/components/tracking-page/locale-selector.js +30 -0
  35. package/addon/components/tracking-page/map.hbs +44 -0
  36. package/addon/components/tracking-page/map.js +328 -0
  37. package/addon/components/tracking-page/order-list.hbs +35 -0
  38. package/addon/components/tracking-page/public-view.hbs +60 -0
  39. package/addon/components/tracking-page/settings-preview.hbs +21 -0
  40. package/addon/components/tracking-page/settings-preview.js +23 -0
  41. package/addon/components/tracking-page/verify-panel.hbs +109 -0
  42. package/addon/components/tracking-page.hbs +142 -0
  43. package/addon/components/tracking-page.js +564 -0
  44. package/addon/components/trailer/form.hbs +2 -2
  45. package/addon/components/vehicle/form.hbs +3 -3
  46. package/addon/components/warranty/form.hbs +2 -2
  47. package/addon/components/widget/live-fleet.js +54 -6
  48. package/addon/components/widget/proof-of-delivery.hbs +49 -0
  49. package/addon/components/widget/proof-of-delivery.js +62 -0
  50. package/addon/components/work-order/form.hbs +3 -3
  51. package/addon/controllers/analytics/reports/index.js +10 -1
  52. package/addon/controllers/connectivity/devices/index/details.js +1 -0
  53. package/addon/controllers/connectivity/devices/index.js +8 -2
  54. package/addon/controllers/connectivity/events/index.js +31 -1
  55. package/addon/controllers/connectivity/sensors/index/details.js +1 -1
  56. package/addon/controllers/connectivity/sensors/index.js +37 -1
  57. package/addon/controllers/maintenance/equipment/index/details.js +2 -2
  58. package/addon/controllers/maintenance/equipment/index.js +28 -9
  59. package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
  60. package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
  61. package/addon/controllers/maintenance/inspection-submissions/index.js +49 -9
  62. package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
  63. package/addon/controllers/maintenance/maintenances/index.js +20 -1
  64. package/addon/controllers/maintenance/parts/index/details.js +2 -2
  65. package/addon/controllers/maintenance/parts/index.js +28 -8
  66. package/addon/controllers/maintenance/schedules/index.js +33 -4
  67. package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
  68. package/addon/controllers/maintenance/work-orders/index.js +39 -8
  69. package/addon/controllers/management/contacts/customers.js +13 -0
  70. package/addon/controllers/management/contacts/index.js +43 -1
  71. package/addon/controllers/management/drivers/index/details.js +1 -0
  72. package/addon/controllers/management/drivers/index.js +32 -2
  73. package/addon/controllers/management/fleets/index/details.js +1 -0
  74. package/addon/controllers/management/fleets/index.js +39 -1
  75. package/addon/controllers/management/fuel-reports/index.js +22 -2
  76. package/addon/controllers/management/fuel-transactions/index.js +43 -7
  77. package/addon/controllers/management/issues/index/details.js +1 -0
  78. package/addon/controllers/management/issues/index.js +22 -2
  79. package/addon/controllers/management/places/index.js +36 -1
  80. package/addon/controllers/management/trailers/index/details.js +1 -0
  81. package/addon/controllers/management/trailers/index.js +50 -2
  82. package/addon/controllers/management/vehicles/index/details.js +1 -0
  83. package/addon/controllers/management/vehicles/index.js +33 -2
  84. package/addon/controllers/management/vendors/index.js +26 -2
  85. package/addon/controllers/operations/orders/index/details.js +50 -7
  86. package/addon/controllers/operations/orders/index.js +59 -4
  87. package/addon/controllers/operations/scheduler/index.js +45 -14
  88. package/addon/controllers/operations/service-rates/index.js +15 -1
  89. package/addon/controllers/settings/index.js +6 -0
  90. package/addon/controllers/settings/map.js +75 -0
  91. package/addon/controllers/settings/orchestrator.js +8 -6
  92. package/addon/controllers/settings/tracking-page.js +212 -0
  93. package/addon/extension.js +97 -1
  94. package/addon/helpers/parcel-box-image.js +16 -0
  95. package/addon/routes/operations/orders/index/details.js +1 -0
  96. package/addon/routes/operations/orders/index/new.js +5 -4
  97. package/addon/routes/operations/scheduler/index.js +1 -1
  98. package/addon/routes/settings/tracking-page.js +31 -0
  99. package/addon/routes.js +1 -0
  100. package/addon/services/contact-actions.js +1 -0
  101. package/addon/services/customer-actions.js +2 -1
  102. package/addon/services/device-actions.js +1 -0
  103. package/addon/services/device-event-actions.js +1 -0
  104. package/addon/services/driver-actions.js +4 -13
  105. package/addon/services/equipment-actions.js +1 -0
  106. package/addon/services/fleet-actions.js +1 -0
  107. package/addon/services/fuel-report-actions.js +1 -0
  108. package/addon/services/inspection-form-actions.js +1 -0
  109. package/addon/services/inspection-submission-actions.js +1 -0
  110. package/addon/services/integrated-vendor-actions.js +1 -0
  111. package/addon/services/issue-actions.js +1 -0
  112. package/addon/services/location.js +69 -21
  113. package/addon/services/maintenance-actions.js +1 -0
  114. package/addon/services/maintenance-schedule-actions.js +2 -1
  115. package/addon/services/map-settings.js +28 -0
  116. package/addon/services/order-actions.js +7 -0
  117. package/addon/services/order-allocation.js +1 -1
  118. package/addon/services/order-presentation.js +129 -0
  119. package/addon/services/part-actions.js +1 -0
  120. package/addon/services/place-actions.js +1 -0
  121. package/addon/services/sensor-actions.js +1 -0
  122. package/addon/services/service-area-actions.js +1 -0
  123. package/addon/services/service-rate-actions.js +1 -0
  124. package/addon/services/telematic-actions.js +1 -0
  125. package/addon/services/trailer-actions.js +4 -14
  126. package/addon/services/vehicle-actions.js +4 -13
  127. package/addon/services/vendor-actions.js +1 -0
  128. package/addon/services/work-order-actions.js +1 -0
  129. package/addon/services/zone-actions.js +1 -0
  130. package/addon/styles/fleetops-engine.css +1391 -0
  131. package/addon/templates/analytics/reports/index/details.hbs +1 -0
  132. package/addon/templates/analytics/reports/index.hbs +1 -0
  133. package/addon/templates/connectivity/devices/index/details.hbs +1 -0
  134. package/addon/templates/connectivity/devices/index.hbs +1 -0
  135. package/addon/templates/connectivity/events/index.hbs +1 -0
  136. package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
  137. package/addon/templates/connectivity/sensors/index.hbs +1 -0
  138. package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
  139. package/addon/templates/maintenance/equipment/index.hbs +2 -0
  140. package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
  141. package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
  142. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
  143. package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
  144. package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
  145. package/addon/templates/maintenance/maintenances/index.hbs +1 -0
  146. package/addon/templates/maintenance/parts/index/details.hbs +1 -0
  147. package/addon/templates/maintenance/parts/index.hbs +2 -0
  148. package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
  149. package/addon/templates/maintenance/schedules/index.hbs +2 -0
  150. package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
  151. package/addon/templates/maintenance/work-orders/index.hbs +1 -0
  152. package/addon/templates/management/contacts/customers/details.hbs +1 -0
  153. package/addon/templates/management/contacts/customers.hbs +1 -0
  154. package/addon/templates/management/contacts/index/details.hbs +1 -0
  155. package/addon/templates/management/contacts/index.hbs +1 -0
  156. package/addon/templates/management/contacts.hbs +1 -0
  157. package/addon/templates/management/drivers/index/details.hbs +1 -0
  158. package/addon/templates/management/drivers/index.hbs +2 -0
  159. package/addon/templates/management/fleets/index/details.hbs +1 -0
  160. package/addon/templates/management/fleets/index.hbs +1 -0
  161. package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
  162. package/addon/templates/management/fuel-reports/index.hbs +1 -0
  163. package/addon/templates/management/fuel-transactions/index.hbs +1 -0
  164. package/addon/templates/management/issues/index/details.hbs +1 -0
  165. package/addon/templates/management/issues/index.hbs +1 -0
  166. package/addon/templates/management/places/index/details.hbs +1 -0
  167. package/addon/templates/management/places/index.hbs +1 -0
  168. package/addon/templates/management/trailers/index/details.hbs +1 -0
  169. package/addon/templates/management/trailers/index.hbs +2 -0
  170. package/addon/templates/management/vehicles/index/details.hbs +1 -0
  171. package/addon/templates/management/vehicles/index.hbs +2 -0
  172. package/addon/templates/management/vendors/index/details.hbs +1 -0
  173. package/addon/templates/management/vendors/index.hbs +2 -0
  174. package/addon/templates/management/vendors/integrated.hbs +1 -0
  175. package/addon/templates/operations/orders/index/details.hbs +1 -0
  176. package/addon/templates/operations/orders/index.hbs +1 -0
  177. package/addon/templates/operations/service-rates/index/details.hbs +1 -0
  178. package/addon/templates/operations/service-rates/index.hbs +1 -0
  179. package/addon/templates/settings/map.hbs +46 -0
  180. package/addon/templates/settings/tracking-page.hbs +318 -0
  181. package/addon/utils/identity-cell-resource.js +2 -56
  182. package/addon/utils/map-default-view.js +132 -0
  183. package/addon/utils/normalize-order-config-flow.js +6 -1
  184. package/addon/utils/placeholder-images.js +4 -69
  185. package/addon/utils/resource-descriptors/assets.js +2 -57
  186. package/addon/utils/resource-descriptors/helpers.js +23 -122
  187. package/addon/utils/resource-descriptors/operations.js +2 -61
  188. package/addon/utils/resource-descriptors/people.js +3 -142
  189. package/addon/utils/resource-descriptors/shared.js +18 -0
  190. package/addon/utils/tracking-live-source.js +123 -0
  191. package/addon/utils/tracking-page-theme.js +123 -0
  192. package/app/components/admin/tracking-page-settings.js +1 -0
  193. package/app/components/tracking-page/full-view.js +1 -0
  194. package/app/components/tracking-page/locale-selector.js +1 -0
  195. package/app/components/tracking-page/map.js +1 -0
  196. package/app/components/tracking-page/order-list.js +1 -0
  197. package/app/components/tracking-page/public-view.js +1 -0
  198. package/app/components/tracking-page/settings-preview.js +1 -0
  199. package/app/components/tracking-page/verify-panel.js +1 -0
  200. package/app/components/tracking-page.js +1 -0
  201. package/app/components/widget/proof-of-delivery.js +1 -0
  202. package/app/controllers/settings/tracking-page.js +1 -0
  203. package/app/helpers/parcel-box-image.js +1 -0
  204. package/app/routes/settings/tracking-page.js +1 -0
  205. package/app/services/order-presentation.js +1 -0
  206. package/app/templates/settings/tracking-page.js +1 -0
  207. package/app/utils/map-default-view.js +1 -0
  208. package/app/utils/tracking-live-source.js +1 -0
  209. package/app/utils/tracking-page-theme.js +1 -0
  210. package/composer.json +2 -2
  211. package/extension.json +1 -1
  212. package/package.json +4 -4
  213. package/scripts/pest-bootstrap.php +4 -0
  214. package/server/config/fleetops.php +4 -0
  215. package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
  216. package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
  217. package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
  218. package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
  219. package/server/resources/views/labels/default.php +2 -2
  220. package/server/resources/views/labels/entity-label.php +2 -2
  221. package/server/resources/views/labels/waypoint-label.php +2 -2
  222. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  223. package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
  224. package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
  225. package/server/src/Events/OrderChanged.php +257 -0
  226. package/server/src/Events/OrderDriverUnassigned.php +68 -0
  227. package/server/src/Exceptions/OrderEditLockedException.php +32 -0
  228. package/server/src/Expansions/CompanyExpansion.php +3 -2
  229. package/server/src/Expansions/UserExpansion.php +12 -10
  230. package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
  231. package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
  232. package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
  233. package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
  234. package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
  235. package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
  236. package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
  237. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
  238. package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
  239. package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
  240. package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
  241. package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
  242. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
  243. package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
  244. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
  245. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
  246. package/server/src/Http/Controllers/Internal/v1/OrderController.php +118 -28
  247. package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
  248. package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
  249. package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
  250. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
  251. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
  252. package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
  253. package/server/src/Http/Filter/ContactFilter.php +1 -1
  254. package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
  255. package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
  256. package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
  257. package/server/src/Http/Filter/OrderFilter.php +39 -4
  258. package/server/src/Http/Filter/ProofFilter.php +18 -0
  259. package/server/src/Http/Filter/RouteFilter.php +18 -0
  260. package/server/src/Http/Filter/TelematicFilter.php +23 -0
  261. package/server/src/Http/Filter/WarrantyFilter.php +23 -0
  262. package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
  263. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
  264. package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
  265. package/server/src/Http/Resources/v1/Index/Order.php +11 -2
  266. package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
  267. package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
  268. package/server/src/Http/Resources/v1/Zone.php +31 -23
  269. package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
  270. package/server/src/Jobs/ProcessAllocationJob.php +16 -6
  271. package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
  272. package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
  273. package/server/src/Listeners/HandleOrderCanceled.php +9 -1
  274. package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
  275. package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
  276. package/server/src/Mail/CustomerCredentialsMail.php +4 -2
  277. package/server/src/Models/AssetConnection.php +3 -0
  278. package/server/src/Models/Contact.php +3 -0
  279. package/server/src/Models/Device.php +28 -0
  280. package/server/src/Models/DeviceEvent.php +7 -0
  281. package/server/src/Models/Driver.php +20 -7
  282. package/server/src/Models/Entity.php +12 -4
  283. package/server/src/Models/Fleet.php +11 -32
  284. package/server/src/Models/FleetDriver.php +3 -0
  285. package/server/src/Models/FleetVehicle.php +3 -0
  286. package/server/src/Models/FuelReport.php +5 -13
  287. package/server/src/Models/Issue.php +4 -16
  288. package/server/src/Models/Order.php +234 -11
  289. package/server/src/Models/OrderConfig.php +129 -0
  290. package/server/src/Models/Payload.php +227 -35
  291. package/server/src/Models/Place.php +7 -1
  292. package/server/src/Models/ServiceArea.php +7 -0
  293. package/server/src/Models/ServiceRate.php +7 -17
  294. package/server/src/Models/TrackingNumber.php +109 -6
  295. package/server/src/Models/TrackingSession.php +33 -0
  296. package/server/src/Models/TrackingSessionGrant.php +22 -0
  297. package/server/src/Models/TrackingStatus.php +6 -0
  298. package/server/src/Models/Vehicle.php +21 -2
  299. package/server/src/Models/Vendor.php +7 -0
  300. package/server/src/Models/Waypoint.php +13 -4
  301. package/server/src/Notifications/OrderAssigned.php +2 -1
  302. package/server/src/Notifications/OrderCanceled.php +7 -1
  303. package/server/src/Notifications/OrderChanged.php +87 -0
  304. package/server/src/Notifications/OrderCompleted.php +2 -1
  305. package/server/src/Notifications/OrderDispatchFailed.php +2 -1
  306. package/server/src/Notifications/OrderDispatched.php +2 -1
  307. package/server/src/Notifications/OrderFailed.php +2 -1
  308. package/server/src/Notifications/OrderUnassigned.php +66 -0
  309. package/server/src/Notifications/WaypointCompleted.php +2 -1
  310. package/server/src/Observers/CompanyUserObserver.php +8 -7
  311. package/server/src/Observers/DriverObserver.php +6 -1
  312. package/server/src/Observers/OrderObserver.php +141 -1
  313. package/server/src/Observers/TrackingNumberObserver.php +18 -6
  314. package/server/src/Observers/UserObserver.php +2 -1
  315. package/server/src/Observers/VehicleObserver.php +8 -3
  316. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
  317. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
  318. package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
  319. package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
  320. package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
  321. package/server/src/Providers/EventServiceProvider.php +11 -0
  322. package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
  323. package/server/src/Support/ApiCacheThrottle.php +76 -0
  324. package/server/src/Support/LiveOrderQuery.php +3 -1
  325. package/server/src/Support/OrderEdit.php +365 -0
  326. package/server/src/Support/OrderEditPolicy.php +292 -0
  327. package/server/src/Support/ProfileAccountManager.php +54 -1
  328. package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
  329. package/server/src/Support/SocketChannels.php +346 -0
  330. package/server/src/Support/Telematics/TelematicService.php +7 -2
  331. package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
  332. package/server/src/Support/TrackingChannel.php +55 -0
  333. package/server/src/Support/TrackingCode.php +242 -0
  334. package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
  335. package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
  336. package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
  337. package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
  338. package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
  339. package/server/src/Support/TrackingPage/TrackingView.php +543 -0
  340. package/server/src/Support/TrackingPublisher.php +261 -0
  341. package/server/src/Support/TrackingScope.php +182 -0
  342. package/server/src/Support/TrackingUpdate.php +402 -0
  343. package/server/src/Support/Utils.php +15 -3
  344. package/server/src/Traits/HasTrackingNumber.php +2 -0
  345. package/server/src/routes.php +39 -7
  346. package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
  347. package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
  348. package/server/tests/ApiEntityControllerContractsTest.php +84 -0
  349. package/server/tests/ApiOrderControllerContractsTest.php +245 -0
  350. package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
  351. package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
  352. package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
  353. package/server/tests/CompactResourceSerializationTest.php +23 -59
  354. package/server/tests/ControllerFilterContractsTest.php +1 -1
  355. package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
  356. package/server/tests/EventContractsTest.php +26 -60
  357. package/server/tests/ExpansionContractsTest.php +15 -2
  358. package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
  359. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
  360. package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
  361. package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
  362. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
  363. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
  364. package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
  365. package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
  366. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
  367. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
  368. package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
  369. package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
  370. package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
  371. package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
  372. package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
  373. package/server/tests/IndexCustomerPhotoTest.php +151 -0
  374. package/server/tests/ModelAccessorContractsTest.php +4 -3
  375. package/server/tests/NotificationAndMailContractsTest.php +5 -8
  376. package/server/tests/ObserverContractsTest.php +72 -10
  377. package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
  378. package/server/tests/OrderConfigLifecycleTest.php +120 -0
  379. package/server/tests/OrderFilterExecutionTest.php +167 -2
  380. package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
  381. package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
  382. package/server/tests/QueueJobContractsTest.php +2 -1
  383. package/server/tests/RequestContractsTest.php +1 -1
  384. package/server/tests/RouteRegistrationExecutionTest.php +26 -0
  385. package/server/tests/RouteSequencingEngineTest.php +17 -0
  386. package/server/tests/SettingControllerContractsTest.php +110 -0
  387. package/server/tests/Support/TrackingPageDatabase.php +200 -0
  388. package/server/tests/TenantScopingTest.php +206 -0
  389. package/server/tests/TrackingPageSettingsTest.php +343 -0
  390. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
  391. package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
  392. package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
  393. package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
  394. package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
  395. package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
  396. package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
  397. package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
  398. package/server/tests/Unit/Models/FleetTest.php +4 -3
  399. package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
  400. package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
  401. package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
  402. package/server/tests/Unit/Models/PayloadTest.php +18 -4
  403. package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
  404. package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
  405. package/server/tests/Unit/Models/VehicleTest.php +16 -2
  406. package/server/tests/Unit/Models/WaypointTest.php +4 -3
  407. package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
  408. package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
  409. package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
  410. package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
  411. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
  412. package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
  413. package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
  414. package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
  415. package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
  416. package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
  417. package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
  418. package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
  419. package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
  420. package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
  421. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
  422. package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
  423. package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
  424. package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
  425. package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
  426. package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
  427. package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
  428. package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
  429. package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
  430. package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
  431. package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
  432. package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
  433. package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
  434. package/server/tests/VehicleResourceContractsTest.php +46 -0
  435. package/server/tests/VroomOrchestrationEngineTest.php +34 -1
  436. package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
  437. package/translations/ar-ae.yml +274 -0
  438. package/translations/bg-bg.yaml +274 -0
  439. package/translations/en-us.yaml +347 -0
  440. package/translations/es-pa.yaml +274 -0
  441. package/translations/fr-fr.yaml +274 -0
  442. package/translations/mn-mn.yaml +274 -0
  443. package/translations/pt-br.yaml +274 -0
  444. package/translations/ru-ru.yaml +276 -2
  445. package/translations/uk-ua.yaml +275 -1
  446. package/translations/vi-vn.yaml +274 -0
  447. package/addon/components/cell/contact-identity.hbs +0 -1
  448. package/addon/components/cell/customer-identity.hbs +0 -1
  449. package/addon/components/cell/driver-identity.hbs +0 -1
  450. package/addon/components/cell/fleet-identity.hbs +0 -1
  451. package/addon/components/cell/order-identity.hbs +0 -1
  452. package/addon/components/cell/place-identity.hbs +0 -1
  453. package/addon/components/cell/vehicle-identity.hbs +0 -1
  454. package/addon/components/cell/vendor-identity.hbs +0 -1
  455. package/addon/components/contact/pill.hbs +0 -24
  456. package/addon/components/contact/summary.hbs +0 -1
  457. package/addon/components/customer/pill.hbs +0 -24
  458. package/addon/components/customer/summary.hbs +0 -1
  459. package/addon/components/driver/pill.hbs +0 -24
  460. package/addon/components/driver/summary.hbs +0 -1
  461. package/addon/components/fleet/pill.hbs +0 -24
  462. package/addon/components/fleet/summary.hbs +0 -1
  463. package/addon/components/order/pill.hbs +0 -24
  464. package/addon/components/order/summary.hbs +0 -1
  465. package/addon/components/place/pill.hbs +0 -24
  466. package/addon/components/place/summary.hbs +0 -1
  467. package/addon/components/select-option/contact.hbs +0 -12
  468. package/addon/components/select-option/customer.hbs +0 -12
  469. package/addon/components/select-option/driver.hbs +0 -12
  470. package/addon/components/select-option/fleet.hbs +0 -12
  471. package/addon/components/select-option/order.hbs +0 -12
  472. package/addon/components/select-option/place.hbs +0 -12
  473. package/addon/components/select-option/vehicle.hbs +0 -12
  474. package/addon/components/select-option/vendor.hbs +0 -12
  475. package/addon/components/vehicle/pill.hbs +0 -24
  476. package/addon/components/vehicle/summary.hbs +0 -1
  477. package/addon/components/vendor/pill.hbs +0 -24
  478. package/addon/components/vendor/summary.hbs +0 -1
  479. package/app/components/cell/contact-identity.js +0 -1
  480. package/app/components/cell/customer-identity.js +0 -1
  481. package/app/components/cell/driver-identity.js +0 -1
  482. package/app/components/cell/fleet-identity.js +0 -1
  483. package/app/components/cell/order-identity.js +0 -1
  484. package/app/components/cell/place-identity.js +0 -1
  485. package/app/components/cell/vehicle-identity.js +0 -1
  486. package/app/components/cell/vendor-identity.js +0 -1
  487. package/app/components/contact/pill.js +0 -1
  488. package/app/components/contact/summary.js +0 -1
  489. package/app/components/customer/pill.js +0 -1
  490. package/app/components/customer/summary.js +0 -1
  491. package/app/components/driver/pill.js +0 -1
  492. package/app/components/driver/summary.js +0 -1
  493. package/app/components/fleet/pill.js +0 -1
  494. package/app/components/fleet/summary.js +0 -1
  495. package/app/components/order/pill.js +0 -1
  496. package/app/components/order/summary.js +0 -1
  497. package/app/components/place/pill.js +0 -1
  498. package/app/components/place/summary.js +0 -1
  499. package/app/components/select-option/contact.js +0 -1
  500. package/app/components/select-option/customer.js +0 -1
  501. package/app/components/select-option/driver.js +0 -1
  502. package/app/components/select-option/fleet.js +0 -1
  503. package/app/components/select-option/order.js +0 -1
  504. package/app/components/select-option/place.js +0 -1
  505. package/app/components/select-option/vehicle.js +0 -1
  506. package/app/components/select-option/vendor.js +0 -1
  507. package/app/components/vehicle/pill.js +0 -1
  508. package/app/components/vehicle/summary.js +0 -1
  509. package/app/components/vendor/pill.js +0 -1
  510. package/app/components/vendor/summary.js +0 -1
@@ -11,7 +11,9 @@ use Fleetbase\Models\Company;
11
11
  use Fleetbase\Models\CompanyUser;
12
12
  use Fleetbase\Models\User;
13
13
  use Fleetbase\Services\SmsService;
14
+ use Illuminate\Support\Facades\DB;
14
15
  use Illuminate\Support\Facades\Mail;
16
+ use Illuminate\Support\Facades\Schema;
15
17
  use Illuminate\Support\Str;
16
18
 
17
19
  /**
@@ -174,7 +176,15 @@ class ProfileAccountManager
174
176
  $user->save();
175
177
  $user->setType($type);
176
178
 
177
- static::attachToCompany($user, $companyUuid, $type);
179
+ try {
180
+ static::attachToCompany($user, $companyUuid, $type);
181
+ } catch (\Throwable $e) {
182
+ // Leave no account behind for a profile that was never created
183
+ static::removeSandboxCopy($user);
184
+ $user->forceDelete();
185
+
186
+ throw $e;
187
+ }
178
188
 
179
189
  return $user;
180
190
  }
@@ -355,6 +365,8 @@ class ProfileAccountManager
355
365
  return null;
356
366
  }
357
367
 
368
+ static::copyAccountToSandbox($user, $companyUuid);
369
+
358
370
  $role = static::ROLES[$type] ?? static::ROLES['contact'];
359
371
  $companyUser = CompanyUser::where(['company_uuid' => $companyUuid, 'user_uuid' => $user->uuid])->first();
360
372
  if (!$companyUser) {
@@ -372,6 +384,47 @@ class ProfileAccountManager
372
384
  return $companyUser;
373
385
  }
374
386
 
387
+ /**
388
+ * Copy the account and its organization into the sandbox database for a
389
+ * test-key request. Users and companies always live in the live database,
390
+ * but memberships follow the request into the sandbox, whose foreign keys
391
+ * need both rows there too.
392
+ */
393
+ public static function copyAccountToSandbox(User $user, ?string $companyUuid = null): void
394
+ {
395
+ if (!session('is_sandbox')) {
396
+ return;
397
+ }
398
+
399
+ $live = DB::connection($user->getConnectionName());
400
+ $sandbox = DB::connection('sandbox');
401
+ $account = (array) $live->table($user->getTable())->where('uuid', $user->uuid)->first();
402
+ $company = $companyUuid ? (array) $live->table('companies')->where('uuid', $companyUuid)->first() : [];
403
+
404
+ Schema::connection('sandbox')->disableForeignKeyConstraints();
405
+
406
+ try {
407
+ if ($company) {
408
+ $sandbox->table('companies')->updateOrInsert(['uuid' => $company['uuid']], array_diff_key($company, ['id' => true]));
409
+ }
410
+
411
+ $account['company_uuid'] = ($account['company_uuid'] ?? null) ?: $companyUuid;
412
+ $sandbox->table($user->getTable())->updateOrInsert(['uuid' => $user->uuid], array_diff_key($account, ['id' => true]));
413
+ } finally {
414
+ Schema::connection('sandbox')->enableForeignKeyConstraints();
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Remove the sandbox copy of an account that is being discarded.
420
+ */
421
+ public static function removeSandboxCopy(User $user): void
422
+ {
423
+ if (session('is_sandbox')) {
424
+ DB::connection('sandbox')->table($user->getTable())->where('uuid', $user->uuid)->delete();
425
+ }
426
+ }
427
+
375
428
  public static function normalizeEmail(?string $email): ?string
376
429
  {
377
430
  $email = is_string($email) ? strtolower(trim($email)) : null;
@@ -15,6 +15,7 @@ use Fleetbase\FleetOps\Models\TrackingNumber;
15
15
  use Fleetbase\FleetOps\Models\TrackingStatus;
16
16
  use Fleetbase\FleetOps\Models\Waypoint;
17
17
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
18
+ use Fleetbase\Support\ApiModelCache;
18
19
  use Fleetbase\Support\TemplateString;
19
20
  use Illuminate\Support\Collection;
20
21
  use Illuminate\Support\Facades\DB;
@@ -185,6 +186,7 @@ trait ResolvesOrderServiceStops
185
186
 
186
187
  $payload->current_waypoint_uuid = $place->uuid;
187
188
  $payload->saveQuietly();
189
+ ApiModelCache::invalidateModelCache($payload, $payload->company_uuid);
188
190
  $payload->setRelation('currentWaypoint', $place);
189
191
 
190
192
  if (($stop['waypoint'] ?? null) instanceof Waypoint) {
@@ -254,6 +256,7 @@ trait ResolvesOrderServiceStops
254
256
  $activityId = $waypoint->insertActivity($activity, $location, $proof);
255
257
  if ($activityId && $waypoint->tracking_number_uuid) {
256
258
  DB::table((new TrackingNumber())->getTable())->where('uuid', $waypoint->tracking_number_uuid)->update(['status_uuid' => $activityId]);
259
+ ApiModelCache::invalidateModelCache(new TrackingNumber(), $payload->company_uuid ?? $order->company_uuid);
257
260
  if ($waypoint->relationLoaded('trackingNumber')) {
258
261
  $waypoint->trackingNumber->status_uuid = $activityId;
259
262
  $waypoint->trackingNumber->unsetRelation('status');
@@ -391,6 +394,7 @@ trait ResolvesOrderServiceStops
391
394
  // @codeCoverageIgnoreEnd
392
395
 
393
396
  DB::table($payload->getTable())->where('uuid', $payload->uuid)->update([$column => $trackingNumberUuid]);
397
+ ApiModelCache::invalidateModelCache($payload, $payload->company_uuid ?? $order->company_uuid);
394
398
  $payload->{$column} = $trackingNumberUuid;
395
399
 
396
400
  return TrackingNumber::where('uuid', $trackingNumberUuid)->first();
@@ -417,6 +421,7 @@ trait ResolvesOrderServiceStops
417
421
 
418
422
  if ($activityId) {
419
423
  DB::table($trackingNumber->getTable())->where('uuid', $trackingNumber->uuid)->update(['status_uuid' => $activityId]);
424
+ ApiModelCache::invalidateModelCache($trackingNumber, $payload->company_uuid ?? $order->company_uuid);
420
425
  $trackingNumber->status_uuid = $activityId;
421
426
  }
422
427
 
@@ -0,0 +1,346 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Models\Contact;
6
+ use Fleetbase\FleetOps\Models\Device;
7
+ use Fleetbase\FleetOps\Models\Driver;
8
+ use Fleetbase\FleetOps\Models\Entity;
9
+ use Fleetbase\FleetOps\Models\Fleet;
10
+ use Fleetbase\FleetOps\Models\IntegratedVendor;
11
+ use Fleetbase\FleetOps\Models\Order;
12
+ use Fleetbase\FleetOps\Models\Payload;
13
+ use Fleetbase\FleetOps\Models\Place;
14
+ use Fleetbase\FleetOps\Models\Position;
15
+ use Fleetbase\FleetOps\Models\PurchaseRate;
16
+ use Fleetbase\FleetOps\Models\ServiceArea;
17
+ use Fleetbase\FleetOps\Models\ServiceQuote;
18
+ use Fleetbase\FleetOps\Models\ServiceRate;
19
+ use Fleetbase\FleetOps\Models\TrackingNumber;
20
+ use Fleetbase\FleetOps\Models\TrackingStatus;
21
+ use Fleetbase\FleetOps\Models\Trailer;
22
+ use Fleetbase\FleetOps\Models\Vehicle;
23
+ use Fleetbase\FleetOps\Models\Vendor;
24
+ use Fleetbase\FleetOps\Models\Waypoint;
25
+ use Fleetbase\FleetOps\Models\Zone;
26
+ use Fleetbase\Models\Company;
27
+ use Fleetbase\Models\User;
28
+ use Fleetbase\Support\SocketCluster\SocketChannelRegistry;
29
+ use Fleetbase\Support\SocketCluster\SocketPrincipal;
30
+ use Illuminate\Database\Eloquent\Builder;
31
+ use Illuminate\Database\Eloquent\Model;
32
+ use Illuminate\Http\Request;
33
+
34
+ /**
35
+ * Who may subscribe to FleetOps realtime channels.
36
+ *
37
+ * Channel names are "{prefix}.{id}" where the prefix is the snake_case model name the lifecycle
38
+ * events broadcast on (plus `driverAssigned`, which the order's assigned-driver channel uses) and
39
+ * the id is the model's uuid or public_id.
40
+ *
41
+ * Console users and API credentials may subscribe to any of their own company's records. Drivers
42
+ * and customers get narrow access only:
43
+ *
44
+ * - a driver: their own driver record, their current vehicle and its devices and positions, and
45
+ * orders assigned to them (directly, or through one of the order's payload entities);
46
+ * - a customer: orders they are the customer of, and the driver and vehicle of those orders while
47
+ * they are active.
48
+ *
49
+ * Public tracking channels (`tracking.{opaque}`) are denied here to everyone: only a `tracking`
50
+ * token, whose scope names the channel exactly, can subscribe to one.
51
+ */
52
+ class SocketChannels
53
+ {
54
+ /**
55
+ * Order statuses after which a customer no longer sees the order's driver or vehicle.
56
+ */
57
+ public const TERMINAL_ORDER_STATUSES = ['completed', 'canceled'];
58
+
59
+ /**
60
+ * Prefixes resolved by the generic model resolver, with their model.
61
+ */
62
+ public const MODEL_PREFIXES = [
63
+ 'order' => Order::class,
64
+ 'driver' => Driver::class,
65
+ 'driverAssigned' => Driver::class,
66
+ 'vehicle' => Vehicle::class,
67
+ 'trailer' => Trailer::class,
68
+ 'device' => Device::class,
69
+ 'waypoint' => Waypoint::class,
70
+ 'entity' => Entity::class,
71
+ 'place' => Place::class,
72
+ 'contact' => Contact::class,
73
+ 'fleet' => Fleet::class,
74
+ 'zone' => Zone::class,
75
+ 'service_area' => ServiceArea::class,
76
+ 'service_rate' => ServiceRate::class,
77
+ 'service_quote' => ServiceQuote::class,
78
+ 'purchase_rate' => PurchaseRate::class,
79
+ 'tracking_status' => TrackingStatus::class,
80
+ 'tracking_number' => TrackingNumber::class,
81
+ 'payload' => Payload::class,
82
+ ];
83
+
84
+ /**
85
+ * Prefixes whose id may name more than one kind of record, with the models tried in order.
86
+ */
87
+ public const MORPH_PREFIXES = [
88
+ 'customer' => [Contact::class, Vendor::class],
89
+ 'vendor' => [Vendor::class, IntegratedVendor::class],
90
+ 'facilitator' => [Vendor::class, IntegratedVendor::class, Contact::class],
91
+ ];
92
+
93
+ /**
94
+ * Register every FleetOps prefix and the driver principal resolver.
95
+ */
96
+ public static function register(SocketChannelRegistry $registry): void
97
+ {
98
+ foreach (self::MODEL_PREFIXES as $prefix => $modelClass) {
99
+ $registry->registerModel($prefix, $modelClass, static::narrowFor($prefix));
100
+ }
101
+
102
+ foreach (self::MORPH_PREFIXES as $prefix => $modelClasses) {
103
+ $registry->register($prefix, static::resolver($modelClasses, [static::class, 'narrowSelf']));
104
+ }
105
+
106
+ // Positions have no public_id, so they are looked up by uuid only.
107
+ $registry->register('position', static::resolver([Position::class], [static::class, 'narrowPosition'], false));
108
+
109
+ // tracking.{opaque} is reachable only through a tracking token's exact scope.
110
+ $registry->register(TrackingChannel::PREFIX, static fn (): bool => false);
111
+
112
+ $registry->registerPrincipalResolver([static::class, 'driverPrincipal']);
113
+ }
114
+
115
+ /**
116
+ * The narrow rule used for driver and customer principals on a generic model prefix.
117
+ */
118
+ public static function narrowFor(string $prefix): ?callable
119
+ {
120
+ return match ($prefix) {
121
+ 'order' => [static::class, 'narrowOrder'],
122
+ 'driver', 'driverAssigned' => [static::class, 'narrowDriver'],
123
+ 'vehicle' => [static::class, 'narrowVehicle'],
124
+ 'device' => [static::class, 'narrowDevice'],
125
+ 'contact' => [static::class, 'narrowSelf'],
126
+ default => null,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * A resolver over one or more models: company match for user and API principals, the narrow
132
+ * rule for driver and customer principals, deny for everyone else.
133
+ *
134
+ * @param array<int, class-string<Model>> $modelClasses
135
+ */
136
+ public static function resolver(array $modelClasses, ?callable $narrow = null, bool $byPublicId = true): \Closure
137
+ {
138
+ return static function (SocketPrincipal $principal, string $id, string $channel = '') use ($modelClasses, $narrow, $byPublicId): bool {
139
+ $narrowed = $principal->kind === 'driver' || $principal->kind === 'customer';
140
+ if ((!$principal->isCompanyScoped() && !$narrowed) || $id === '') {
141
+ return false;
142
+ }
143
+
144
+ $model = static::find($modelClasses, $principal, $id, $byPublicId);
145
+ if ($model === null) {
146
+ return false;
147
+ }
148
+
149
+ if ($principal->isCompanyScoped()) {
150
+ return $principal->cid !== null && $model->company_uuid === $principal->cid;
151
+ }
152
+
153
+ return $narrow !== null && (bool) $narrow($principal, $model);
154
+ };
155
+ }
156
+
157
+ /**
158
+ * The first of the models whose uuid (or public_id) is the id, in the principal's environment.
159
+ *
160
+ * @param array<int, class-string<Model>> $modelClasses
161
+ */
162
+ public static function find(array $modelClasses, SocketPrincipal $principal, string $id, bool $byPublicId = true): ?Model
163
+ {
164
+ foreach ($modelClasses as $modelClass) {
165
+ $model = $modelClass::on(static::connection($principal))
166
+ ->where(function ($query) use ($id, $byPublicId) {
167
+ $query->where('uuid', $id);
168
+ if ($byPublicId) {
169
+ $query->orWhere('public_id', $id);
170
+ }
171
+ })
172
+ ->first();
173
+
174
+ if ($model) {
175
+ return $model;
176
+ }
177
+ }
178
+
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * The database connection for the principal's environment; null is the default one.
184
+ */
185
+ public static function connection(SocketPrincipal $principal): ?string
186
+ {
187
+ return $principal->env === 'test' ? 'sandbox' : null;
188
+ }
189
+
190
+ /**
191
+ * Orders: a driver's when assigned to them (directly or on one of its payload entities), a customer's own.
192
+ */
193
+ public static function narrowOrder(SocketPrincipal $principal, Model $order): bool
194
+ {
195
+ if ($principal->kind === 'customer') {
196
+ return static::isSelf($principal, $order->customer_uuid);
197
+ }
198
+
199
+ if ($principal->kind !== 'driver') {
200
+ return false;
201
+ }
202
+
203
+ if (static::isSelf($principal, $order->driver_assigned_uuid)) {
204
+ return true;
205
+ }
206
+
207
+ return !empty($order->payload_uuid)
208
+ && Entity::on($order->getConnectionName())
209
+ ->where('payload_uuid', $order->payload_uuid)
210
+ ->where('driver_assigned_uuid', $principal->sub)
211
+ ->exists();
212
+ }
213
+
214
+ /**
215
+ * Drivers: a driver only themselves; a customer the driver of one of their active orders.
216
+ */
217
+ public static function narrowDriver(SocketPrincipal $principal, Model $driver): bool
218
+ {
219
+ if ($principal->kind === 'driver') {
220
+ return static::isSelf($principal, $driver->uuid);
221
+ }
222
+
223
+ return $principal->kind === 'customer'
224
+ && static::activeCustomerOrders($principal, $driver->getConnectionName())
225
+ ->where('driver_assigned_uuid', $driver->uuid)
226
+ ->exists();
227
+ }
228
+
229
+ /**
230
+ * Vehicles: a driver their current vehicle; a customer the vehicle of one of their active orders,
231
+ * whether assigned to the order or driven by the order's driver.
232
+ */
233
+ public static function narrowVehicle(SocketPrincipal $principal, Model $vehicle): bool
234
+ {
235
+ if ($principal->kind === 'driver') {
236
+ return static::driverVehicleUuid($principal, $vehicle->getConnectionName()) === $vehicle->uuid;
237
+ }
238
+
239
+ if ($principal->kind !== 'customer') {
240
+ return false;
241
+ }
242
+
243
+ $connection = $vehicle->getConnectionName();
244
+ $drivers = Driver::on($connection)->where('vehicle_uuid', $vehicle->uuid)->select('uuid')->toBase();
245
+
246
+ return static::activeCustomerOrders($principal, $connection)
247
+ ->where(function ($query) use ($vehicle, $drivers) {
248
+ $query->where('vehicle_assigned_uuid', $vehicle->uuid)->orWhereIn('driver_assigned_uuid', $drivers);
249
+ })
250
+ ->exists();
251
+ }
252
+
253
+ /**
254
+ * Devices: a driver those attached to themselves or to their current vehicle.
255
+ */
256
+ public static function narrowDevice(SocketPrincipal $principal, Model $device): bool
257
+ {
258
+ return $principal->kind === 'driver' && static::ownsDriverSubject($principal, $device->attachable_uuid, $device->getConnectionName());
259
+ }
260
+
261
+ /**
262
+ * Positions: a driver those recorded for themselves or for their current vehicle.
263
+ */
264
+ public static function narrowPosition(SocketPrincipal $principal, Model $position): bool
265
+ {
266
+ return $principal->kind === 'driver' && static::ownsDriverSubject($principal, $position->subject_uuid, $position->getConnectionName());
267
+ }
268
+
269
+ /**
270
+ * Contacts, customers, vendors, facilitators: a customer only themselves.
271
+ */
272
+ public static function narrowSelf(SocketPrincipal $principal, Model $model): bool
273
+ {
274
+ return $principal->kind === 'customer' && static::isSelf($principal, $model->uuid);
275
+ }
276
+
277
+ /**
278
+ * Claim a Sanctum-authenticated user who is a driver of the current company as a driver principal.
279
+ */
280
+ public static function driverPrincipal(Request $request, $user): ?SocketPrincipal
281
+ {
282
+ if (!$user instanceof User || !$user->uuid) {
283
+ return null;
284
+ }
285
+
286
+ $companyUuid = session('company') ?: $user->company_uuid;
287
+ if (!$companyUuid) {
288
+ return null;
289
+ }
290
+
291
+ $driver = Driver::where('user_uuid', $user->uuid)->where('company_uuid', $companyUuid)->first();
292
+ if (!$driver) {
293
+ return null;
294
+ }
295
+
296
+ $connection = $driver->getConnectionName();
297
+ $cpid = Company::on($connection)->where('uuid', $companyUuid)->value('public_id');
298
+
299
+ return new SocketPrincipal(
300
+ kind: 'driver',
301
+ sub: (string) $driver->uuid,
302
+ cid: (string) $companyUuid,
303
+ cpid: is_string($cpid) && $cpid !== '' ? $cpid : null,
304
+ env: $connection === 'sandbox' ? 'test' : 'live',
305
+ ids: array_values(array_filter([$driver->uuid, $driver->public_id, $user->uuid, $user->public_id], fn ($id) => is_string($id) && $id !== '')),
306
+ adm: false,
307
+ );
308
+ }
309
+
310
+ /**
311
+ * The customer's orders that are not completed or canceled.
312
+ */
313
+ protected static function activeCustomerOrders(SocketPrincipal $principal, ?string $connection): Builder
314
+ {
315
+ return Order::on($connection)
316
+ ->whereIn('customer_uuid', array_values(array_unique([$principal->sub, ...$principal->ids])))
317
+ ->whereNotIn('status', self::TERMINAL_ORDER_STATUSES);
318
+ }
319
+
320
+ /**
321
+ * Whether a record's subject is the driver principal or the driver's current vehicle.
322
+ */
323
+ protected static function ownsDriverSubject(SocketPrincipal $principal, ?string $subjectUuid, ?string $connection): bool
324
+ {
325
+ if (!$subjectUuid) {
326
+ return false;
327
+ }
328
+
329
+ return static::isSelf($principal, $subjectUuid) || static::driverVehicleUuid($principal, $connection) === $subjectUuid;
330
+ }
331
+
332
+ /**
333
+ * Whether an id is the principal's own (its subject or one of its ids).
334
+ */
335
+ protected static function isSelf(SocketPrincipal $principal, mixed $id): bool
336
+ {
337
+ return is_string($id) && $id !== '' && ($id === $principal->sub || $principal->owns($id));
338
+ }
339
+
340
+ protected static function driverVehicleUuid(SocketPrincipal $principal, ?string $connection): ?string
341
+ {
342
+ $vehicleUuid = Driver::on($connection)->where('uuid', $principal->sub)->value('vehicle_uuid');
343
+
344
+ return is_string($vehicleUuid) && $vehicleUuid !== '' ? $vehicleUuid : null;
345
+ }
346
+ }
@@ -13,6 +13,8 @@ use Fleetbase\FleetOps\Models\Sensor;
13
13
  use Fleetbase\FleetOps\Models\Telematic;
14
14
  use Fleetbase\FleetOps\Models\Trailer;
15
15
  use Fleetbase\FleetOps\Models\Vehicle;
16
+ use Fleetbase\FleetOps\Support\ApiCacheThrottle;
17
+ use Fleetbase\FleetOps\Support\TrackingPublisher;
16
18
  use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
17
19
  use Illuminate\Support\Carbon;
18
20
  use Illuminate\Support\Facades\Crypt;
@@ -759,7 +761,8 @@ class TelematicService
759
761
  'last_provider' => $event->provider,
760
762
  'last_telemetry_data' => array_filter($eventData, fn ($value) => $value !== null),
761
763
  ]);
762
- $trailer->save();
764
+ // Every telemetry event saves the trailer: model events fire, cache invalidation is coalesced
765
+ ApiCacheThrottle::telemetryWrite($trailer, fn () => $trailer->save());
763
766
 
764
767
  $broadcast = new TrailerLocationChanged($trailer, ['source' => 'telematics', 'device_event_uuid' => $event->uuid, 'provider' => $event->provider, 'position_at' => $event->occurred_at?->toISOString()]);
765
768
  $this->broadcastTelemetry($broadcast, $eventData['_ordered_telemetry'] ?? false);
@@ -802,7 +805,8 @@ class TelematicService
802
805
  'fuel_level' => $eventData['fuel_level'] ?? null,
803
806
  ], fn ($value) => $value !== null),
804
807
  ]);
805
- $vehicle->save();
808
+ // Every telemetry event saves the vehicle: model events fire, cache invalidation is coalesced
809
+ ApiCacheThrottle::telemetryWrite($vehicle, fn () => $vehicle->save());
806
810
 
807
811
  $broadcast = new VehicleLocationChanged($vehicle, [
808
812
  'source' => 'telematics',
@@ -811,6 +815,7 @@ class TelematicService
811
815
  'position_at' => $event->occurred_at?->toISOString(),
812
816
  ]);
813
817
  $this->broadcastTelemetry($broadcast, $eventData['_ordered_telemetry'] ?? false);
818
+ app(TrackingPublisher::class)->vehicleMoved($vehicle);
814
819
  }
815
820
 
816
821
  protected function broadcastTelemetry(object $event, bool $afterCommit): void
@@ -4,6 +4,7 @@ namespace Fleetbase\FleetOps\Support\Telematics\Telemetry;
4
4
 
5
5
  use Fleetbase\FleetOps\Jobs\ProcessTelematicDelivery;
6
6
  use Fleetbase\FleetOps\Models\Telematic;
7
+ use Fleetbase\Support\ApiModelCache;
7
8
  use Illuminate\Support\Facades\Crypt;
8
9
  use Illuminate\Support\Facades\DB;
9
10
  use Illuminate\Support\Str;
@@ -98,10 +99,16 @@ class Inbox
98
99
  return;
99
100
  }
100
101
  // Written without model events: sweeps run every minute and the model logs every save.
102
+ $connectionStatus = in_array($connection->status, ['error', 'synchronizing'], true) ? 'active' : $connection->status;
101
103
  DB::table('telematics')->where('uuid', $connection->uuid)->update([
102
- 'status' => in_array($connection->status, ['error', 'synchronizing'], true) ? 'active' : $connection->status,
104
+ 'status' => $connectionStatus,
103
105
  'meta' => json_encode(array_merge($meta, $result), JSON_THROW_ON_ERROR),
104
106
  ]);
107
+ // Cached device lists embed the telematic. Only a status change invalidates
108
+ // them: the sync meta changes on every sweep and may lag by the list TTL.
109
+ if ($connectionStatus !== $connection->status) {
110
+ ApiModelCache::invalidateModelCache($connection, $connection->company_uuid);
111
+ }
105
112
  });
106
113
  }
107
114
  }
@@ -0,0 +1,55 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\Support\SocketCluster\SocketToken;
6
+
7
+ /**
8
+ * The public tracking channel of one customer on one order: `tracking.{opaque}`.
9
+ *
10
+ * The opaque id is derived by core-api (SocketToken::trackingId): lowercase, unpadded base32 of
11
+ * HMAC-SHA256(tracking key, "{order_uuid}:{customer_type}:{customer_uuid}"), first 26 characters,
12
+ * where the tracking key is derived from SOCKETCLUSTER_AUTH_KEY. Nobody can guess or enumerate it
13
+ * without the auth key, and it reveals nothing about the order.
14
+ *
15
+ * The channel registry denies the `tracking` prefix to every principal; the only way in is a
16
+ * `tracking` token from token(), whose scope names exactly this channel.
17
+ */
18
+ final class TrackingChannel
19
+ {
20
+ public const PREFIX = 'tracking';
21
+
22
+ /**
23
+ * The channel name for a customer's scope.
24
+ *
25
+ * @throws \RuntimeException when socket authentication is not configured
26
+ */
27
+ public static function name(TrackingScope $scope): string
28
+ {
29
+ return self::PREFIX . '.' . static::opaqueId($scope);
30
+ }
31
+
32
+ /**
33
+ * The opaque id for a customer's scope.
34
+ *
35
+ * @throws \RuntimeException when socket authentication is not configured
36
+ */
37
+ public static function opaqueId(TrackingScope $scope): string
38
+ {
39
+ return SocketToken::trackingId($scope->order_uuid, $scope->customer_type, $scope->customer_uuid);
40
+ }
41
+
42
+ /**
43
+ * Mint the token (kind `tracking`, 1800 s) that may subscribe to this scope's channel and nothing else.
44
+ *
45
+ * The caller (the public tracking API) checks its own grant for the scope first.
46
+ *
47
+ * @return array{token: string, expires_in: int, expires_at: string}
48
+ *
49
+ * @throws \RuntimeException when socket authentication is not configured
50
+ */
51
+ public static function token(TrackingScope $scope): array
52
+ {
53
+ return SocketToken::forTracking($scope);
54
+ }
55
+ }