@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
@@ -130,12 +130,13 @@ class FleetOpsPasswordDriverFake extends Driver
130
130
 
131
131
  class FleetOpsPasswordControllerProbe extends DriverController
132
132
  {
133
- public static ?Driver $driver = null;
134
- public static bool $driverMissing = false;
135
- public static ?User $identityUser = null;
136
- public static mixed $resetCode = null;
137
- public static array $sentCodes = [];
138
- public static bool $sendThrows = false;
133
+ public static ?Driver $driver = null;
134
+ public static bool $driverMissing = false;
135
+ public static ?User $identityUser = null;
136
+ public static mixed $resetCode = null;
137
+ public static array $sentCodes = [];
138
+ public static bool $sendThrows = false;
139
+ public static ?Throwable $sendRefusal = null;
139
140
 
140
141
  public function findDriver(string $id, array $with = []): Driver
141
142
  {
@@ -180,6 +181,9 @@ class FleetOpsPasswordControllerProbe extends DriverController
180
181
 
181
182
  protected static function sendResetCode(User $user, string $identity): void
182
183
  {
184
+ if (static::$sendRefusal) {
185
+ throw static::$sendRefusal;
186
+ }
183
187
  if (static::$sendThrows) {
184
188
  throw new RuntimeException('sms gateway down');
185
189
  }
@@ -205,6 +209,7 @@ beforeEach(function () {
205
209
  FleetOpsPasswordControllerProbe::$resetCode = null;
206
210
  FleetOpsPasswordControllerProbe::$sentCodes = [];
207
211
  FleetOpsPasswordControllerProbe::$sendThrows = false;
212
+ FleetOpsPasswordControllerProbe::$sendRefusal = null;
208
213
  });
209
214
 
210
215
  test('change password requires both the current and the new one', function () {
@@ -331,6 +336,16 @@ test('forgot password reports a delivery failure rather than pretending it sent'
331
336
  expect($response->getStatusCode())->toBe(400);
332
337
  });
333
338
 
339
+ test('forgot password passes an sms guard refusal through with its status', function () {
340
+ FleetOpsPasswordControllerProbe::$identityUser = fleetopsPasswordUser();
341
+ FleetOpsPasswordControllerProbe::$sendRefusal = Fleetbase\Exceptions\SmsBlockedException::limited('destination_cooldown', 30);
342
+
343
+ $response = (new FleetOpsPasswordControllerProbe())->forgotPassword(new Request(['identity' => '+6581000001']));
344
+
345
+ expect($response->getStatusCode())->toBe(429)
346
+ ->and($response->getData())->toBe(['error' => 'Unable to send a verification code right now. Try again in a minute.']);
347
+ });
348
+
334
349
  test('reset password requires identity, code and a new password', function () {
335
350
  $response = (new FleetOpsPasswordControllerProbe())->resetPassword(new Request(['identity' => 'a@b.test']));
336
351
 
@@ -362,23 +377,13 @@ test('reset password gives one answer for an unknown identity and a bad code ali
362
377
  ->and($unknownIdentity->getData())->toBe($badCode->getData());
363
378
  });
364
379
 
365
- test('reset password sets the password, spends the code, and ends every session', function () {
380
+ test('reset password sets the password and ends every session once the code verified', function () {
366
381
  $user = fleetopsPasswordUser();
367
382
  FleetOpsPasswordControllerProbe::$identityUser = $user;
368
383
 
369
- $deleted = false;
370
- FleetOpsPasswordControllerProbe::$resetCode = new class($deleted) {
371
- public function __construct(public bool &$deleted)
372
- {
373
- }
374
-
375
- public function delete(): bool
376
- {
377
- $this->deleted = true;
378
-
379
- return true;
380
- }
381
- };
384
+ // findResetCode() hands back the code it verified and used up; nothing more is done
385
+ // to it here.
386
+ FleetOpsPasswordControllerProbe::$resetCode = (object) ['uuid' => 'reset-code'];
382
387
 
383
388
  $response = (new FleetOpsPasswordControllerProbe())->resetPassword(
384
389
  new Request(['identity' => 'driver@example.test', 'code' => '123456', 'password' => 'newpassword'])
@@ -386,7 +391,6 @@ test('reset password sets the password, spends the code, and ends every session'
386
391
 
387
392
  expect($response->getStatusCode())->toBe(200)
388
393
  ->and($user->saved)->toHaveCount(1)
389
- ->and($deleted)->toBeTrue()
390
394
  // A reset is a recovery from losing control; nothing keeps working.
391
395
  ->and($user->tokensDeleted)->toBeTrue();
392
396
  });
@@ -4,8 +4,11 @@ use Fleetbase\FleetOps\Http\Controllers\Api\v1\EntityController;
4
4
  use Fleetbase\FleetOps\Http\Requests\CreateEntityRequest;
5
5
  use Fleetbase\FleetOps\Http\Requests\UpdateEntityRequest;
6
6
  use Fleetbase\FleetOps\Models\Entity;
7
+ use Fleetbase\FleetOps\Models\Order;
7
8
  use Fleetbase\FleetOps\Models\Payload;
8
9
  use Fleetbase\FleetOps\Models\Place;
10
+ use Fleetbase\FleetOps\Support\OrderEdit;
11
+ use Fleetbase\FleetOps\Support\OrderEditPolicy;
9
12
  use Illuminate\Database\Eloquent\ModelNotFoundException;
10
13
  use Illuminate\Http\Request;
11
14
 
@@ -327,3 +330,84 @@ test('api entity controller returns not found responses for missing entities', f
327
330
  'status' => 404,
328
331
  ]);
329
332
  })->with(['update', 'find', 'delete']);
333
+
334
+ /**
335
+ * Entities that belong to an order: the order's edit uses the real lock policy and records its commit.
336
+ */
337
+ class FleetOpsApiEntityOrderControllerProbe extends FleetOpsApiEntityControllerProbe
338
+ {
339
+ public ?Order $order = null;
340
+
341
+ public array $edits = [];
342
+
343
+ protected function orderEditForPayload(?string $payloadUuid): ?OrderEdit
344
+ {
345
+ if (!$this->order) {
346
+ return null;
347
+ }
348
+
349
+ return $this->edits[] = new FleetOpsApiEntityOrderEditFake($this->order);
350
+ }
351
+ }
352
+
353
+ class FleetOpsApiEntityOrderEditFake extends OrderEdit
354
+ {
355
+ public int $commits = 0;
356
+
357
+ public function __construct(public Order $order)
358
+ {
359
+ $this->policy = OrderEditPolicy::for($order);
360
+ }
361
+
362
+ public function commit(array $extra = [], bool $notifyDriver = true): array
363
+ {
364
+ $this->commits++;
365
+
366
+ return ['items'];
367
+ }
368
+ }
369
+
370
+ function fleetopsApiEntityOrderController(string $status): FleetOpsApiEntityOrderControllerProbe
371
+ {
372
+ $controller = new FleetOpsApiEntityOrderControllerProbe();
373
+ $controller->order = new Order();
374
+ $controller->order->setRawAttributes(['uuid' => 'order-uuid', 'public_id' => 'order_entity', 'status' => $status], true);
375
+ $controller->entity = new FleetOpsApiEntityFake();
376
+ $controller->entity->setRawAttributes(['uuid' => 'entity-uuid', 'payload_uuid' => 'payload-uuid'], true);
377
+
378
+ return $controller;
379
+ }
380
+
381
+ test('api entity controller announces item edits on orders that allow them', function () {
382
+ $controller = fleetopsApiEntityOrderController('created');
383
+
384
+ $controller->create(new FleetOpsCreateEntityRequestFake(['name' => 'Latte', 'payload' => 'payload_public']));
385
+ $controller->update('entity_public', new FleetOpsUpdateEntityRequestFake(['name' => 'Iced Latte']));
386
+ $controller->delete('entity_public', new Request());
387
+
388
+ expect(array_map(fn ($edit) => $edit->commits, $controller->edits))->toBe([1, 1, 1])
389
+ ->and($controller->createdEntities)->toHaveCount(1)
390
+ ->and($controller->entity->deletedForTest)->toBeTrue();
391
+ });
392
+
393
+ test('api entity controller refuses item edits the order status locks', function () {
394
+ $dispatched = fleetopsApiEntityOrderController('dispatched');
395
+ $created = $dispatched->create(new FleetOpsCreateEntityRequestFake(['name' => 'Latte', 'payload' => 'payload_public']));
396
+ $deleted = $dispatched->delete('entity_public', new Request());
397
+
398
+ expect($created->getStatusCode())->toBe(422)
399
+ ->and($created->getData(true)['locked'])->toBe(['items' => 'Items cannot be changed once a driver has the order.'])
400
+ ->and($deleted->getStatusCode())->toBe(422)
401
+ ->and($dispatched->createdEntities)->toBe([])
402
+ ->and($dispatched->entity->deletedForTest)->toBeFalse();
403
+
404
+ // once ready, an item may be removed but not changed
405
+ $ready = fleetopsApiEntityOrderController('ready');
406
+ $updated = $ready->update('entity_public', new FleetOpsUpdateEntityRequestFake(['name' => 'Iced Latte']));
407
+ $ready->delete('entity_public', new Request());
408
+
409
+ expect($updated->getStatusCode())->toBe(422)
410
+ ->and($updated->getData(true)['error'])->toBe('Once the order is ready, items can only be removed.')
411
+ ->and($ready->entity->updatedPayloads)->toBe([])
412
+ ->and($ready->entity->deletedForTest)->toBeTrue();
413
+ });
@@ -1,5 +1,6 @@
1
1
  <?php
2
2
 
3
+ use Fleetbase\FleetOps\Exceptions\OrderEditLockedException;
3
4
  use Fleetbase\FleetOps\Http\Controllers\Api\v1\OrderController;
4
5
  use Fleetbase\FleetOps\Http\Requests\CreateOrderRequest;
5
6
  use Fleetbase\FleetOps\Http\Requests\ScheduleOrderRequest;
@@ -12,6 +13,8 @@ use Fleetbase\FleetOps\Models\Place;
12
13
  use Fleetbase\FleetOps\Models\Proof;
13
14
  use Fleetbase\FleetOps\Models\ServiceQuote;
14
15
  use Fleetbase\FleetOps\Models\Waypoint;
16
+ use Fleetbase\FleetOps\Support\OrderEdit;
17
+ use Fleetbase\FleetOps\Support\OrderEditPolicy;
15
18
  use Fleetbase\FleetOps\Support\OrderTracker;
16
19
  use Fleetbase\Models\File;
17
20
  use Illuminate\Database\Eloquent\ModelNotFoundException;
@@ -41,6 +44,8 @@ class FleetOpsApiOrderCrudControllerProbe extends OrderController
41
44
  public bool $missingOrderConfig = false;
42
45
  public mixed $resolvedSubject = null;
43
46
  public ?string $companyUuid = 'company-uuid';
47
+ public ?FleetOpsApiOrderEditFake $edit = null;
48
+ public array $editLockedWith = [];
44
49
 
45
50
  protected function findOrder(string $id, array $with = [], array $withCount = []): Order
46
51
  {
@@ -54,6 +59,26 @@ class FleetOpsApiOrderCrudControllerProbe extends OrderController
54
59
  return $this->order;
55
60
  }
56
61
 
62
+ public ?FleetOpsApiOrderPayloadFake $switchedPayload = null;
63
+
64
+ protected function findPayloadByUuid(?string $uuid): ?Payload
65
+ {
66
+ return $this->switchedPayload;
67
+ }
68
+
69
+ protected function beginOrderEdit(Order $order): OrderEdit
70
+ {
71
+ $this->edit ??= new FleetOpsApiOrderEditFake($order);
72
+ $this->edit->lockWith = $this->editLockedWith;
73
+
74
+ return $this->edit;
75
+ }
76
+
77
+ protected function lockedEditResponse(OrderEditLockedException $exception)
78
+ {
79
+ return ['apiError' => $exception->getMessage(), 'status' => 422, 'locked' => $exception->locked];
80
+ }
81
+
57
82
  protected function drivingDistanceAndTime(mixed $origin, mixed $destination): mixed
58
83
  {
59
84
  return $this->matrix ?? (object) ['distance' => 1200, 'time' => 360];
@@ -244,6 +269,43 @@ class FleetOpsApiOrderCrudControllerProbe extends OrderController
244
269
  }
245
270
  }
246
271
 
272
+ /**
273
+ * Records the edit guard and commit instead of reading and announcing through the database.
274
+ */
275
+ class FleetOpsApiOrderEditFake extends OrderEdit
276
+ {
277
+ public array $guarded = [];
278
+ public array $lockWith = [];
279
+ public int $commits = 0;
280
+
281
+ public function __construct(public Order $order)
282
+ {
283
+ $this->policy = OrderEditPolicy::for($order);
284
+ }
285
+
286
+ public function requestedChanges(array $input, string $entitiesMode = self::ENTITIES_APPEND): array
287
+ {
288
+ return ['input' => $input, 'entities_mode' => $entitiesMode];
289
+ }
290
+
291
+ public function guard(array $changes): static
292
+ {
293
+ $this->guarded[] = $changes;
294
+ if ($this->lockWith) {
295
+ throw new OrderEditLockedException($this->lockWith);
296
+ }
297
+
298
+ return $this;
299
+ }
300
+
301
+ public function commit(array $extra = [], bool $notifyDriver = true): array
302
+ {
303
+ $this->commits++;
304
+
305
+ return [];
306
+ }
307
+ }
308
+
247
309
  class FleetOpsApiOrderCrudFake extends Order
248
310
  {
249
311
  public array $lookups = [];
@@ -383,6 +445,16 @@ class FleetOpsApiOrderCrudFake extends Order
383
445
  return $this;
384
446
  }
385
447
 
448
+ public ?string $cancelReasonForTest = null;
449
+
450
+ public function cancelWithReason(?string $reason = null)
451
+ {
452
+ $this->cancelledForTest = true;
453
+ $this->cancelReasonForTest = $reason;
454
+
455
+ return $this;
456
+ }
457
+
386
458
  public function tracker(): OrderTracker
387
459
  {
388
460
  return $this->trackerFake;
@@ -447,6 +519,13 @@ class FleetOpsApiOrderPayloadFake extends Payload
447
519
  return $this;
448
520
  }
449
521
 
522
+ public function syncEntities($entities = []): array
523
+ {
524
+ $this->calls[] = ['sync-entities', $entities];
525
+
526
+ return ['created' => [], 'updated' => [], 'deleted' => []];
527
+ }
528
+
450
529
  public function setCurrentWaypoint(Place|Waypoint $destination, bool $save = true): Payload
451
530
  {
452
531
  $this->calls[] = ['current', $destination instanceof Place, $save];
@@ -753,6 +832,25 @@ test('api order controller updates by payload id and clears route waypoints', fu
753
832
  ->and($payload->removedWaypoints)->toBeTrue();
754
833
  });
755
834
 
835
+ test('api order controller moves the cash to collect onto the payload it switches to by id', function () {
836
+ $order = new FleetOpsApiOrderCrudFake();
837
+ $order->payload = new FleetOpsApiOrderPayloadFake();
838
+ $switched = new FleetOpsApiOrderPayloadFake();
839
+
840
+ $controller = new FleetOpsApiOrderCrudControllerProbe();
841
+ $controller->order = $order;
842
+ $controller->switchedPayload = $switched;
843
+
844
+ $controller->update('order-public', new FleetOpsApiOrderUpdateRequestFake([
845
+ 'payload' => 'payload_public',
846
+ 'cod_amount' => '1800',
847
+ ]));
848
+
849
+ expect($order->updates[0])->toMatchArray(['payload_uuid' => 'payloads-uuid'])
850
+ ->and($switched->getAttributes()['cod_amount'])->toBe(1800)
851
+ ->and($switched->savedForTest)->toBeTrue();
852
+ });
853
+
756
854
  test('api order controller finds deletes and updates distance matrices without database records', function () {
757
855
  $order = new FleetOpsApiOrderCrudFake();
758
856
  $controller = new FleetOpsApiOrderCrudControllerProbe();
@@ -1028,3 +1126,150 @@ test('api order controller captures QR proof payloads and validates error branch
1028
1126
  'status' => 400,
1029
1127
  ]);
1030
1128
  });
1129
+
1130
+ test('api order controller captures QR proof from tracking url codes and barcodes', function () {
1131
+ session(['company' => 'company-uuid']);
1132
+
1133
+ $subject = (object) [
1134
+ 'uuid' => 'subject-uuid',
1135
+ 'public_id' => 'waypoint_subject',
1136
+ 'trackingNumber' => (object) ['tracking_number' => 'ACM1234567890SG'],
1137
+ ];
1138
+
1139
+ // The url printed by current labels, from any host, and the Code 128 barcode.
1140
+ foreach ([
1141
+ 'https://console.fleetbase.test/track-order?order=ACM1234567890SG&r=waypoint_subject&v=1',
1142
+ 'https://old-host.example/track-order?order=ACM1234567890SG&r=waypoint_subject&v=1',
1143
+ 'ACM1234567890SG',
1144
+ ] as $code) {
1145
+ $controller = new FleetOpsApiOrderCrudControllerProbe();
1146
+ $controller->order = new FleetOpsApiOrderCrudFake();
1147
+ $controller->resolvedSubject = $subject;
1148
+
1149
+ expect($controller->captureQrScan(new Request(['code' => $code]), 'order-public', 'waypoint_subject')['resource'])->toBe('proof')
1150
+ ->and($controller->createdProofs[0]['subject_uuid'])->toBe('subject-uuid');
1151
+ }
1152
+
1153
+ // A scanner's whole result object, rather than its value, is refused as before
1154
+ // instead of failing the request.
1155
+ $controller = new FleetOpsApiOrderCrudControllerProbe();
1156
+ $controller->order = new FleetOpsApiOrderCrudFake();
1157
+ $controller->resolvedSubject = $subject;
1158
+
1159
+ expect($controller->captureQrScan(new Request(['code' => ['type' => 'qr', 'value' => 'ACM1234567890SG']]), 'order-public', 'waypoint_subject'))->toBe([
1160
+ 'apiError' => 'Unable to validate QR code data.',
1161
+ 'status' => 400,
1162
+ ]);
1163
+
1164
+ // Another parcel's label is refused, whether it names another owner or, naming none,
1165
+ // another tracking number.
1166
+ foreach ([
1167
+ 'https://console.fleetbase.test/track-order?order=ACM1234567890SG&r=waypoint_other&v=1',
1168
+ 'https://console.fleetbase.test/track-order?order=ACM0000000000SG&v=1',
1169
+ ] as $code) {
1170
+ $controller = new FleetOpsApiOrderCrudControllerProbe();
1171
+ $controller->order = new FleetOpsApiOrderCrudFake();
1172
+ $controller->resolvedSubject = $subject;
1173
+
1174
+ expect($controller->captureQrScan(new Request(['code' => $code]), 'order-public', 'waypoint_subject'))->toBe([
1175
+ 'apiError' => 'Unable to validate QR code data.',
1176
+ 'status' => 400,
1177
+ ])->and($controller->createdProofs)->toBe([]);
1178
+ }
1179
+ });
1180
+
1181
+ test('api order controller refuses order edits the status locks with a 422 and writes nothing', function () {
1182
+ $order = new FleetOpsApiOrderCrudFake(['status' => 'completed']);
1183
+ $payload = new FleetOpsApiOrderPayloadFake();
1184
+ $order->payload = $payload;
1185
+
1186
+ $controller = new FleetOpsApiOrderCrudControllerProbe();
1187
+ $controller->order = $order;
1188
+ $controller->editLockedWith = ['dropoff' => 'The order is completed and can no longer be changed.'];
1189
+
1190
+ $response = $controller->update('order-public', new FleetOpsApiOrderUpdateRequestFake([
1191
+ 'payload' => ['dropoff' => ['name' => 'Somewhere else']],
1192
+ ]));
1193
+
1194
+ expect($response)->toBe([
1195
+ 'apiError' => 'The order is completed and can no longer be changed.',
1196
+ 'status' => 422,
1197
+ 'locked' => ['dropoff' => 'The order is completed and can no longer be changed.'],
1198
+ ])
1199
+ ->and($payload->calls)->toBe([])
1200
+ ->and($payload->savedForTest)->toBeFalse()
1201
+ ->and($order->updates)->toBe([])
1202
+ ->and($controller->edit->commits)->toBe(0);
1203
+ });
1204
+
1205
+ test('api order controller passes the edit to the guard, keeps cod 0, replaces entities and commits once', function () {
1206
+ $order = new FleetOpsApiOrderCrudFake(['status' => 'dispatched']);
1207
+ $payload = new FleetOpsApiOrderPayloadFake();
1208
+ $order->payload = $payload;
1209
+
1210
+ $controller = new FleetOpsApiOrderCrudControllerProbe();
1211
+ $controller->order = $order;
1212
+
1213
+ $controller->update('order-public', new FleetOpsApiOrderUpdateRequestFake([
1214
+ 'payload' => [
1215
+ 'dropoff' => ['name' => 'New drop'],
1216
+ 'entities' => [['id' => 'entity_keep', 'name' => 'Latte']],
1217
+ 'entities_mode' => 'replace',
1218
+ 'cod_amount' => 0,
1219
+ ],
1220
+ 'notes' => 'Leave at the door',
1221
+ ]));
1222
+
1223
+ expect($controller->edit->guarded)->toBe([[
1224
+ 'input' => [
1225
+ 'dropoff' => ['name' => 'New drop'],
1226
+ 'entities' => [['id' => 'entity_keep', 'name' => 'Latte']],
1227
+ 'notes' => 'Leave at the door',
1228
+ 'cod_amount' => 0,
1229
+ ],
1230
+ 'entities_mode' => 'replace',
1231
+ ]])
1232
+ ->and($payload->calls)->toContain(['sync-entities', [['id' => 'entity_keep', 'name' => 'Latte']]])
1233
+ ->and(collect($payload->calls)->where(0, 'entities')->all())->toBe([])
1234
+ ->and($payload->getAttributes()['cod_amount'])->toBe(0)
1235
+ ->and(collect($payload->calls)->where(0, 'current')->all())->toBe([])
1236
+ ->and($controller->edit->commits)->toBe(1);
1237
+ });
1238
+
1239
+ test('api order controller reads a top-level cod amount and appends entities by default', function () {
1240
+ $order = new FleetOpsApiOrderCrudFake();
1241
+ $payload = new FleetOpsApiOrderPayloadFake();
1242
+ $order->payload = $payload;
1243
+
1244
+ $controller = new FleetOpsApiOrderCrudControllerProbe();
1245
+ $controller->order = $order;
1246
+
1247
+ $controller->update('order-public', new FleetOpsApiOrderUpdateRequestFake([
1248
+ 'entities' => [['name' => 'Crate']],
1249
+ 'cod_amount' => '2500',
1250
+ ]));
1251
+
1252
+ expect($payload->calls)->toContain(['entities', [['name' => 'Crate']]])
1253
+ ->and($payload->getAttributes()['cod_amount'])->toBe(2500)
1254
+ ->and($controller->edit->guarded[0]['input'])->toMatchArray(['cod_amount' => '2500'])
1255
+ ->and($controller->edit->guarded[0]['entities_mode'])->toBe('append');
1256
+ });
1257
+
1258
+ test('api order controller cancels with a reason when one is given', function () {
1259
+ $order = new FleetOpsApiOrderCrudFake();
1260
+ $controller = new FleetOpsApiOrderCrudControllerProbe();
1261
+ $controller->order = $order;
1262
+
1263
+ $controller->cancelOrder('order-public', new Request(['reason' => ' Customer moved away ']));
1264
+
1265
+ expect($order->cancelledForTest)->toBeTrue()
1266
+ ->and($order->cancelReasonForTest)->toBe('Customer moved away');
1267
+
1268
+ $plain = new FleetOpsApiOrderCrudFake();
1269
+ $controller = new FleetOpsApiOrderCrudControllerProbe();
1270
+ $controller->order = $plain;
1271
+ $controller->cancelOrder('order-public', new Request());
1272
+
1273
+ expect($plain->cancelledForTest)->toBeTrue()
1274
+ ->and($plain->cancelReasonForTest)->toBeNull();
1275
+ });
@@ -3,9 +3,12 @@
3
3
  use Fleetbase\FleetOps\Http\Controllers\Api\v1\PayloadController;
4
4
  use Fleetbase\FleetOps\Http\Requests\CreatePayloadRequest;
5
5
  use Fleetbase\FleetOps\Http\Requests\UpdatePayloadRequest;
6
+ use Fleetbase\FleetOps\Models\Order;
6
7
  use Fleetbase\FleetOps\Models\Payload;
7
8
  use Fleetbase\FleetOps\Models\Place;
8
9
  use Fleetbase\FleetOps\Models\Waypoint;
10
+ use Fleetbase\FleetOps\Support\OrderEdit;
11
+ use Fleetbase\FleetOps\Support\OrderEditPolicy;
9
12
  use Illuminate\Database\Eloquent\ModelNotFoundException;
10
13
  use Illuminate\Http\Request;
11
14
 
@@ -124,6 +127,15 @@ class FleetOpsApiPayloadEndpointFake extends Payload
124
127
  return $this;
125
128
  }
126
129
 
130
+ public array $entitySyncs = [];
131
+
132
+ public function syncEntities($entities = []): array
133
+ {
134
+ $this->entitySyncs[] = $entities;
135
+
136
+ return ['created' => [], 'updated' => [], 'deleted' => []];
137
+ }
138
+
127
139
  public function getPickupOrFirstWaypoint(): ?Place
128
140
  {
129
141
  return $this->firstWaypoint;
@@ -312,3 +324,112 @@ test('api payload controller returns not found responses for missing payloads',
312
324
  'status' => 404,
313
325
  ]);
314
326
  })->with(['update', 'find', 'delete']);
327
+
328
+ /**
329
+ * A payload that belongs to an order: the order's edit is recorded instead of read from the database.
330
+ */
331
+ class FleetOpsApiPayloadOrderControllerProbe extends FleetOpsApiPayloadControllerProbe
332
+ {
333
+ public ?Order $order = null;
334
+
335
+ public ?FleetOpsApiPayloadOrderEditFake $edit = null;
336
+
337
+ protected function orderForPayload(Payload $payload): ?Order
338
+ {
339
+ return $this->order;
340
+ }
341
+
342
+ protected function beginOrderEdit(Order $order): OrderEdit
343
+ {
344
+ return $this->edit ??= new FleetOpsApiPayloadOrderEditFake($order);
345
+ }
346
+ }
347
+
348
+ class FleetOpsApiPayloadOrderEditFake extends OrderEdit
349
+ {
350
+ public array $requested = [];
351
+ public int $commits = 0;
352
+
353
+ public function __construct(public Order $order)
354
+ {
355
+ $this->policy = OrderEditPolicy::for($order);
356
+ }
357
+
358
+ public function requestedChanges(array $input, string $entitiesMode = self::ENTITIES_APPEND): array
359
+ {
360
+ $this->requested[] = [$input, $entitiesMode];
361
+
362
+ return parent::requestedChanges(array_diff_key($input, ['entities' => true, 'cod_amount' => true]), $entitiesMode)
363
+ + (isset($input['entities']) ? ['items' => ['adds' => $entitiesMode !== self::ENTITIES_REPLACE]] : []);
364
+ }
365
+
366
+ public function commit(array $extra = [], bool $notifyDriver = true): array
367
+ {
368
+ $this->commits++;
369
+
370
+ return [];
371
+ }
372
+ }
373
+
374
+ function fleetopsApiPayloadOrderController(string $status): FleetOpsApiPayloadOrderControllerProbe
375
+ {
376
+ $controller = new FleetOpsApiPayloadOrderControllerProbe();
377
+ $controller->payload = new FleetOpsApiPayloadEndpointFake();
378
+ $controller->payload->firstWaypoint = fleetopsApiPayloadPlace();
379
+ $controller->order = new Order();
380
+ $controller->order->setRawAttributes(['uuid' => 'order-uuid', 'public_id' => 'order_payload', 'status' => $status], true);
381
+
382
+ return $controller;
383
+ }
384
+
385
+ test('api payload controller keeps a cash to collect of zero and still skips empty values', function () {
386
+ $controller = fleetopsApiPayloadController();
387
+
388
+ $response = $controller->update('payload-public', fleetopsUpdatePayloadRequest([
389
+ 'cod_amount' => 0,
390
+ 'type' => '',
391
+ 'provider' => null,
392
+ ]));
393
+
394
+ // the first fill is the model constructor's
395
+ expect(end($response['payload']->filledPayloads))->toBe(['cod_amount' => 0])
396
+ ->and($response['payload']->getAttributes()['cod_amount'])->toBe(0);
397
+ });
398
+
399
+ test('api payload controller edits an order payload through the order edit', function () {
400
+ $controller = fleetopsApiPayloadOrderController('dispatched');
401
+
402
+ $response = $controller->update('payload-public', fleetopsUpdatePayloadRequest([
403
+ 'dropoff' => ['public_id' => 'dropoff-public'],
404
+ 'cod_amount' => 1500,
405
+ ]));
406
+
407
+ expect($response['payload']->dropoffAssignments)->toBe([['public_id' => 'dropoff-public']])
408
+ ->and($response['payload']->currentWaypointAssignments)->toBe([])
409
+ ->and($controller->edit->requested[0])->toBe([['dropoff' => ['public_id' => 'dropoff-public'], 'cod_amount' => 1500], 'append'])
410
+ ->and($controller->edit->commits)->toBe(1);
411
+ });
412
+
413
+ test('api payload controller replaces entities on request and refuses locked item edits', function () {
414
+ $controller = fleetopsApiPayloadOrderController('created');
415
+
416
+ $response = $controller->update('payload-public', fleetopsUpdatePayloadRequest([
417
+ 'entities' => [['id' => 'entity_keep']],
418
+ 'entities_mode' => 'replace',
419
+ ]));
420
+
421
+ expect($response['payload']->entitySyncs)->toBe([[['id' => 'entity_keep']]])
422
+ ->and($response['payload']->entityAssignments)->toBe([]);
423
+
424
+ $locked = fleetopsApiPayloadOrderController('dispatched');
425
+ $response = $locked->update('payload-public', fleetopsUpdatePayloadRequest([
426
+ 'entities' => [['name' => 'Extra']],
427
+ ]));
428
+
429
+ expect($response->getStatusCode())->toBe(422)
430
+ ->and($response->getData(true)['error'])->toBe('Items cannot be changed once a driver has the order.')
431
+ ->and($response->getData(true)['locked'])->toBe(['items' => 'Items cannot be changed once a driver has the order.'])
432
+ ->and($locked->payload->entityAssignments)->toBe([])
433
+ ->and($locked->payload->savedForTest)->toBeFalse()
434
+ ->and($locked->edit->commits)->toBe(0);
435
+ });
@@ -537,5 +537,5 @@ test('api place controller returns not found errors for missing places', functio
537
537
  ? $controller->update('missing-place', fleetopsUpdatePlaceRequest([]))
538
538
  : $controller->{$method}('missing-place', new Request());
539
539
 
540
- expect($response)->toBe(['apiError' => 'Place resource not found.', 'status' => 400]);
540
+ expect($response)->toBe(['apiError' => 'Place resource not found.', 'status' => 404]);
541
541
  })->with(['update', 'find', 'delete']);
@@ -68,9 +68,9 @@ class FleetOpsApiTrackingNumberControllerProbe extends TrackingNumberController
68
68
  return ['resource' => 'deleted-tracking-number', 'trackingNumber' => $trackingNumber];
69
69
  }
70
70
 
71
- protected function findQrModel(array $tables, array $where)
71
+ protected function findQrModel(string $code)
72
72
  {
73
- $this->qrLookups[] = [$tables, $where];
73
+ $this->qrLookups[] = $code;
74
74
 
75
75
  return $this->qrModel;
76
76
  }
@@ -206,15 +206,12 @@ test('api tracking number controller decodes qr models and reports missing value
206
206
  $controller = new FleetOpsApiTrackingNumberControllerProbe();
207
207
  $controller->qrModel = $model;
208
208
 
209
- $response = $controller->fromQR(fleetopsDecodeTrackingNumberQrRequest(['code' => 'order-uuid']));
209
+ $code = 'https://console.fleetbase.test/track-order?order=ACM1234567890SG&r=order_public&v=1';
210
+ $response = $controller->fromQR(fleetopsDecodeTrackingNumberQrRequest(['code' => $code]));
210
211
 
212
+ // The raw scanned value goes to the resolver, which understands every format.
211
213
  expect($response)->toBe(['resource' => 'qr-model', 'model' => $model])
212
- ->and($controller->qrLookups)->toBe([
213
- [
214
- ['entities', 'orders'],
215
- ['uuid' => 'order-uuid'],
216
- ],
217
- ]);
214
+ ->and($controller->qrLookups)->toBe([$code]);
218
215
 
219
216
  $controller = new FleetOpsApiTrackingNumberControllerProbe();
220
217