@fleetbase/fleetops-engine 0.6.71 → 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 (378) hide show
  1. package/RELEASE.md +15 -18
  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/form/route.hbs +1 -1
  16. package/addon/components/order/form/route.js +13 -1
  17. package/addon/components/service-rate/details.hbs +1 -1
  18. package/addon/components/service-rate/form.hbs +1 -1
  19. package/addon/components/tracking-page/full-view.hbs +276 -0
  20. package/addon/components/tracking-page/full-view.js +474 -0
  21. package/addon/components/tracking-page/locale-selector.js +30 -0
  22. package/addon/components/tracking-page/map.hbs +44 -0
  23. package/addon/components/tracking-page/map.js +328 -0
  24. package/addon/components/tracking-page/order-list.hbs +35 -0
  25. package/addon/components/tracking-page/public-view.hbs +60 -0
  26. package/addon/components/tracking-page/settings-preview.hbs +21 -0
  27. package/addon/components/tracking-page/settings-preview.js +23 -0
  28. package/addon/components/tracking-page/verify-panel.hbs +109 -0
  29. package/addon/components/tracking-page.hbs +142 -0
  30. package/addon/components/tracking-page.js +564 -0
  31. package/addon/components/trailer/form.hbs +2 -2
  32. package/addon/components/vehicle/form.hbs +3 -3
  33. package/addon/components/warranty/form.hbs +2 -2
  34. package/addon/components/widget/live-fleet.js +54 -6
  35. package/addon/components/widget/proof-of-delivery.hbs +49 -0
  36. package/addon/components/widget/proof-of-delivery.js +62 -0
  37. package/addon/components/work-order/form.hbs +3 -3
  38. package/addon/controllers/operations/orders/index/details.js +19 -3
  39. package/addon/controllers/operations/scheduler/index.js +45 -14
  40. package/addon/controllers/settings/index.js +6 -0
  41. package/addon/controllers/settings/map.js +75 -0
  42. package/addon/controllers/settings/orchestrator.js +8 -6
  43. package/addon/controllers/settings/tracking-page.js +212 -0
  44. package/addon/extension.js +65 -1
  45. package/addon/helpers/parcel-box-image.js +16 -0
  46. package/addon/routes/operations/orders/index/new.js +5 -4
  47. package/addon/routes/operations/scheduler/index.js +1 -1
  48. package/addon/routes/settings/tracking-page.js +31 -0
  49. package/addon/routes.js +1 -0
  50. package/addon/services/location.js +69 -21
  51. package/addon/services/map-settings.js +28 -0
  52. package/addon/services/order-allocation.js +1 -1
  53. package/addon/styles/fleetops-engine.css +1391 -0
  54. package/addon/templates/settings/map.hbs +46 -0
  55. package/addon/templates/settings/tracking-page.hbs +318 -0
  56. package/addon/utils/identity-cell-resource.js +2 -56
  57. package/addon/utils/map-default-view.js +132 -0
  58. package/addon/utils/placeholder-images.js +4 -69
  59. package/addon/utils/resource-descriptors/assets.js +2 -57
  60. package/addon/utils/resource-descriptors/helpers.js +23 -122
  61. package/addon/utils/resource-descriptors/operations.js +2 -61
  62. package/addon/utils/resource-descriptors/people.js +3 -142
  63. package/addon/utils/resource-descriptors/shared.js +18 -0
  64. package/addon/utils/tracking-live-source.js +123 -0
  65. package/addon/utils/tracking-page-theme.js +123 -0
  66. package/app/components/admin/tracking-page-settings.js +1 -0
  67. package/app/components/tracking-page/full-view.js +1 -0
  68. package/app/components/tracking-page/locale-selector.js +1 -0
  69. package/app/components/tracking-page/map.js +1 -0
  70. package/app/components/tracking-page/order-list.js +1 -0
  71. package/app/components/tracking-page/public-view.js +1 -0
  72. package/app/components/tracking-page/settings-preview.js +1 -0
  73. package/app/components/tracking-page/verify-panel.js +1 -0
  74. package/app/components/tracking-page.js +1 -0
  75. package/app/components/widget/proof-of-delivery.js +1 -0
  76. package/app/controllers/settings/tracking-page.js +1 -0
  77. package/app/helpers/parcel-box-image.js +1 -0
  78. package/app/routes/settings/tracking-page.js +1 -0
  79. package/app/templates/settings/tracking-page.js +1 -0
  80. package/app/utils/map-default-view.js +1 -0
  81. package/app/utils/tracking-live-source.js +1 -0
  82. package/app/utils/tracking-page-theme.js +1 -0
  83. package/composer.json +2 -2
  84. package/extension.json +1 -1
  85. package/package.json +4 -4
  86. package/scripts/pest-bootstrap.php +4 -0
  87. package/server/config/fleetops.php +4 -0
  88. package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
  89. package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
  90. package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
  91. package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
  92. package/server/resources/views/labels/default.php +2 -2
  93. package/server/resources/views/labels/entity-label.php +2 -2
  94. package/server/resources/views/labels/waypoint-label.php +2 -2
  95. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  96. package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
  97. package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
  98. package/server/src/Events/OrderChanged.php +257 -0
  99. package/server/src/Events/OrderDriverUnassigned.php +68 -0
  100. package/server/src/Exceptions/OrderEditLockedException.php +32 -0
  101. package/server/src/Expansions/CompanyExpansion.php +3 -2
  102. package/server/src/Expansions/UserExpansion.php +12 -10
  103. package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
  104. package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
  105. package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
  106. package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
  107. package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
  108. package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
  109. package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
  110. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
  111. package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
  112. package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
  113. package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
  114. package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
  115. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
  116. package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
  117. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
  118. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
  119. package/server/src/Http/Controllers/Internal/v1/OrderController.php +84 -24
  120. package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
  121. package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
  122. package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
  123. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
  124. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
  125. package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
  126. package/server/src/Http/Filter/ContactFilter.php +1 -1
  127. package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
  128. package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
  129. package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
  130. package/server/src/Http/Filter/OrderFilter.php +39 -4
  131. package/server/src/Http/Filter/ProofFilter.php +18 -0
  132. package/server/src/Http/Filter/RouteFilter.php +18 -0
  133. package/server/src/Http/Filter/TelematicFilter.php +23 -0
  134. package/server/src/Http/Filter/WarrantyFilter.php +23 -0
  135. package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
  136. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
  137. package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
  138. package/server/src/Http/Resources/v1/Index/Order.php +11 -2
  139. package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
  140. package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
  141. package/server/src/Http/Resources/v1/Zone.php +31 -23
  142. package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
  143. package/server/src/Jobs/ProcessAllocationJob.php +16 -6
  144. package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
  145. package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
  146. package/server/src/Listeners/HandleOrderCanceled.php +9 -1
  147. package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
  148. package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
  149. package/server/src/Mail/CustomerCredentialsMail.php +4 -2
  150. package/server/src/Models/AssetConnection.php +3 -0
  151. package/server/src/Models/Contact.php +3 -0
  152. package/server/src/Models/Device.php +28 -0
  153. package/server/src/Models/DeviceEvent.php +7 -0
  154. package/server/src/Models/Driver.php +20 -7
  155. package/server/src/Models/Entity.php +12 -4
  156. package/server/src/Models/Fleet.php +11 -32
  157. package/server/src/Models/FleetDriver.php +3 -0
  158. package/server/src/Models/FleetVehicle.php +3 -0
  159. package/server/src/Models/FuelReport.php +5 -13
  160. package/server/src/Models/Issue.php +4 -16
  161. package/server/src/Models/Order.php +200 -9
  162. package/server/src/Models/Payload.php +227 -35
  163. package/server/src/Models/Place.php +7 -1
  164. package/server/src/Models/ServiceArea.php +7 -0
  165. package/server/src/Models/ServiceRate.php +7 -17
  166. package/server/src/Models/TrackingNumber.php +101 -2
  167. package/server/src/Models/TrackingSession.php +33 -0
  168. package/server/src/Models/TrackingSessionGrant.php +22 -0
  169. package/server/src/Models/TrackingStatus.php +6 -0
  170. package/server/src/Models/Vehicle.php +21 -2
  171. package/server/src/Models/Vendor.php +7 -0
  172. package/server/src/Models/Waypoint.php +13 -4
  173. package/server/src/Notifications/OrderAssigned.php +2 -1
  174. package/server/src/Notifications/OrderCanceled.php +7 -1
  175. package/server/src/Notifications/OrderChanged.php +87 -0
  176. package/server/src/Notifications/OrderCompleted.php +2 -1
  177. package/server/src/Notifications/OrderDispatchFailed.php +2 -1
  178. package/server/src/Notifications/OrderDispatched.php +2 -1
  179. package/server/src/Notifications/OrderFailed.php +2 -1
  180. package/server/src/Notifications/OrderUnassigned.php +66 -0
  181. package/server/src/Notifications/WaypointCompleted.php +2 -1
  182. package/server/src/Observers/CompanyUserObserver.php +8 -7
  183. package/server/src/Observers/DriverObserver.php +6 -1
  184. package/server/src/Observers/OrderObserver.php +141 -1
  185. package/server/src/Observers/TrackingNumberObserver.php +18 -6
  186. package/server/src/Observers/UserObserver.php +2 -1
  187. package/server/src/Observers/VehicleObserver.php +8 -3
  188. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
  189. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
  190. package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
  191. package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
  192. package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
  193. package/server/src/Providers/EventServiceProvider.php +11 -0
  194. package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
  195. package/server/src/Support/ApiCacheThrottle.php +76 -0
  196. package/server/src/Support/LiveOrderQuery.php +3 -1
  197. package/server/src/Support/OrderEdit.php +365 -0
  198. package/server/src/Support/OrderEditPolicy.php +292 -0
  199. package/server/src/Support/ProfileAccountManager.php +54 -1
  200. package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
  201. package/server/src/Support/SocketChannels.php +346 -0
  202. package/server/src/Support/Telematics/TelematicService.php +7 -2
  203. package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
  204. package/server/src/Support/TrackingChannel.php +55 -0
  205. package/server/src/Support/TrackingCode.php +242 -0
  206. package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
  207. package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
  208. package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
  209. package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
  210. package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
  211. package/server/src/Support/TrackingPage/TrackingView.php +543 -0
  212. package/server/src/Support/TrackingPublisher.php +261 -0
  213. package/server/src/Support/TrackingScope.php +182 -0
  214. package/server/src/Support/TrackingUpdate.php +402 -0
  215. package/server/src/Support/Utils.php +15 -3
  216. package/server/src/Traits/HasTrackingNumber.php +2 -0
  217. package/server/src/routes.php +39 -7
  218. package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
  219. package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
  220. package/server/tests/ApiEntityControllerContractsTest.php +84 -0
  221. package/server/tests/ApiOrderControllerContractsTest.php +245 -0
  222. package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
  223. package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
  224. package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
  225. package/server/tests/CompactResourceSerializationTest.php +23 -59
  226. package/server/tests/ControllerFilterContractsTest.php +1 -1
  227. package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
  228. package/server/tests/EventContractsTest.php +26 -60
  229. package/server/tests/ExpansionContractsTest.php +15 -2
  230. package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
  231. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
  232. package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
  233. package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
  234. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
  235. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
  236. package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
  237. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
  238. package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
  239. package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
  240. package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
  241. package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
  242. package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
  243. package/server/tests/IndexCustomerPhotoTest.php +151 -0
  244. package/server/tests/ModelAccessorContractsTest.php +4 -3
  245. package/server/tests/NotificationAndMailContractsTest.php +5 -8
  246. package/server/tests/ObserverContractsTest.php +72 -10
  247. package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
  248. package/server/tests/OrderFilterExecutionTest.php +167 -2
  249. package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
  250. package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
  251. package/server/tests/QueueJobContractsTest.php +2 -1
  252. package/server/tests/RequestContractsTest.php +1 -1
  253. package/server/tests/RouteRegistrationExecutionTest.php +26 -0
  254. package/server/tests/RouteSequencingEngineTest.php +17 -0
  255. package/server/tests/SettingControllerContractsTest.php +110 -0
  256. package/server/tests/Support/TrackingPageDatabase.php +200 -0
  257. package/server/tests/TenantScopingTest.php +206 -0
  258. package/server/tests/TrackingPageSettingsTest.php +343 -0
  259. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
  260. package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
  261. package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
  262. package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
  263. package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
  264. package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
  265. package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
  266. package/server/tests/Unit/Models/FleetTest.php +4 -3
  267. package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
  268. package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
  269. package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
  270. package/server/tests/Unit/Models/PayloadTest.php +18 -4
  271. package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
  272. package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
  273. package/server/tests/Unit/Models/VehicleTest.php +16 -2
  274. package/server/tests/Unit/Models/WaypointTest.php +4 -3
  275. package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
  276. package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
  277. package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
  278. package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
  279. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
  280. package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
  281. package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
  282. package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
  283. package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
  284. package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
  285. package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
  286. package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
  287. package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
  288. package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
  289. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
  290. package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
  291. package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
  292. package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
  293. package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
  294. package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
  295. package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
  296. package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
  297. package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
  298. package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
  299. package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
  300. package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
  301. package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
  302. package/server/tests/VehicleResourceContractsTest.php +46 -0
  303. package/server/tests/VroomOrchestrationEngineTest.php +34 -1
  304. package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
  305. package/translations/ar-ae.yml +274 -0
  306. package/translations/bg-bg.yaml +274 -0
  307. package/translations/en-us.yaml +287 -0
  308. package/translations/es-pa.yaml +274 -0
  309. package/translations/fr-fr.yaml +274 -0
  310. package/translations/mn-mn.yaml +274 -0
  311. package/translations/pt-br.yaml +274 -0
  312. package/translations/ru-ru.yaml +276 -2
  313. package/translations/uk-ua.yaml +275 -1
  314. package/translations/vi-vn.yaml +274 -0
  315. package/addon/components/cell/contact-identity.hbs +0 -1
  316. package/addon/components/cell/customer-identity.hbs +0 -1
  317. package/addon/components/cell/driver-identity.hbs +0 -1
  318. package/addon/components/cell/fleet-identity.hbs +0 -1
  319. package/addon/components/cell/order-identity.hbs +0 -1
  320. package/addon/components/cell/place-identity.hbs +0 -1
  321. package/addon/components/cell/vehicle-identity.hbs +0 -1
  322. package/addon/components/cell/vendor-identity.hbs +0 -1
  323. package/addon/components/contact/pill.hbs +0 -24
  324. package/addon/components/contact/summary.hbs +0 -1
  325. package/addon/components/customer/pill.hbs +0 -24
  326. package/addon/components/customer/summary.hbs +0 -1
  327. package/addon/components/driver/pill.hbs +0 -24
  328. package/addon/components/driver/summary.hbs +0 -1
  329. package/addon/components/fleet/pill.hbs +0 -24
  330. package/addon/components/fleet/summary.hbs +0 -1
  331. package/addon/components/order/pill.hbs +0 -24
  332. package/addon/components/order/summary.hbs +0 -1
  333. package/addon/components/place/pill.hbs +0 -24
  334. package/addon/components/place/summary.hbs +0 -1
  335. package/addon/components/select-option/contact.hbs +0 -12
  336. package/addon/components/select-option/customer.hbs +0 -12
  337. package/addon/components/select-option/driver.hbs +0 -12
  338. package/addon/components/select-option/fleet.hbs +0 -12
  339. package/addon/components/select-option/order.hbs +0 -12
  340. package/addon/components/select-option/place.hbs +0 -12
  341. package/addon/components/select-option/vehicle.hbs +0 -12
  342. package/addon/components/select-option/vendor.hbs +0 -12
  343. package/addon/components/vehicle/pill.hbs +0 -24
  344. package/addon/components/vehicle/summary.hbs +0 -1
  345. package/addon/components/vendor/pill.hbs +0 -24
  346. package/addon/components/vendor/summary.hbs +0 -1
  347. package/app/components/cell/contact-identity.js +0 -1
  348. package/app/components/cell/customer-identity.js +0 -1
  349. package/app/components/cell/driver-identity.js +0 -1
  350. package/app/components/cell/fleet-identity.js +0 -1
  351. package/app/components/cell/order-identity.js +0 -1
  352. package/app/components/cell/place-identity.js +0 -1
  353. package/app/components/cell/vehicle-identity.js +0 -1
  354. package/app/components/cell/vendor-identity.js +0 -1
  355. package/app/components/contact/pill.js +0 -1
  356. package/app/components/contact/summary.js +0 -1
  357. package/app/components/customer/pill.js +0 -1
  358. package/app/components/customer/summary.js +0 -1
  359. package/app/components/driver/pill.js +0 -1
  360. package/app/components/driver/summary.js +0 -1
  361. package/app/components/fleet/pill.js +0 -1
  362. package/app/components/fleet/summary.js +0 -1
  363. package/app/components/order/pill.js +0 -1
  364. package/app/components/order/summary.js +0 -1
  365. package/app/components/place/pill.js +0 -1
  366. package/app/components/place/summary.js +0 -1
  367. package/app/components/select-option/contact.js +0 -1
  368. package/app/components/select-option/customer.js +0 -1
  369. package/app/components/select-option/driver.js +0 -1
  370. package/app/components/select-option/fleet.js +0 -1
  371. package/app/components/select-option/order.js +0 -1
  372. package/app/components/select-option/place.js +0 -1
  373. package/app/components/select-option/vehicle.js +0 -1
  374. package/app/components/select-option/vendor.js +0 -1
  375. package/app/components/vehicle/pill.js +0 -1
  376. package/app/components/vehicle/summary.js +0 -1
  377. package/app/components/vendor/pill.js +0 -1
  378. package/app/components/vendor/summary.js +0 -1
@@ -0,0 +1,143 @@
1
+ <?php
2
+
3
+ if (!class_exists('FleetOpsOrderCancelReasonRecorder', false)) {
4
+ class FleetOpsOrderCancelReasonRecorder
5
+ {
6
+ public static array $events = [];
7
+
8
+ public static function dispatch($job): object
9
+ {
10
+ if ($job instanceof Closure) {
11
+ $job();
12
+ }
13
+
14
+ return new class {
15
+ public function afterCommit(): self
16
+ {
17
+ return $this;
18
+ }
19
+ };
20
+ }
21
+ }
22
+ }
23
+
24
+ if (!function_exists('Fleetbase\FleetOps\Models\dispatch')) {
25
+ eval('namespace Fleetbase\FleetOps\Models; function dispatch($job) { return \FleetOpsOrderCancelReasonRecorder::dispatch($job); }');
26
+ }
27
+
28
+ if (!function_exists('Fleetbase\FleetOps\Models\event')) {
29
+ eval('namespace Fleetbase\FleetOps\Models; function event($event = null) { \FleetOpsOrderCancelReasonRecorder::$events[] = $event; return $event; }');
30
+ }
31
+
32
+ if (!class_exists('Fleetbase\FleetOps\Events\OrderCanceled', false)) {
33
+ eval('namespace Fleetbase\FleetOps\Events; class OrderCanceled { public $activity = null; public $waypoint = null; public function __construct(public $order) {} public function getModelRecord() { return $this->order; } }');
34
+ }
35
+
36
+ use Fleetbase\FleetOps\Events\OrderCanceled;
37
+ use Fleetbase\FleetOps\Flow\Activity;
38
+ use Fleetbase\FleetOps\Listeners\HandleOrderCanceled;
39
+ use Fleetbase\FleetOps\Models\Driver;
40
+ use Fleetbase\FleetOps\Models\Order;
41
+ use Fleetbase\FleetOps\Models\OrderConfig;
42
+
43
+ /**
44
+ * Covers canceling an order with a reason: the reason becomes the canceled
45
+ * status details and rides on the OrderCanceled event, while the config's
46
+ * activity is left untouched and a plain cancel carries no activity.
47
+ */
48
+ class FleetOpsOrderCancelReasonOrder extends Order
49
+ {
50
+ public array $activities = [];
51
+
52
+ public function updateActivity(?Activity $activity = null, $proof = null): Order
53
+ {
54
+ $this->activities[] = $activity;
55
+ $this->status = $activity?->code;
56
+
57
+ return $this;
58
+ }
59
+ }
60
+
61
+ class FleetOpsOrderCancelReasonConfig extends OrderConfig
62
+ {
63
+ public ?Activity $canceled = null;
64
+
65
+ public function getCanceledActivity()
66
+ {
67
+ return $this->canceled;
68
+ }
69
+ }
70
+
71
+ function fleetopsOrderCancelReasonOrder(): array
72
+ {
73
+ FleetOpsOrderCancelReasonRecorder::$events = [];
74
+
75
+ $config = new FleetOpsOrderCancelReasonConfig();
76
+ $config->canceled = new Activity(['code' => 'canceled', 'status' => 'Order canceled', 'details' => 'Order was canceled']);
77
+
78
+ $order = new FleetOpsOrderCancelReasonOrder();
79
+ $order->setRawAttributes(['uuid' => 'order-uuid', 'status' => 'dispatched'], true);
80
+ $order->setRelation('orderConfig', $config);
81
+
82
+ return [$order, $config];
83
+ }
84
+
85
+ test('canceling with a reason records it as the canceled details and on the event', function () {
86
+ [$order, $config] = fleetopsOrderCancelReasonOrder();
87
+
88
+ $order->cancelWithReason(' Customer moved house ');
89
+
90
+ $event = collect(FleetOpsOrderCancelReasonRecorder::$events)->first(fn ($event) => $event instanceof OrderCanceled);
91
+
92
+ expect($order->status)->toBe('canceled')
93
+ ->and($order->activities[0]->get('details'))->toBe('Customer moved house')
94
+ ->and($config->canceled->get('details'))->toBe('Order was canceled')
95
+ ->and($event)->not->toBeNull()
96
+ ->and($event->activity->get('details'))->toBe('Customer moved house');
97
+ });
98
+
99
+ test('a plain cancel keeps the config activity and sends no reason', function () {
100
+ [$order, $config] = fleetopsOrderCancelReasonOrder();
101
+
102
+ $order->cancel();
103
+
104
+ $event = collect(FleetOpsOrderCancelReasonRecorder::$events)->first(fn ($event) => $event instanceof OrderCanceled);
105
+
106
+ expect($order->activities[0])->toBe($config->canceled)
107
+ ->and($event->activity)->toBeNull();
108
+ });
109
+
110
+ test('the canceled listener passes the reason on to the assigned driver', function () {
111
+ [$order] = fleetopsOrderCancelReasonOrder();
112
+ $order->setRawAttributes(array_merge($order->getAttributes(), ['driver_assigned_uuid' => 'driver-uuid']));
113
+ $order->setRelation('facilitator', null);
114
+ $order->setRelation('trackingNumber', null);
115
+
116
+ $driver = new class extends Driver {
117
+ public array $notified = [];
118
+
119
+ public function notify($instance)
120
+ {
121
+ $this->notified[] = $instance;
122
+ }
123
+ };
124
+
125
+ $listener = new class($driver) extends HandleOrderCanceled {
126
+ public function __construct(private Driver $driverForTest)
127
+ {
128
+ }
129
+
130
+ protected function findAssignedDriver($order): ?Driver
131
+ {
132
+ return $this->driverForTest;
133
+ }
134
+ };
135
+
136
+ $event = new OrderCanceled($order);
137
+ $event->activity = new Activity(['code' => 'canceled', 'details' => 'Customer moved house']);
138
+ $listener->handle($event);
139
+
140
+ expect($driver->notified)->toHaveCount(1)
141
+ ->and($driver->notified[0]->reason)->toBe('Customer moved house')
142
+ ->and($driver->notified[0]->data)->toBe(['id' => null, 'type' => 'order_canceled', 'reason' => 'Customer moved house']);
143
+ });
@@ -240,6 +240,66 @@ test('distance and time setters use origin and destination positions', function
240
240
  expect($origin?->getLat())->toBe(1.20);
241
241
  });
242
242
 
243
+ test('a driver without a reported location does not turn the order origin into null island', function () {
244
+ $connection = fleetopsOrderPayloadBoot();
245
+ // Sunshine -> Melton, Victoria: a ~25 km straight-line run
246
+ $connection->table('places')->insert([
247
+ ['uuid' => '11111111-1111-4111-8111-111111111111', 'public_id' => 'place_vicpickup', 'company_uuid' => 'company-1', 'location' => fleetopsOrderPayloadWkb(-37.78, 144.83)],
248
+ ['uuid' => '22222222-2222-4222-8222-222222222222', 'public_id' => 'place_vicdropoff', 'company_uuid' => 'company-1', 'location' => fleetopsOrderPayloadWkb(-37.68, 144.58)],
249
+ ]);
250
+ $connection->table('payloads')->insert(['uuid' => 'payload-1', 'company_uuid' => 'company-1', 'pickup_uuid' => '11111111-1111-4111-8111-111111111111', 'dropoff_uuid' => '22222222-2222-4222-8222-222222222222']);
251
+ $connection->table('users')->insert(['uuid' => 'user-1', 'company_uuid' => 'company-1', 'type' => 'user']);
252
+ // Drivers are created with a POINT(0 0) placeholder until their first location report
253
+ $connection->table('drivers')->insert(['uuid' => '44444444-4444-4444-8444-444444444444', 'public_id' => 'driver_vicnoloc', 'company_uuid' => 'company-1', 'user_uuid' => 'user-1', 'location' => fleetopsOrderPayloadWkb(0, 0)]);
254
+ $connection->table('orders')->insert(['uuid' => 'order-1', 'company_uuid' => 'company-1', 'payload_uuid' => 'payload-1', 'driver_assigned_uuid' => '44444444-4444-4444-8444-444444444444']);
255
+
256
+ $order = Order::query()->where('uuid', 'order-1')->first();
257
+ $origin = $order->getCurrentOriginPosition();
258
+ expect($origin?->getLat())->toBe(-37.78)
259
+ ->and($origin?->getLng())->toBe(144.83);
260
+
261
+ $order->setPreliminaryDistanceAndTime();
262
+ $distance = (float) $connection->table('orders')->value('distance');
263
+ $time = (float) $connection->table('orders')->value('time');
264
+ expect($distance)->toBeGreaterThan(20000)->toBeLessThan(30000)
265
+ ->and($time)->toBe(round($distance / 100) * 7.2);
266
+
267
+ $connection->table('orders')->where('uuid', 'order-1')->update(['distance' => null, 'time' => null]);
268
+ $order = Order::query()->where('uuid', 'order-1')->first();
269
+ $order->setDistanceAndTime(['provider' => 'calculate']);
270
+ expect((float) $connection->table('orders')->value('distance'))->toBe($distance)
271
+ ->and((float) $connection->table('orders')->value('time'))->toBe($time);
272
+
273
+ // Once the driver reports a real position it becomes the origin again
274
+ $connection->table('drivers')->update(['location' => fleetopsOrderPayloadWkb(-37.70, 144.70)]);
275
+ $order = Order::query()->where('uuid', 'order-1')->first();
276
+ expect($order->getCurrentOriginPosition()?->getLat())->toBe(-37.70);
277
+ });
278
+
279
+ test('distance setters leave the order untouched when an endpoint is an ungeocoded placeholder', function () {
280
+ $connection = fleetopsOrderPayloadBoot();
281
+ $connection->table('places')->insert([
282
+ ['uuid' => '11111111-1111-4111-8111-111111111111', 'public_id' => 'place_vicpickup', 'company_uuid' => 'company-1', 'location' => fleetopsOrderPayloadWkb(-37.78, 144.83)],
283
+ ['uuid' => '22222222-2222-4222-8222-222222222222', 'public_id' => 'place_ungeocoded', 'company_uuid' => 'company-1', 'location' => fleetopsOrderPayloadWkb(0, 0)],
284
+ ]);
285
+ $connection->table('payloads')->insert([
286
+ ['uuid' => 'payload-1', 'company_uuid' => 'company-1', 'pickup_uuid' => '11111111-1111-4111-8111-111111111111', 'dropoff_uuid' => '22222222-2222-4222-8222-222222222222'],
287
+ ['uuid' => 'payload-2', 'company_uuid' => 'company-1', 'pickup_uuid' => '22222222-2222-4222-8222-222222222222', 'dropoff_uuid' => '11111111-1111-4111-8111-111111111111'],
288
+ ]);
289
+ $connection->table('orders')->insert([
290
+ ['uuid' => 'order-1', 'company_uuid' => 'company-1', 'payload_uuid' => 'payload-1'],
291
+ ['uuid' => 'order-2', 'company_uuid' => 'company-1', 'payload_uuid' => 'payload-2'],
292
+ ]);
293
+
294
+ foreach (['order-1', 'order-2'] as $uuid) {
295
+ $order = Order::query()->where('uuid', $uuid)->first();
296
+ expect($order->setPreliminaryDistanceAndTime())->toBe($order)
297
+ ->and($order->setDistanceAndTime(['provider' => 'calculate']))->toBe($order)
298
+ ->and($connection->table('orders')->where('uuid', $uuid)->value('distance'))->toBeNull()
299
+ ->and($connection->table('orders')->where('uuid', $uuid)->value('time'))->toBeNull();
300
+ }
301
+ });
302
+
243
303
  test('config resolution prefers uuid then company default', function () {
244
304
  $connection = fleetopsOrderPayloadBoot();
245
305
  $flow = json_encode([
@@ -205,10 +205,12 @@ test('assign driver covers instance string and invalid identifier branches', fun
205
205
  expect($assigned->assignDriver($driver))->toBe($assigned)
206
206
  ->and($assigned->notified)->toBe([]);
207
207
 
208
- // Driver instance assignment notifies and saves
208
+ // Driver instance assignment saves; the order observer sends the assignment
209
+ // notification after the save, so assignDriver does not send it as well
209
210
  $order->assignDriver($driver);
210
211
  expect($order->driver_assigned_uuid)->toBe('driver-1')
211
- ->and($order->notified)->toBe(['driver-assigned'])
212
+ ->and($order->notified)->toBe([])
213
+ ->and($order->silentDriverAssignment)->toBeFalse()
212
214
  ->and($connection->table('orders')->value('driver_assigned_uuid'))->toBe('driver-1');
213
215
 
214
216
  // Public id resolution recurses into instance assignment
@@ -32,7 +32,8 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
32
32
  use Illuminate\Database\Eloquent\Relations\HasOne;
33
33
  use Illuminate\Database\SQLiteConnection;
34
34
 
35
- class FleetOpsPayloadUnitRelationCountFake
35
+ // Extends the relation the model method now declares, so the override stays compatible; the parent constructor is skipped on purpose.
36
+ class FleetOpsPayloadUnitRelationCountFake extends HasManyThrough
36
37
  {
37
38
  public function __construct(private readonly Payload $payload, private readonly string $relation)
38
39
  {
@@ -44,6 +45,19 @@ class FleetOpsPayloadUnitRelationCountFake
44
45
  }
45
46
  }
46
47
 
48
+ // Extends the relation the model method now declares, so the override stays compatible; the parent constructor is skipped on purpose.
49
+ class FleetOpsPayloadUnitEntityCountFake extends HasMany
50
+ {
51
+ public function __construct(private readonly Payload $payload)
52
+ {
53
+ }
54
+
55
+ public function count(): int
56
+ {
57
+ return $this->payload->getRelation('entities')?->count() ?? 0;
58
+ }
59
+ }
60
+
47
61
  class FleetOpsPayloadUnitDatabaseProbe
48
62
  {
49
63
  public function __construct(private SQLiteConnection $connection)
@@ -147,14 +161,14 @@ class FleetOpsPayloadUnitFake extends Payload
147
161
  public array $activityUpdates = [];
148
162
  public array $currentWaypointWrites = [];
149
163
 
150
- public function waypoints()
164
+ public function waypoints(): HasManyThrough
151
165
  {
152
166
  return new FleetOpsPayloadUnitRelationCountFake($this, 'waypoints');
153
167
  }
154
168
 
155
- public function entities()
169
+ public function entities(): HasMany
156
170
  {
157
- return new FleetOpsPayloadUnitRelationCountFake($this, 'entities');
171
+ return new FleetOpsPayloadUnitEntityCountFake($this);
158
172
  }
159
173
 
160
174
  public function hasMeta($key): bool
@@ -92,12 +92,12 @@ class FleetOpsServicableRate extends ServiceRate
92
92
  return parent::getAttribute($key);
93
93
  }
94
94
 
95
- public function rateFees()
95
+ public function rateFees(): Illuminate\Database\Eloquent\Relations\HasMany
96
96
  {
97
97
  return $this->hasMany(Fleetbase\FleetOps\Models\ServiceRateFee::class, 'service_rate_uuid', 'uuid');
98
98
  }
99
99
 
100
- public function parcelFees()
100
+ public function parcelFees(): Illuminate\Database\Eloquent\Relations\HasMany
101
101
  {
102
102
  return $this->hasMany(Fleetbase\FleetOps\Models\ServiceRateParcelFee::class, 'service_rate_uuid', 'uuid');
103
103
  }
@@ -84,13 +84,21 @@ class FleetOpsTrackingNumberInsertFake extends TrackingNumber
84
84
 
85
85
  beforeEach(function () {
86
86
  FleetOpsTrackingNumberInsertFake::resetInsertFake();
87
+
88
+ $this->previousConsoleConfig = config('fleetbase.console');
89
+ config(['fleetbase.console' => ['host' => 'console.fleetbase.test', 'subdomain' => null, 'secure' => true]]);
90
+ });
91
+
92
+ afterEach(function () {
93
+ config(['fleetbase.console' => $this->previousConsoleConfig]);
87
94
  });
88
95
 
89
96
  test('tracking number insert filters values and creates initial owner status', function () {
90
97
  $owner = new Order();
91
98
  $owner->setRawAttributes([
92
- 'uuid' => 'order-uuid',
93
- 'status' => 'pending',
99
+ 'uuid' => 'order-uuid',
100
+ 'public_id' => 'order_public',
101
+ 'status' => 'pending',
94
102
  ], true);
95
103
 
96
104
  $uuid = FleetOpsTrackingNumberInsertFake::insertGetUuid([
@@ -114,8 +122,10 @@ test('tracking number insert filters values and creates initial owner status', f
114
122
  'owner_uuid' => 'order-uuid',
115
123
  'owner_type' => Utils::getMutationType($owner),
116
124
  'tracking_number' => 'TRACK-AE',
117
- 'qr_code' => 'QRCODE:order-uuid',
118
- 'barcode' => 'PDF417:order-uuid',
125
+ // The QR carries the tracking url naming the owner by public id, never its uuid;
126
+ // the barcode carries the bare tracking number printed beneath it.
127
+ 'qr_code' => 'QRCODE,M:https://console.fleetbase.test/~/track-order?order=TRACK-AE&r=order_public&v=1',
128
+ 'barcode' => 'C128:TRACK-AE',
119
129
  'meta' => '{"source":"api"}',
120
130
  ])->and($inserted)->not->toHaveKey('not_allowed');
121
131
 
@@ -147,8 +157,8 @@ test('tracking number insert defaults region location and skips side effects whe
147
157
  ->and(FleetOpsTrackingNumberInsertFake::$insertedValues[0])->toMatchArray([
148
158
  'uuid' => 'tracking-uuid',
149
159
  'tracking_number' => 'TRACK-SG',
150
- 'qr_code' => 'QRCODE:',
151
- 'barcode' => 'PDF417:',
160
+ 'qr_code' => 'QRCODE,M:https://console.fleetbase.test/~/track-order?order=TRACK-SG&v=1',
161
+ 'barcode' => 'C128:TRACK-SG',
152
162
  ])
153
163
  ->and(FleetOpsTrackingNumberInsertFake::$createdStatuses)->toBe([])
154
164
  ->and(FleetOpsTrackingNumberInsertFake::$statusUpdates)->toBe([])
@@ -197,3 +207,54 @@ test('tracking numbers generate unique values and resolve or fail lookups', func
197
207
  ->and(TrackingNumber::findTrackingOrFail('track_findone1')->uuid)->toBe('tn-find-1')
198
208
  ->and(fn () => TrackingNumber::findTrackingOrFail('missing-number'))->toThrow(Illuminate\Database\Eloquent\ModelNotFoundException::class);
199
209
  });
210
+
211
+ test('tracking number insert takes the owner public id from callers without an owner model', function () {
212
+ // Waypoints and entities insert their owner with a raw query and pass only ids.
213
+ FleetOpsTrackingNumberInsertFake::insertGetUuid([
214
+ 'owner_uuid' => 'waypoint-uuid',
215
+ 'owner_type' => 'Fleetbase\\FleetOps\\Models\\Waypoint',
216
+ 'owner_public_id' => 'waypoint_public',
217
+ 'region' => 'SG',
218
+ ]);
219
+
220
+ $inserted = FleetOpsTrackingNumberInsertFake::$insertedValues[0];
221
+
222
+ expect($inserted['qr_code'])->toBe('QRCODE,M:https://console.fleetbase.test/~/track-order?order=TRACK-SG&r=waypoint_public&v=1')
223
+ // A helper key, not a column: the fillable filter keeps it out of the insert.
224
+ ->and($inserted)->not->toHaveKey('owner_public_id')
225
+ ->and($inserted['owner_uuid'])->toBe('waypoint-uuid');
226
+ });
227
+
228
+ test('tracking number code images render a medium-correction QR and a Code 128 barcode', function () {
229
+ $calls = new ArrayObject();
230
+ foreach (['DNS2D', 'DNS1D'] as $facade) {
231
+ app()->instance($facade, new class($facade, $calls) {
232
+ public function __construct(private string $facade, private ArrayObject $calls)
233
+ {
234
+ }
235
+
236
+ public function getBarcodePNG(...$arguments)
237
+ {
238
+ $this->calls[] = array_merge([$this->facade], $arguments);
239
+
240
+ return $this->facade . '-png';
241
+ }
242
+ });
243
+ Illuminate\Support\Facades\Facade::clearResolvedInstance($facade);
244
+ }
245
+
246
+ try {
247
+ expect(TrackingNumber::codeImages('ACM1234567890SG', 'entity_public'))->toBe([
248
+ 'qr_code' => 'DNS2D-png',
249
+ 'barcode' => 'DNS1D-png',
250
+ ])->and($calls->getArrayCopy())->toBe([
251
+ ['DNS2D', 'https://console.fleetbase.test/~/track-order?order=ACM1234567890SG&r=entity_public&v=1', 'QRCODE,M'],
252
+ ['DNS1D', 'ACM1234567890SG', 'C128', 2, 60],
253
+ ]);
254
+ } finally {
255
+ foreach (['DNS2D', 'DNS1D'] as $facade) {
256
+ app()->forgetInstance($facade);
257
+ Illuminate\Support\Facades\Facade::clearResolvedInstance($facade);
258
+ }
259
+ }
260
+ });
@@ -100,6 +100,17 @@ class FleetOpsVehicleModelFindResultFake
100
100
  {
101
101
  }
102
102
 
103
+ public function where($column, $operator = null): static
104
+ {
105
+ if (is_callable($column)) {
106
+ $column($this->query);
107
+ } else {
108
+ $this->query->calls[] = ['where', [$column, $operator]];
109
+ }
110
+
111
+ return $this;
112
+ }
113
+
103
114
  public function first(): ?Vehicle
104
115
  {
105
116
  return $this->result;
@@ -117,6 +128,8 @@ class FleetOpsVehicleModelFindableFake extends Vehicle
117
128
 
118
129
  if (is_callable($column)) {
119
130
  $column($query);
131
+ } else {
132
+ $query->calls[] = ['where', [$column, $operator]];
120
133
  }
121
134
 
122
135
  self::$lastQuery = $query;
@@ -258,9 +271,10 @@ test('vehicle import parses single vehicle name and find by name builds identifi
258
271
  FleetOpsVehicleModelFindableFake::$result = $found;
259
272
 
260
273
  expect(FleetOpsVehicleModelFindableFake::findByName('ABC-123'))->toBe($found)
274
+ ->and(FleetOpsVehicleModelFindableFake::$lastQuery->calls[0])->toBe(['where', ['company_uuid', 'company-vehicle']])
261
275
  ->and(FleetOpsVehicleModelFindableFake::$lastQuery->calls)->toContain(['where', ['public_id', 'ABC-123']])
262
276
  ->and(FleetOpsVehicleModelFindableFake::$lastQuery->calls)->toContain(['orWhere', ['plate_number', 'ABC-123']])
263
277
  ->and(FleetOpsVehicleModelFindableFake::$lastQuery->calls)->toContain(['orWhere', ['fuel_card_number', 'ABC-123']])
264
- ->and(FleetOpsVehicleModelFindableFake::$lastQuery->calls[7][0])->toBe('orWhereRaw')
265
- ->and(FleetOpsVehicleModelFindableFake::$lastQuery->calls[8][0])->toBe('orWhereRaw');
278
+ ->and(FleetOpsVehicleModelFindableFake::$lastQuery->calls[8][0])->toBe('orWhereRaw')
279
+ ->and(FleetOpsVehicleModelFindableFake::$lastQuery->calls[9][0])->toBe('orWhereRaw');
266
280
  });
@@ -218,9 +218,10 @@ test('waypoint insert filters values and creates payload tracking number', funct
218
218
  ])->and($inserted)->not->toHaveKey('not_allowed')
219
219
  ->and(FleetOpsWaypointInsertFake::$createdTracking)->toHaveCount(1)
220
220
  ->and(FleetOpsWaypointInsertFake::$createdTracking[0])->toMatchArray([
221
- 'owner_uuid' => 'waypoint-uuid',
222
- 'owner_type' => Utils::getModelClassName('waypoint'),
223
- 'region' => 'AE',
221
+ 'owner_uuid' => 'waypoint-uuid',
222
+ 'owner_type' => Utils::getModelClassName('waypoint'),
223
+ 'owner_public_id' => FleetOpsWaypointInsertFake::$insertedValues[0]['public_id'],
224
+ 'region' => 'AE',
224
225
  ])
225
226
  ->and(FleetOpsWaypointInsertFake::$createdTracking[0]['location'])->toBe('POINT(55.2708 25.2048)')
226
227
  ->and(FleetOpsWaypointInsertFake::$trackingNumberWrites)->toBe([
@@ -0,0 +1,59 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Order;
4
+ use Fleetbase\FleetOps\Models\TrackingNumber;
5
+ use Fleetbase\FleetOps\Notifications\OrderChanged;
6
+ use Fleetbase\FleetOps\Notifications\OrderUnassigned;
7
+ use NotificationChannels\Apn\ApnChannel;
8
+ use NotificationChannels\Fcm\FcmChannel;
9
+
10
+ /**
11
+ * Covers the driver pushes for order changes and unassignment: their titles,
12
+ * bodies and the data the apps read.
13
+ */
14
+ function fleetopsOrderChangeNotificationOrder(): Order
15
+ {
16
+ $trackingNumber = new TrackingNumber();
17
+ $trackingNumber->setRawAttributes(['uuid' => 'tn-1', 'tracking_number' => 'FLB-CHANGE-1'], true);
18
+
19
+ $order = new Order();
20
+ $order->setRawAttributes([
21
+ 'uuid' => 'order-uuid',
22
+ 'public_id' => 'order_change',
23
+ 'company_uuid' => 'company-1',
24
+ 'driver_assigned_uuid' => 'driver-uuid',
25
+ ], true);
26
+ $order->setRelation('trackingNumber', $trackingNumber);
27
+ $order->setRelation('driverAssigned', null);
28
+
29
+ return $order;
30
+ }
31
+
32
+ test('order changed pushes the changes to the driver with fcm safe data', function () {
33
+ $notification = new OrderChanged(fleetopsOrderChangeNotificationOrder(), ['dropoff', 'items'], 'Drop-off changed to 8 Marina View · Items changed');
34
+
35
+ expect($notification->title)->toBe('Order FLB-CHANGE-1 changed')
36
+ ->and($notification->message)->toBe('Drop-off changed to 8 Marina View · Items changed')
37
+ ->and($notification->data)->toBe(['type' => 'order_changed', 'id' => 'order_change', 'changes' => ['dropoff', 'items']])
38
+ ->and($notification->fcmData())->toBe(['type' => 'order_changed', 'id' => 'order_change', 'changes' => 'dropoff,items'])
39
+ ->and($notification->via(null))->toBe(['broadcast', FcmChannel::class, ApnChannel::class])
40
+ ->and($notification->toArray())->toMatchArray(['event' => 'order.changed_notification', 'data' => $notification->data])
41
+ ->and(array_map(fn ($channel) => $channel->name, $notification->broadcastOn()))->toContain('driver.driver-uuid')
42
+ ->and((new OrderChanged(fleetopsOrderChangeNotificationOrder()))->message)->toBe('This order was changed, tap for details.');
43
+
44
+ // The push builders reach for the FCM and APN clients, which the harness does not configure
45
+ expect(fn () => $notification->toFcm(null))->toThrow(Exception::class)
46
+ ->and(fn () => $notification->toApn(null))->toThrow(Error::class);
47
+ });
48
+
49
+ test('order unassigned tells the driver the order is no longer theirs', function () {
50
+ $notification = new OrderUnassigned(fleetopsOrderChangeNotificationOrder());
51
+
52
+ expect($notification->title)->toBe('Order FLB-CHANGE-1 unassigned')
53
+ ->and($notification->data)->toBe(['type' => 'order_unassigned', 'id' => 'order_change'])
54
+ ->and($notification->via(null))->toBe([FcmChannel::class, ApnChannel::class])
55
+ ->and($notification->toArray()['event'])->toBe('order.unassigned_notification');
56
+
57
+ expect(fn () => $notification->toFcm(null))->toThrow(Exception::class)
58
+ ->and(fn () => $notification->toApn(null))->toThrow(Error::class);
59
+ });
@@ -141,7 +141,7 @@ test('completed order notification formats mail and array payloads from order tr
141
141
  'No further action is necessary.'
142
142
  )
143
143
  ->and($mail->actionText)->toBe('Track Order')
144
- ->and($mail->actionUrl)->toContain('order=ORDER-TRACK');
144
+ ->and($mail->actionUrl)->toBe('https://console.fleetbase.test/~/track/ORDER-TRACK');
145
145
  });
146
146
 
147
147
  test('failed order notification formats mail and array payloads from waypoint tracking', function () {
@@ -166,7 +166,7 @@ test('failed order notification formats mail and array payloads from waypoint tr
166
166
  'No further action is necessary.'
167
167
  )
168
168
  ->and($mail->actionText)->toBe('Track Order')
169
- ->and($mail->actionUrl)->toContain('order=WP-FAILED');
169
+ ->and($mail->actionUrl)->toBe('https://console.fleetbase.test/~/track/WP-FAILED');
170
170
  });
171
171
 
172
172
  test('dispatched order notification formats mail from waypoint tracking fallback', function () {
@@ -181,5 +181,5 @@ test('dispatched order notification formats mail from waypoint tracking fallback
181
181
  ->and($mail->subject)->toBe('Order WP-DISPATCHED has been dispatched!')
182
182
  ->and($mail->introLines)->toBe(['An order has just been dispatched to you and is ready to be started.'])
183
183
  ->and($mail->actionText)->toBe('Track Order')
184
- ->and($mail->actionUrl)->toContain('order=WP-DISPATCHED');
184
+ ->and($mail->actionUrl)->toBe('https://console.fleetbase.test/~/track/WP-DISPATCHED');
185
185
  });