@fleetbase/fleetops-engine 0.6.70 → 0.6.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (510) hide show
  1. package/RELEASE.md +18 -47
  2. package/addon/components/admin/tracking-page-settings.hbs +56 -0
  3. package/addon/components/admin/tracking-page-settings.js +67 -0
  4. package/addon/components/device/form.hbs +2 -2
  5. package/addon/components/driver/details.hbs +1 -1
  6. package/addon/components/driver/form.hbs +1 -1
  7. package/addon/components/equipment/form.hbs +1 -1
  8. package/addon/components/layout/fleet-ops-sidebar.js +11 -3
  9. package/addon/components/maintenance/form.hbs +3 -3
  10. package/addon/components/map/google-live-map.js +2 -2
  11. package/addon/components/map/leaflet-live-map.js +45 -32
  12. package/addon/components/orchestrator/phase-builder.js +2 -1
  13. package/addon/components/orchestrator-workbench.hbs +7 -1
  14. package/addon/components/orchestrator-workbench.js +16 -2
  15. package/addon/components/order/details/activity.hbs +8 -6
  16. package/addon/components/order/details/activity.js +9 -0
  17. package/addon/components/order/details.hbs +29 -0
  18. package/addon/components/order/details.js +26 -0
  19. package/addon/components/order/form/details.hbs +30 -4
  20. package/addon/components/order/form/details.js +46 -0
  21. package/addon/components/order/form/route.hbs +5 -3
  22. package/addon/components/order/form/route.js +24 -1
  23. package/addon/components/order/form.hbs +12 -0
  24. package/addon/components/order/form.js +21 -1
  25. package/addon/components/order/kanban.js +72 -0
  26. package/addon/components/order-config-manager/activity-flow.js +45 -10
  27. package/addon/components/order-config-manager/lifecycle.hbs +250 -0
  28. package/addon/components/order-config-manager/lifecycle.js +358 -0
  29. package/addon/components/order-config-manager.js +5 -1
  30. package/addon/components/service-rate/details.hbs +1 -1
  31. package/addon/components/service-rate/form.hbs +1 -1
  32. package/addon/components/tracking-page/full-view.hbs +276 -0
  33. package/addon/components/tracking-page/full-view.js +474 -0
  34. package/addon/components/tracking-page/locale-selector.js +30 -0
  35. package/addon/components/tracking-page/map.hbs +44 -0
  36. package/addon/components/tracking-page/map.js +328 -0
  37. package/addon/components/tracking-page/order-list.hbs +35 -0
  38. package/addon/components/tracking-page/public-view.hbs +60 -0
  39. package/addon/components/tracking-page/settings-preview.hbs +21 -0
  40. package/addon/components/tracking-page/settings-preview.js +23 -0
  41. package/addon/components/tracking-page/verify-panel.hbs +109 -0
  42. package/addon/components/tracking-page.hbs +142 -0
  43. package/addon/components/tracking-page.js +564 -0
  44. package/addon/components/trailer/form.hbs +2 -2
  45. package/addon/components/vehicle/form.hbs +3 -3
  46. package/addon/components/warranty/form.hbs +2 -2
  47. package/addon/components/widget/live-fleet.js +54 -6
  48. package/addon/components/widget/proof-of-delivery.hbs +49 -0
  49. package/addon/components/widget/proof-of-delivery.js +62 -0
  50. package/addon/components/work-order/form.hbs +3 -3
  51. package/addon/controllers/analytics/reports/index.js +10 -1
  52. package/addon/controllers/connectivity/devices/index/details.js +1 -0
  53. package/addon/controllers/connectivity/devices/index.js +8 -2
  54. package/addon/controllers/connectivity/events/index.js +31 -1
  55. package/addon/controllers/connectivity/sensors/index/details.js +1 -1
  56. package/addon/controllers/connectivity/sensors/index.js +37 -1
  57. package/addon/controllers/maintenance/equipment/index/details.js +2 -2
  58. package/addon/controllers/maintenance/equipment/index.js +28 -9
  59. package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
  60. package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
  61. package/addon/controllers/maintenance/inspection-submissions/index.js +49 -9
  62. package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
  63. package/addon/controllers/maintenance/maintenances/index.js +20 -1
  64. package/addon/controllers/maintenance/parts/index/details.js +2 -2
  65. package/addon/controllers/maintenance/parts/index.js +28 -8
  66. package/addon/controllers/maintenance/schedules/index.js +33 -4
  67. package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
  68. package/addon/controllers/maintenance/work-orders/index.js +39 -8
  69. package/addon/controllers/management/contacts/customers.js +13 -0
  70. package/addon/controllers/management/contacts/index.js +43 -1
  71. package/addon/controllers/management/drivers/index/details.js +1 -0
  72. package/addon/controllers/management/drivers/index.js +32 -2
  73. package/addon/controllers/management/fleets/index/details.js +1 -0
  74. package/addon/controllers/management/fleets/index.js +39 -1
  75. package/addon/controllers/management/fuel-reports/index.js +22 -2
  76. package/addon/controllers/management/fuel-transactions/index.js +43 -7
  77. package/addon/controllers/management/issues/index/details.js +1 -0
  78. package/addon/controllers/management/issues/index.js +22 -2
  79. package/addon/controllers/management/places/index.js +36 -1
  80. package/addon/controllers/management/trailers/index/details.js +1 -0
  81. package/addon/controllers/management/trailers/index.js +50 -2
  82. package/addon/controllers/management/vehicles/index/details.js +1 -0
  83. package/addon/controllers/management/vehicles/index.js +33 -2
  84. package/addon/controllers/management/vendors/index.js +26 -2
  85. package/addon/controllers/operations/orders/index/details.js +50 -7
  86. package/addon/controllers/operations/orders/index.js +59 -4
  87. package/addon/controllers/operations/scheduler/index.js +45 -14
  88. package/addon/controllers/operations/service-rates/index.js +15 -1
  89. package/addon/controllers/settings/index.js +6 -0
  90. package/addon/controllers/settings/map.js +75 -0
  91. package/addon/controllers/settings/orchestrator.js +8 -6
  92. package/addon/controllers/settings/tracking-page.js +212 -0
  93. package/addon/extension.js +97 -1
  94. package/addon/helpers/parcel-box-image.js +16 -0
  95. package/addon/routes/operations/orders/index/details.js +1 -0
  96. package/addon/routes/operations/orders/index/new.js +5 -4
  97. package/addon/routes/operations/scheduler/index.js +1 -1
  98. package/addon/routes/settings/tracking-page.js +31 -0
  99. package/addon/routes.js +1 -0
  100. package/addon/services/contact-actions.js +1 -0
  101. package/addon/services/customer-actions.js +2 -1
  102. package/addon/services/device-actions.js +1 -0
  103. package/addon/services/device-event-actions.js +1 -0
  104. package/addon/services/driver-actions.js +4 -13
  105. package/addon/services/equipment-actions.js +1 -0
  106. package/addon/services/fleet-actions.js +1 -0
  107. package/addon/services/fuel-report-actions.js +1 -0
  108. package/addon/services/inspection-form-actions.js +1 -0
  109. package/addon/services/inspection-submission-actions.js +1 -0
  110. package/addon/services/integrated-vendor-actions.js +1 -0
  111. package/addon/services/issue-actions.js +1 -0
  112. package/addon/services/location.js +69 -21
  113. package/addon/services/maintenance-actions.js +1 -0
  114. package/addon/services/maintenance-schedule-actions.js +2 -1
  115. package/addon/services/map-settings.js +28 -0
  116. package/addon/services/order-actions.js +7 -0
  117. package/addon/services/order-allocation.js +1 -1
  118. package/addon/services/order-presentation.js +129 -0
  119. package/addon/services/part-actions.js +1 -0
  120. package/addon/services/place-actions.js +1 -0
  121. package/addon/services/sensor-actions.js +1 -0
  122. package/addon/services/service-area-actions.js +1 -0
  123. package/addon/services/service-rate-actions.js +1 -0
  124. package/addon/services/telematic-actions.js +1 -0
  125. package/addon/services/trailer-actions.js +4 -14
  126. package/addon/services/vehicle-actions.js +4 -13
  127. package/addon/services/vendor-actions.js +1 -0
  128. package/addon/services/work-order-actions.js +1 -0
  129. package/addon/services/zone-actions.js +1 -0
  130. package/addon/styles/fleetops-engine.css +1391 -0
  131. package/addon/templates/analytics/reports/index/details.hbs +1 -0
  132. package/addon/templates/analytics/reports/index.hbs +1 -0
  133. package/addon/templates/connectivity/devices/index/details.hbs +1 -0
  134. package/addon/templates/connectivity/devices/index.hbs +1 -0
  135. package/addon/templates/connectivity/events/index.hbs +1 -0
  136. package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
  137. package/addon/templates/connectivity/sensors/index.hbs +1 -0
  138. package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
  139. package/addon/templates/maintenance/equipment/index.hbs +2 -0
  140. package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
  141. package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
  142. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
  143. package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
  144. package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
  145. package/addon/templates/maintenance/maintenances/index.hbs +1 -0
  146. package/addon/templates/maintenance/parts/index/details.hbs +1 -0
  147. package/addon/templates/maintenance/parts/index.hbs +2 -0
  148. package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
  149. package/addon/templates/maintenance/schedules/index.hbs +2 -0
  150. package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
  151. package/addon/templates/maintenance/work-orders/index.hbs +1 -0
  152. package/addon/templates/management/contacts/customers/details.hbs +1 -0
  153. package/addon/templates/management/contacts/customers.hbs +1 -0
  154. package/addon/templates/management/contacts/index/details.hbs +1 -0
  155. package/addon/templates/management/contacts/index.hbs +1 -0
  156. package/addon/templates/management/contacts.hbs +1 -0
  157. package/addon/templates/management/drivers/index/details.hbs +1 -0
  158. package/addon/templates/management/drivers/index.hbs +2 -0
  159. package/addon/templates/management/fleets/index/details.hbs +1 -0
  160. package/addon/templates/management/fleets/index.hbs +1 -0
  161. package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
  162. package/addon/templates/management/fuel-reports/index.hbs +1 -0
  163. package/addon/templates/management/fuel-transactions/index.hbs +1 -0
  164. package/addon/templates/management/issues/index/details.hbs +1 -0
  165. package/addon/templates/management/issues/index.hbs +1 -0
  166. package/addon/templates/management/places/index/details.hbs +1 -0
  167. package/addon/templates/management/places/index.hbs +1 -0
  168. package/addon/templates/management/trailers/index/details.hbs +1 -0
  169. package/addon/templates/management/trailers/index.hbs +2 -0
  170. package/addon/templates/management/vehicles/index/details.hbs +1 -0
  171. package/addon/templates/management/vehicles/index.hbs +2 -0
  172. package/addon/templates/management/vendors/index/details.hbs +1 -0
  173. package/addon/templates/management/vendors/index.hbs +2 -0
  174. package/addon/templates/management/vendors/integrated.hbs +1 -0
  175. package/addon/templates/operations/orders/index/details.hbs +1 -0
  176. package/addon/templates/operations/orders/index.hbs +1 -0
  177. package/addon/templates/operations/service-rates/index/details.hbs +1 -0
  178. package/addon/templates/operations/service-rates/index.hbs +1 -0
  179. package/addon/templates/settings/map.hbs +46 -0
  180. package/addon/templates/settings/tracking-page.hbs +318 -0
  181. package/addon/utils/identity-cell-resource.js +2 -56
  182. package/addon/utils/map-default-view.js +132 -0
  183. package/addon/utils/normalize-order-config-flow.js +6 -1
  184. package/addon/utils/placeholder-images.js +4 -69
  185. package/addon/utils/resource-descriptors/assets.js +2 -57
  186. package/addon/utils/resource-descriptors/helpers.js +23 -122
  187. package/addon/utils/resource-descriptors/operations.js +2 -61
  188. package/addon/utils/resource-descriptors/people.js +3 -142
  189. package/addon/utils/resource-descriptors/shared.js +18 -0
  190. package/addon/utils/tracking-live-source.js +123 -0
  191. package/addon/utils/tracking-page-theme.js +123 -0
  192. package/app/components/admin/tracking-page-settings.js +1 -0
  193. package/app/components/tracking-page/full-view.js +1 -0
  194. package/app/components/tracking-page/locale-selector.js +1 -0
  195. package/app/components/tracking-page/map.js +1 -0
  196. package/app/components/tracking-page/order-list.js +1 -0
  197. package/app/components/tracking-page/public-view.js +1 -0
  198. package/app/components/tracking-page/settings-preview.js +1 -0
  199. package/app/components/tracking-page/verify-panel.js +1 -0
  200. package/app/components/tracking-page.js +1 -0
  201. package/app/components/widget/proof-of-delivery.js +1 -0
  202. package/app/controllers/settings/tracking-page.js +1 -0
  203. package/app/helpers/parcel-box-image.js +1 -0
  204. package/app/routes/settings/tracking-page.js +1 -0
  205. package/app/services/order-presentation.js +1 -0
  206. package/app/templates/settings/tracking-page.js +1 -0
  207. package/app/utils/map-default-view.js +1 -0
  208. package/app/utils/tracking-live-source.js +1 -0
  209. package/app/utils/tracking-page-theme.js +1 -0
  210. package/composer.json +2 -2
  211. package/extension.json +1 -1
  212. package/package.json +4 -4
  213. package/scripts/pest-bootstrap.php +4 -0
  214. package/server/config/fleetops.php +4 -0
  215. package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
  216. package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
  217. package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
  218. package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
  219. package/server/resources/views/labels/default.php +2 -2
  220. package/server/resources/views/labels/entity-label.php +2 -2
  221. package/server/resources/views/labels/waypoint-label.php +2 -2
  222. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  223. package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
  224. package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
  225. package/server/src/Events/OrderChanged.php +257 -0
  226. package/server/src/Events/OrderDriverUnassigned.php +68 -0
  227. package/server/src/Exceptions/OrderEditLockedException.php +32 -0
  228. package/server/src/Expansions/CompanyExpansion.php +3 -2
  229. package/server/src/Expansions/UserExpansion.php +12 -10
  230. package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
  231. package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
  232. package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
  233. package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
  234. package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
  235. package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
  236. package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
  237. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
  238. package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
  239. package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
  240. package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
  241. package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
  242. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
  243. package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
  244. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
  245. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
  246. package/server/src/Http/Controllers/Internal/v1/OrderController.php +118 -28
  247. package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
  248. package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
  249. package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
  250. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
  251. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
  252. package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
  253. package/server/src/Http/Filter/ContactFilter.php +1 -1
  254. package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
  255. package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
  256. package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
  257. package/server/src/Http/Filter/OrderFilter.php +39 -4
  258. package/server/src/Http/Filter/ProofFilter.php +18 -0
  259. package/server/src/Http/Filter/RouteFilter.php +18 -0
  260. package/server/src/Http/Filter/TelematicFilter.php +23 -0
  261. package/server/src/Http/Filter/WarrantyFilter.php +23 -0
  262. package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
  263. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
  264. package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
  265. package/server/src/Http/Resources/v1/Index/Order.php +11 -2
  266. package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
  267. package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
  268. package/server/src/Http/Resources/v1/Zone.php +31 -23
  269. package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
  270. package/server/src/Jobs/ProcessAllocationJob.php +16 -6
  271. package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
  272. package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
  273. package/server/src/Listeners/HandleOrderCanceled.php +9 -1
  274. package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
  275. package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
  276. package/server/src/Mail/CustomerCredentialsMail.php +4 -2
  277. package/server/src/Models/AssetConnection.php +3 -0
  278. package/server/src/Models/Contact.php +3 -0
  279. package/server/src/Models/Device.php +28 -0
  280. package/server/src/Models/DeviceEvent.php +7 -0
  281. package/server/src/Models/Driver.php +20 -7
  282. package/server/src/Models/Entity.php +12 -4
  283. package/server/src/Models/Fleet.php +11 -32
  284. package/server/src/Models/FleetDriver.php +3 -0
  285. package/server/src/Models/FleetVehicle.php +3 -0
  286. package/server/src/Models/FuelReport.php +5 -13
  287. package/server/src/Models/Issue.php +4 -16
  288. package/server/src/Models/Order.php +234 -11
  289. package/server/src/Models/OrderConfig.php +129 -0
  290. package/server/src/Models/Payload.php +227 -35
  291. package/server/src/Models/Place.php +7 -1
  292. package/server/src/Models/ServiceArea.php +7 -0
  293. package/server/src/Models/ServiceRate.php +7 -17
  294. package/server/src/Models/TrackingNumber.php +109 -6
  295. package/server/src/Models/TrackingSession.php +33 -0
  296. package/server/src/Models/TrackingSessionGrant.php +22 -0
  297. package/server/src/Models/TrackingStatus.php +6 -0
  298. package/server/src/Models/Vehicle.php +21 -2
  299. package/server/src/Models/Vendor.php +7 -0
  300. package/server/src/Models/Waypoint.php +13 -4
  301. package/server/src/Notifications/OrderAssigned.php +2 -1
  302. package/server/src/Notifications/OrderCanceled.php +7 -1
  303. package/server/src/Notifications/OrderChanged.php +87 -0
  304. package/server/src/Notifications/OrderCompleted.php +2 -1
  305. package/server/src/Notifications/OrderDispatchFailed.php +2 -1
  306. package/server/src/Notifications/OrderDispatched.php +2 -1
  307. package/server/src/Notifications/OrderFailed.php +2 -1
  308. package/server/src/Notifications/OrderUnassigned.php +66 -0
  309. package/server/src/Notifications/WaypointCompleted.php +2 -1
  310. package/server/src/Observers/CompanyUserObserver.php +8 -7
  311. package/server/src/Observers/DriverObserver.php +6 -1
  312. package/server/src/Observers/OrderObserver.php +141 -1
  313. package/server/src/Observers/TrackingNumberObserver.php +18 -6
  314. package/server/src/Observers/UserObserver.php +2 -1
  315. package/server/src/Observers/VehicleObserver.php +8 -3
  316. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
  317. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
  318. package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
  319. package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
  320. package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
  321. package/server/src/Providers/EventServiceProvider.php +11 -0
  322. package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
  323. package/server/src/Support/ApiCacheThrottle.php +76 -0
  324. package/server/src/Support/LiveOrderQuery.php +3 -1
  325. package/server/src/Support/OrderEdit.php +365 -0
  326. package/server/src/Support/OrderEditPolicy.php +292 -0
  327. package/server/src/Support/ProfileAccountManager.php +54 -1
  328. package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
  329. package/server/src/Support/SocketChannels.php +346 -0
  330. package/server/src/Support/Telematics/TelematicService.php +7 -2
  331. package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
  332. package/server/src/Support/TrackingChannel.php +55 -0
  333. package/server/src/Support/TrackingCode.php +242 -0
  334. package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
  335. package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
  336. package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
  337. package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
  338. package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
  339. package/server/src/Support/TrackingPage/TrackingView.php +543 -0
  340. package/server/src/Support/TrackingPublisher.php +261 -0
  341. package/server/src/Support/TrackingScope.php +182 -0
  342. package/server/src/Support/TrackingUpdate.php +402 -0
  343. package/server/src/Support/Utils.php +15 -3
  344. package/server/src/Traits/HasTrackingNumber.php +2 -0
  345. package/server/src/routes.php +39 -7
  346. package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
  347. package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
  348. package/server/tests/ApiEntityControllerContractsTest.php +84 -0
  349. package/server/tests/ApiOrderControllerContractsTest.php +245 -0
  350. package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
  351. package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
  352. package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
  353. package/server/tests/CompactResourceSerializationTest.php +23 -59
  354. package/server/tests/ControllerFilterContractsTest.php +1 -1
  355. package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
  356. package/server/tests/EventContractsTest.php +26 -60
  357. package/server/tests/ExpansionContractsTest.php +15 -2
  358. package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
  359. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
  360. package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
  361. package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
  362. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
  363. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
  364. package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
  365. package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
  366. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
  367. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
  368. package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
  369. package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
  370. package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
  371. package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
  372. package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
  373. package/server/tests/IndexCustomerPhotoTest.php +151 -0
  374. package/server/tests/ModelAccessorContractsTest.php +4 -3
  375. package/server/tests/NotificationAndMailContractsTest.php +5 -8
  376. package/server/tests/ObserverContractsTest.php +72 -10
  377. package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
  378. package/server/tests/OrderConfigLifecycleTest.php +120 -0
  379. package/server/tests/OrderFilterExecutionTest.php +167 -2
  380. package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
  381. package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
  382. package/server/tests/QueueJobContractsTest.php +2 -1
  383. package/server/tests/RequestContractsTest.php +1 -1
  384. package/server/tests/RouteRegistrationExecutionTest.php +26 -0
  385. package/server/tests/RouteSequencingEngineTest.php +17 -0
  386. package/server/tests/SettingControllerContractsTest.php +110 -0
  387. package/server/tests/Support/TrackingPageDatabase.php +200 -0
  388. package/server/tests/TenantScopingTest.php +206 -0
  389. package/server/tests/TrackingPageSettingsTest.php +343 -0
  390. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
  391. package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
  392. package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
  393. package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
  394. package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
  395. package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
  396. package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
  397. package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
  398. package/server/tests/Unit/Models/FleetTest.php +4 -3
  399. package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
  400. package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
  401. package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
  402. package/server/tests/Unit/Models/PayloadTest.php +18 -4
  403. package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
  404. package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
  405. package/server/tests/Unit/Models/VehicleTest.php +16 -2
  406. package/server/tests/Unit/Models/WaypointTest.php +4 -3
  407. package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
  408. package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
  409. package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
  410. package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
  411. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
  412. package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
  413. package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
  414. package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
  415. package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
  416. package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
  417. package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
  418. package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
  419. package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
  420. package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
  421. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
  422. package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
  423. package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
  424. package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
  425. package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
  426. package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
  427. package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
  428. package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
  429. package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
  430. package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
  431. package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
  432. package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
  433. package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
  434. package/server/tests/VehicleResourceContractsTest.php +46 -0
  435. package/server/tests/VroomOrchestrationEngineTest.php +34 -1
  436. package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
  437. package/translations/ar-ae.yml +274 -0
  438. package/translations/bg-bg.yaml +274 -0
  439. package/translations/en-us.yaml +347 -0
  440. package/translations/es-pa.yaml +274 -0
  441. package/translations/fr-fr.yaml +274 -0
  442. package/translations/mn-mn.yaml +274 -0
  443. package/translations/pt-br.yaml +274 -0
  444. package/translations/ru-ru.yaml +276 -2
  445. package/translations/uk-ua.yaml +275 -1
  446. package/translations/vi-vn.yaml +274 -0
  447. package/addon/components/cell/contact-identity.hbs +0 -1
  448. package/addon/components/cell/customer-identity.hbs +0 -1
  449. package/addon/components/cell/driver-identity.hbs +0 -1
  450. package/addon/components/cell/fleet-identity.hbs +0 -1
  451. package/addon/components/cell/order-identity.hbs +0 -1
  452. package/addon/components/cell/place-identity.hbs +0 -1
  453. package/addon/components/cell/vehicle-identity.hbs +0 -1
  454. package/addon/components/cell/vendor-identity.hbs +0 -1
  455. package/addon/components/contact/pill.hbs +0 -24
  456. package/addon/components/contact/summary.hbs +0 -1
  457. package/addon/components/customer/pill.hbs +0 -24
  458. package/addon/components/customer/summary.hbs +0 -1
  459. package/addon/components/driver/pill.hbs +0 -24
  460. package/addon/components/driver/summary.hbs +0 -1
  461. package/addon/components/fleet/pill.hbs +0 -24
  462. package/addon/components/fleet/summary.hbs +0 -1
  463. package/addon/components/order/pill.hbs +0 -24
  464. package/addon/components/order/summary.hbs +0 -1
  465. package/addon/components/place/pill.hbs +0 -24
  466. package/addon/components/place/summary.hbs +0 -1
  467. package/addon/components/select-option/contact.hbs +0 -12
  468. package/addon/components/select-option/customer.hbs +0 -12
  469. package/addon/components/select-option/driver.hbs +0 -12
  470. package/addon/components/select-option/fleet.hbs +0 -12
  471. package/addon/components/select-option/order.hbs +0 -12
  472. package/addon/components/select-option/place.hbs +0 -12
  473. package/addon/components/select-option/vehicle.hbs +0 -12
  474. package/addon/components/select-option/vendor.hbs +0 -12
  475. package/addon/components/vehicle/pill.hbs +0 -24
  476. package/addon/components/vehicle/summary.hbs +0 -1
  477. package/addon/components/vendor/pill.hbs +0 -24
  478. package/addon/components/vendor/summary.hbs +0 -1
  479. package/app/components/cell/contact-identity.js +0 -1
  480. package/app/components/cell/customer-identity.js +0 -1
  481. package/app/components/cell/driver-identity.js +0 -1
  482. package/app/components/cell/fleet-identity.js +0 -1
  483. package/app/components/cell/order-identity.js +0 -1
  484. package/app/components/cell/place-identity.js +0 -1
  485. package/app/components/cell/vehicle-identity.js +0 -1
  486. package/app/components/cell/vendor-identity.js +0 -1
  487. package/app/components/contact/pill.js +0 -1
  488. package/app/components/contact/summary.js +0 -1
  489. package/app/components/customer/pill.js +0 -1
  490. package/app/components/customer/summary.js +0 -1
  491. package/app/components/driver/pill.js +0 -1
  492. package/app/components/driver/summary.js +0 -1
  493. package/app/components/fleet/pill.js +0 -1
  494. package/app/components/fleet/summary.js +0 -1
  495. package/app/components/order/pill.js +0 -1
  496. package/app/components/order/summary.js +0 -1
  497. package/app/components/place/pill.js +0 -1
  498. package/app/components/place/summary.js +0 -1
  499. package/app/components/select-option/contact.js +0 -1
  500. package/app/components/select-option/customer.js +0 -1
  501. package/app/components/select-option/driver.js +0 -1
  502. package/app/components/select-option/fleet.js +0 -1
  503. package/app/components/select-option/order.js +0 -1
  504. package/app/components/select-option/place.js +0 -1
  505. package/app/components/select-option/vehicle.js +0 -1
  506. package/app/components/select-option/vendor.js +0 -1
  507. package/app/components/vehicle/pill.js +0 -1
  508. package/app/components/vehicle/summary.js +0 -1
  509. package/app/components/vendor/pill.js +0 -1
  510. package/app/components/vendor/summary.js +0 -1
@@ -0,0 +1,61 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Http\Filter\ContactFilter;
4
+ use Fleetbase\FleetOps\Models\Contact;
5
+ use Fleetbase\Http\Filter\Filter;
6
+ use Illuminate\Database\ConnectionResolver;
7
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
8
+ use Illuminate\Database\SQLiteConnection;
9
+
10
+ afterEach(function () {
11
+ EloquentModel::unsetConnectionResolver();
12
+ });
13
+
14
+ function contactFilterAddressBoot(): SQLiteConnection
15
+ {
16
+ $connection = new SQLiteConnection(new PDO('sqlite::memory:'));
17
+ $resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
18
+ $resolver->setDefaultConnection('mysql');
19
+ EloquentModel::setConnectionResolver($resolver);
20
+
21
+ $tables = [
22
+ 'contacts' => ['uuid', 'public_id', 'company_uuid', 'place_uuid', 'name'],
23
+ 'places' => ['uuid', 'public_id', 'company_uuid', 'owner_uuid', 'name', 'street1', 'street2', 'country', 'province', 'district', 'city', 'postal_code', 'phone'],
24
+ ];
25
+ foreach ($tables as $table => $columns) {
26
+ $connection->getSchemaBuilder()->create($table, function ($blueprint) use ($columns) {
27
+ $blueprint->increments('id');
28
+ foreach ($columns as $column) {
29
+ $blueprint->string($column)->nullable();
30
+ }
31
+ $blueprint->timestamps();
32
+ $blueprint->timestamp('deleted_at')->nullable();
33
+ });
34
+ }
35
+
36
+ $connection->table('places')->insert([
37
+ ['uuid' => 'place-1', 'company_uuid' => 'co-1', 'street1' => '1 Harbour Road', 'city' => 'Singapore'],
38
+ ['uuid' => 'place-2', 'company_uuid' => 'co-1', 'street1' => '9 Orchard Lane', 'city' => 'Singapore'],
39
+ ]);
40
+ $connection->table('contacts')->insert([
41
+ ['uuid' => 'contact-1', 'company_uuid' => 'co-1', 'place_uuid' => 'place-1', 'name' => 'Harbour Contact'],
42
+ ['uuid' => 'contact-2', 'company_uuid' => 'co-1', 'place_uuid' => 'place-2', 'name' => 'Orchard Contact'],
43
+ ['uuid' => 'contact-3', 'company_uuid' => 'co-1', 'place_uuid' => null, 'name' => 'Placeless Contact'],
44
+ ]);
45
+
46
+ return $connection;
47
+ }
48
+
49
+ test('the contact address filter matches the address the contact list shows', function () {
50
+ contactFilterAddressBoot();
51
+
52
+ $builder = Contact::query();
53
+ $filter = (new ReflectionClass(ContactFilter::class))->newInstanceWithoutConstructor();
54
+ $property = new ReflectionProperty(Filter::class, 'builder');
55
+ $property->setAccessible(true);
56
+ $property->setValue($filter, $builder);
57
+
58
+ $filter->address('harbour');
59
+
60
+ expect($builder->pluck('uuid')->all())->toBe(['contact-1']);
61
+ });
@@ -6,6 +6,7 @@ use Fleetbase\FleetOps\Models\Driver;
6
6
  use Fleetbase\FleetOps\Models\Order;
7
7
  use Fleetbase\FleetOps\Models\Payload;
8
8
  use Fleetbase\FleetOps\Models\TrackingNumber;
9
+ use Fleetbase\FleetOps\Models\TrackingStatus;
9
10
  use Fleetbase\FleetOps\Models\Vehicle;
10
11
  use Fleetbase\FleetOps\Models\Vendor;
11
12
  use Illuminate\Http\Request;
@@ -83,6 +84,35 @@ test('index order resource resolves loaded relations into lightweight payloads',
83
84
  ->and($resolved['meta'])->toBe(['_index_resource' => true]);
84
85
  });
85
86
 
87
+ test('index order resource reports the newest tracking status whatever order they load in', function () {
88
+ $connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
89
+ $resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
90
+ $resolver->setDefaultConnection('mysql');
91
+ Illuminate\Database\Eloquent\Model::setConnectionResolver($resolver);
92
+ app()->instance('request', Request::create('/v1/orders', 'GET'));
93
+ session(['company' => 'company-1']);
94
+
95
+ $status = function (int $id, string $code, string $createdAt) {
96
+ $status = new TrackingStatus();
97
+ $status->setRawAttributes(['id' => $id, 'uuid' => "ts-{$code}", 'status' => ucfirst($code), 'code' => $code, 'created_at' => $createdAt], true);
98
+
99
+ return $status;
100
+ };
101
+
102
+ // The relation has no order, so statuses arrive oldest first; two share a second
103
+ $order = fleetopsIndexOrderResourceOrder();
104
+ $order->setRelation('trackingStatuses', collect([
105
+ $status(1, 'created', '2026-10-13 09:00:00'),
106
+ $status(2, 'dispatched', '2026-10-13 09:05:00'),
107
+ $status(3, 'started', '2026-10-13 09:05:00'),
108
+ ]));
109
+
110
+ $resolved = (new IndexOrderResource($order))->resolve(Request::create('/v1/orders', 'GET'));
111
+
112
+ expect($resolved['latest_status'])->toBe('Started')
113
+ ->and($resolved['latest_status_code'])->toBe('started');
114
+ });
115
+
86
116
  /**
87
117
  * The index Payload resource only emits pickup/dropoff when the payload can
88
118
  * actually produce a place. Both accessors short-circuit on a directly related
@@ -0,0 +1,266 @@
1
+ <?php
2
+
3
+ if (!function_exists('Fleetbase\FleetOps\Models\dispatch')) {
4
+ eval('namespace Fleetbase\FleetOps\Models; function dispatch($job) { return \FleetOpsOrderUnitDispatchRecorder::record($job); }');
5
+ }
6
+
7
+ if (!function_exists('Fleetbase\FleetOps\Models\event')) {
8
+ eval('namespace Fleetbase\FleetOps\Models; function event($event = null) { \FleetOpsOrderUnitDispatchRecorder::$events[] = $event; return $event; }');
9
+ }
10
+
11
+ if (!function_exists('Fleetbase\FleetOps\Models\now')) {
12
+ eval('namespace Fleetbase\FleetOps\Models; function now($tz = null) { return \Illuminate\Support\Carbon::now($tz); }');
13
+ }
14
+
15
+ if (!class_exists('Fleetbase\FleetOps\Events\OrderDispatched', false)) {
16
+ eval('namespace Fleetbase\FleetOps\Events; class OrderDispatched { public function __construct(public $order) {} }');
17
+ }
18
+
19
+ if (!class_exists('FleetOpsOrderUnitDispatchRecorder', false)) {
20
+ class FleetOpsOrderUnitDispatchRecorder
21
+ {
22
+ public static array $jobs = [];
23
+ public static array $events = [];
24
+
25
+ public static function reset(): void
26
+ {
27
+ static::$jobs = [];
28
+ static::$events = [];
29
+ }
30
+
31
+ public static function record($job): object
32
+ {
33
+ static::$jobs[] = $job;
34
+
35
+ if ($job instanceof Closure) {
36
+ $job();
37
+ }
38
+
39
+ return new class {
40
+ public function afterCommit(): self
41
+ {
42
+ return $this;
43
+ }
44
+ };
45
+ }
46
+ }
47
+ }
48
+
49
+ use Fleetbase\FleetOps\Events\OrderDispatched;
50
+ use Fleetbase\FleetOps\Flow\Activity;
51
+ use Fleetbase\FleetOps\Jobs\FinalizeApiOrderCreation;
52
+ use Fleetbase\FleetOps\Models\Order;
53
+ use Fleetbase\FleetOps\Models\OrderConfig;
54
+ use Fleetbase\FleetOps\Models\ServiceQuote;
55
+ use Fleetbase\FleetOps\Models\TrackingStatus;
56
+ use Illuminate\Support\Carbon;
57
+
58
+ class FleetOpsQueuedDispatchOrderFake extends Order
59
+ {
60
+ public bool $quietSaved = false;
61
+ public array $calls = [];
62
+ public array $activityRows = [];
63
+ public array $statuses = [];
64
+ public ?OrderConfig $fakeConfig = null;
65
+
66
+ public function getDateFormat()
67
+ {
68
+ return 'Y-m-d H:i:s';
69
+ }
70
+
71
+ public function load($relations)
72
+ {
73
+ return $this;
74
+ }
75
+
76
+ public function saveQuietly(array $options = []): bool
77
+ {
78
+ $this->quietSaved = true;
79
+
80
+ return true;
81
+ }
82
+
83
+ public function flushAttributesCache(): bool
84
+ {
85
+ return true;
86
+ }
87
+
88
+ public function config(): ?OrderConfig
89
+ {
90
+ return $this->fakeConfig;
91
+ }
92
+
93
+ public function notifyDriverAssigned(): void
94
+ {
95
+ $this->calls[] = 'notifyDriverAssigned';
96
+ }
97
+
98
+ public function setPreliminaryDistanceAndTime(): void
99
+ {
100
+ $this->calls[] = 'setPreliminaryDistanceAndTime';
101
+ }
102
+
103
+ public function purchaseServiceQuote($serviceQuote, $meta = [])
104
+ {
105
+ $this->calls[] = 'purchaseServiceQuote';
106
+ }
107
+
108
+ public function insertActivity(Activity $activity, $location = [], $proof = null): string
109
+ {
110
+ $this->activityRows[] = $activity->code;
111
+
112
+ return 'tracking_status_public';
113
+ }
114
+
115
+ public function setStatus(?string $status, $andSave = true)
116
+ {
117
+ $this->statuses[] = $status;
118
+ $this->status = $status;
119
+
120
+ return $this;
121
+ }
122
+ }
123
+
124
+ class FleetOpsQueuedDispatchConfigFake extends OrderConfig
125
+ {
126
+ public function getDispatchActivity(): ?Activity
127
+ {
128
+ return new Activity(['code' => 'dispatched']);
129
+ }
130
+ }
131
+
132
+ class FleetOpsQueuedDispatchJobProbe extends FinalizeApiOrderCreation
133
+ {
134
+ public ?Order $order = null;
135
+ public array $ready = [];
136
+
137
+ protected function findOrder(): ?Order
138
+ {
139
+ return $this->order;
140
+ }
141
+
142
+ protected function findServiceQuote(): ?ServiceQuote
143
+ {
144
+ return null;
145
+ }
146
+
147
+ protected function fireOrderReady(Order $order): void
148
+ {
149
+ $this->ready[] = $order->uuid;
150
+ }
151
+ }
152
+
153
+ function fleetopsQueuedDispatchTrackingStatus(string $code): TrackingStatus
154
+ {
155
+ $status = new TrackingStatus();
156
+ $status->code = $code;
157
+
158
+ return $status;
159
+ }
160
+
161
+ function fleetopsQueuedDispatchOrder(array $attributes = [], array $trackingCodes = ['CREATED'], array $lifecycle = []): FleetOpsQueuedDispatchOrderFake
162
+ {
163
+ $config = new FleetOpsQueuedDispatchConfigFake();
164
+ $config->setRawAttributes(['meta' => json_encode($lifecycle ? ['lifecycle' => $lifecycle] : [])], true);
165
+
166
+ $order = new FleetOpsQueuedDispatchOrderFake();
167
+ $order->setRawAttributes(array_merge([
168
+ 'uuid' => 'order-uuid',
169
+ 'tracking_number_uuid' => 'tracking-number-uuid',
170
+ 'status' => 'created',
171
+ 'dispatched' => false,
172
+ 'started' => false,
173
+ ], $attributes), true);
174
+ $order->fakeConfig = $config;
175
+ $order->setRelation('trackingStatuses', collect(array_map('fleetopsQueuedDispatchTrackingStatus', $trackingCodes)));
176
+ $order->setRelation('driverAssigned', null);
177
+ $order->setRelation('payload', null);
178
+
179
+ return $order;
180
+ }
181
+
182
+ function fleetopsRunQueuedDispatch(Order $order): FleetOpsQueuedDispatchJobProbe
183
+ {
184
+ $job = new FleetOpsQueuedDispatchJobProbe($order->uuid, null, true);
185
+ $job->order = $order;
186
+ $job->handle();
187
+
188
+ return $job;
189
+ }
190
+
191
+ beforeEach(function () {
192
+ FleetOpsOrderUnitDispatchRecorder::reset();
193
+ Carbon::setTestNow(Carbon::parse('2026-10-11 09:30:00'));
194
+ });
195
+
196
+ afterEach(function () {
197
+ Carbon::setTestNow();
198
+ });
199
+
200
+ test('queued api dispatch still dispatches an order that is waiting at created', function () {
201
+ $order = fleetopsQueuedDispatchOrder();
202
+
203
+ $job = fleetopsRunQueuedDispatch($order);
204
+
205
+ expect($order->dispatched)->toBeTrue()
206
+ ->and($order->dispatched_at->toDateTimeString())->toBe('2026-10-11 09:30:00')
207
+ ->and($order->status)->toBe('dispatched')
208
+ ->and($order->activityRows)->toBe(['dispatched'])
209
+ ->and(FleetOpsOrderUnitDispatchRecorder::$events)->toHaveCount(1)
210
+ ->and(FleetOpsOrderUnitDispatchRecorder::$events[0])->toBeInstanceOf(OrderDispatched::class)
211
+ ->and($job->ready)->toBe(['order-uuid']);
212
+ });
213
+
214
+ test('queued api dispatch is a no-op once the order was dispatched and started', function () {
215
+ $dispatchedAt = Carbon::parse('2026-10-11 09:00:00');
216
+ $startedAt = Carbon::parse('2026-10-11 09:10:00');
217
+ $order = fleetopsQueuedDispatchOrder([
218
+ 'status' => 'started',
219
+ 'dispatched' => true,
220
+ 'dispatched_at' => $dispatchedAt->toDateTimeString(),
221
+ 'started' => true,
222
+ 'started_at' => $startedAt->toDateTimeString(),
223
+ ], ['CREATED', 'DISPATCHED', 'STARTED']);
224
+
225
+ $job = fleetopsRunQueuedDispatch($order);
226
+
227
+ expect($order->status)->toBe('started')
228
+ ->and($order->dispatched_at->toDateTimeString())->toBe('2026-10-11 09:00:00')
229
+ ->and($order->started_at->toDateTimeString())->toBe('2026-10-11 09:10:00')
230
+ ->and($order->quietSaved)->toBeFalse()
231
+ ->and($order->activityRows)->toBe([])
232
+ ->and($order->statuses)->toBe([])
233
+ ->and(FleetOpsOrderUnitDispatchRecorder::$jobs)->toBe([])
234
+ ->and(FleetOpsOrderUnitDispatchRecorder::$events)->toBe([])
235
+ ->and($order->calls)->toBe(['notifyDriverAssigned', 'setPreliminaryDistanceAndTime', 'purchaseServiceQuote'])
236
+ ->and($job->ready)->toBe(['order-uuid']);
237
+ });
238
+
239
+ test('an order is only awaiting dispatch at its initial status with nothing recorded', function () {
240
+ expect(fleetopsQueuedDispatchOrder()->isAwaitingDispatch())->toBeTrue()
241
+ ->and(fleetopsQueuedDispatchOrder(['status' => null])->isAwaitingDispatch())->toBeTrue()
242
+ ->and(fleetopsQueuedDispatchOrder(['status' => 'pending'], ['PENDING'], ['initial' => 'pending'])->isAwaitingDispatch())->toBeTrue()
243
+ ->and(fleetopsQueuedDispatchOrder(['dispatched' => true])->isAwaitingDispatch())->toBeFalse()
244
+ ->and(fleetopsQueuedDispatchOrder(['dispatched_at' => '2026-10-11 09:00:00'])->isAwaitingDispatch())->toBeFalse()
245
+ ->and(fleetopsQueuedDispatchOrder(['started' => true])->isAwaitingDispatch())->toBeFalse()
246
+ ->and(fleetopsQueuedDispatchOrder(['started_at' => '2026-10-11 09:10:00'])->isAwaitingDispatch())->toBeFalse()
247
+ ->and(fleetopsQueuedDispatchOrder(['status' => 'enroute'])->isAwaitingDispatch())->toBeFalse()
248
+ ->and(fleetopsQueuedDispatchOrder(['status' => 'completed'])->isAwaitingDispatch())->toBeFalse()
249
+ ->and(fleetopsQueuedDispatchOrder([], ['CREATED', 'DISPATCHED'])->isAwaitingDispatch())->toBeFalse();
250
+
251
+ $withoutConfig = fleetopsQueuedDispatchOrder(['status' => 'pending']);
252
+ $withoutConfig->fakeConfig = null;
253
+
254
+ expect($withoutConfig->isAwaitingDispatch())->toBeFalse();
255
+ });
256
+
257
+ test('a terminal or later status on a custom lifecycle is never dispatched by the queued job', function () {
258
+ $order = fleetopsQueuedDispatchOrder(['status' => 'delivered'], ['PENDING', 'DELIVERED'], ['initial' => 'pending', 'completed' => 'delivered']);
259
+
260
+ fleetopsRunQueuedDispatch($order);
261
+
262
+ expect($order->status)->toBe('delivered')
263
+ ->and($order->dispatched)->toBeFalse()
264
+ ->and($order->activityRows)->toBe([])
265
+ ->and(FleetOpsOrderUnitDispatchRecorder::$events)->toBe([]);
266
+ });
@@ -14,8 +14,11 @@ use Fleetbase\FleetOps\Listeners\HandleGeofenceEntered;
14
14
  use Fleetbase\FleetOps\Models\Driver;
15
15
  use Fleetbase\FleetOps\Models\GeofenceEventLog;
16
16
  use Fleetbase\FleetOps\Models\Order;
17
+ use Fleetbase\FleetOps\Models\Payload;
18
+ use Fleetbase\FleetOps\Models\Place;
17
19
  use Fleetbase\FleetOps\Models\TrackingStatus;
18
20
  use Fleetbase\FleetOps\Models\Vehicle;
21
+ use Fleetbase\FleetOps\Models\Waypoint;
19
22
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
20
23
  use Illuminate\Database\ConnectionResolver;
21
24
  use Illuminate\Database\Eloquent\Model as EloquentModel;
@@ -96,8 +99,12 @@ function fleetopsHandleGeofenceEnteredConnection(): SQLiteConnection
96
99
  $connection = new SQLiteConnection($pdo, 'default');
97
100
  $connection->statement('create table geofence_events_log (uuid varchar(64) primary key, company_uuid varchar(64), driver_uuid varchar(64) null, vehicle_uuid varchar(64) null, order_uuid varchar(64) null, subject_uuid varchar(64) null, subject_type varchar(255) null, subject_name varchar(255) null, geofence_uuid varchar(64), geofence_type varchar(64), geofence_name varchar(255) null, event_type varchar(64), latitude numeric null, longitude numeric null, speed_kmh numeric null, dwell_duration_minutes integer null, occurred_at datetime null, created_at datetime null, updated_at datetime null)');
98
101
 
102
+ $connection->statement('create table tracking_numbers (id integer primary key autoincrement, uuid varchar(64), public_id varchar(64) null, company_uuid varchar(64) null, status_uuid varchar(64) null, created_at datetime null, updated_at datetime null, deleted_at datetime null)');
103
+ $connection->statement('create table tracking_statuses (id integer primary key autoincrement, uuid varchar(64), public_id varchar(64) null, company_uuid varchar(64) null, tracking_number_uuid varchar(64) null, code varchar(64) null, complete integer null, created_at datetime null, updated_at datetime null, deleted_at datetime null)');
104
+
99
105
  $resolver = new ConnectionResolver([
100
106
  'default' => $connection,
107
+ 'mysql' => $connection,
101
108
  ]);
102
109
  $resolver->setDefaultConnection('default');
103
110
  EloquentModel::setConnectionResolver($resolver);
@@ -212,7 +219,73 @@ test('geofence entered listener writes vehicle entry logs with current order con
212
219
  ->and($log->subject_name)->toBe('Dock Van');
213
220
  });
214
221
 
222
+ function fleetopsEnteredZone(string $name, float $lat, float $lng): object
223
+ {
224
+ return new class($name, $lat, $lng) {
225
+ public string $uuid;
226
+ public string $public_id;
227
+
228
+ public function __construct(public string $name, private float $lat, private float $lng)
229
+ {
230
+ $this->uuid = $name . '-uuid';
231
+ $this->public_id = $name . '-public';
232
+ }
233
+
234
+ public function getLatitudeAttribute(): float
235
+ {
236
+ return $this->lat;
237
+ }
238
+
239
+ public function getLongitudeAttribute(): float
240
+ {
241
+ return $this->lng;
242
+ }
243
+ };
244
+ }
245
+
246
+ function fleetopsEnteredPlace(string $uuid, ?Point $location): Place
247
+ {
248
+ $place = new Place();
249
+ $place->setRawAttributes(['uuid' => $uuid, 'public_id' => $uuid . '-public', 'location' => $location], true);
250
+
251
+ return $place;
252
+ }
253
+
254
+ /**
255
+ * Build a pickup -> dropoff payload. The pickup sits on the default event
256
+ * zone centroid; the dropoff is roughly 9km away.
257
+ */
258
+ function fleetopsEnteredEndpointPayload(?string $currentWaypointUuid = null, ?string $pickupTrackingNumberUuid = null): Payload
259
+ {
260
+ $payload = new Payload();
261
+ $payload->setRawAttributes([
262
+ 'uuid' => 'payload-uuid',
263
+ 'current_waypoint_uuid' => $currentWaypointUuid,
264
+ 'pickup_tracking_number_uuid' => $pickupTrackingNumberUuid,
265
+ ], true);
266
+ $payload->setRelation('pickup', fleetopsEnteredPlace('pickup-place', new Point(1.3521, 103.8198)));
267
+ $payload->setRelation('dropoff', fleetopsEnteredPlace('dropoff-place', new Point(1.3000, 103.9000)));
268
+ $payload->setRelation('waypoints', collect());
269
+ $payload->setRelation('waypointMarkers', collect());
270
+
271
+ return $payload;
272
+ }
273
+
274
+ function fleetopsEnteredCompleteStatus(SQLiteConnection $connection, string $trackingNumberUuid): void
275
+ {
276
+ $connection->table('tracking_numbers')->insert(['uuid' => $trackingNumberUuid, 'status_uuid' => $trackingNumberUuid . '-status']);
277
+ $connection->table('tracking_statuses')->insert(['uuid' => $trackingNumberUuid . '-status', 'tracking_number_uuid' => $trackingNumberUuid, 'code' => 'completed', 'complete' => 1]);
278
+ }
279
+
280
+ function fleetopsEnteredArrive(Order $order, object $zone): void
281
+ {
282
+ $driver = fleetopsEnteredDriver($order);
283
+
284
+ (new HandleGeofenceEntered())->handle(fleetopsEnteredEvent($driver, $zone));
285
+ }
286
+
215
287
  test('geofence entered arrival handles destination and status failures', function () {
288
+ fleetopsHandleGeofenceEnteredConnection();
216
289
  $listener = new HandleGeofenceEntered();
217
290
  $arrival = new ReflectionMethod(HandleGeofenceEntered::class, 'handleOrderArrival');
218
291
  $arrival->setAccessible(true);
@@ -220,37 +293,124 @@ test('geofence entered arrival handles destination and status failures', functio
220
293
  $driver = fleetopsEnteredDriver();
221
294
  $event = fleetopsEnteredEvent($driver);
222
295
 
223
- $destinationFailure = fleetopsEnteredOrder(new class {
224
- public function getPickupOrCurrentWaypoint(): mixed
296
+ $throwingPayload = new class extends Payload {
297
+ public function loadMissing($relations)
225
298
  {
226
299
  throw new RuntimeException('destination failed');
227
300
  }
228
- });
301
+ };
302
+ $destinationFailure = fleetopsEnteredOrder($throwingPayload);
229
303
  $arrival->invoke($listener, $driver, $event->geofence, $destinationFailure, $event);
230
304
 
231
- $placeFailure = fleetopsEnteredOrder(new class {
232
- public function getPickupOrCurrentWaypoint(): object
233
- {
234
- return new class {
235
- public function getPlace(): mixed
236
- {
237
- throw new RuntimeException('place failed');
238
- }
239
- };
240
- }
241
- });
242
- $arrival->invoke($listener, $driver, $event->geofence, $placeFailure, $event);
305
+ $emptyPayload = new Payload();
306
+ $emptyPayload->setRelation('pickup', null);
307
+ $emptyPayload->setRelation('dropoff', null);
308
+ $emptyPayload->setRelation('waypoints', collect());
309
+ $emptyPayload->setRelation('waypointMarkers', collect());
310
+ $noStops = fleetopsEnteredOrder($emptyPayload);
311
+ $arrival->invoke($listener, $driver, $event->geofence, $noStops, $event);
243
312
 
244
- $statusFailure = fleetopsEnteredOrder(new class {
245
- public function getPickupOrCurrentWaypoint(): object
246
- {
247
- return (object) ['place' => (object) ['location' => new Point(1.3521, 103.8198)]];
248
- }
249
- });
313
+ $statusFailure = fleetopsEnteredOrder(fleetopsEnteredEndpointPayload());
250
314
  $statusFailure->throwOnStatus = true;
251
315
  $arrival->invoke($listener, $driver, $event->geofence, $statusFailure, $event);
252
316
 
253
317
  expect($destinationFailure->calls)->toBe([])
254
- ->and($placeFailure->calls)->toBe([])
318
+ ->and($noStops->calls)->toBe([])
255
319
  ->and($statusFailure->calls)->toBe([]);
256
320
  });
321
+
322
+ test('geofence entered arrival targets the pickup before the pickup is completed', function () {
323
+ fleetopsHandleGeofenceEnteredConnection();
324
+
325
+ $atDropoff = fleetopsEnteredOrder(fleetopsEnteredEndpointPayload());
326
+ fleetopsEnteredArrive($atDropoff, fleetopsEnteredZone('dropoff-zone', 1.3000, 103.9000));
327
+
328
+ $atPickup = fleetopsEnteredOrder(fleetopsEnteredEndpointPayload('pickup-place'));
329
+ fleetopsEnteredArrive($atPickup, fleetopsEnteredZone('pickup-zone', 1.3521, 103.8198));
330
+
331
+ expect($atDropoff->calls)->toBe([])
332
+ ->and($atDropoff->status)->toBe('dispatched')
333
+ ->and($atPickup->calls[0])->toBe(['setStatus', 'arrived', true])
334
+ ->and($atPickup->calls[1][1]->get('details'))->toBe('Driver entered destination geofence "pickup-zone".');
335
+ });
336
+
337
+ test('geofence entered arrival targets the dropoff once the order has advanced past the pickup', function () {
338
+ fleetopsHandleGeofenceEnteredConnection();
339
+
340
+ $atPickup = fleetopsEnteredOrder(fleetopsEnteredEndpointPayload('dropoff-place'));
341
+ fleetopsEnteredArrive($atPickup, fleetopsEnteredZone('pickup-zone', 1.3521, 103.8198));
342
+
343
+ $atDropoff = fleetopsEnteredOrder(fleetopsEnteredEndpointPayload('dropoff-place'));
344
+ fleetopsEnteredArrive($atDropoff, fleetopsEnteredZone('dropoff-zone', 1.3000, 103.9000));
345
+
346
+ expect($atPickup->calls)->toBe([])
347
+ ->and($atDropoff->calls[0])->toBe(['setStatus', 'arrived', true])
348
+ ->and($atDropoff->calls[1][1]->get('details'))->toBe('Driver entered destination geofence "dropoff-zone".');
349
+ });
350
+
351
+ test('geofence entered arrival skips a completed pickup the payload pointer has not advanced past', function () {
352
+ $connection = fleetopsHandleGeofenceEnteredConnection();
353
+ fleetopsEnteredCompleteStatus($connection, 'pickup-tracking');
354
+
355
+ $atPickup = fleetopsEnteredOrder(fleetopsEnteredEndpointPayload('pickup-place', 'pickup-tracking'));
356
+ fleetopsEnteredArrive($atPickup, fleetopsEnteredZone('pickup-zone', 1.3521, 103.8198));
357
+
358
+ $atDropoff = fleetopsEnteredOrder(fleetopsEnteredEndpointPayload(null, 'pickup-tracking'));
359
+ fleetopsEnteredArrive($atDropoff, fleetopsEnteredZone('dropoff-zone', 1.3000, 103.9000));
360
+
361
+ expect($atPickup->calls)->toBe([])
362
+ ->and($atDropoff->calls[0])->toBe(['setStatus', 'arrived', true]);
363
+ });
364
+
365
+ test('geofence entered arrival targets the current stop of a multi-waypoint order', function () {
366
+ $connection = fleetopsHandleGeofenceEnteredConnection();
367
+ fleetopsEnteredCompleteStatus($connection, 'first-tracking');
368
+
369
+ $multiDropPayload = function (?string $currentWaypointUuid): Payload {
370
+ $payload = new Payload();
371
+ $payload->setRawAttributes(['uuid' => 'multi-payload-uuid', 'current_waypoint_uuid' => $currentWaypointUuid], true);
372
+ $payload->setRelation('pickup', null);
373
+ $payload->setRelation('dropoff', null);
374
+ $payload->setRelation('waypoints', collect());
375
+
376
+ $markers = collect([
377
+ ['first', 'first-tracking', new Point(1.3521, 103.8198)],
378
+ ['second', null, new Point(1.3000, 103.9000)],
379
+ ['third', null, new Point(1.4000, 103.7000)],
380
+ ])->map(function (array $stop, int $index) {
381
+ [$name, $trackingNumberUuid, $location] = $stop;
382
+
383
+ $waypoint = new Waypoint();
384
+ $waypoint->setRawAttributes([
385
+ 'uuid' => $name . '-waypoint',
386
+ 'place_uuid' => $name . '-place',
387
+ 'tracking_number_uuid' => $trackingNumberUuid,
388
+ 'order' => $index,
389
+ ], true);
390
+ $waypoint->setRelation('place', fleetopsEnteredPlace($name . '-place', $location));
391
+ $waypoint->setRelation('trackingNumber', null);
392
+
393
+ return $waypoint;
394
+ });
395
+ $payload->setRelation('waypointMarkers', $markers);
396
+
397
+ return $payload;
398
+ };
399
+
400
+ $atCompletedStop = fleetopsEnteredOrder($multiDropPayload('second-place'));
401
+ fleetopsEnteredArrive($atCompletedStop, fleetopsEnteredZone('first-zone', 1.3521, 103.8198));
402
+
403
+ $atLaterStop = fleetopsEnteredOrder($multiDropPayload('second-place'));
404
+ fleetopsEnteredArrive($atLaterStop, fleetopsEnteredZone('third-zone', 1.4000, 103.7000));
405
+
406
+ $atCurrentStop = fleetopsEnteredOrder($multiDropPayload('second-place'));
407
+ fleetopsEnteredArrive($atCurrentStop, fleetopsEnteredZone('second-zone', 1.3000, 103.9000));
408
+
409
+ $staleFirstStop = fleetopsEnteredOrder($multiDropPayload(null));
410
+ fleetopsEnteredArrive($staleFirstStop, fleetopsEnteredZone('second-zone', 1.3000, 103.9000));
411
+
412
+ expect($atCompletedStop->calls)->toBe([])
413
+ ->and($atLaterStop->calls)->toBe([])
414
+ ->and($atCurrentStop->calls[0])->toBe(['setStatus', 'arrived', true])
415
+ ->and($staleFirstStop->calls[0])->toBe(['setStatus', 'arrived', true]);
416
+ });
@@ -21,6 +21,11 @@ class FleetOpsUserRemovedCompanyDriverQueryFake
21
21
  {
22
22
  public bool $deleted = false;
23
23
 
24
+ public function get(): Illuminate\Support\Collection
25
+ {
26
+ return collect([$this]);
27
+ }
28
+
24
29
  public function delete(): void
25
30
  {
26
31
  $this->deleted = true;