@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
@@ -0,0 +1,402 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\TrackingStatus;
8
+ use Fleetbase\FleetOps\Models\Vehicle;
9
+ use Fleetbase\FleetOps\Tracking\TrackingStop;
10
+ use Illuminate\Support\Carbon;
11
+ use Illuminate\Support\Collection;
12
+ use Illuminate\Support\Str;
13
+
14
+ /**
15
+ * The payload published to one customer's public tracking channel.
16
+ *
17
+ * It carries only what that customer may see: their own stops (status, ETA, timeline), whole-route
18
+ * counts, the driver's first name, the vehicle's label and online flag, and the vehicle's rounded
19
+ * position. It never carries another customer's stops, places, items or people, the driver's
20
+ * photo or phone, or free-text activity details. The driver and position are included only while
21
+ * the order is en route and this customer still has a stop to be served.
22
+ */
23
+ final class TrackingUpdate
24
+ {
25
+ public const EVENT_UPDATED = 'tracking.updated';
26
+
27
+ public const EVENT_LOCATION = 'tracking.location';
28
+
29
+ public const REASON_STATUS = 'status';
30
+
31
+ public const REASON_STOP = 'stop';
32
+
33
+ public const REASON_ASSIGNMENT = 'assignment';
34
+
35
+ public const REASON_ETA = 'eta';
36
+
37
+ public const REASON_LOCATION = 'location';
38
+
39
+ public const REASON_ROUTE = 'route';
40
+
41
+ /**
42
+ * Decimal places kept on published coordinates: 4 places is roughly 11 m.
43
+ */
44
+ public const COORDINATE_PRECISION = 4;
45
+
46
+ public const TERMINAL_STATUSES = ['completed', 'canceled'];
47
+
48
+ /**
49
+ * Most timeline entries published per tracking number.
50
+ */
51
+ public const TIMELINE_LIMIT = 20;
52
+
53
+ /**
54
+ * @var Collection<int, TrackingStop>
55
+ */
56
+ private Collection $stops;
57
+
58
+ /**
59
+ * @param iterable<TrackingStop> $stops every stop of the order, in route order
60
+ * @param array<int, array> $etas ETA per stop sequence: seconds, at, distance_m
61
+ * @param array<string, array<int, array>> $timelines timeline entries per tracking number uuid
62
+ */
63
+ public function __construct(
64
+ private Order $order,
65
+ private TrackingScope $scope,
66
+ iterable $stops = [],
67
+ private array $etas = [],
68
+ private array $timelines = [],
69
+ ) {
70
+ $this->stops = collect($stops)->filter(fn ($stop) => $stop instanceof TrackingStop)->values();
71
+ }
72
+
73
+ /**
74
+ * Build the update for a scope from the order's stops and, optionally, a tracking intelligence result.
75
+ *
76
+ * @param iterable<TrackingStop> $stops
77
+ * @param array $tracker a TrackingIntelligenceService::track() result; its route legs give the ETAs
78
+ */
79
+ public static function for(Order $order, TrackingScope $scope, iterable $stops, array $tracker = []): self
80
+ {
81
+ $update = new self($order, $scope, $stops, static::etasFromTracker($tracker));
82
+
83
+ $update->timelines = static::loadTimelines($update->timelineTrackingNumbers(), $order->getConnectionName());
84
+
85
+ return $update;
86
+ }
87
+
88
+ /**
89
+ * The full update: order state, route counts, this customer's stops, driver and position.
90
+ */
91
+ public function toArray(string $reason = self::REASON_STATUS): array
92
+ {
93
+ return $this->envelope(self::EVENT_UPDATED, $reason, [
94
+ 'order' => $this->orderState(),
95
+ 'route' => $this->route(),
96
+ 'stops' => $this->ownStops()->map(fn (TrackingStop $stop) => $this->stop($stop))->values()->all(),
97
+ 'driver' => $this->driver(),
98
+ 'location' => $this->location(),
99
+ ]);
100
+ }
101
+
102
+ /**
103
+ * The position-only update, or null when this customer may not see the vehicle right now.
104
+ */
105
+ public function locationPayload(): ?array
106
+ {
107
+ $location = $this->location();
108
+ if ($location === null) {
109
+ return null;
110
+ }
111
+
112
+ return $this->envelope(self::EVENT_LOCATION, self::REASON_LOCATION, [
113
+ 'driver' => $this->driver(),
114
+ 'location' => $location,
115
+ ]);
116
+ }
117
+
118
+ /**
119
+ * This customer's stops.
120
+ *
121
+ * @return Collection<int, TrackingStop>
122
+ */
123
+ public function ownStops(): Collection
124
+ {
125
+ return $this->stops->filter(fn (TrackingStop $stop) => $this->scope->ownsStop($this->order, $stop->waypoint))->values();
126
+ }
127
+
128
+ public function isTerminal(): bool
129
+ {
130
+ return in_array(strtolower((string) $this->order->status), self::TERMINAL_STATUSES, true);
131
+ }
132
+
133
+ /**
134
+ * Whether the order is under way: started and not yet completed or canceled.
135
+ */
136
+ public function isEnRoute(): bool
137
+ {
138
+ if ($this->isTerminal()) {
139
+ return false;
140
+ }
141
+
142
+ return (bool) $this->order->started || !empty($this->order->started_at) || strtolower((string) $this->order->status) === 'started';
143
+ }
144
+
145
+ public function customerHasIncompleteStop(): bool
146
+ {
147
+ return $this->ownStops()->contains(fn (TrackingStop $stop) => !$stop->completed);
148
+ }
149
+
150
+ /**
151
+ * Whether the driver and the vehicle position may be shown to this customer now.
152
+ */
153
+ public function showsVehicle(): bool
154
+ {
155
+ return $this->isEnRoute() && $this->customerHasIncompleteStop();
156
+ }
157
+
158
+ /**
159
+ * Whole-route counts: stops in total, stops done, and stops still ahead of this customer's next one.
160
+ */
161
+ public function route(): array
162
+ {
163
+ $next = $this->ownStops()->first(fn (TrackingStop $stop) => !$stop->completed);
164
+
165
+ return [
166
+ 'total_stops' => $this->stops->count(),
167
+ 'completed_stops' => $this->stops->filter(fn (TrackingStop $stop) => $stop->completed)->count(),
168
+ 'stops_before' => $next ? $this->stops->filter(fn (TrackingStop $stop) => !$stop->completed && (int) $stop->sequence < (int) $next->sequence)->count() : 0,
169
+ ];
170
+ }
171
+
172
+ /**
173
+ * The driver as this customer may see them: first name, vehicle label, online flag.
174
+ */
175
+ public function driver(): ?array
176
+ {
177
+ $driver = $this->assignedDriver();
178
+ if (!$driver || !$this->showsVehicle()) {
179
+ return null;
180
+ }
181
+
182
+ return [
183
+ 'name' => static::firstName($driver->name),
184
+ 'vehicle' => $this->vehicleLabel(),
185
+ 'online' => (bool) $driver->online,
186
+ ];
187
+ }
188
+
189
+ /**
190
+ * The vehicle position rounded to ~10 m, its heading and when it was seen.
191
+ */
192
+ public function location(): ?array
193
+ {
194
+ if (!$this->showsVehicle()) {
195
+ return null;
196
+ }
197
+
198
+ $source = $this->locationSource();
199
+ if (!$source) {
200
+ return null;
201
+ }
202
+
203
+ [$subject, $point] = $source;
204
+
205
+ return [
206
+ 'latitude' => round($point->getLat(), self::COORDINATE_PRECISION),
207
+ 'longitude' => round($point->getLng(), self::COORDINATE_PRECISION),
208
+ 'heading' => is_numeric($subject->heading) ? (int) round((float) $subject->heading) : null,
209
+ 'seen_at' => $subject->updated_at ? Carbon::parse($subject->updated_at)->toISOString() : null,
210
+ ];
211
+ }
212
+
213
+ /**
214
+ * Tracking numbers whose timeline this customer may read: their stops', plus the order's own for the order customer.
215
+ *
216
+ * @return array<int, string>
217
+ */
218
+ public function timelineTrackingNumbers(): array
219
+ {
220
+ $uuids = $this->ownStops()->map(fn (TrackingStop $stop) => $stop->trackingNumberUuid)->all();
221
+
222
+ if ($this->scope->isOrderCustomer($this->order)) {
223
+ $uuids[] = $this->order->tracking_number_uuid;
224
+ }
225
+
226
+ return array_values(array_unique(array_filter($uuids, fn ($uuid) => is_string($uuid) && $uuid !== '')));
227
+ }
228
+
229
+ /**
230
+ * ETAs keyed by stop sequence from a tracking intelligence result's route legs.
231
+ */
232
+ public static function etasFromTracker(array $tracker): array
233
+ {
234
+ $etas = [];
235
+ foreach ((array) data_get($tracker, 'route.legs', []) as $leg) {
236
+ $sequence = data_get($leg, 'stop.sequence');
237
+ if ($sequence === null) {
238
+ continue;
239
+ }
240
+
241
+ $etas[(int) $sequence] = [
242
+ 'seconds' => is_numeric(data_get($leg, 'eta_seconds')) ? (int) round((float) data_get($leg, 'eta_seconds')) : null,
243
+ 'at' => data_get($leg, 'eta_at'),
244
+ 'distance_m' => is_numeric(data_get($leg, 'distance_m')) ? (int) round((float) data_get($leg, 'distance_m')) : null,
245
+ ];
246
+ }
247
+
248
+ return $etas;
249
+ }
250
+
251
+ /**
252
+ * Timeline entries per tracking number: status, code, complete and time only.
253
+ *
254
+ * @param array<int, string> $trackingNumberUuids
255
+ *
256
+ * @return array<string, array<int, array>>
257
+ */
258
+ public static function loadTimelines(array $trackingNumberUuids, ?string $connection = null): array
259
+ {
260
+ if ($trackingNumberUuids === []) {
261
+ return [];
262
+ }
263
+
264
+ return TrackingStatus::on($connection)
265
+ ->whereIn('tracking_number_uuid', $trackingNumberUuids)
266
+ ->orderBy('created_at')
267
+ ->get(['tracking_number_uuid', 'status', 'code', 'complete', 'created_at'])
268
+ ->groupBy('tracking_number_uuid')
269
+ ->map(fn (Collection $statuses) => $statuses->take(-self::TIMELINE_LIMIT)->map(fn (TrackingStatus $status) => static::timelineEntry($status))->values()->all())
270
+ ->all();
271
+ }
272
+
273
+ /**
274
+ * The first word of a name.
275
+ */
276
+ public static function firstName(?string $name): ?string
277
+ {
278
+ $name = trim((string) $name);
279
+
280
+ return $name === '' ? null : Str::before($name, ' ');
281
+ }
282
+
283
+ private function envelope(string $event, string $reason, array $data): array
284
+ {
285
+ return [
286
+ 'event' => $event,
287
+ 'reason' => $reason,
288
+ 'sent_at' => Carbon::now()->toISOString(),
289
+ 'data' => $data,
290
+ ];
291
+ }
292
+
293
+ private function orderState(): array
294
+ {
295
+ $status = strtolower((string) $this->order->status);
296
+
297
+ return [
298
+ 'status' => $status !== '' ? $status : null,
299
+ 'en_route' => $this->isEnRoute(),
300
+ 'completed' => $status === 'completed',
301
+ 'canceled' => $status === 'canceled',
302
+ 'timeline' => $this->scope->isOrderCustomer($this->order) ? $this->timeline($this->order->tracking_number_uuid) : [],
303
+ ];
304
+ }
305
+
306
+ private function stop(TrackingStop $stop): array
307
+ {
308
+ $eta = $stop->completed ? null : ($this->etas[(int) $stop->sequence] ?? null);
309
+
310
+ return [
311
+ 'id' => $stop->waypoint?->public_id,
312
+ 'type' => $stop->type,
313
+ 'sequence' => $stop->sequence,
314
+ 'status' => $stop->status,
315
+ 'complete' => $stop->completed,
316
+ 'eta' => $eta,
317
+ 'timeline' => $this->timeline($stop->trackingNumberUuid),
318
+ ];
319
+ }
320
+
321
+ private function timeline(?string $trackingNumberUuid): array
322
+ {
323
+ return $trackingNumberUuid ? ($this->timelines[$trackingNumberUuid] ?? []) : [];
324
+ }
325
+
326
+ private static function timelineEntry(TrackingStatus $status): array
327
+ {
328
+ $createdAt = $status->getRawOriginal('created_at') ?? $status->created_at;
329
+
330
+ return [
331
+ 'status' => $status->status,
332
+ 'code' => $status->code !== null ? strtolower((string) $status->code) : null,
333
+ 'complete' => (bool) $status->complete,
334
+ 'created_at' => $createdAt ? Carbon::parse($createdAt)->toISOString() : null,
335
+ ];
336
+ }
337
+
338
+ private function assignedDriver(): ?Driver
339
+ {
340
+ $driver = $this->order->driverAssigned;
341
+
342
+ return $driver instanceof Driver ? $driver : null;
343
+ }
344
+
345
+ private function assignedVehicle(): ?Vehicle
346
+ {
347
+ $vehicle = $this->order->vehicleAssigned ?? $this->assignedDriver()?->vehicle;
348
+
349
+ return $vehicle instanceof Vehicle ? $vehicle : null;
350
+ }
351
+
352
+ private function vehicleLabel(): ?string
353
+ {
354
+ $label = trim((string) $this->assignedVehicle()?->display_name);
355
+
356
+ return $label === '' ? null : $label;
357
+ }
358
+
359
+ /**
360
+ * The most recently updated of the driver and the vehicle that has a usable position.
361
+ *
362
+ * @return array{0: Driver|Vehicle, 1: \Fleetbase\LaravelMysqlSpatial\Types\Point}|null
363
+ */
364
+ private function locationSource(): ?array
365
+ {
366
+ $candidates = [];
367
+ foreach ([$this->assignedDriver(), $this->assignedVehicle()] as $subject) {
368
+ $point = $subject ? static::usablePoint($subject) : null;
369
+ if ($point) {
370
+ $candidates[] = [$subject, $point];
371
+ }
372
+ }
373
+
374
+ usort($candidates, fn (array $a, array $b) => static::seenAt($b[0]) <=> static::seenAt($a[0]));
375
+
376
+ return $candidates[0] ?? null;
377
+ }
378
+
379
+ private static function seenAt(Driver|Vehicle $subject): int
380
+ {
381
+ return $subject->updated_at ? Carbon::parse($subject->updated_at)->getTimestamp() : 0;
382
+ }
383
+
384
+ private static function usablePoint(Driver|Vehicle $subject)
385
+ {
386
+ try {
387
+ $point = $subject->location ? Utils::getPointFromMixed($subject->location) : null;
388
+ } catch (\Throwable) {
389
+ return null;
390
+ }
391
+
392
+ if (!$point) {
393
+ return null;
394
+ }
395
+
396
+ $lat = (float) $point->getLat();
397
+ $lng = (float) $point->getLng();
398
+ $valid = is_finite($lat) && is_finite($lng) && abs($lat) <= 90 && abs($lng) <= 180 && !(abs($lat) < 0.000001 && abs($lng) < 0.000001);
399
+
400
+ return $valid ? $point : null;
401
+ }
402
+ }
@@ -1220,8 +1220,8 @@ class Utils extends FleetbaseUtils
1220
1220
  {
1221
1221
  $latitude = deg2rad($latitude);
1222
1222
  $longitude = deg2rad($longitude);
1223
- // convert meters to km
1224
- $radius = ($meters * 1000) / 6378137;
1223
+ // angular radius: metres over the earth's equatorial radius in metres
1224
+ $radius = $meters / 6378137;
1225
1225
  // create circle coordinates
1226
1226
  $coords = collect();
1227
1227
  // loop through the array and write path linestrings — stop short of 360
@@ -1231,7 +1231,8 @@ class Utils extends FleetbaseUtils
1231
1231
  $radial = deg2rad($i);
1232
1232
  $lat_rad = asin(sin($latitude) * cos($radius) + cos($latitude) * sin($radius) * cos($radial));
1233
1233
  $dlon_rad = atan2(sin($radial) * sin($radius) * cos($latitude), cos($radius) - sin($latitude) * sin($lat_rad));
1234
- $lon_rad = fmod($longitude + $dlon_rad + M_PI, 2 * M_PI) - M_PI;
1234
+ // shift by 3π, not π: fmod keeps the dividend's sign, so a negative sum would not wrap
1235
+ $lon_rad = fmod($longitude + $dlon_rad + 3 * M_PI, 2 * M_PI) - M_PI;
1235
1236
  $coords->push([rad2deg($lat_rad), rad2deg($lon_rad)]);
1236
1237
  }
1237
1238
 
@@ -1603,4 +1604,15 @@ class Utils extends FleetbaseUtils
1603
1604
 
1604
1605
  return $phone;
1605
1606
  }
1607
+
1608
+ /**
1609
+ * Re-sign a URL stored as a string (e.g. a legacy `avatar_url` value) when it points into the
1610
+ * private S3 media bucket, so it keeps working once the bucket stops allowing public reads.
1611
+ * Other values pass through unchanged. Falls back to the raw value on core-api releases that
1612
+ * predate File::signStoredUrl().
1613
+ */
1614
+ public static function signStoredFileUrl(?string $url): ?string
1615
+ {
1616
+ return method_exists(\Fleetbase\Models\File::class, 'signStoredUrl') ? \Fleetbase\Models\File::signStoredUrl($url) : $url;
1617
+ }
1606
1618
  }
@@ -9,6 +9,7 @@ use Fleetbase\FleetOps\Models\TrackingNumber;
9
9
  use Fleetbase\FleetOps\Models\TrackingStatus;
10
10
  use Fleetbase\FleetOps\Support\Utils;
11
11
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
12
+ use Fleetbase\Support\ApiModelCache;
12
13
  use Fleetbase\Support\TemplateString;
13
14
  use Illuminate\Database\Eloquent\Model;
14
15
  use Illuminate\Support\Facades\DB;
@@ -28,6 +29,7 @@ trait HasTrackingNumber
28
29
  ], $model);
29
30
 
30
31
  DB::table($model->getTable())->where('uuid', $model->uuid)->update(['tracking_number_uuid' => $trackingNumberId]);
32
+ ApiModelCache::invalidateModelCache($model, data_get($model, 'company_uuid', session('company')));
31
33
 
32
34
  if ($model->isFillable('tracking_number_uuid')) {
33
35
  $model->tracking_number_uuid = $trackingNumberId;
@@ -20,6 +20,23 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
20
20
  // limiter has its own prefix so the two do not share one counter.
21
21
  // Every answer is JSON, so a refused submission reaches the page with
22
22
  // its reasons instead of as a redirect back to it.
23
+ // The public customer tracking page. A tracking number alone gives a coarse status;
24
+ // a one-time code to the customer's own contact details unlocks their stops. Lookups
25
+ // are rate limited inside the controller, so a throttled lookup answers exactly like
26
+ // an unknown number; code sends and checks have limits of their own there too.
27
+ $router->prefix('public/track')->namespace('Public')->middleware([Fleetbase\FleetOps\Http\Middleware\ForceJsonResponse::class])->group(function ($router) {
28
+ $router->get('config', 'TrackingController@config');
29
+ $router->get('account/orders', 'TrackingController@accountOrders');
30
+ $router->delete('session', 'TrackingController@signOut');
31
+ $router->get('{number}', 'TrackingController@show');
32
+ $router->post('{number}/codes', 'TrackingController@sendCode');
33
+ $router->post('{number}/codes/verify', 'TrackingController@verifyCode');
34
+ $router->put('{number}/instructions', 'TrackingController@updateInstructions');
35
+ $router->get('{number}/proofs/{id}', 'TrackingController@proof');
36
+ $router->post('{number}/report', 'TrackingController@report');
37
+ $router->get('{number}/socket-token', 'TrackingController@socketToken');
38
+ });
39
+
23
40
  $router->prefix('public')->namespace('Public')->middleware([Fleetbase\FleetOps\Http\Middleware\ForceJsonResponse::class, 'throttle:60,1,inspection-public'])->group(function ($router) {
24
41
  $router->get('inspections/forms/{id}', 'PublicInspectionController@show');
25
42
  $router->post('inspections/forms/{id}/submit', 'PublicInspectionController@submit');
@@ -37,14 +54,16 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
37
54
  // drivers routes
38
55
  $router->group(['prefix' => 'drivers', 'middleware' => []], function () use ($router) {
39
56
  $router->post('register-device', 'DriverController@registerDevice');
40
- $router->post('login-with-sms', 'DriverController@loginWithPhone');
57
+ // Routes that send a text message carry a small per-address bucket on top
58
+ // of the API credential throttle; the SMS guard in core bounds the spend.
59
+ $router->post('login-with-sms', 'DriverController@loginWithPhone')->middleware('throttle:5,1,fleetops-sms');
41
60
  $router->post('verify-code', 'DriverController@verifyCode');
42
61
  $router->post('login', 'DriverController@login');
43
62
  // Password management. Deliberately separate from the driver
44
63
  // update: changing a password requires proving the old one, and
45
64
  // resetting it requires a code, neither of which a general PUT
46
65
  // can express.
47
- $router->post('forgot-password', 'DriverController@forgotPassword');
66
+ $router->post('forgot-password', 'DriverController@forgotPassword')->middleware('throttle:5,1,fleetops-sms');
48
67
  $router->post('reset-password', 'DriverController@resetPassword');
49
68
  $router->post('{id}/change-password', 'DriverController@changePassword');
50
69
  $router->post('{id}/simulate', 'DriverController@simulate');
@@ -80,12 +99,12 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
80
99
  // - Authenticated endpoints: API key + `Customer-Token` header (Sanctum)
81
100
  $router->group(['prefix' => 'customers', 'middleware' => []], function () use ($router) {
82
101
  // Public auth flows
83
- $router->post('request-creation-code', 'CustomerController@requestCreationCode');
102
+ $router->post('request-creation-code', 'CustomerController@requestCreationCode')->middleware('throttle:5,1,fleetops-sms');
84
103
  $router->post('/', 'CustomerController@create');
85
104
  $router->post('login', 'CustomerController@login');
86
- $router->post('login-with-sms', 'CustomerController@loginWithPhone');
105
+ $router->post('login-with-sms', 'CustomerController@loginWithPhone')->middleware('throttle:5,1,fleetops-sms');
87
106
  $router->post('verify-code', 'CustomerController@verifyCode');
88
- $router->post('forgot-password', 'CustomerController@forgotPassword');
107
+ $router->post('forgot-password', 'CustomerController@forgotPassword')->middleware('throttle:5,1,fleetops-sms');
89
108
  $router->post('reset-password', 'CustomerController@resetPassword');
90
109
 
91
110
  // Authenticated (require Customer-Token)
@@ -398,7 +417,10 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
398
417
  $router->group(
399
418
  ['prefix' => 'v1/fleet-ops', 'namespace' => 'v1'],
400
419
  function ($router) {
401
- $router->get('lookup', 'OrderController@lookup');
420
+ // The public Track Order page: no session, the tracking number is
421
+ // the credential. Rate limited per address so numbers cannot be
422
+ // enumerated, on a limiter of its own.
423
+ $router->get('lookup', 'OrderController@lookup')->middleware('throttle:30,1,tracking-lookup');
402
424
  }
403
425
  );
404
426
 
@@ -562,7 +584,12 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
562
584
  $router->post('import', $controller('import'));
563
585
  }
564
586
  );
565
- $router->fleetbaseRoutes('proofs');
587
+ $router->fleetbaseRoutes(
588
+ 'proofs',
589
+ function ($router, $controller) {
590
+ $router->get('recent', $controller('recent'));
591
+ }
592
+ );
566
593
  $router->fleetbaseRoutes('purchase-rates');
567
594
  $router->fleetbaseRoutes('routes');
568
595
  $router->fleetbaseRoutes('positions', function ($router, $controller) {
@@ -806,6 +833,11 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
806
833
  $router->post('tracking-settings', 'SettingController@saveTrackingSettings');
807
834
  $router->get('admin-tracking-settings', 'SettingController@getAdminTrackingSettings');
808
835
  $router->post('admin-tracking-settings', 'SettingController@saveAdminTrackingSettings');
836
+ $router->get('tracking-page-settings', 'SettingController@getTrackingPageSettings');
837
+ $router->post('tracking-page-settings', 'SettingController@saveTrackingPageSettings');
838
+ $router->post('tracking-page-settings/validate-slug', 'SettingController@validateTrackingPageSlug');
839
+ $router->get('admin-tracking-page-settings', 'SettingController@getAdminTrackingPageSettings');
840
+ $router->post('admin-tracking-page-settings', 'SettingController@saveAdminTrackingPageSettings');
809
841
  $router->get('map', 'SettingController@getMapSettings');
810
842
  $router->post('map', 'SettingController@saveMapSettings');
811
843
  $router->get('admin-map', 'SettingController@getAdminMapSettings');
@@ -1,5 +1,6 @@
1
1
  <?php
2
2
 
3
+ use Fleetbase\Exceptions\SmsBlockedException;
3
4
  use Fleetbase\FleetOps\Exceptions\UserAlreadyExistsException;
4
5
  use Fleetbase\FleetOps\Http\Controllers\Api\v1\CustomerController;
5
6
  use Fleetbase\FleetOps\Http\Requests\CreateCustomerOrderRequest;
@@ -14,6 +15,7 @@ use Fleetbase\FleetOps\Models\Place;
14
15
  use Fleetbase\FleetOps\Models\ServiceQuote;
15
16
  use Fleetbase\FleetOps\Models\Waypoint;
16
17
  use Fleetbase\Models\User;
18
+ use Fleetbase\Support\Sms\PhoneValidator;
17
19
  use Illuminate\Database\Eloquent\ModelNotFoundException;
18
20
  use Illuminate\Http\Request;
19
21
 
@@ -56,6 +58,7 @@ class FleetOpsApiCustomerControllerProbe extends CustomerController
56
58
  public mixed $placesResult = null;
57
59
  public bool $emailVerificationFails = false;
58
60
  public bool $smsVerificationFails = false;
61
+ public ?Throwable $smsVerificationThrows = null;
59
62
  public bool $verificationExists = true;
60
63
  public bool $passwordMatches = true;
61
64
  public bool $findOrderFails = false;
@@ -164,6 +167,9 @@ class FleetOpsApiCustomerControllerProbe extends CustomerController
164
167
 
165
168
  protected function generateSmsVerification(User $user, string $for, array $options): mixed
166
169
  {
170
+ if ($this->smsVerificationThrows) {
171
+ throw $this->smsVerificationThrows;
172
+ }
167
173
  if ($this->smsVerificationFails) {
168
174
  throw new RuntimeException('sms failed');
169
175
  }
@@ -692,12 +698,9 @@ test('api customer controller sends creation verification codes and updates stub
692
698
  ]));
693
699
 
694
700
  expect($created)->toBe(['status' => 'ok'])
695
- ->and($controller->createdUsers[0])->toMatchArray([
696
- 'company_uuid' => 'company-uuid',
697
- 'name' => 'Jane',
698
- 'email' => 'jane@example.test',
699
- 'phone' => '+15551234567',
700
- ])
701
+ // Nothing is persisted for an unverified identity: the code's subject is an unsaved user.
702
+ ->and($controller->createdUsers)->toBe([])
703
+ ->and($controller->emailVerifications[0][0])->toBeNull()
701
704
  ->and($controller->emailVerifications[0][1])->toBe('fleetops_create_customer')
702
705
  ->and($updated)->toBe(['status' => 'ok'])
703
706
  ->and($stub->name)->toBe('Stub Name')
@@ -844,7 +847,6 @@ test('api customer controller resets forgotten passwords', function () {
844
847
  ->and(fleetopsApiCustomerJson($reset))->toBe(['status' => 'ok'])
845
848
  ->and($controller->genericUser->savedForTest)->toBeTrue()
846
849
  ->and($controller->genericUser->tokensDeleted)->toBeTrue()
847
- ->and($controller->verificationCode->deleted)->toBeTrue()
848
850
  ->and(fleetopsApiCustomerJson($unknown->forgotPassword(Request::create('/v1/customers/forgot-password', 'POST', [
849
851
  'identity' => 'missing@example.test',
850
852
  ]))))->toBe(['status' => 'ok'])
@@ -982,6 +984,51 @@ test('api customer controller handles verification delivery fallbacks and profil
982
984
  ]))))->toBe(['error' => 'update failed']);
983
985
  });
984
986
 
987
+ test('api customer controller answers an sms guard refusal with its status, not the generic error', function () {
988
+ // A paused organization: the sign-up code request answers 423 with the wait.
989
+ $paused = fleetopsApiCustomerController();
990
+ $paused->activeUser = null;
991
+ $paused->smsVerificationThrows = SmsBlockedException::paused('company:company-uuid', 120);
992
+ $creation = $paused->requestCreationCode(new VerifyCreateCustomerRequest([
993
+ 'mode' => 'sms',
994
+ 'identity' => '15550000000',
995
+ ]));
996
+
997
+ // Over budget with no email on file: the 429 and its Retry-After reach the app.
998
+ $limited = fleetopsApiCustomerController();
999
+ $limited->activeUser->email = null;
1000
+ $limited->smsVerificationThrows = SmsBlockedException::limited('company_per_hour', 45);
1001
+ $login = $limited->loginWithPhone(Request::create('/v1/customers/login/phone', 'POST', [
1002
+ 'phone' => '15551234567',
1003
+ ]));
1004
+
1005
+ // Over budget with an email on file: the code still goes by email.
1006
+ $fallback = fleetopsApiCustomerController();
1007
+ $fallback->smsVerificationThrows = SmsBlockedException::limited('company_per_hour', 45);
1008
+ $fallback->activeUser->email = 'jane@example.test';
1009
+ $byEmail = $fallback->loginWithPhone(Request::create('/v1/customers/login/phone', 'POST', [
1010
+ 'phone' => '15551234567',
1011
+ ]));
1012
+
1013
+ // A number that can never be texted: the reset request answers 422.
1014
+ $invalid = fleetopsApiCustomerController();
1015
+ $invalid->smsVerificationThrows = SmsBlockedException::invalidDestination(PhoneValidator::REASON_TYPE_NOT_ALLOWED);
1016
+ $forgot = $invalid->forgotPassword(Request::create('/v1/customers/forgot-password', 'POST', [
1017
+ 'identity' => '15551234567',
1018
+ ]));
1019
+
1020
+ expect($creation->getStatusCode())->toBe(423)
1021
+ ->and($creation->headers->get('Retry-After'))->toBe('120')
1022
+ ->and(fleetopsApiCustomerJson($creation))->toBe(['error' => 'Text messages are temporarily unavailable. Try again later.'])
1023
+ ->and($login->getStatusCode())->toBe(429)
1024
+ ->and($login->headers->get('Retry-After'))->toBe('45')
1025
+ ->and(fleetopsApiCustomerJson($login))->toBe(['error' => 'Unable to send a verification code right now. Try again in a minute.'])
1026
+ ->and(fleetopsApiCustomerJson($byEmail))->toBe(['status' => 'ok', 'method' => 'email'])
1027
+ ->and($fallback->emailVerifications[0][1])->toBe('fleetops_customer_login')
1028
+ ->and($forgot->getStatusCode())->toBe(422)
1029
+ ->and(fleetopsApiCustomerJson($forgot))->toBe(['error' => 'The phone number provided cannot receive text messages.']);
1030
+ });
1031
+
985
1032
  /**
986
1033
  * Run a callback with an app container that supports environment().
987
1034
  *