@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
@@ -2739,8 +2739,8 @@ test('tracking number and waypoint webhook resources serialize tracking contract
2739
2739
  'scheduled_at' => '2026-07-30 09:00:00',
2740
2740
  ]);
2741
2741
 
2742
- // The decoded QR content is a debug-only field and must never ride along on a webhook,
2743
- // which is a separate literal payload rather than a filtered toArray().
2742
+ // The webhook is a separate literal payload rather than a filtered toArray(), and its
2743
+ // shape is unchanged: the QR content is not added to it.
2744
2744
  expect(array_key_exists('qr_code_content', (new TrackingNumberResource($trackingNumber))->toWebhookPayload()))->toBeFalse();
2745
2745
 
2746
2746
  expect((new TrackingNumberResource($trackingNumber))->toWebhookPayload())->toMatchArray([
@@ -2790,14 +2790,15 @@ test('tracking number and waypoint webhook resources serialize tracking contract
2790
2790
  ]);
2791
2791
  });
2792
2792
 
2793
- test('tracking number resource publishes the qr code content only in debug mode', function () {
2794
- // qr_code is a base64 PNG generated from owner_uuid, and the endpoints that consume a
2795
- // scanned code match on that uuid. Debug builds publish the value beside the image so
2796
- // an automated client can follow the flow without decoding a PNG; production must not.
2793
+ test('tracking number resource publishes a scannable qr code content beside the image', function () {
2794
+ // qr_code is a base64 PNG; qr_code_content is the text current labels encode in it,
2795
+ // a tracking url naming the tracking number and the subject's public id. Both are in
2796
+ // the resource already, so it is published in every environment.
2797
2797
  $trackingNumber = fleetopsCompactResourceFixture([
2798
- 'tracking_number' => 'TN-DEBUG',
2799
- 'owner_uuid' => 'owner-uuid-under-the-qr',
2798
+ 'tracking_number' => 'TN-SCAN',
2799
+ 'owner_uuid' => 'owner-uuid-never-published',
2800
2800
  'owner_type' => 'Fleetbase\\FleetOps\\Models\\Order',
2801
+ 'owner' => (object) ['public_id' => 'order_public'],
2801
2802
  'region' => 'sg',
2802
2803
  'qr_code' => 'qr-data',
2803
2804
  'barcode' => 'barcode-data',
@@ -2811,70 +2812,33 @@ test('tracking number resource publishes the qr code content only in debug mode'
2811
2812
  // cover the index resource and the webhook payload.
2812
2813
  $previousContainer = Illuminate\Container\Container::getInstance();
2813
2814
  $container = new class extends Illuminate\Container\Container {
2814
- public bool $debugMode = false;
2815
-
2816
2815
  public function environment(...$environments)
2817
2816
  {
2818
2817
  return in_array('testing', $environments, true) || $environments === [] ? 'testing' : false;
2819
2818
  }
2820
-
2821
- // Stands in for the framework's Application::hasDebugModeEnabled().
2822
- public function hasDebugModeEnabled()
2823
- {
2824
- return $this->debugMode;
2825
- }
2826
2819
  };
2827
2820
  $container->instance('config', new Illuminate\Config\Repository([
2828
2821
  'fleetbase' => ['console' => ['host' => 'console.fleetbase.test', 'subdomain' => null, 'secure' => true]],
2829
2822
  ]));
2830
2823
  Illuminate\Container\Container::setInstance($container);
2831
2824
 
2832
- // Drive the REAL accessor through the container rather than overriding it, so the
2833
- // debug check itself is exercised and not just the resource's use of it.
2834
- $container->debugMode = true;
2835
- $withDebug = new TrackingNumberResource($trackingNumber);
2836
-
2837
2825
  try {
2838
- $debugPayload = $withDebug->resolve($request);
2839
-
2840
- $container->debugMode = false;
2841
- $productionPayload = (new TrackingNumberResource($trackingNumber))->resolve($request);
2826
+ $payload = (new TrackingNumberResource($trackingNumber))->resolve($request);
2827
+ $withoutNumber = (new TrackingNumberResource(fleetopsCompactResourceFixture([
2828
+ 'tracking_number' => null,
2829
+ 'owner_type' => 'Fleetbase\\FleetOps\\Models\\Order',
2830
+ 'owner' => null,
2831
+ ])))->resolve($request);
2842
2832
  } finally {
2843
2833
  Illuminate\Container\Container::setInstance($previousContainer);
2844
2834
  }
2845
2835
 
2846
- // Present and exactly the value the QR was generated from — not a public id.
2847
- expect($debugPayload['qr_code_content'])->toBe('owner-uuid-under-the-qr')
2848
- ->and($debugPayload['qr_code'])->toBe('qr-data')
2849
- // Absent, not null: `when()` drops the key entirely, so a production response is
2850
- // byte-identical to one from before the field existed.
2851
- ->and(array_key_exists('qr_code_content', $productionPayload))->toBeFalse()
2852
- ->and($productionPayload['qr_code'])->toBe('qr-data');
2853
- });
2854
-
2855
- test('tracking number resource fails closed when the debug state cannot be determined', function () {
2856
- // Any failure to resolve the debug state must answer false rather than defaulting to
2857
- // exposure. The real accessor is exercised here with no usable application bound.
2858
- $expose = new ReflectionMethod(TrackingNumberResource::class, 'exposesQrCodeContent');
2859
- $expose->setAccessible(true);
2860
-
2861
- $previous = Illuminate\Container\Container::getInstance();
2862
-
2863
- try {
2864
- // A container that is not an Application has no hasDebugModeEnabled().
2865
- Illuminate\Container\Container::setInstance(new Illuminate\Container\Container());
2866
- expect($expose->invoke(null))->toBeFalse();
2867
-
2868
- // And an accessor that throws must also answer false rather than propagating —
2869
- // a resource must not be able to fail serialization over a debug check.
2870
- Illuminate\Container\Container::setInstance(new class extends Illuminate\Container\Container {
2871
- public function hasDebugModeEnabled()
2872
- {
2873
- throw new RuntimeException('debug state unavailable');
2874
- }
2875
- });
2876
- expect($expose->invoke(null))->toBeFalse();
2877
- } finally {
2878
- Illuminate\Container\Container::setInstance($previous);
2879
- }
2836
+ expect($payload['qr_code_content'])->toBe('https://console.fleetbase.test/~/track-order?order=TN-SCAN&r=order_public&v=1')
2837
+ ->and($payload['qr_code_content'])->not->toContain('owner-uuid')
2838
+ ->and($payload['url'])->toBe('https://console.fleetbase.test/~/track/TN-SCAN')
2839
+ ->and($payload['qr_code'])->toBe('qr-data')
2840
+ // Nothing to scan without a tracking number: the key is absent, not null.
2841
+ ->and(array_key_exists('qr_code_content', $withoutNumber))->toBeFalse()
2842
+ // Nor is there a tracking page to link to.
2843
+ ->and(array_key_exists('url', $withoutNumber))->toBeFalse();
2880
2844
  });
@@ -1152,7 +1152,7 @@ test('contact filter records identity address and date filters', function () {
1152
1152
  ->and(collect($query->calls)->where(0, 'whereBetween')->values())->toHaveCount(1)
1153
1153
  ->and(collect($query->calls)->where(0, 'whereDate')->values())->toHaveCount(1);
1154
1154
 
1155
- $address = collect($query->calls)->first(fn ($call) => $call[0] === 'whereHas' && $call[1] === 'addresses');
1155
+ $address = collect($query->calls)->first(fn ($call) => $call[0] === 'whereHas' && $call[1] === 'place');
1156
1156
 
1157
1157
  expect($address[2])->toBe([
1158
1158
  ['search', '1 Test Street'],
@@ -74,7 +74,7 @@ test('customer portal welcome email is opt in and does not create organization i
74
74
  ->not->toContain('send_invite');
75
75
 
76
76
  expect($customerCredentials)->toContain('customer portal access is ready')
77
- ->and($customerCredentials)->toContain("Utils::consoleUrl(\$accessUrlSlug ?: 'customer-portal')")
77
+ ->and($customerCredentials)->toContain("Utils::consoleUrl(\$accessUrlSlug ? 'customer-access/' . \$accessUrlSlug : 'customer-portal')")
78
78
  ->and($customerEmail)->toContain('Your customer portal access is ready')
79
79
  ->and($customerEmail)->toContain('Sign in to customer portal')
80
80
  ->and($customerEmail)->toContain('Temporary password:')
@@ -33,6 +33,8 @@ use Fleetbase\FleetOps\Models\FuelProviderTransaction;
33
33
  use Fleetbase\FleetOps\Models\FuelReport;
34
34
  use Fleetbase\FleetOps\Models\GeofenceEventLog;
35
35
  use Fleetbase\FleetOps\Models\Order;
36
+ use Fleetbase\FleetOps\Models\Payload;
37
+ use Fleetbase\FleetOps\Models\Place;
36
38
  use Fleetbase\FleetOps\Models\TrackingStatus;
37
39
  use Fleetbase\FleetOps\Models\Vehicle;
38
40
  use Fleetbase\FleetOps\Models\Waypoint;
@@ -231,6 +233,11 @@ class FleetOpsUserRemovedDriverDeleteQueryFake
231
233
  {
232
234
  public bool $deleted = false;
233
235
 
236
+ public function get(): Illuminate\Support\Collection
237
+ {
238
+ return collect([$this]);
239
+ }
240
+
234
241
  public function delete(): void
235
242
  {
236
243
  $this->deleted = true;
@@ -832,8 +839,19 @@ test('geofence entered listener handles arrival branch outcomes', function () {
832
839
  'name' => 'Event Zone',
833
840
  ], 'zone', new Point(1.3521, 103.8198));
834
841
 
835
- $place = (object) ['location' => new Point(1.3521, 103.8198)];
836
- $destination = (object) ['place' => $place];
842
+ $payloadTo = function (?Point $location): Payload {
843
+ $pickup = new Place();
844
+ $pickup->setRawAttributes(['uuid' => 'pickup-place-uuid', 'location' => $location], true);
845
+
846
+ $payload = new Payload();
847
+ $payload->setRawAttributes(['uuid' => 'arrival-payload-uuid'], true);
848
+ $payload->setRelation('pickup', $pickup);
849
+ $payload->setRelation('dropoff', null);
850
+ $payload->setRelation('waypoints', collect());
851
+ $payload->setRelation('waypointMarkers', collect());
852
+
853
+ return $payload;
854
+ };
837
855
  $nearGeofence = new class {
838
856
  public string $uuid = 'near-zone-uuid';
839
857
  public string $public_id = 'near_zone_public';
@@ -896,66 +914,14 @@ test('geofence entered listener handles arrival branch outcomes', function () {
896
914
  return $order;
897
915
  };
898
916
 
899
- $missingPlaceOrder = $makeOrder((object) [
900
- 'getPickupOrCurrentWaypoint' => null,
901
- ]);
902
- $missingPlaceOrder->setRelation('payload', new class($destination) {
903
- public function __construct(private object $destination)
904
- {
905
- }
906
-
907
- public function getPickupOrCurrentWaypoint(): object
908
- {
909
- return (object) ['place' => (object) ['location' => null]];
910
- }
911
- });
912
-
913
- $farOrder = $makeOrder(new class($destination) {
914
- public function __construct(private object $destination)
915
- {
916
- }
917
-
918
- public function getPickupOrCurrentWaypoint(): object
919
- {
920
- return $this->destination;
921
- }
922
- });
923
-
924
- $badCentroidOrder = $makeOrder(new class($destination) {
925
- public function __construct(private object $destination)
926
- {
927
- }
928
-
929
- public function getPickupOrCurrentWaypoint(): object
930
- {
931
- return $this->destination;
932
- }
933
- });
934
-
935
- $customer = new FleetOpsGeofenceArrivalCustomerFake();
936
- $successOrder = $makeOrder(new class($destination) {
937
- public function __construct(private object $destination)
938
- {
939
- }
940
-
941
- public function getPickupOrCurrentWaypoint(): object
942
- {
943
- return $this->destination;
944
- }
945
- }, $customer);
946
-
917
+ $missingPlaceOrder = $makeOrder($payloadTo(null));
918
+ $farOrder = $makeOrder($payloadTo(new Point(1.3521, 103.8198)));
919
+ $badCentroidOrder = $makeOrder($payloadTo(new Point(1.3521, 103.8198)));
920
+ $customer = new FleetOpsGeofenceArrivalCustomerFake();
921
+ $successOrder = $makeOrder($payloadTo(new Point(1.3521, 103.8198)), $customer);
947
922
  $throwingCustomer = new FleetOpsGeofenceArrivalCustomerFake();
948
923
  $throwingCustomer->shouldThrow = true;
949
- $notificationFailureOrder = $makeOrder(new class($destination) {
950
- public function __construct(private object $destination)
951
- {
952
- }
953
-
954
- public function getPickupOrCurrentWaypoint(): object
955
- {
956
- return $this->destination;
957
- }
958
- }, $throwingCustomer);
924
+ $notificationFailureOrder = $makeOrder($payloadTo(new Point(1.3521, 103.8198)), $throwingCustomer);
959
925
 
960
926
  $arrival->invoke($listener, $driver, $nearGeofence, $missingPlaceOrder, $event);
961
927
  $arrival->invoke($listener, $driver, $farGeofence, $farOrder, $event);
@@ -11,7 +11,9 @@ function bindFleetOpsExpansionClosure(Closure $closure, object $target): Closure
11
11
  return $closure->bindTo($target, $target::class);
12
12
  }
13
13
 
14
- class FleetOpsExpansionRelationFake
14
+ // The expansion closures declare HasOne / HasMany return types, so the fakes extend
15
+ // those relations; the parent constructor is skipped on purpose.
16
+ trait FleetOpsExpansionRecordsRelationCalls
15
17
  {
16
18
  public array $calls = [];
17
19
 
@@ -34,6 +36,16 @@ class FleetOpsExpansionRelationFake
34
36
  }
35
37
  }
36
38
 
39
+ class FleetOpsExpansionRelationFake extends Illuminate\Database\Eloquent\Relations\HasOne
40
+ {
41
+ use FleetOpsExpansionRecordsRelationCalls;
42
+ }
43
+
44
+ class FleetOpsExpansionManyRelationFake extends Illuminate\Database\Eloquent\Relations\HasMany
45
+ {
46
+ use FleetOpsExpansionRecordsRelationCalls;
47
+ }
48
+
37
49
  class FleetOpsExpansionModelFake
38
50
  {
39
51
  public array $calls = [];
@@ -41,9 +53,10 @@ class FleetOpsExpansionModelFake
41
53
 
42
54
  public function __construct()
43
55
  {
44
- foreach (['driver', 'driverProfiles', 'customer', 'contact'] as $name) {
56
+ foreach (['driver', 'customer', 'contact'] as $name) {
45
57
  $this->relations[$name] = new FleetOpsExpansionRelationFake($name);
46
58
  }
59
+ $this->relations['driverProfiles'] = new FleetOpsExpansionManyRelationFake('driverProfiles');
47
60
  }
48
61
 
49
62
  public function hasMany($class)
@@ -452,14 +452,16 @@ test('identity checks session context and user lookups resolve', function () {
452
452
  test('verification codes files contacts and customer auth resolve', function () {
453
453
  $connection = fleetopsCustomerHelperBoot();
454
454
  $connection->table('verification_codes')->insert(['uuid' => 'vc-1', 'subject_uuid' => 'user-1', 'subject_type' => 'user', 'code' => '123456', 'for' => 'customer_verification']);
455
+ // A code is used up by the lookup that accepts it, so each check gets its own row.
456
+ $connection->table('verification_codes')->insert(['uuid' => 'vc-2', 'subject_uuid' => 'user-1', 'subject_type' => 'user', 'code' => '654321', 'for' => 'customer_verification']);
455
457
  $connection->table('files')->insert(['uuid' => 'file-1', 'public_id' => 'file_custhelp1', 'company_uuid' => 'company-1', 'name' => 'photo.png']);
456
458
  $connection->table('contacts')->insert(['uuid' => 'contact-1', 'public_id' => 'contact_custhelp1', 'company_uuid' => 'company-1', 'name' => 'Casey', 'type' => 'customer']);
457
459
 
458
460
  $probe = new FleetOpsCustomerControllerProbe();
459
461
 
460
- expect($probe->callHelper('verificationCodeExists', ['code' => '123456', 'for' => 'customer_verification']))->toBeTrue()
461
- ->and($probe->callHelper('findVerificationCode', ['code' => '123456'])?->uuid)->toBe('vc-1')
462
- ->and($probe->callHelper('findVerificationCode', ['code' => '999999']))->toBeNull()
462
+ expect($probe->callHelper('verificationCodeExists', ['code' => '654321', 'for' => 'customer_verification']))->toBeTrue()
463
+ ->and($probe->callHelper('findVerificationCode', ['code' => '123456', 'for' => 'customer_verification'])?->uuid)->toBe('vc-1')
464
+ ->and($probe->callHelper('findVerificationCode', ['code' => '999999', 'for' => 'customer_verification']))->toBeNull()
463
465
  ->and($probe->callHelper('findFileByPublicId', 'file_custhelp1')?->uuid)->toBe('file-1');
464
466
 
465
467
  expect($probe->callHelper('findCustomerContact', ['uuid' => 'contact-1'])?->name)->toBe('Casey')
@@ -829,3 +831,27 @@ test('customer place photo and quote seams resolve through their real bodies', f
829
831
  expect($connection->table('contacts')->where('uuid', '88888888-8888-4888-8888-888888888820')->value('photo_uuid'))->toBe('file-b64-1');
830
832
  app()->forgetInstance(CustomerAuth::APP_BINDING);
831
833
  });
834
+
835
+ test('a wrong code counts against the live code, a right one uses it up, and a used code never verifies again', function () {
836
+ $connection = fleetopsCustomerHelperBoot();
837
+ $controller = new CustomerController();
838
+
839
+ $connection->table('verification_codes')->insert([
840
+ ['uuid' => 'vc-older', 'code' => '101010', 'for' => 'fleetops_create_customer', 'meta' => json_encode(['identity' => 'spend@example.com']), 'status' => 'active', 'created_at' => '2026-10-13 09:00:00'],
841
+ ['uuid' => 'vc-newer', 'code' => '202020', 'for' => 'fleetops_create_customer', 'meta' => json_encode(['identity' => 'spend@example.com']), 'status' => 'active', 'created_at' => '2026-10-13 09:05:00'],
842
+ ]);
843
+
844
+ $wrong = $controller->create(CreateCustomerRequest::create('/v1/customers', 'POST', ['code' => '999999', 'identity' => 'spend@example.com', 'name' => 'Spender', 'password' => 'secret']));
845
+ expect($wrong->getData(true)['error'])->toContain('Invalid verification code')
846
+ ->and(json_decode($connection->table('verification_codes')->where('uuid', 'vc-newer')->value('meta'), true)['attempts'])->toBe(1)
847
+ ->and(json_decode($connection->table('verification_codes')->where('uuid', 'vc-older')->value('meta'), true)['attempts'])->toBe(1);
848
+
849
+ // The older code still verifies while it is live, and is used up by doing so.
850
+ $controller->create(CreateCustomerRequest::create('/v1/customers', 'POST', ['code' => '101010', 'identity' => 'spend@example.com', 'name' => 'Spender', 'password' => 'secret']));
851
+ expect($connection->table('contacts')->where('email', 'spend@example.com')->count())->toBe(1)
852
+ ->and($connection->table('verification_codes')->where('uuid', 'vc-older')->value('status'))->toBe('used')
853
+ ->and($connection->table('verification_codes')->where('uuid', 'vc-newer')->value('status'))->toBe('active');
854
+
855
+ $again = $controller->create(CreateCustomerRequest::create('/v1/customers', 'POST', ['code' => '101010', 'identity' => 'spend@example.com', 'name' => 'Spender', 'password' => 'secret']));
856
+ expect($again->getData(true)['error'])->toContain('Invalid verification code');
857
+ });
@@ -304,6 +304,29 @@ test('phone login reports the sms method when the transport delivers', function
304
304
  ->and($connection->table('verification_codes')->where('for', 'driver_login')->count())->toBe(1);
305
305
  });
306
306
 
307
+ test('phone login passes an sms guard refusal through once no email can carry the code', function () {
308
+ $connection = fleetopsDriverAuthBoot();
309
+ app()->instance('twilio', new class {
310
+ public function message($to, $text, $mediaUrls = [], $params = [])
311
+ {
312
+ throw Fleetbase\Exceptions\SmsBlockedException::paused('platform', 300);
313
+ }
314
+ });
315
+ Fleetbase\Twilio\Support\Laravel\Facade::clearResolvedInstances();
316
+
317
+ // With an email on file the refusal is handled like any send failure: by email.
318
+ app()->instance('request', Request::create('/x', 'POST', ['phone' => '6591234567']));
319
+ $byEmail = (new DriverController())->loginWithPhone();
320
+ expect($byEmail->getData(true))->toBe(['status' => 'OK', 'method' => 'email']);
321
+
322
+ // Without one, the app gets the refusal itself: 423 and how long to wait.
323
+ $connection->table('users')->where('uuid', 'user-1')->update(['email' => null]);
324
+ $refused = (new DriverController())->loginWithPhone();
325
+ expect($refused->getStatusCode())->toBe(423)
326
+ ->and($refused->headers->get('Retry-After'))->toBe('300')
327
+ ->and($refused->getData(true)['error'])->toBe('Text messages are temporarily unavailable. Try again later.');
328
+ });
329
+
307
330
  test('code verification handles unknown users invalid codes bypass and success', function () {
308
331
  $connection = fleetopsDriverAuthBoot();
309
332
  $controller = new DriverController();
@@ -135,19 +135,20 @@ function fleetopsOrderNearbyBoot(): SQLiteConnection
135
135
 
136
136
  $schema = $connection->getSchemaBuilder();
137
137
  $tables = [
138
- 'orders' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'payload_uuid', 'driver_assigned_uuid', 'customer_uuid', 'customer_type', 'facilitator_uuid', 'facilitator_type', 'tracking_number_uuid', 'status', 'pod_required', 'dispatched', 'scheduled_at', 'type'],
139
- 'payloads' => ['uuid', 'public_id', 'company_uuid', 'pickup_uuid', 'dropoff_uuid', 'return_uuid', 'current_waypoint_uuid', 'type'],
140
- 'places' => ['uuid', 'public_id', 'company_uuid', 'name', 'street1', 'city', 'country', 'location', 'meta'],
141
- 'waypoints' => ['uuid', 'public_id', 'company_uuid', 'payload_uuid', 'place_uuid'],
142
- 'entities' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'payload_uuid', 'name'],
143
- 'drivers' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'location'],
144
- 'users' => ['uuid', 'public_id', 'company_uuid', 'type'],
145
- 'contacts' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'name'],
146
- 'vendors' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'name'],
147
- 'companies' => ['uuid', 'public_id', 'name', 'options'],
148
- 'tracking_numbers' => ['uuid', 'public_id', 'company_uuid', 'tracking_number', 'owner_uuid', 'owner_type', 'status_uuid', '_key'],
149
- 'tracking_statuses' => ['uuid', 'public_id', 'company_uuid', 'tracking_number_uuid', 'code', 'status', 'details'],
150
- 'directives' => ['uuid', 'company_uuid', 'permission_uuid', 'subject_type', 'subject_uuid', 'key', 'rules'],
138
+ 'orders' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'payload_uuid', 'driver_assigned_uuid', 'customer_uuid', 'customer_type', 'facilitator_uuid', 'facilitator_type', 'tracking_number_uuid', 'status', 'pod_required', 'dispatched', 'scheduled_at', 'type'],
139
+ 'payloads' => ['uuid', 'public_id', 'company_uuid', 'pickup_uuid', 'dropoff_uuid', 'return_uuid', 'current_waypoint_uuid', 'type'],
140
+ 'places' => ['uuid', 'public_id', 'company_uuid', 'name', 'street1', 'city', 'country', 'location', 'meta'],
141
+ 'waypoints' => ['uuid', 'public_id', 'company_uuid', 'payload_uuid', 'place_uuid'],
142
+ 'entities' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'payload_uuid', 'name'],
143
+ 'drivers' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'location'],
144
+ 'users' => ['uuid', 'public_id', 'company_uuid', 'type'],
145
+ 'contacts' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'name'],
146
+ 'vendors' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'name'],
147
+ 'integrated_vendors' => ['uuid', 'public_id', 'company_uuid', 'provider'],
148
+ 'companies' => ['uuid', 'public_id', 'name', 'options'],
149
+ 'tracking_numbers' => ['uuid', 'public_id', 'company_uuid', 'tracking_number', 'owner_uuid', 'owner_type', 'status_uuid', '_key'],
150
+ 'tracking_statuses' => ['uuid', 'public_id', 'company_uuid', 'tracking_number_uuid', 'code', 'status', 'details'],
151
+ 'directives' => ['uuid', 'company_uuid', 'permission_uuid', 'subject_type', 'subject_uuid', 'key', 'rules'],
151
152
  ];
152
153
  foreach ($tables as $table => $columns) {
153
154
  $schema->create($table, function ($blueprint) use ($columns) {
@@ -276,7 +277,6 @@ test('facilitator and customer filters constrain by public and internal ids', fu
276
277
  'facilitator' => 'vendor_nearby1',
277
278
  'customer' => 'contact_nearby1',
278
279
  ]));
279
- // MorphTo whereHas subqueries build and constrain without matching in
280
- // the harness morph map
281
- expect($result->count())->toBeGreaterThanOrEqual(0);
280
+ // Both ids resolve to the uuids the order stores, so exactly that order matches.
281
+ expect($result->pluck('uuid')->all())->toBe(['order-1']);
282
282
  });
@@ -73,20 +73,21 @@ function fleetopsOrderQueryFiltersBoot(): SQLiteConnection
73
73
 
74
74
  $schema = $connection->getSchemaBuilder();
75
75
  $tables = [
76
- 'orders' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'payload_uuid', 'driver_assigned_uuid', 'vehicle_assigned_uuid', 'customer_uuid', 'customer_type', 'facilitator_uuid', 'facilitator_type', 'tracking_number_uuid', 'status', 'pod_required', 'dispatched', 'scheduled_at', 'type'],
77
- 'payloads' => ['uuid', 'public_id', 'company_uuid', 'pickup_uuid', 'dropoff_uuid', 'return_uuid', 'current_waypoint_uuid', 'type'],
78
- 'places' => ['uuid', 'public_id', 'company_uuid', 'name', 'location'],
79
- 'waypoints' => ['uuid', 'public_id', 'company_uuid', 'payload_uuid', 'place_uuid', 'location'],
80
- 'entities' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'payload_uuid', 'tracking_number_uuid', 'name'],
81
- 'tracking_numbers' => ['uuid', 'public_id', 'company_uuid', 'tracking_number', 'owner_uuid', 'status_uuid'],
82
- 'tracking_statuses' => ['uuid', 'public_id', 'company_uuid', 'tracking_number_uuid', 'code', 'status'],
83
- 'drivers' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'location'],
84
- 'users' => ['uuid', 'public_id', 'company_uuid'],
85
- 'vehicles' => ['uuid', 'public_id', 'company_uuid'],
86
- 'contacts' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'name'],
87
- 'vendors' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'name'],
88
- 'companies' => ['uuid', 'public_id', 'name', 'options'],
89
- 'directives' => ['uuid', 'company_uuid', 'permission_uuid', 'subject_type', 'subject_uuid', 'key', 'rules'],
76
+ 'orders' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'payload_uuid', 'driver_assigned_uuid', 'vehicle_assigned_uuid', 'customer_uuid', 'customer_type', 'facilitator_uuid', 'facilitator_type', 'tracking_number_uuid', 'status', 'pod_required', 'dispatched', 'scheduled_at', 'type'],
77
+ 'payloads' => ['uuid', 'public_id', 'company_uuid', 'pickup_uuid', 'dropoff_uuid', 'return_uuid', 'current_waypoint_uuid', 'type'],
78
+ 'places' => ['uuid', 'public_id', 'company_uuid', 'name', 'location'],
79
+ 'waypoints' => ['uuid', 'public_id', 'company_uuid', 'payload_uuid', 'place_uuid', 'location'],
80
+ 'entities' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'payload_uuid', 'tracking_number_uuid', 'name'],
81
+ 'tracking_numbers' => ['uuid', 'public_id', 'company_uuid', 'tracking_number', 'owner_uuid', 'status_uuid'],
82
+ 'tracking_statuses' => ['uuid', 'public_id', 'company_uuid', 'tracking_number_uuid', 'code', 'status'],
83
+ 'drivers' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'location'],
84
+ 'users' => ['uuid', 'public_id', 'company_uuid'],
85
+ 'vehicles' => ['uuid', 'public_id', 'company_uuid'],
86
+ 'contacts' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'name'],
87
+ 'vendors' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'name'],
88
+ 'integrated_vendors' => ['uuid', 'public_id', 'company_uuid', 'provider'],
89
+ 'companies' => ['uuid', 'public_id', 'name', 'options'],
90
+ 'directives' => ['uuid', 'company_uuid', 'permission_uuid', 'subject_type', 'subject_uuid', 'key', 'rules'],
90
91
  ];
91
92
  foreach ($tables as $table => $columns) {
92
93
  $schema->create($table, function ($blueprint) use ($columns) {
@@ -182,3 +183,29 @@ test('query supports entity status arrays', function () {
182
183
 
183
184
  expect($result->count())->toBe(0);
184
185
  });
186
+
187
+ test('query finds orders by facilitator and customer public ids', function () {
188
+ $connection = fleetopsOrderQueryFiltersBoot();
189
+ $connection->table('payloads')->insert(['uuid' => 'payload-1', 'public_id' => 'payload_test', 'company_uuid' => 'company-1']);
190
+ $connection->table('vendors')->insert(['uuid' => 'vendor-1', 'public_id' => 'vendor_facil', 'internal_id' => 'VND-1', 'company_uuid' => 'company-1']);
191
+ $connection->table('integrated_vendors')->insert(['uuid' => 'integrated-vendor-1', 'public_id' => 'integrated_vendor_facil', 'company_uuid' => 'company-1']);
192
+ $connection->table('contacts')->insert(['uuid' => 'contact-1', 'public_id' => 'contact_cust', 'company_uuid' => 'company-1']);
193
+ // The stored type is the class name, which is not the morph class the
194
+ // morph map gives these models — the mismatch that emptied every result.
195
+ $connection->table('orders')->insert([
196
+ ['uuid' => 'order-1', 'public_id' => 'order_a', 'company_uuid' => 'company-1', 'payload_uuid' => 'payload-1', 'facilitator_uuid' => 'vendor-1', 'facilitator_type' => 'Fleetbase\FleetOps\Models\Vendor', 'customer_uuid' => 'contact-1', 'customer_type' => 'Fleetbase\FleetOps\Models\Contact'],
197
+ ['uuid' => 'order-2', 'public_id' => 'order_b', 'company_uuid' => 'company-1', 'payload_uuid' => 'payload-1', 'facilitator_uuid' => 'integrated-vendor-1', 'facilitator_type' => 'Fleetbase\FleetOps\Models\IntegratedVendor', 'customer_uuid' => 'vendor-1', 'customer_type' => 'Fleetbase\FleetOps\Models\Vendor'],
198
+ ['uuid' => 'order-3', 'public_id' => 'order_c', 'company_uuid' => 'company-1', 'payload_uuid' => 'payload-1', 'facilitator_uuid' => null, 'facilitator_type' => null, 'customer_uuid' => null, 'customer_type' => null],
199
+ ]);
200
+
201
+ $publicIds = fn (array $input) => (new OrderController())->query(fleetopsOrderQueryFiltersRequest($input))->pluck('public_id')->sort()->values()->all();
202
+
203
+ expect($publicIds(['facilitator' => 'vendor_facil']))->toBe(['order_a'])
204
+ ->and($publicIds(['facilitator' => 'VND-1']))->toBe(['order_a'])
205
+ ->and($publicIds(['facilitator' => 'integrated_vendor_facil']))->toBe(['order_b'])
206
+ ->and($publicIds(['customer' => 'contact_cust']))->toBe(['order_a'])
207
+ ->and($publicIds(['customer' => 'vendor_facil']))->toBe(['order_b'])
208
+ // An unknown id matches nothing, never every order.
209
+ ->and($publicIds(['facilitator' => 'vendor_missing']))->toBe([])
210
+ ->and($publicIds(['customer' => 'contact_missing']))->toBe([]);
211
+ });
@@ -133,28 +133,63 @@ test('tracking number helpers look up owners create records and wrap resources',
133
133
  ->and($helper('deletedTrackingNumberResource', $found))->toBeInstanceOf(Fleetbase\FleetOps\Http\Resources\v1\DeletedResource::class)
134
134
  ->and($helper('jsonResponse', ['ok' => true], 200))->toBeInstanceOf(Illuminate\Http\JsonResponse::class);
135
135
 
136
- // Qr model resolution finds models and wraps them in fleet-ops v1 resources
137
- $qrModel = $helper('findQrModel', ['entities'], ['public_id' => 'entity_tnhelper1']);
138
- expect($qrModel)->not->toBeNull()
139
- ->and($helper('qrModelResource', $qrModel))->toBeInstanceOf(Fleetbase\FleetOps\Http\Resources\v1\Entity::class);
140
-
141
- // A table with no eloquent mapping resolves to null rather than a raw row.
142
- //
143
- // The raw-row fallback was removed: it passed the ARRAY of table names to
144
- // DB::table(), which stringified it to the literal table "Array" and threw
145
- // SQLSTATE[42S02], so every unresolvable QR code answered 500 instead of the 400 the
146
- // caller already handles. It could not have helped either way — qrModelResource()
147
- // serializes through a typed resource keyed on the model class, and a raw stdClass
148
- // row has none.
149
- $connection->table('tracking_statuses')->insert([
150
- 'uuid' => '44444444-4444-4444-8444-444444444403',
151
- 'public_id' => 'tracking_status_tnhelper1',
152
- 'code' => 'DISPATCHED',
153
- 'status' => 'Dispatched',
136
+ // Qr model resolution goes through the shared resolver and wraps the owner in its
137
+ // fleet-ops v1 resource.
138
+ $connection->table('tracking_numbers')->insert([
139
+ 'uuid' => '44444444-4444-4444-8444-444444444404',
140
+ 'public_id' => 'track_tnhelper2',
141
+ 'company_uuid' => 'company-1',
142
+ 'tracking_number' => 'FLB-HELPER-2',
143
+ 'owner_uuid' => '44444444-4444-4444-8444-444444444402',
144
+ 'owner_type' => Fleetbase\FleetOps\Models\Entity::class,
145
+ ]);
146
+
147
+ $qrModel = $helper('findQrModel', 'https://console.fleetbase.test/track-order?order=FLB-HELPER-2&r=entity_tnhelper1&v=1');
148
+ expect($qrModel)->toBeInstanceOf(Fleetbase\FleetOps\Models\Entity::class)
149
+ ->and($helper('qrModelResource', $qrModel))->toBeInstanceOf(Fleetbase\FleetOps\Http\Resources\v1\Entity::class)
150
+ ->and($helper('findQrModel', '{{qr_code}}'))->toBeNull();
151
+ });
152
+
153
+ test('tracking number owners resolve from every code format within the company', function () {
154
+ $connection = fleetopsTrackingNumberHelpersBoot();
155
+ $connection->table('orders')->insert(['uuid' => '55555555-5555-4555-8555-555555555501', 'public_id' => 'order_codes1', 'company_uuid' => 'company-1', 'status' => 'created']);
156
+ $connection->table('entities')->insert(['uuid' => '55555555-5555-4555-8555-555555555502', 'public_id' => 'entity_codes1', 'company_uuid' => 'company-1', 'name' => 'Parcel']);
157
+ $connection->table('orders')->insert(['uuid' => '55555555-5555-4555-8555-555555555503', 'public_id' => 'order_codes2', 'company_uuid' => 'company-2', 'status' => 'created']);
158
+ $connection->table('tracking_numbers')->insert([
159
+ ['uuid' => 'tn-codes-1', 'public_id' => 'track_codes1', 'company_uuid' => 'company-1', 'tracking_number' => 'ACM1111111111SG', 'owner_uuid' => '55555555-5555-4555-8555-555555555501', 'owner_type' => Fleetbase\FleetOps\Models\Order::class],
160
+ // Entity::insertGetUuid() inserts raw, storing the owner class with a leading slash.
161
+ ['uuid' => 'tn-codes-2', 'public_id' => 'track_codes2', 'company_uuid' => 'company-1', 'tracking_number' => 'ACM2222222222SG', 'owner_uuid' => '55555555-5555-4555-8555-555555555502', 'owner_type' => '\\' . Fleetbase\FleetOps\Models\Entity::class],
162
+ ['uuid' => 'tn-codes-3', 'public_id' => 'track_codes3', 'company_uuid' => 'company-2', 'tracking_number' => 'OTH3333333333SG', 'owner_uuid' => '55555555-5555-4555-8555-555555555503', 'owner_type' => Fleetbase\FleetOps\Models\Order::class],
154
163
  ]);
155
164
 
156
- expect($helper('findQrModel', ['tracking_statuses'], ['public_id' => 'tracking_status_tnhelper1']))->toBeNull()
157
- // And a mapped table that simply matches nothing is also null, which is the path
158
- // an unknown or unresolved code takes.
159
- ->and($helper('findQrModel', ['entities', 'orders'], ['uuid' => '{{qr_code}}']))->toBeNull();
165
+ $owner = fn (?string $code, ?string $company = 'company-1') => TrackingNumber::findOwnerByCode($code, $company)?->public_id;
166
+
167
+ // Labels in circulation carry the owner's bare uuid.
168
+ expect($owner('55555555-5555-4555-8555-555555555501'))->toBe('order_codes1')
169
+ // Current labels carry a tracking url; the host is irrelevant.
170
+ ->and($owner('https://console.fleetbase.test/track-order?order=ACM1111111111SG&r=order_codes1&v=1'))->toBe('order_codes1')
171
+ ->and($owner('https://old-host.example/track-order?order=ACM2222222222SG&v=1'))->toBe('entity_codes1')
172
+ // The Code 128 barcode, or a value typed by hand: a tracking number or public id.
173
+ ->and($owner('ACM2222222222SG'))->toBe('entity_codes1')
174
+ ->and($owner('entity_codes1'))->toBe('entity_codes1')
175
+ ->and($owner('https://console.fleetbase.test/track-order?r=order_codes1'))->toBe('order_codes1');
176
+
177
+ // A url whose two identifiers disagree, or that names an unknown tracking number, is
178
+ // refused rather than resolved through the half that matches.
179
+ expect($owner('https://console.fleetbase.test/track-order?order=ACM1111111111SG&r=entity_codes1&v=1'))->toBeNull()
180
+ ->and($owner('https://console.fleetbase.test/track-order?order=ACM9999999999SG&r=order_codes1&v=1'))->toBeNull()
181
+ // Unknown values, unmapped prefixes, and values that are no public id at all.
182
+ ->and($owner('55555555-5555-4555-8555-555555555599'))->toBeNull()
183
+ ->and($owner('ACM9999999999SG'))->toBeNull()
184
+ ->and($owner('vehicle_codes1'))->toBeNull()
185
+ ->and($owner('order_missing'))->toBeNull()
186
+ ->and($owner(''))->toBeNull();
187
+
188
+ // Another company's parcel never resolves, in any format, and without a company
189
+ // nothing does.
190
+ expect($owner('55555555-5555-4555-8555-555555555503'))->toBeNull()
191
+ ->and($owner('OTH3333333333SG'))->toBeNull()
192
+ ->and($owner('order_codes2'))->toBeNull()
193
+ ->and($owner('order_codes2', 'company-2'))->toBe('order_codes2')
194
+ ->and($owner('55555555-5555-4555-8555-555555555501', null))->toBeNull();
160
195
  });