@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
@@ -370,8 +370,7 @@ test('order dispatch failed notification exposes channels array and mail payload
370
370
  ->and($mail->subject)->toBe('Order TN-FAILED dispatch has failed!')
371
371
  ->and($mail->introLines)->toBe(['No driver accepted the order'])
372
372
  ->and($mail->actionText)->toBe('Track Order')
373
- ->and($mail->actionUrl)->toContain('track-order')
374
- ->and($mail->actionUrl)->toContain('TN-FAILED');
373
+ ->and($mail->actionUrl)->toEndWith('/~/track/TN-FAILED');
375
374
  });
376
375
 
377
376
  test('order dispatched notification exposes channels array and mail payloads', function () {
@@ -449,8 +448,7 @@ test('order assigned notification handles scheduled and unscheduled contracts',
449
448
  ->and($mail->subject)->toBe('New order TN-ASSIGNED assigned!')
450
449
  ->and($mail->introLines)->toBe(['You have a new order assigned, tap for details.'])
451
450
  ->and($mail->actionText)->toBe('Track Order')
452
- ->and($mail->actionUrl)->toContain('track-order')
453
- ->and($mail->actionUrl)->toContain('TN-ASSIGNED');
451
+ ->and($mail->actionUrl)->toEndWith('/~/track/TN-ASSIGNED');
454
452
 
455
453
  $scheduledOrder = notificationTestOrderWithRelations(['scheduled_at' => '2026-08-01 10:30:00']);
456
454
  $scheduled = new OrderAssigned($scheduledOrder);
@@ -545,7 +543,7 @@ test('terminal order notifications expose shared broadcast and array payload con
545
543
  'event' => 'order.canceled_notification',
546
544
  'title' => 'Order WP-TRACK-123 was canceled',
547
545
  'body' => 'Order WP-TRACK-123 has been canceled. customer requested cancel',
548
- 'data' => ['id' => 'order_public', 'type' => 'order_canceled'],
546
+ 'data' => ['id' => 'order_public', 'type' => 'order_canceled', 'reason' => 'customer requested cancel'],
549
547
  ])
550
548
  ->and($failed->toArray())->toMatchArray([
551
549
  'event' => 'order.failed_notification',
@@ -599,8 +597,7 @@ test('waypoint completed notification exposes channels array and mail payloads',
599
597
  'No further action is necessary.',
600
598
  ])
601
599
  ->and($mail->actionText)->toBe('Track Order')
602
- ->and($mail->actionUrl)->toContain('track-order')
603
- ->and($mail->actionUrl)->toContain('WP-COMPLETE');
600
+ ->and($mail->actionUrl)->toEndWith('/~/track/WP-COMPLETE');
604
601
  });
605
602
 
606
603
  test('work order dispatched mail exposes subject and markdown context', function () {
@@ -704,5 +701,5 @@ test('customer credentials mail reads the portal slug from the customer company
704
701
 
705
702
  $content = fleetOpsNotificationWithEnvironment(fn () => (new CustomerCredentialsMail('plain-secret', $customer))->content());
706
703
 
707
- expect($content->with['customerPortalUrl'])->toBe('https://console.fleetbase.test/acme-portal');
704
+ expect($content->with['customerPortalUrl'])->toBe('https://console.fleetbase.test/customer-access/acme-portal');
708
705
  })->with([null, 'other-company']);
@@ -286,7 +286,7 @@ class FleetOpsVehicleObserverProbe extends VehicleObserver
286
286
  return $this->driver && $identifier === $this->identifier ? $this->driver : null;
287
287
  }
288
288
 
289
- protected function deleteDriversAssignedTo(Vehicle $vehicle): mixed
289
+ protected function unassignDriversFrom(Vehicle $vehicle): int
290
290
  {
291
291
  $this->deleted[] = $vehicle->uuid;
292
292
 
@@ -397,11 +397,16 @@ class FleetOpsTrackingNumberObserverProbe extends TrackingNumberObserver
397
397
  return 'TN-' . $trackingNumber->region;
398
398
  }
399
399
 
400
- protected function generateBarcode(string $ownerUuid, string $type): string
400
+ protected function generateCodeImages(string $trackingNumber, ?string $ownerPublicId): array
401
401
  {
402
- $this->barcodes[] = [$ownerUuid, $type];
402
+ $this->barcodes[] = [$trackingNumber, $ownerPublicId];
403
403
 
404
- return $type . '-png';
404
+ return ['qr_code' => 'qr-png', 'barcode' => 'barcode-png'];
405
+ }
406
+
407
+ protected function ownerPublicId(TrackingNumber $trackingNumber): ?string
408
+ {
409
+ return $trackingNumber->owner_uuid ? 'order_public' : null;
405
410
  }
406
411
 
407
412
  protected function createTrackingStatus(array $attributes): TrackingStatus
@@ -531,12 +536,12 @@ class FleetOpsCompanyUserObserverProbe extends CompanyUserObserver
531
536
  public array $deleted = [];
532
537
  public ?Driver $driver = null;
533
538
 
534
- protected function deleteDrivers(string $userUuid): void
539
+ protected function deleteDrivers(string $userUuid, ?string $companyUuid): void
535
540
  {
536
541
  $this->deleted[] = $userUuid;
537
542
  }
538
543
 
539
- protected function findDriver(string $userUuid): ?Driver
544
+ protected function findDriver(string $userUuid, ?string $companyUuid): ?Driver
540
545
  {
541
546
  return $this->driver;
542
547
  }
@@ -1077,6 +1082,8 @@ test('order observer ignores non dispatched start transitions', function () {
1077
1082
  test('order observer invalidates cache on delete and forwards to integrated vendors', function () {
1078
1083
  Cache::swap(new Repository(new ArrayStore()));
1079
1084
  session(['company' => 'company-uuid']);
1085
+ // Deleting cascades to the order's children, so the queries need a database.
1086
+ Fleetbase\Tests\Support\TrackingPageDatabase::boot();
1080
1087
 
1081
1088
  $order = new FleetOpsOrderObserverOrderFake();
1082
1089
  $order->uuid = 'order-deleted-uuid';
@@ -1095,6 +1102,8 @@ test('order observer invalidates cache on delete and forwards to integrated vend
1095
1102
  $integrated->integratedVendor = true;
1096
1103
 
1097
1104
  expect(fn () => (new OrderObserver())->deleted($integrated))->toThrow(Error::class);
1105
+
1106
+ Illuminate\Database\Eloquent\Model::unsetConnectionResolver();
1098
1107
  });
1099
1108
 
1100
1109
  test('contact observer creates syncs normalizes and deletes associated users', function () {
@@ -1292,11 +1301,12 @@ test('tracking number observer generates codes and creates initial tracking stat
1292
1301
  $observer->created($trackingNumber);
1293
1302
 
1294
1303
  expect($trackingNumber->tracking_number)->toBe('TN-sg')
1295
- ->and($trackingNumber->qr_code)->toBe('QRCODE-png')
1296
- ->and($trackingNumber->barcode)->toBe('PDF417-png')
1304
+ ->and($trackingNumber->qr_code)->toBe('qr-png')
1305
+ ->and($trackingNumber->barcode)->toBe('barcode-png')
1306
+ // Codes are generated from the new tracking number and the owner's public id,
1307
+ // never from the owner's uuid.
1297
1308
  ->and($observer->barcodes)->toBe([
1298
- ['owner-uuid', 'QRCODE'],
1299
- ['owner-uuid', 'PDF417'],
1309
+ ['TN-sg', 'order_public'],
1300
1310
  ])
1301
1311
  ->and($observer->statuses[0])->toMatchArray([
1302
1312
  'company_uuid' => 'company-uuid',
@@ -1309,6 +1319,58 @@ test('tracking number observer generates codes and creates initial tracking stat
1309
1319
  ->and($trackingNumber->ownerStatuses)->toHaveCount(1);
1310
1320
  });
1311
1321
 
1322
+ test('tracking number observer reads the owner public id and renders codes through the model', function () {
1323
+ $observer = new TrackingNumberObserver();
1324
+ $owner = new ReflectionMethod($observer, 'ownerPublicId');
1325
+ $images = new ReflectionMethod($observer, 'generateCodeImages');
1326
+ $owner->setAccessible(true);
1327
+ $images->setAccessible(true);
1328
+
1329
+ // No owner: nothing to look up.
1330
+ expect($owner->invoke($observer, new TrackingNumber()))->toBeNull();
1331
+
1332
+ $order = new Order();
1333
+ $order->setRawAttributes(['uuid' => 'owner-uuid', 'public_id' => 'order_public'], true);
1334
+ $trackingNumber = new TrackingNumber();
1335
+ $trackingNumber->setRawAttributes(['owner_uuid' => 'owner-uuid', 'owner_type' => Order::class], true);
1336
+ $trackingNumber->setRelation('owner', $order);
1337
+
1338
+ expect($owner->invoke($observer, $trackingNumber))->toBe('order_public');
1339
+
1340
+ // An owner without a public id yields none rather than an empty `r` parameter.
1341
+ $order->setRawAttributes(['uuid' => 'owner-uuid', 'public_id' => ''], true);
1342
+ expect($owner->invoke($observer, $trackingNumber))->toBeNull();
1343
+
1344
+ $previousConsole = config('fleetbase.console');
1345
+ config(['fleetbase.console' => ['host' => 'console.fleetbase.test', 'secure' => true]]);
1346
+ foreach (['DNS2D', 'DNS1D'] as $facade) {
1347
+ app()->instance($facade, new class($facade) {
1348
+ public function __construct(private string $facade)
1349
+ {
1350
+ }
1351
+
1352
+ public function getBarcodePNG($code, $type, ...$size)
1353
+ {
1354
+ return $this->facade . ':' . $type . ':' . $code;
1355
+ }
1356
+ });
1357
+ Illuminate\Support\Facades\Facade::clearResolvedInstance($facade);
1358
+ }
1359
+
1360
+ try {
1361
+ expect($images->invoke($observer, 'TN-sg', 'order_public'))->toBe([
1362
+ 'qr_code' => 'DNS2D:QRCODE,M:https://console.fleetbase.test/~/track-order?order=TN-sg&r=order_public&v=1',
1363
+ 'barcode' => 'DNS1D:C128:TN-sg',
1364
+ ]);
1365
+ } finally {
1366
+ config(['fleetbase.console' => $previousConsole]);
1367
+ foreach (['DNS2D', 'DNS1D'] as $facade) {
1368
+ app()->forgetInstance($facade);
1369
+ Illuminate\Support\Facades\Facade::clearResolvedInstance($facade);
1370
+ }
1371
+ }
1372
+ });
1373
+
1312
1374
  test('zone observer invalidates service area cache for lifecycle events and original service area', function () {
1313
1375
  $zone = new Zone();
1314
1376
  $zone->setRawAttributes([
@@ -20,7 +20,8 @@ use Fleetbase\FleetOps\Models\Waypoint;
20
20
  use Fleetbase\FleetOps\Support\ResolvesOrderServiceStops;
21
21
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
22
22
 
23
- class ActivityFlowRelationCountFake
23
+ // Extends the relation the model method now declares, so the override stays compatible; the parent constructor is skipped on purpose.
24
+ class ActivityFlowRelationCountFake extends Illuminate\Database\Eloquent\Relations\HasManyThrough
24
25
  {
25
26
  public function __construct(private int $count)
26
27
  {
@@ -67,7 +68,7 @@ class ActivityFlowPayloadFake extends Payload
67
68
  return $this;
68
69
  }
69
70
 
70
- public function waypoints()
71
+ public function waypoints(): Illuminate\Database\Eloquent\Relations\HasManyThrough
71
72
  {
72
73
  return new ActivityFlowRelationCountFake($this->waypoints?->count() ?? 0);
73
74
  }
@@ -0,0 +1,120 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\OrderConfig;
4
+
5
+ /**
6
+ * Opt-in configured lifecycles (`meta.lifecycle`): a config can define its own initial, completed,
7
+ * canceled and terminal activities, disable dispatch and require configured transitions, without
8
+ * changing the default dispatch lifecycle of configs that do not opt in.
9
+ */
10
+ function lifecycleActivity(string $code, array $children = [], bool $complete = false): array
11
+ {
12
+ return ['key' => $code, 'code' => $code, 'status' => ucfirst(str_replace('_', ' ', $code)), 'details' => '', 'activities' => $children, 'logic' => [], 'events' => [], 'complete' => $complete];
13
+ }
14
+
15
+ function customLifecycleConfig(array $lifecycle = []): OrderConfig
16
+ {
17
+ $config = new OrderConfig();
18
+ $config->setRawAttributes([
19
+ 'flow' => json_encode([
20
+ 'requested' => lifecycleActivity('requested', ['assigned', 'cancelled']),
21
+ 'assigned' => lifecycleActivity('assigned', ['handed_over', 'cancelled']),
22
+ 'handed_over' => lifecycleActivity('handed_over', ['active']),
23
+ 'active' => lifecycleActivity('active', ['extended', 'returned']),
24
+ 'extended' => lifecycleActivity('extended', ['active']),
25
+ 'returned' => lifecycleActivity('returned', ['completed']),
26
+ 'completed' => lifecycleActivity('completed', [], true),
27
+ 'cancelled' => lifecycleActivity('cancelled'),
28
+ ]),
29
+ 'meta' => json_encode(['lifecycle' => array_replace([
30
+ 'initial' => 'requested',
31
+ 'completed' => 'completed',
32
+ 'canceled' => 'cancelled',
33
+ 'terminal' => ['completed', 'cancelled'],
34
+ 'dispatch' => false,
35
+ 'strict_transitions' => true,
36
+ ], $lifecycle)]),
37
+ ]);
38
+
39
+ return $config;
40
+ }
41
+
42
+ function defaultLifecycleConfig(): OrderConfig
43
+ {
44
+ $config = new OrderConfig();
45
+ $config->setRawAttributes([
46
+ 'flow' => json_encode([
47
+ 'created' => lifecycleActivity('created', ['dispatched']),
48
+ 'dispatched' => lifecycleActivity('dispatched', ['started']),
49
+ 'started' => lifecycleActivity('started', ['completed']),
50
+ 'completed' => lifecycleActivity('completed', [], true),
51
+ ]),
52
+ 'meta' => json_encode([]),
53
+ ]);
54
+
55
+ return $config;
56
+ }
57
+
58
+ test('configs without meta.lifecycle keep the default dispatch semantics', function () {
59
+ $config = defaultLifecycleConfig();
60
+
61
+ expect($config->hasConfiguredLifecycle())->toBeFalse()
62
+ ->and($config->getInitialStatusCode())->toBe('created')
63
+ ->and($config->allowsDispatch())->toBeTrue()
64
+ ->and($config->hasStrictTransitions())->toBeFalse()
65
+ ->and($config->getTerminalActivityCodes())->toBe(['completed', 'canceled'])
66
+ ->and($config->getCanceledActivity()->code)->toBe('canceled')
67
+ ->and($config->getInitialActivity()->code)->toBe('created');
68
+ });
69
+
70
+ test('a configured lifecycle names its own initial, cancel and completion activities', function () {
71
+ $config = customLifecycleConfig();
72
+
73
+ expect($config->hasConfiguredLifecycle())->toBeTrue()
74
+ ->and($config->getInitialStatusCode())->toBe('requested')
75
+ ->and($config->getInitialActivity()->code)->toBe('requested')
76
+ ->and($config->getCanceledActivity()->code)->toBe('cancelled')
77
+ ->and($config->getCompletedActivity()->code)->toBe('completed')
78
+ ->and($config->allowsDispatch())->toBeFalse()
79
+ ->and($config->hasStrictTransitions())->toBeTrue()
80
+ ->and($config->isTerminalActivityCode('cancelled'))->toBeTrue()
81
+ ->and($config->isTerminalActivityCode('canceled'))->toBeFalse();
82
+ });
83
+
84
+ test('transitions follow only the configured graph', function () {
85
+ $config = customLifecycleConfig();
86
+
87
+ expect($config->canTransition('requested', 'assigned'))->toBeTrue()
88
+ ->and($config->canTransition('handed_over', 'active'))->toBeTrue()
89
+ ->and($config->canTransition('active', 'extended'))->toBeTrue()
90
+ ->and($config->canTransition('extended', 'active'))->toBeTrue()
91
+ ->and($config->canTransition('returned', 'completed'))->toBeTrue()
92
+ // skipped or foreign steps
93
+ ->and($config->canTransition('requested', 'handed_over'))->toBeFalse()
94
+ ->and($config->canTransition('requested', 'dispatched'))->toBeFalse()
95
+ ->and($config->canTransition('active', 'cancelled'))->toBeFalse()
96
+ // terminal and unknown sources
97
+ ->and($config->canTransition('completed', 'requested'))->toBeFalse()
98
+ ->and($config->canTransition('cancelled', 'assigned'))->toBeFalse()
99
+ ->and($config->canTransition(null, 'requested'))->toBeFalse()
100
+ ->and($config->canTransition('nope', 'assigned'))->toBeFalse();
101
+ });
102
+
103
+ test('a configuration-only change (added activity) is honoured without code changes', function () {
104
+ $config = customLifecycleConfig();
105
+ $flow = json_decode($config->getAttributes()['flow'], true);
106
+ $flow['active']['activities'][] = 'inspection';
107
+ $flow['inspection'] = lifecycleActivity('inspection', ['returned']);
108
+ $config->setRawAttributes(['flow' => json_encode($flow), 'meta' => $config->getAttributes()['meta']]);
109
+
110
+ expect($config->canTransition('active', 'inspection'))->toBeTrue()
111
+ ->and($config->canTransition('inspection', 'returned'))->toBeTrue()
112
+ ->and($config->canTransition('inspection', 'completed'))->toBeFalse();
113
+ });
114
+
115
+ test('dispatch can be disabled with string or numeric falsy values', function () {
116
+ foreach ([false, 'false', 0, '0'] as $value) {
117
+ expect(customLifecycleConfig(['dispatch' => $value])->allowsDispatch())->toBeFalse();
118
+ }
119
+ expect(customLifecycleConfig(['dispatch' => true])->allowsDispatch())->toBeTrue();
120
+ });
@@ -124,6 +124,173 @@ function fleetopsOrderFilter(FleetOpsRecordingOrderFilterBuilder $builder, array
124
124
  return $filter;
125
125
  }
126
126
 
127
+ /**
128
+ * A filter whose request resolves to an internal console route.
129
+ *
130
+ * `Http::isInternalRequest()` reads the resolved route's uri, so the console's
131
+ * uuid pass-through is only reachable through a route resolver.
132
+ */
133
+ function fleetopsInternalOrderFilter(FleetOpsRecordingOrderFilterBuilder $builder): OrderFilter
134
+ {
135
+ $uri = 'int/v1/fleet-ops/orders';
136
+ $request = Request::create('/' . $uri, 'GET');
137
+ $session = app('session.store');
138
+ $session->put('company', 'company_test');
139
+ $request->setLaravelSession($session);
140
+ $request->setRouteResolver(fn () => new class($uri) {
141
+ public array $action = [];
142
+
143
+ public function __construct(private string $uri)
144
+ {
145
+ }
146
+
147
+ public function uri(): string
148
+ {
149
+ return $this->uri;
150
+ }
151
+ });
152
+
153
+ $filter = new OrderFilter($request);
154
+ $reflection = new ReflectionClass($filter);
155
+ $property = $reflection->getParentClass()->getProperty('builder');
156
+ $property->setAccessible(true);
157
+ $property->setValue($filter, $builder);
158
+
159
+ return $filter;
160
+ }
161
+
162
+ /**
163
+ * Customer and facilitator filters resolve public ids to uuids, which is a real query.
164
+ *
165
+ * @return array<string, array<string, string>>
166
+ */
167
+ function fleetopsOrderFilterRelationDatabase(): array
168
+ {
169
+ $connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
170
+ $resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
171
+ $resolver->setDefaultConnection('mysql');
172
+ Illuminate\Database\Eloquent\Model::setConnectionResolver($resolver);
173
+ app()->instance('db', new class($connection) {
174
+ public function __construct(public Illuminate\Database\SQLiteConnection $c)
175
+ {
176
+ }
177
+
178
+ public function connection($name = null)
179
+ {
180
+ return $this->c;
181
+ }
182
+
183
+ public function __call($method, $arguments)
184
+ {
185
+ return $this->c->{$method}(...$arguments);
186
+ }
187
+ });
188
+ Illuminate\Support\Facades\DB::clearResolvedInstance('db');
189
+
190
+ $schema = $connection->getSchemaBuilder();
191
+ foreach (['vendors', 'integrated_vendors', 'contacts'] as $table) {
192
+ $schema->create($table, function ($blueprint) {
193
+ $blueprint->increments('id');
194
+ foreach (['uuid', 'public_id', 'internal_id', 'company_uuid', 'name', '_key'] as $column) {
195
+ $blueprint->string($column)->nullable();
196
+ }
197
+ $blueprint->timestamps();
198
+ $blueprint->timestamp('deleted_at')->nullable();
199
+ });
200
+ }
201
+
202
+ $rows = [
203
+ 'vendors' => ['uuid' => '77777777-7777-4777-8777-777777777701', 'public_id' => 'vendor_ofilterone', 'internal_id' => 'VND-1'],
204
+ 'integrated_vendors' => ['uuid' => '77777777-7777-4777-8777-777777777702', 'public_id' => 'integrated_vendor_ofilter'],
205
+ 'contacts' => ['uuid' => '77777777-7777-4777-8777-777777777703', 'public_id' => 'contact_ofilterone'],
206
+ ];
207
+
208
+ foreach ($rows as $table => $row) {
209
+ $connection->table($table)->insert($row + ['company_uuid' => 'company_test']);
210
+ }
211
+
212
+ // Another tenant's vendor is never reachable by its public id.
213
+ $connection->table('vendors')->insert(['uuid' => '77777777-7777-4777-8777-777777777709', 'public_id' => 'vendor_otherco', 'company_uuid' => 'another-company']);
214
+
215
+ return $rows;
216
+ }
217
+
218
+ /**
219
+ * @return array<int, array<int, string>>
220
+ */
221
+ function fleetopsOrderFilterWhereInValues(FleetOpsRecordingOrderFilterBuilder $builder, string $column): array
222
+ {
223
+ return collect($builder->methodCalls('whereIn'))
224
+ ->filter(fn ($call) => $call[1] === $column)
225
+ ->map(fn ($call) => $call[2])
226
+ ->values()
227
+ ->all();
228
+ }
229
+
230
+ test('order filter resolves facilitator public ids across vendors integrated vendors and contacts', function () {
231
+ $rows = fleetopsOrderFilterRelationDatabase();
232
+ $builder = new FleetOpsRecordingOrderFilterBuilder();
233
+ $filter = fleetopsOrderFilter($builder);
234
+
235
+ $filter->facilitator($rows['vendors']['public_id']);
236
+ $filter->facilitator($rows['vendors']['internal_id']);
237
+ $filter->facilitator($rows['integrated_vendors']['public_id']);
238
+ $filter->facilitator($rows['contacts']['public_id']);
239
+ // The public API never accepts a raw uuid, an unknown id, or another tenant's id.
240
+ $filter->facilitator($rows['vendors']['uuid']);
241
+ $filter->facilitator('vendor_missing');
242
+ $filter->facilitator('vendor_otherco');
243
+
244
+ // The filter used to compare the public id against `facilitator_uuid`
245
+ // directly, which could never match. An id that resolves to nothing now
246
+ // yields an empty list, so it matches no order rather than every order.
247
+ expect(fleetopsOrderFilterWhereInValues($builder, 'facilitator_uuid'))->toBe([
248
+ [$rows['vendors']['uuid']],
249
+ [$rows['vendors']['uuid']],
250
+ [$rows['integrated_vendors']['uuid']],
251
+ [$rows['contacts']['uuid']],
252
+ [],
253
+ [],
254
+ [],
255
+ ]);
256
+ });
257
+
258
+ test('order filter resolves customer public ids and keeps the customer portal user match', function () {
259
+ $rows = fleetopsOrderFilterRelationDatabase();
260
+ $builder = new FleetOpsRecordingOrderFilterBuilder();
261
+ $filter = fleetopsOrderFilter($builder);
262
+
263
+ $filter->customer($rows['contacts']['public_id']);
264
+ $filter->customer($rows['vendors']['public_id']);
265
+ $filter->customer('portal-user-uuid');
266
+
267
+ $userMatches = collect($builder->methodCalls('orWhereHas'))
268
+ ->filter(fn ($call) => $call[1] === 'authenticatableCustomer')
269
+ ->map(fn ($call) => $call[2]->calls)
270
+ ->values()
271
+ ->all();
272
+
273
+ expect(fleetopsOrderFilterWhereInValues($builder, 'customer_uuid'))->toBe([
274
+ [$rows['contacts']['uuid']],
275
+ [$rows['vendors']['uuid']],
276
+ [],
277
+ ])
278
+ // A customer portal user still reaches their orders by user uuid.
279
+ ->and($userMatches[2])->toBe([['where', 'user_uuid', ['portal-user-uuid']]]);
280
+ });
281
+
282
+ test('order filter keeps console uuids working for customer and facilitator', function () {
283
+ $rows = fleetopsOrderFilterRelationDatabase();
284
+ $builder = new FleetOpsRecordingOrderFilterBuilder();
285
+ $filter = fleetopsInternalOrderFilter($builder);
286
+
287
+ $filter->customer($rows['contacts']['uuid']);
288
+ $filter->facilitator($rows['integrated_vendors']['uuid']);
289
+
290
+ expect(fleetopsOrderFilterWhereInValues($builder, 'customer_uuid'))->toBe([[$rows['contacts']['uuid']]])
291
+ ->and(fleetopsOrderFilterWhereInValues($builder, 'facilitator_uuid'))->toBe([[$rows['integrated_vendors']['uuid']]]);
292
+ });
293
+
127
294
  test('order filter applies internal and public base scopes with eager loading', function () {
128
295
  $builder = new FleetOpsRecordingOrderFilterBuilder();
129
296
  $filter = fleetopsOrderFilter($builder);
@@ -161,9 +328,7 @@ test('order filter identity and relation filters support uuid and public identif
161
328
 
162
329
  $filter->status('active');
163
330
  $filter->status(['created', 'started']);
164
- $filter->customer('customer_uuid');
165
331
  $filter->authenticatedCustomer('user_uuid');
166
- $filter->facilitator('facilitator_uuid');
167
332
  $filter->type('transport');
168
333
  $filter->orderConfig('transport');
169
334
  $filter->payload($uuid);
@@ -222,3 +222,23 @@ test('process allocation job commits assignments and skips missing or already as
222
222
  '[ProcessAllocationJob] Committed 4 assignments, 1 unassigned for company company-assign.',
223
223
  ]);
224
224
  });
225
+
226
+ test('process allocation job logs when the selected engine was replaced by the fallback', function () {
227
+ $fallback = new class(['assignments' => [], 'unassigned' => ['order_one'], 'summary' => []]) extends FleetOpsProcessAllocationEngineFake {
228
+ public function getIdentifier(): string
229
+ {
230
+ return 'greedy';
231
+ }
232
+ };
233
+
234
+ $job = new FleetOpsProcessAllocationJobProbe('company-fallback');
235
+ $job->engine = 'vroom';
236
+ $job->orders = collect([fleetOpsProcessAllocationOrder('order_one')]);
237
+ $job->vehicles = collect([(object) ['public_id' => 'vehicle_one']]);
238
+
239
+ $job->handle(fleetOpsProcessAllocationRegistry($fallback));
240
+
241
+ expect($fallback->calls)->toHaveCount(1)
242
+ ->and($job->messages[0])->toBe('[ProcessAllocationJob] Used the greedy engine instead of vroom for company company-fallback: No orchestration engine registered with identifier \'vroom\'. Available engines: greedy')
243
+ ->and($job->messages[1])->toContain('Committed 0 assignments, 1 unassigned');
244
+ });