@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
@@ -2,8 +2,10 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Resources\v1\Index\Customer as CustomerIndexResource;
5
6
  use Fleetbase\FleetOps\Models\Order;
6
7
  use Illuminate\Database\Eloquent\Builder;
8
+ use Illuminate\Database\Eloquent\Relations\MorphTo;
7
9
 
8
10
  class LiveOrderQuery
9
11
  {
@@ -65,7 +67,7 @@ class LiveOrderQuery
65
67
  'vehicleAssigned' => function ($query) {
66
68
  $query->without(['fleets', 'vendor']);
67
69
  },
68
- 'customer',
70
+ 'customer' => static fn (MorphTo $relation) => CustomerIndexResource::withPhoto($relation),
69
71
  'facilitator',
70
72
  ]);
71
73
  }
@@ -0,0 +1,365 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Events\OrderChanged;
6
+ use Fleetbase\FleetOps\Models\Entity;
7
+ use Fleetbase\FleetOps\Models\Order;
8
+ use Fleetbase\FleetOps\Models\Payload;
9
+ use Fleetbase\FleetOps\Models\Place;
10
+ use Illuminate\Support\Arr;
11
+ use Illuminate\Support\Carbon;
12
+ use Illuminate\Support\Facades\Log;
13
+
14
+ /**
15
+ * One edit of an order, from the request that changes it to the events that announce it.
16
+ *
17
+ * begin() records the order as it is; guard() refuses changes its status locks
18
+ * (see OrderEditPolicy); commit() works out what actually changed and then:
19
+ *
20
+ * - keeps the driver on the right stop when the route changed (reconcileCurrentWaypoint),
21
+ * - recomputes the order's distance and time and republishes public tracking,
22
+ * - flags item changes made while the order is being prepared (`meta.kitchen_changed_at`),
23
+ * - broadcasts `order.changed` and pushes the change to the assigned driver.
24
+ *
25
+ * $edit = OrderEdit::begin($order);
26
+ * $edit->guard($edit->requestedChanges($input));
27
+ * // ...apply the edit...
28
+ * $edit->commit();
29
+ */
30
+ class OrderEdit
31
+ {
32
+ public const ENTITIES_APPEND = 'append';
33
+ public const ENTITIES_REPLACE = 'replace';
34
+
35
+ protected OrderEditPolicy $policy;
36
+
37
+ protected ?array $routeBefore = null;
38
+
39
+ protected array $entitiesBefore = [];
40
+
41
+ protected mixed $codBefore = null;
42
+
43
+ protected ?string $scheduledBefore = null;
44
+
45
+ protected ?string $notesBefore = null;
46
+
47
+ protected ?string $customerBefore = null;
48
+
49
+ protected array $marked = [];
50
+
51
+ public function __construct(public Order $order)
52
+ {
53
+ $this->policy = OrderEditPolicy::for($order);
54
+
55
+ $payload = $this->currentPayload();
56
+ if ($payload instanceof Payload) {
57
+ $this->routeBefore = $payload->routeSignature();
58
+ $this->entitiesBefore = $this->entitySnapshot($payload);
59
+ $this->codBefore = $payload->cod_amount;
60
+ }
61
+
62
+ $this->scheduledBefore = static::timeString($order->scheduled_at);
63
+ $this->notesBefore = $order->notes;
64
+ $this->customerBefore = $order->customer_uuid;
65
+ }
66
+
67
+ public static function begin(Order $order): static
68
+ {
69
+ return new static($order);
70
+ }
71
+
72
+ public function policy(): OrderEditPolicy
73
+ {
74
+ return $this->policy;
75
+ }
76
+
77
+ /**
78
+ * Normalize an entities mode from a request value: `replace` (or `sync`) replaces
79
+ * the list, anything else appends as the API always has.
80
+ */
81
+ public static function entitiesMode(mixed $value): string
82
+ {
83
+ return in_array(strtolower((string) $value), ['replace', 'sync'], true) ? self::ENTITIES_REPLACE : self::ENTITIES_APPEND;
84
+ }
85
+
86
+ /**
87
+ * The policy fields an edit request touches, with the context the policy checks.
88
+ *
89
+ * Keys read from $input (presence matters): pickup, dropoff, return, waypoints,
90
+ * entities, scheduled_at, notes, cod_amount, customer.
91
+ *
92
+ * @return array<string, array|bool>
93
+ */
94
+ public function requestedChanges(array $input, string $entitiesMode = self::ENTITIES_APPEND): array
95
+ {
96
+ $changes = [];
97
+
98
+ foreach (OrderEditPolicy::ROUTE_FIELDS as $field) {
99
+ if (array_key_exists($field, $input)) {
100
+ $changes[$field] = true;
101
+ }
102
+ }
103
+
104
+ $entities = $input['entities'] ?? null;
105
+ if (is_array($entities) && ($entities !== [] || $entitiesMode === self::ENTITIES_REPLACE)) {
106
+ $payload = $this->currentPayload();
107
+ $adds = true;
108
+ if ($entitiesMode === self::ENTITIES_REPLACE && $payload instanceof Payload) {
109
+ $adds = !$payload->entitiesListOnlyNamesExisting($entities);
110
+ }
111
+ $changes[OrderEditPolicy::FIELD_ITEMS] = ['adds' => $adds];
112
+ }
113
+
114
+ if (array_key_exists('scheduled_at', $input) && static::timeString($input['scheduled_at']) !== $this->scheduledBefore) {
115
+ $changes[OrderEditPolicy::FIELD_SCHEDULE] = ['from' => $this->scheduledBefore, 'to' => $input['scheduled_at']];
116
+ }
117
+
118
+ if (array_key_exists('notes', $input) && (string) $input['notes'] !== (string) $this->notesBefore) {
119
+ $changes[OrderEditPolicy::FIELD_NOTES] = true;
120
+ }
121
+
122
+ if (array_key_exists('cod_amount', $input) && !static::sameAmount($input['cod_amount'], $this->codBefore)) {
123
+ $changes[OrderEditPolicy::FIELD_COD_AMOUNT] = true;
124
+ }
125
+
126
+ if (array_key_exists('customer', $input) && !empty($input['customer']) && !$this->isCurrentCustomer($input['customer'])) {
127
+ $changes[OrderEditPolicy::FIELD_CONTACT] = true;
128
+ }
129
+
130
+ return $changes;
131
+ }
132
+
133
+ /**
134
+ * Refuse the edit when its status locks any requested field.
135
+ *
136
+ * @throws \Fleetbase\FleetOps\Exceptions\OrderEditLockedException
137
+ */
138
+ public function guard(array $changes): static
139
+ {
140
+ $this->policy->assert($changes);
141
+
142
+ return $this;
143
+ }
144
+
145
+ /**
146
+ * Record a change the snapshot cannot see (for example a total recomputed elsewhere).
147
+ */
148
+ public function markChanged(string ...$changes): static
149
+ {
150
+ $this->marked = array_values(array_unique(array_merge($this->marked, $changes)));
151
+
152
+ return $this;
153
+ }
154
+
155
+ /**
156
+ * Work out what changed since begin(), apply the follow-up effects and announce it.
157
+ *
158
+ * @param array $extra order.changed data overrides (summary, total, currency, changed_by, ...)
159
+ * @param bool $notifyDriver push the change to the assigned driver when it concerns them
160
+ *
161
+ * @return string[] the changes announced
162
+ */
163
+ public function commit(array $extra = [], bool $notifyDriver = true): array
164
+ {
165
+ $order = $this->order;
166
+ $payload = $this->freshPayload();
167
+ $changes = [];
168
+
169
+ $routeChanges = [];
170
+ if ($payload instanceof Payload) {
171
+ $routeAfter = $payload->routeSignature();
172
+ $routeChanges = $this->routeBefore ? Payload::routeChanges($this->routeBefore, $routeAfter) : ['pickup', 'dropoff', 'waypoints'];
173
+ $routeChanges = array_values(array_filter($routeChanges, function ($part) use ($routeAfter) {
174
+ // A route part that was and still is empty did not change.
175
+ return !empty($this->routeBefore[$part] ?? null) || !empty($routeAfter[$part]);
176
+ }));
177
+
178
+ if ($routeChanges) {
179
+ $payload->reconcileCurrentWaypoint($this->routeBefore ?? [], $this->isUnderWay());
180
+ }
181
+
182
+ if ($this->entitySnapshot($payload) !== $this->entitiesBefore) {
183
+ $changes[] = OrderEditPolicy::FIELD_ITEMS;
184
+ }
185
+
186
+ if (!static::sameAmount($payload->cod_amount, $this->codBefore)) {
187
+ $changes[] = OrderEditPolicy::FIELD_COD_AMOUNT;
188
+ }
189
+
190
+ $order->setRelation('payload', $payload);
191
+ }
192
+
193
+ $changes = array_merge($routeChanges, $changes);
194
+
195
+ if (static::timeString($order->scheduled_at) !== $this->scheduledBefore) {
196
+ $changes[] = OrderEditPolicy::FIELD_SCHEDULE;
197
+ }
198
+
199
+ if ((string) $order->notes !== (string) $this->notesBefore) {
200
+ $changes[] = OrderEditPolicy::FIELD_NOTES;
201
+ }
202
+
203
+ if ($order->customer_uuid !== $this->customerBefore) {
204
+ $changes[] = OrderEditPolicy::FIELD_CONTACT;
205
+ }
206
+
207
+ $changes = array_values(array_unique(array_merge($changes, $this->marked)));
208
+
209
+ if ($routeChanges) {
210
+ $this->afterRouteChanged();
211
+ }
212
+
213
+ if (in_array(OrderEditPolicy::FIELD_ITEMS, $changes, true) && $this->policy->flagsKitchenChanges()) {
214
+ $this->attempt(fn () => $order->updateMeta('kitchen_changed_at', Carbon::now()->toIso8601ZuluString()));
215
+ }
216
+
217
+ if ($changes) {
218
+ $summary = $extra['summary'] ?? static::summarize($changes, $payload, $order);
219
+ unset($extra['summary']);
220
+ $this->announce($changes, (string) $summary, $extra, $notifyDriver);
221
+ }
222
+
223
+ return $changes;
224
+ }
225
+
226
+ /**
227
+ * One line describing the changes, e.g. "Drop-off changed to 8 Marina View · Items changed".
228
+ */
229
+ public static function summarize(array $changes, ?Payload $payload = null, ?Order $order = null): string
230
+ {
231
+ $parts = [];
232
+
233
+ foreach ($changes as $change) {
234
+ $parts[] = match ($change) {
235
+ 'dropoff' => static::placeLine('Drop-off', $payload?->dropoff),
236
+ 'pickup' => static::placeLine('Pickup', $payload?->pickup),
237
+ 'return' => static::placeLine('Return', $payload?->return),
238
+ 'waypoints' => 'Stops changed',
239
+ 'items' => 'Items changed',
240
+ 'schedule' => $order?->scheduled_at ? 'Rescheduled to ' . Carbon::parse($order->scheduled_at)->format('j M H:i') : 'Schedule cleared',
241
+ 'notes' => 'Notes updated',
242
+ 'cod_amount' => $payload && $payload->cod_amount !== null ? 'Cash to collect changed to ' . trim(($payload->cod_currency ?? '') . ' ' . $payload->cod_amount) : 'Cash to collect changed',
243
+ 'contact' => 'Customer changed',
244
+ 'total' => 'Total changed',
245
+ default => ucfirst(str_replace('_', ' ', (string) $change)) . ' changed',
246
+ };
247
+ }
248
+
249
+ return implode(' · ', array_filter($parts));
250
+ }
251
+
252
+ protected function afterRouteChanged(): void
253
+ {
254
+ $order = $this->order;
255
+
256
+ $this->attempt(fn () => $order->setDistanceAndTime());
257
+ $this->attempt(fn () => app(TrackingPublisher::class)->routeChanged($order));
258
+ }
259
+
260
+ protected function announce(array $changes, string $summary, array $extra, bool $notifyDriver): void
261
+ {
262
+ OrderChanged::dispatchFor($this->order, $changes, $summary, $extra, $notifyDriver);
263
+ }
264
+
265
+ /**
266
+ * Whether the driver is already working the order, so a route change must keep their place.
267
+ */
268
+ protected function isUnderWay(): bool
269
+ {
270
+ return (bool) $this->order->started || $this->policy->stage() === OrderEditPolicy::STAGE_IN_TRANSIT;
271
+ }
272
+
273
+ protected function currentPayload(): ?Payload
274
+ {
275
+ $payload = $this->order->relationLoaded('payload') ? $this->order->getRelation('payload') : $this->attempt(fn () => $this->order->payload);
276
+
277
+ return $payload instanceof Payload ? $payload : null;
278
+ }
279
+
280
+ protected function freshPayload(): ?Payload
281
+ {
282
+ if (!$this->order->payload_uuid) {
283
+ return $this->currentPayload();
284
+ }
285
+
286
+ $payload = $this->attempt(fn () => Payload::where('uuid', $this->order->payload_uuid)->withoutGlobalScopes()->first());
287
+
288
+ return $payload instanceof Payload ? $payload : $this->currentPayload();
289
+ }
290
+
291
+ /**
292
+ * Entity uuid => fingerprint of its stored attributes.
293
+ */
294
+ protected function entitySnapshot(Payload $payload): array
295
+ {
296
+ if (!$payload->uuid) {
297
+ return [];
298
+ }
299
+
300
+ $entities = $this->attempt(fn () => Entity::where('payload_uuid', $payload->uuid)->get()) ?? collect();
301
+
302
+ return $entities->mapWithKeys(function (Entity $entity) {
303
+ $attributes = Arr::except($entity->getAttributes(), ['id', 'created_at', 'updated_at', 'qr_code', 'barcode']);
304
+ ksort($attributes);
305
+
306
+ return [$entity->uuid => md5((string) json_encode($attributes))];
307
+ })->sortKeys()->all();
308
+ }
309
+
310
+ protected function isCurrentCustomer(mixed $customer): bool
311
+ {
312
+ if (!is_string($customer)) {
313
+ return false;
314
+ }
315
+
316
+ if ($customer === $this->customerBefore) {
317
+ return true;
318
+ }
319
+
320
+ return $this->attempt(fn () => data_get($this->order, 'customer.public_id')) === $customer;
321
+ }
322
+
323
+ protected static function placeLine(string $label, mixed $place): string
324
+ {
325
+ if (!$place instanceof Place) {
326
+ return $label . ' changed';
327
+ }
328
+
329
+ $address = trim((string) ($place->address ?: $place->name ?: $place->street1));
330
+
331
+ return $address !== '' ? $label . ' changed to ' . $address : $label . ' changed';
332
+ }
333
+
334
+ protected static function timeString(mixed $value): ?string
335
+ {
336
+ if (empty($value)) {
337
+ return null;
338
+ }
339
+
340
+ try {
341
+ return Carbon::parse($value)->toIso8601ZuluString();
342
+ } catch (\Throwable) {
343
+ return (string) $value;
344
+ }
345
+ }
346
+
347
+ protected static function sameAmount(mixed $a, mixed $b): bool
348
+ {
349
+ $a = ($a === '' || $a === null) ? null : (is_numeric($a) ? (float) $a : $a);
350
+ $b = ($b === '' || $b === null) ? null : (is_numeric($b) ? (float) $b : $b);
351
+
352
+ return $a === $b;
353
+ }
354
+
355
+ protected function attempt(callable $callback): mixed
356
+ {
357
+ try {
358
+ return $callback();
359
+ } catch (\Throwable $exception) {
360
+ Log::debug('Order edit follow-up failed', ['order' => $this->order->public_id, 'error' => $exception->getMessage()]);
361
+
362
+ return null;
363
+ }
364
+ }
365
+ }
@@ -0,0 +1,292 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Exceptions\OrderEditLockedException;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\OrderConfig;
8
+ use Illuminate\Support\Carbon;
9
+
10
+ /**
11
+ * Decides which parts of an order may still be edited at its current status.
12
+ *
13
+ * The policy groups statuses into four stages:
14
+ *
15
+ * - open (created, pending, accepted/confirmed, preparing): everything is editable.
16
+ * - ready (ready, pickup_ready): route, contact, notes and cash to collect are editable;
17
+ * items may only be removed and the schedule may only move earlier.
18
+ * - in transit (dispatched, started, enroute, driver_*): route (the driver is rerouted),
19
+ * contact, notes and cash to collect are editable; items and schedule are locked.
20
+ * - terminal (the config's complete or terminal activity, canceled, expired): nothing is editable.
21
+ *
22
+ * Fields are named the way edit requests and `order.changed` events name them:
23
+ * pickup, dropoff, return, waypoints, items, schedule, contact, notes, cod_amount.
24
+ */
25
+ class OrderEditPolicy
26
+ {
27
+ public const STAGE_OPEN = 'open';
28
+ public const STAGE_READY = 'ready';
29
+ public const STAGE_IN_TRANSIT = 'in_transit';
30
+ public const STAGE_TERMINAL = 'terminal';
31
+
32
+ public const FIELD_PICKUP = 'pickup';
33
+ public const FIELD_DROPOFF = 'dropoff';
34
+ public const FIELD_RETURN = 'return';
35
+ public const FIELD_WAYPOINTS = 'waypoints';
36
+ public const FIELD_ITEMS = 'items';
37
+ public const FIELD_SCHEDULE = 'schedule';
38
+ public const FIELD_CONTACT = 'contact';
39
+ public const FIELD_NOTES = 'notes';
40
+ public const FIELD_COD_AMOUNT = 'cod_amount';
41
+
42
+ public const FIELDS = [
43
+ self::FIELD_PICKUP,
44
+ self::FIELD_DROPOFF,
45
+ self::FIELD_RETURN,
46
+ self::FIELD_WAYPOINTS,
47
+ self::FIELD_ITEMS,
48
+ self::FIELD_SCHEDULE,
49
+ self::FIELD_CONTACT,
50
+ self::FIELD_NOTES,
51
+ self::FIELD_COD_AMOUNT,
52
+ ];
53
+
54
+ public const ROUTE_FIELDS = [self::FIELD_PICKUP, self::FIELD_DROPOFF, self::FIELD_RETURN, self::FIELD_WAYPOINTS];
55
+
56
+ public const OPEN_STATUSES = ['created', 'pending', 'accepted', 'confirmed', 'preparing'];
57
+ public const READY_STATUSES = ['ready', 'pickup_ready', 'ready_for_pickup'];
58
+ public const IN_TRANSIT_STATUSES = ['dispatched', 'started', 'enroute', 'en_route', 'in_progress', 'picked_up', 'arrived', 'out_for_delivery'];
59
+ public const TERMINAL_STATUSES = ['completed', 'canceled', 'cancelled', 'expired'];
60
+
61
+ public const RESTRICTION_REMOVAL_ONLY = 'removal_only';
62
+ public const RESTRICTION_EARLIER_ONLY = 'earlier_only';
63
+
64
+ protected ?string $stage = null;
65
+
66
+ public function __construct(public Order $order)
67
+ {
68
+ }
69
+
70
+ public static function for(Order $order): self
71
+ {
72
+ return new static($order);
73
+ }
74
+
75
+ /**
76
+ * The order status the policy reads, normalized to snake case.
77
+ */
78
+ public function status(): string
79
+ {
80
+ return strtolower((string) ($this->order->status ?: 'created'));
81
+ }
82
+
83
+ /**
84
+ * The edit stage of the order.
85
+ */
86
+ public function stage(): string
87
+ {
88
+ if ($this->stage !== null) {
89
+ return $this->stage;
90
+ }
91
+
92
+ $status = $this->status();
93
+
94
+ if ($this->isTerminal()) {
95
+ return $this->stage = self::STAGE_TERMINAL;
96
+ }
97
+
98
+ if (in_array($status, self::READY_STATUSES, true)) {
99
+ return $this->stage = self::STAGE_READY;
100
+ }
101
+
102
+ if ((bool) $this->order->started || in_array($status, self::IN_TRANSIT_STATUSES, true) || str_starts_with($status, 'driver_')) {
103
+ return $this->stage = self::STAGE_IN_TRANSIT;
104
+ }
105
+
106
+ if (in_array($status, self::OPEN_STATUSES, true)) {
107
+ return $this->stage = self::STAGE_OPEN;
108
+ }
109
+
110
+ // A custom status after dispatch means the order is out with a driver.
111
+ return $this->stage = (bool) $this->order->dispatched ? self::STAGE_IN_TRANSIT : self::STAGE_OPEN;
112
+ }
113
+
114
+ /**
115
+ * Whether the order is completed, canceled or expired: completed meaning the
116
+ * order config's complete activity or one of its terminal activity codes.
117
+ */
118
+ public function isTerminal(): bool
119
+ {
120
+ $status = $this->status();
121
+
122
+ if (in_array($status, self::TERMINAL_STATUSES, true)) {
123
+ return true;
124
+ }
125
+
126
+ if (in_array($status, array_merge(self::OPEN_STATUSES, self::READY_STATUSES, self::IN_TRANSIT_STATUSES), true)) {
127
+ return false;
128
+ }
129
+
130
+ $config = $this->orderConfig();
131
+ if (!$config) {
132
+ return false;
133
+ }
134
+
135
+ try {
136
+ if ($config->isTerminalActivityCode($status)) {
137
+ return true;
138
+ }
139
+
140
+ $activity = $config->getActivityByCode($status);
141
+
142
+ return $activity ? $activity->complete() : false;
143
+ } catch (\Throwable) {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Fields locked outright at this stage, each with the reason.
150
+ *
151
+ * @return array<string, string>
152
+ */
153
+ public function locked(): array
154
+ {
155
+ $stage = $this->stage();
156
+
157
+ if ($stage === self::STAGE_TERMINAL) {
158
+ $reason = sprintf('The order is %s and can no longer be changed.', str_replace('_', ' ', $this->status()));
159
+
160
+ return array_fill_keys(self::FIELDS, $reason);
161
+ }
162
+
163
+ if ($stage === self::STAGE_IN_TRANSIT) {
164
+ return [
165
+ self::FIELD_ITEMS => 'Items cannot be changed once a driver has the order.',
166
+ self::FIELD_SCHEDULE => 'The schedule cannot be changed once a driver has the order.',
167
+ ];
168
+ }
169
+
170
+ return [];
171
+ }
172
+
173
+ /**
174
+ * Fields that are only partly editable at this stage.
175
+ *
176
+ * @return array<string, string> field => removal_only|earlier_only
177
+ */
178
+ public function restrictions(): array
179
+ {
180
+ if ($this->stage() === self::STAGE_READY) {
181
+ return [
182
+ self::FIELD_ITEMS => self::RESTRICTION_REMOVAL_ONLY,
183
+ self::FIELD_SCHEDULE => self::RESTRICTION_EARLIER_ONLY,
184
+ ];
185
+ }
186
+
187
+ return [];
188
+ }
189
+
190
+ /**
191
+ * Whether the field may be edited at all at this stage.
192
+ */
193
+ public function allows(string $field): bool
194
+ {
195
+ return !array_key_exists($field, $this->locked());
196
+ }
197
+
198
+ /**
199
+ * Why a change to the field is refused, or null when it is allowed.
200
+ *
201
+ * Context keys:
202
+ * - items: `adds` (bool) whether the change adds or changes items rather than only removing them.
203
+ * - schedule: `from` and `to` (the current and requested scheduled time).
204
+ */
205
+ public function check(string $field, array $context = []): ?string
206
+ {
207
+ $locked = $this->locked();
208
+ if (isset($locked[$field])) {
209
+ return $locked[$field];
210
+ }
211
+
212
+ $restriction = $this->restrictions()[$field] ?? null;
213
+
214
+ if ($restriction === self::RESTRICTION_REMOVAL_ONLY && ($context['adds'] ?? true)) {
215
+ return 'Once the order is ready, items can only be removed.';
216
+ }
217
+
218
+ if ($restriction === self::RESTRICTION_EARLIER_ONLY && !static::isEarlier($context['from'] ?? null, $context['to'] ?? null)) {
219
+ return 'Once the order is ready, the schedule can only be moved earlier.';
220
+ }
221
+
222
+ return null;
223
+ }
224
+
225
+ /**
226
+ * Refuse the edit when any requested field is locked.
227
+ *
228
+ * @param array<string, array|bool> $changes field => context (see check()), or field => true
229
+ *
230
+ * @throws OrderEditLockedException
231
+ */
232
+ public function assert(array $changes): void
233
+ {
234
+ $refused = [];
235
+
236
+ foreach ($changes as $field => $context) {
237
+ $reason = $this->check($field, is_array($context) ? $context : []);
238
+ if ($reason !== null) {
239
+ $refused[$field] = $reason;
240
+ }
241
+ }
242
+
243
+ if ($refused) {
244
+ throw new OrderEditLockedException($refused);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Whether item changes at this status must be flagged for the kitchen.
250
+ */
251
+ public function flagsKitchenChanges(): bool
252
+ {
253
+ return $this->status() === 'preparing';
254
+ }
255
+
256
+ /**
257
+ * The policy as data, for clients that mirror it.
258
+ */
259
+ public function toArray(): array
260
+ {
261
+ return [
262
+ 'stage' => $this->stage(),
263
+ 'status' => $this->status(),
264
+ 'locked' => (object) $this->locked(),
265
+ 'restrictions' => (object) $this->restrictions(),
266
+ ];
267
+ }
268
+
269
+ protected function orderConfig(): ?OrderConfig
270
+ {
271
+ try {
272
+ $config = $this->order->relationLoaded('orderConfig') ? $this->order->orderConfig : $this->order->orderConfig()->first();
273
+ } catch (\Throwable) {
274
+ return null;
275
+ }
276
+
277
+ return $config instanceof OrderConfig ? $config : null;
278
+ }
279
+
280
+ protected static function isEarlier(mixed $from, mixed $to): bool
281
+ {
282
+ if (empty($from) || empty($to)) {
283
+ return false;
284
+ }
285
+
286
+ try {
287
+ return Carbon::parse($to)->lessThanOrEqualTo(Carbon::parse($from));
288
+ } catch (\Throwable) {
289
+ return false;
290
+ }
291
+ }
292
+ }