@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,479 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Contact;
4
+ use Fleetbase\FleetOps\Models\Driver;
5
+ use Fleetbase\FleetOps\Models\Entity;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\Place;
8
+ use Fleetbase\FleetOps\Models\Proof;
9
+ use Fleetbase\FleetOps\Models\TrackingNumber;
10
+ use Fleetbase\FleetOps\Models\Waypoint;
11
+ use Fleetbase\FleetOps\Support\TrackingPage\TrackingPageConfig;
12
+ use Fleetbase\FleetOps\Support\TrackingPage\TrackingTarget;
13
+ use Fleetbase\FleetOps\Support\TrackingPage\TrackingView;
14
+ use Fleetbase\FleetOps\Support\TrackingScope;
15
+ use Fleetbase\FleetOps\Tracking\TrackingContextBuilder;
16
+ use Fleetbase\FleetOps\Tracking\TrackingIntelligenceService;
17
+ use Fleetbase\FleetOps\Tracking\TrackingStop;
18
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
19
+ use Fleetbase\Tests\Support\TrackingPageDatabase;
20
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
21
+ use Illuminate\Support\Carbon;
22
+ use Illuminate\Support\Collection;
23
+
24
+ class TrackingViewPlace extends Place
25
+ {
26
+ protected $casts = [];
27
+ protected $appends = [];
28
+
29
+ public function getAddressAttribute()
30
+ {
31
+ return $this->attributes['street1'] ?? null;
32
+ }
33
+ }
34
+
35
+ class TrackingViewDriver extends Driver
36
+ {
37
+ protected $casts = [];
38
+ protected $appends = [];
39
+ }
40
+
41
+ /**
42
+ * A TrackingView whose stops, tracker, items, proofs and instructions come from the test.
43
+ */
44
+ class TrackingViewProbe extends TrackingView
45
+ {
46
+ public Collection $stops;
47
+ public array $tracker = [];
48
+ public ?Collection $entities = null;
49
+ public ?Collection $proofRows = null;
50
+ public array $proofQueries = [];
51
+
52
+ protected function contextStopsFor(Order $order): Collection
53
+ {
54
+ return $this->stops;
55
+ }
56
+
57
+ protected function trackerFor(Order $order): array
58
+ {
59
+ return $this->tracker;
60
+ }
61
+
62
+ protected function entitiesFor(Order $order, array $placeUuids, TrackingScope $scope): Collection
63
+ {
64
+ return $this->entities ?? parent::entitiesFor($order, $placeUuids, $scope);
65
+ }
66
+
67
+ protected function proofsFor(array $subjectUuids): Collection
68
+ {
69
+ $this->proofQueries[] = $subjectUuids;
70
+
71
+ return $this->proofRows ?? parent::proofsFor($subjectUuids);
72
+ }
73
+ }
74
+
75
+ afterEach(function () {
76
+ Carbon::setTestNow();
77
+ EloquentModel::unsetConnectionResolver();
78
+ });
79
+
80
+ function trackingViewPlace(string $uuid, string $name, float $lat, float $lng): TrackingViewPlace
81
+ {
82
+ $place = new TrackingViewPlace();
83
+ $place->setRawAttributes(['uuid' => $uuid, 'public_id' => 'place_' . $uuid, 'name' => $name, 'street1' => $name . ' street', 'location' => new Point($lat, $lng)], true);
84
+
85
+ return $place;
86
+ }
87
+
88
+ function trackingViewWaypoint(string $uuid, ?string $customer, array $extra = []): Waypoint
89
+ {
90
+ $waypoint = new Waypoint();
91
+ $waypoint->setRawAttributes(array_merge(['uuid' => $uuid, 'public_id' => 'waypoint_' . $uuid, 'customer_uuid' => $customer, 'customer_type' => $customer ? Contact::class : null], $extra), true);
92
+
93
+ return $waypoint;
94
+ }
95
+
96
+ /**
97
+ * An order with a pickup and two stops: stop A for contact-a, stop B for contact-b.
98
+ */
99
+ function trackingViewOrder(array $attributes = []): Order
100
+ {
101
+ $order = new Order();
102
+ $order->setRawAttributes(array_merge([
103
+ 'uuid' => 'order-1',
104
+ 'company_uuid' => 'co-1',
105
+ 'payload_uuid' => 'payload-1',
106
+ 'customer_uuid' => 'contact-shipper',
107
+ 'customer_type' => Contact::class,
108
+ 'tracking_number_uuid' => 'tn-order',
109
+ 'status' => 'started',
110
+ 'started' => true,
111
+ 'dispatched' => true,
112
+ 'notes' => 'Gate code 1234',
113
+ 'meta' => json_encode(['secret' => 'internal']),
114
+ 'updated_at' => '2026-10-08 08:30:00',
115
+ ], $attributes), true);
116
+
117
+ $driver = new TrackingViewDriver();
118
+ $driver->setRawAttributes(['uuid' => 'driver-1', 'name' => 'Daniel Okafor', 'phone' => '+15550001111', 'online' => 1, 'heading' => 92, 'location' => new Point(1.30123456, 103.80123456), 'updated_at' => '2026-10-08 08:59:00'], true);
119
+ // A driver's name is their user's.
120
+ $user = new Fleetbase\Models\User();
121
+ $user->setRawAttributes(['uuid' => 'user-driver-1', 'name' => 'Daniel Okafor'], true);
122
+ $driver->setRelation('user', $user);
123
+ $order->setRelation('driverAssigned', $driver);
124
+ $order->setRelation('vehicleAssigned', null);
125
+ $order->setRelation('payload', null);
126
+
127
+ return $order;
128
+ }
129
+
130
+ function trackingViewStops(bool $aComplete = false): Collection
131
+ {
132
+ return collect([
133
+ new TrackingStop('place-pickup', 'place_pickup', 'pickup', 'completed', trackingViewPlace('pickup', 'Sender Warehouse', 1.2876, 103.8456), null, true, 0, null),
134
+ new TrackingStop('place-a', 'place_a', 'waypoint', $aComplete ? 'completed' : 'pending', trackingViewPlace('place-a', 'Maya Flat', 1.3333333, 103.7777777), trackingViewWaypoint('wp-a', 'contact-a', ['time_window_start' => '2026-10-08 14:10:00', 'time_window_end' => '2026-10-08 14:40:00']), $aComplete, 1, 'tn-a'),
135
+ new TrackingStop('place-b', 'place_b', 'waypoint', 'pending', trackingViewPlace('place-b', 'Other Customer House', 1.4, 103.9), trackingViewWaypoint('wp-b', 'contact-b'), false, 2, 'tn-b'),
136
+ ]);
137
+ }
138
+
139
+ function trackingViewTarget(Order $order, ?TrackingScope $scope, array $config = [], ?Waypoint $waypoint = null): TrackingTarget
140
+ {
141
+ $trackingNumber = new TrackingNumber();
142
+ $trackingNumber->setRawAttributes(['uuid' => 'tn-a', 'tracking_number' => 'NOR1000000002US'], true);
143
+
144
+ return new TrackingTarget($order, $scope, $trackingNumber, TrackingTarget::KIND_STOP, $waypoint, TrackingPageConfig::sanitize($config));
145
+ }
146
+
147
+ function trackingViewTracker(): array
148
+ {
149
+ return [
150
+ 'confidence' => 'high',
151
+ 'insights' => ['is_delayed' => true],
152
+ 'route' => [
153
+ 'polyline' => '_p~iF~ps|U_ulLnnqC',
154
+ 'legs' => [
155
+ ['stop' => ['sequence' => 1], 'eta_seconds' => 1500, 'eta_at' => '2026-10-08T14:24:00.000000Z'],
156
+ ['stop' => ['sequence' => 2], 'eta_seconds' => 2400, 'eta_at' => '2026-10-08T14:40:00.000000Z'],
157
+ ],
158
+ ],
159
+ 'eta' => ['completion_at' => '2026-10-08T15:00:00.000000Z', 'completion_seconds' => 3600],
160
+ ];
161
+ }
162
+
163
+ function trackingViewEntity(string $uuid, ?string $destination, array $extra = []): Entity
164
+ {
165
+ $entity = new Entity();
166
+ $entity->setRawAttributes(array_merge(['uuid' => $uuid, 'public_id' => 'entity_' . $uuid, 'name' => 'Item ' . $uuid, 'description' => 'Boxed', 'destination_uuid' => $destination, 'price' => '1500', 'currency' => 'SGD', 'meta' => json_encode(['quantity' => 2, 'secret' => 'x'])], $extra), true);
167
+ $entity->setRelation('trackingNumber', null);
168
+
169
+ return $entity;
170
+ }
171
+
172
+ test('branding falls back to the company, then the instance', function () {
173
+ $view = new TrackingView();
174
+ $company = (object) ['name' => 'Northwind Couriers', 'logo_uuid' => 'logo-1', 'logo_url' => 'https://cdn.example/logo.png', 'phone' => '+15105550142', 'website_url' => 'https://northwind.example'];
175
+
176
+ $branding = $view->branding(TrackingPageConfig::sanitize(['branding' => ['accent' => '#5CC9BE', 'support_email' => 'help@northwind.example']]), $company);
177
+ expect($branding)->toMatchArray([
178
+ 'name' => 'Northwind Couriers',
179
+ 'logo_url' => 'https://cdn.example/logo.png',
180
+ 'accent' => '#5CC9BE',
181
+ 'ink' => '#14191A',
182
+ 'support_phone' => '+15105550142',
183
+ 'support_email' => 'help@northwind.example',
184
+ 'website' => 'https://northwind.example',
185
+ 'powered_by' => true,
186
+ ]);
187
+
188
+ $instance = $view->branding(['branding' => ['display_name' => 'Fleetbase']], null, 'https://instance.example/logo.png');
189
+ expect($instance['name'])->toBe('Fleetbase')
190
+ ->and($instance['logo_url'])->toBe('https://instance.example/logo.png')
191
+ ->and($instance['accent'])->toBe(TrackingPageConfig::DEFAULT_ACCENT)
192
+ ->and($instance['locales'])->toBe(TrackingPageConfig::LOCALES);
193
+ });
194
+
195
+ test('the coarse stage follows the order and the customer stops', function () {
196
+ // The order's date casts read the date format from its connection.
197
+ TrackingPageDatabase::boot();
198
+ $view = new TrackingViewProbe();
199
+ $scopeA = new TrackingScope('order-1', Contact::class, 'contact-a');
200
+ $update = fn (Order $order, bool $complete = false) => new Fleetbase\FleetOps\Support\TrackingUpdate($order, $scopeA, trackingViewStops($complete));
201
+
202
+ expect($view->stage(trackingViewOrder(['status' => 'canceled']), null))->toBe('canceled')
203
+ ->and($view->stage(trackingViewOrder(['status' => 'attempt_failed']), null))->toBe('issue')
204
+ ->and($view->stage(trackingViewOrder(['status' => 'completed']), null))->toBe('delivered')
205
+ ->and($view->stage(trackingViewOrder(), $update(trackingViewOrder(), true)))->toBe('delivered')
206
+ ->and($view->stage(trackingViewOrder(), $update(trackingViewOrder())))->toBe('in_transit')
207
+ ->and($view->stage(trackingViewOrder(['started' => false, 'status' => 'dispatched']), null))->toBe('dispatched')
208
+ ->and($view->stage(trackingViewOrder(['started' => false, 'dispatched' => false, 'status' => 'created', 'scheduled_at' => '2099-01-01 09:00:00']), null))->toBe('scheduled')
209
+ ->and($view->stage(trackingViewOrder(['started' => false, 'dispatched' => false, 'status' => 'created']), null))->toBe('preparing');
210
+ });
211
+
212
+ test('a tracking number alone shows the stage, the last update and how to verify', function () {
213
+ // The order's date casts read the date format from its connection.
214
+ TrackingPageDatabase::boot();
215
+ $view = new TrackingViewProbe();
216
+ $view->stops = trackingViewStops();
217
+ $scopeA = new TrackingScope('order-1', Contact::class, 'contact-a');
218
+
219
+ $public = $view->publicView(trackingViewTarget(trackingViewOrder(), $scopeA), ['name' => 'Northwind'], [['type' => 'sms', 'masked' => '•••• •• 4821']], true);
220
+ expect($public)->toBe([
221
+ 'level' => 0,
222
+ 'tracking_number' => 'NOR1000000002US',
223
+ 'company' => ['name' => 'Northwind'],
224
+ 'stage' => 'in_transit',
225
+ 'last_update_on' => '2026-10-08',
226
+ 'verify' => ['channels' => [['type' => 'sms', 'masked' => '•••• •• 4821']]],
227
+ 'sign_in' => true,
228
+ ]);
229
+
230
+ $hidden = $view->publicView(trackingViewTarget(trackingViewOrder(), null, ['access' => ['public_status' => false]]), [], [], false);
231
+ expect($hidden['stage'])->toBeNull()->and($hidden['last_update_on'])->toBeNull();
232
+ });
233
+
234
+ test('a verified customer sees only their own stop on an order with several customers', function () {
235
+ Carbon::setTestNow(Carbon::parse('2026-10-08 09:00:00'));
236
+ $db = TrackingPageDatabase::boot();
237
+ TrackingPageDatabase::insert($db, 'tracking_statuses', ['uuid' => 'ts-1', 'tracking_number_uuid' => 'tn-a', 'status' => 'Waypoint Created', 'code' => 'CREATED', 'complete' => 0, 'created_at' => '2026-10-08 08:00:00']);
238
+ TrackingPageDatabase::insert($db, 'tracking_statuses', ['uuid' => 'ts-2', 'tracking_number_uuid' => 'tn-a', 'status' => 'Out for delivery', 'code' => 'ENROUTE', 'complete' => 0, 'created_at' => '2026-10-08 08:45:00']);
239
+
240
+ $view = new TrackingViewProbe();
241
+ $view->stops = trackingViewStops();
242
+ $view->tracker = trackingViewTracker();
243
+ $view->entities = collect([trackingViewEntity('a1', 'place-a')]);
244
+ $order = trackingViewOrder(['meta' => json_encode(['recipient_instructions' => [sha1('order-1:' . Contact::class . ':contact-a') => 'Leave with concierge']])]);
245
+ $scopeA = new TrackingScope('order-1', Contact::class, 'contact-a', $order);
246
+
247
+ $data = $view->fullView(trackingViewTarget($order, $scopeA, ['visibility' => ['instructions_edit' => true]], $view->stops[1]->waypoint), ['name' => 'Northwind'], TrackingView::VIEWER_VERIFIED, '2026-10-09T09:00:00.000000Z');
248
+
249
+ expect($data['level'])->toBe(1)
250
+ ->and($data['viewer'])->toBe('verified')
251
+ ->and($data['stage'])->toBe('in_transit')
252
+ ->and($data['status'])->toBe(['code' => 'started', 'delayed' => true, 'confidence' => 'high'])
253
+ ->and($data['focus_stop'])->toBe('waypoint_wp-a')
254
+ ->and($data['route'])->toBe(['total_stops' => 3, 'completed_stops' => 1, 'stops_before' => 0])
255
+ ->and($data['stops'])->toHaveCount(1)
256
+ ->and($data['stops'][0])->toMatchArray([
257
+ 'id' => 'waypoint_wp-a',
258
+ 'marker' => '1',
259
+ 'sequence' => 1,
260
+ 'complete' => false,
261
+ 'place' => ['name' => 'Maya Flat', 'address' => 'Maya Flat street', 'latitude' => 1.33333, 'longitude' => 103.77778],
262
+ ])
263
+ ->and($data['stops'][0]['eta']['at'])->toBe('2026-10-08T14:24:00.000000Z')
264
+ ->and($data['stops'][0]['window']['start'])->toBe('2026-10-08T14:10:00.000000Z')
265
+ ->and($data['eta'])->toBe(['at' => '2026-10-08T14:24:00.000000Z', 'seconds' => 1500, 'confidence' => 'high'])
266
+ ->and($data['driver'])->toBe(['name' => 'Daniel', 'vehicle' => null, 'online' => true])
267
+ ->and($data['location']['latitude'])->toBe(1.3012)
268
+ // The pickup shows by name; the line would trace the other customer's stop.
269
+ ->and($data['map'])->toBe(['pickups' => [['name' => 'Sender Warehouse', 'marker' => 'P', 'store' => null, 'latitude' => 1.2876, 'longitude' => 103.8456, 'complete' => true]], 'polyline' => null])
270
+ ->and($data['items'])->toBe([['id' => 'entity_a1', 'stop' => 'waypoint_wp-a', 'name' => 'Item a1', 'description' => 'Boxed', 'quantity' => 2]])
271
+ // The stop's own setup entry is left out; the rest name the stop.
272
+ ->and($data['timeline'])->toBe([['status' => 'Out for delivery', 'code' => 'enroute', 'complete' => false, 'created_at' => '2026-10-08T08:45:00.000000Z', 'stop' => 'waypoint_wp-a', 'place' => 'Maya Flat']])
273
+ ->and($data['instructions'])->toBe(['text' => 'Leave with concierge', 'editable' => true])
274
+ ->and($data['actions'])->toBe(['report' => true, 'driver_contact' => 'company'])
275
+ ->and($data['session'])->toBe(['expires_at' => '2026-10-09T09:00:00.000000Z'])
276
+ ->and($data['stale_after'])->toBe(300)
277
+ ->and($data['proofs'])->toBe([]);
278
+
279
+ // Nothing of the other customer, the driver or the order internals.
280
+ $json = json_encode($data);
281
+ foreach (['Other Customer House', 'waypoint_wp-b', 'Okafor', '+15550001111', 'Gate code', 'internal', 'order-1', 'contact-a', 'co-1', 'SGD'] as $leak) {
282
+ expect($json)->not->toContain($leak);
283
+ }
284
+ });
285
+
286
+ test('the route line, prices and proofs appear only when the customer may see them', function () {
287
+ Carbon::setTestNow(Carbon::parse('2026-10-08 09:00:00'));
288
+ TrackingPageDatabase::boot();
289
+
290
+ // Every stop belongs to the order customer: the route line is theirs to see.
291
+ $order = trackingViewOrder(['status' => 'completed']);
292
+ $scope = new TrackingScope('order-1', Contact::class, 'contact-shipper', $order);
293
+ $view = new TrackingViewProbe();
294
+ $view->stops = collect([
295
+ new TrackingStop('place-pickup', 'place_pickup', 'pickup', 'completed', trackingViewPlace('pickup', 'Sender', 1.28, 103.84), null, true, 0, null),
296
+ new TrackingStop('place-d', 'place_d', 'dropoff', 'completed', trackingViewPlace('place-d', 'Home', 1.35, 103.85), null, true, 1, null),
297
+ ]);
298
+ $view->tracker = trackingViewTracker();
299
+ $view->entities = collect([trackingViewEntity('d1', null, ['photo_uuid' => 'photo-1'])]);
300
+ $signature = new Proof();
301
+ $signature->setRawAttributes(['public_id' => 'proof_sig', 'raw_data' => 'iVBOR', 'created_at' => '2026-10-08 08:50:00'], true);
302
+ $photo = new Proof();
303
+ $photo->setRawAttributes(['public_id' => 'proof_photo', 'file_uuid' => 'file-1', 'created_at' => '2026-10-08 08:51:00'], true);
304
+ $scan = new Proof();
305
+ $scan->setRawAttributes(['public_id' => 'proof_scan', 'remarks' => 'Verified by QR Code Scan'], true);
306
+ $view->proofRows = collect([$signature, $photo, $scan]);
307
+
308
+ $data = $view->fullView(trackingViewTarget($order, $scope, ['visibility' => ['item_prices' => true, 'pod_photo' => false]]), [], TrackingView::VIEWER_ACCOUNT);
309
+
310
+ expect($data['level'])->toBe(2)
311
+ ->and($data['stage'])->toBe('delivered')
312
+ ->and($data['driver'])->toBeNull()
313
+ ->and($data['location'])->toBeNull()
314
+ ->and($data['eta'])->toBeNull()
315
+ // Every stop is this customer's, so the route line shows with no vehicle on the map.
316
+ ->and($data['map']['polyline'])->toBe('_p~iF~ps|U_ulLnnqC')
317
+ ->and($data['items'][0])->toMatchArray(['price' => '1500', 'currency' => 'SGD'])
318
+ ->and($data['items'][0]['photo_url'])->toBeString()
319
+ ->and(collect($data['proofs'])->pluck('type')->all())->toBe(['signature', 'scan'])
320
+ ->and($view->proofQueries[0])->toBe(['d1', 'order-1']);
321
+
322
+ // While under way, with nothing but this customer's stops, the line is drawn.
323
+ $order = trackingViewOrder();
324
+ $view->stops = collect([
325
+ new TrackingStop('place-pickup', 'place_pickup', 'pickup', 'completed', trackingViewPlace('pickup', 'Sender', 1.28, 103.84), null, true, 0, null),
326
+ new TrackingStop('place-d', 'place_d', 'dropoff', 'pending', trackingViewPlace('place-d', 'Home', 1.35, 103.85), null, false, 1, null),
327
+ ]);
328
+ $view->tracker = ['eta' => ['completion_at' => '2026-10-08T10:00:00.000000Z', 'completion_seconds' => 3600], 'route' => ['polyline' => 'abc']];
329
+ $data = $view->fullView(trackingViewTarget($order, new TrackingScope('order-1', Contact::class, 'contact-shipper', $order)), [], TrackingView::VIEWER_STAFF);
330
+
331
+ expect($data['map']['polyline'])->toBe('abc')
332
+ ->and($data['eta'])->toBe(['at' => '2026-10-08T10:00:00.000000Z', 'seconds' => 3600, 'confidence' => null])
333
+ ->and($data['items'][0])->not->toHaveKey('price');
334
+
335
+ // The company can hide the map, the driver's name, the vehicle and the items.
336
+ $view->tracker = [];
337
+ $hidden = $view->fullView(trackingViewTarget($order, new TrackingScope('order-1', Contact::class, 'contact-shipper', $order), ['visibility' => ['map' => false, 'driver_name' => false, 'vehicle' => false, 'items' => false]]), [], TrackingView::VIEWER_VERIFIED);
338
+ expect($hidden['map'])->toBeNull()
339
+ ->and($hidden['location'])->toBeNull()
340
+ ->and($hidden['driver'])->toBe(['name' => null, 'vehicle' => null, 'online' => true])
341
+ ->and($hidden['items'])->toBe([])
342
+ ->and($hidden['eta'])->toBeNull();
343
+
344
+ // An order that names no customer still renders for staff, with no stops of anyone's.
345
+ $none = $view->fullView(trackingViewTarget(trackingViewOrder(['customer_uuid' => null, 'customer_type' => null]), null), [], TrackingView::VIEWER_STAFF);
346
+ expect($none['stops'])->toBe([])->and($none['instructions']['text'])->toBeNull();
347
+ });
348
+
349
+ test('proof types and proofs a scope may open', function () {
350
+ TrackingPageDatabase::boot();
351
+ $file = new Fleetbase\Models\File();
352
+ $file->setRawAttributes(['type' => 'signature'], true);
353
+ $signed = new Proof();
354
+ $signed->setRelation('file', $file);
355
+
356
+ $view = new class extends TrackingViewProbe {
357
+ public ?Proof $found = null;
358
+
359
+ protected function findProof(string $publicId): ?Proof
360
+ {
361
+ return $this->found;
362
+ }
363
+ };
364
+ $view->stops = trackingViewStops(true);
365
+ $view->entities = collect();
366
+ $proof = new Proof();
367
+ $proof->setRawAttributes(['public_id' => 'proof_a', 'file_uuid' => 'file-a', 'subject_uuid' => 'wp-a'], true);
368
+ $view->proofRows = collect([$proof]);
369
+ $view->found = $proof;
370
+
371
+ $target = trackingViewTarget(trackingViewOrder(), new TrackingScope('order-1', Contact::class, 'contact-a'));
372
+
373
+ expect(TrackingView::proofType($signed))->toBe('signature')
374
+ ->and($view->proofFor($target, 'proof_a'))->toBe($proof)
375
+ ->and($view->proofFor($target, 'proof_other'))->toBeNull()
376
+ ->and($view->proofQueries[0])->toBe(['wp-a'])
377
+ ->and($view->fullView($target, [], TrackingView::VIEWER_VERIFIED)['proofs'][0])->toMatchArray(['id' => 'proof_a', 'stop' => 'waypoint_wp-a']);
378
+ });
379
+
380
+ test('a storefront order is collected from each store and delivered to the customer', function () {
381
+ TrackingPageDatabase::boot();
382
+
383
+ // The first store is the payload pickup; storefront adds the others as waypoints
384
+ // with no type, so they read as dropoffs.
385
+ $order = trackingViewOrder(['type' => 'storefront', 'status' => 'dispatched', 'started' => false]);
386
+ $order->setRelation('driverAssigned', null);
387
+ // Both stores are storefront store locations: one with a logo, one whose store
388
+ // can't be loaded here.
389
+ $orchard = trackingViewPlace('orchard', 'Orchard Grocers', 1.3048, 103.8318);
390
+ $orchard->setRawAttributes([...$orchard->getAttributes(), 'owner_type' => 'Fleetbase\\Storefront\\Models\\Store', 'owner_uuid' => 'store-1'], true);
391
+ $orchard->setRelation('owner', (object) ['logo_uuid' => 'file-1', 'logo_url' => 'https://cdn.example/orchard.png']);
392
+ $rochor = trackingViewPlace('rochor', 'Rochor Noodle House', 1.3039, 103.8536);
393
+ $rochor->setRawAttributes([...$rochor->getAttributes(), 'owner_type' => 'Fleetbase\\Storefront\\Models\\Store', 'owner_uuid' => 'store-2'], true);
394
+
395
+ $view = new TrackingViewProbe();
396
+ $view->stops = collect([
397
+ new TrackingStop('place-orchard', 'place_orchard', 'pickup', null, $orchard, null, false, 1, null),
398
+ new TrackingStop('place-rochor', 'place_rochor', 'waypoint', null, $rochor, trackingViewWaypoint('wp-rochor', null, ['type' => 'dropoff']), false, 2, 'tn-rochor'),
399
+ new TrackingStop('place-home', 'place_home', 'dropoff', null, trackingViewPlace('home', 'Singapore Home', 1.3007, 103.9106), null, false, 3, null),
400
+ ]);
401
+ $view->tracker = ['route' => ['polyline' => 'abc']];
402
+ $view->entities = collect();
403
+
404
+ $data = $view->fullView(trackingViewTarget($order, new TrackingScope('order-1', Contact::class, 'contact-shipper', $order)), [], TrackingView::VIEWER_STAFF);
405
+
406
+ expect($data['stops'])->toHaveCount(1)
407
+ ->and($data['stops'][0]['place']['name'])->toBe('Singapore Home')
408
+ ->and(collect($data['map']['pickups'])->pluck('name')->all())->toBe(['Orchard Grocers', 'Rochor Noodle House'])
409
+ // Marked as the console's order maps mark them: P, the waypoint's number, D.
410
+ ->and(collect($data['map']['pickups'])->pluck('marker')->all())->toBe(['P', '1'])
411
+ ->and($data['stops'][0]['marker'])->toBe('D')
412
+ ->and(collect($data['map']['pickups'])->pluck('store')->all())->toBe([['logo_url' => 'https://cdn.example/orchard.png'], ['logo_url' => null]])
413
+ ->and($data['location'])->toBeNull()
414
+ ->and($data['map']['polyline'])->toBe('abc');
415
+
416
+ // Elsewhere, only a waypoint marked as a pickup is one.
417
+ $marked = new TrackingStop('place-x', 'place_x', 'waypoint', null, null, trackingViewWaypoint('wp-x', null, ['type' => 'pickup']));
418
+ $drop = new TrackingStop('place-y', 'place_y', 'waypoint', null, null, trackingViewWaypoint('wp-y', null, ['type' => 'dropoff']));
419
+ expect(TrackingView::isPickup($marked, null))->toBeTrue()
420
+ ->and(TrackingView::isPickup($drop, 'default'))->toBeFalse()
421
+ ->and(TrackingView::isPickup(new TrackingStop('place-z', 'place_z', 'dropoff', null, null), 'storefront'))->toBeFalse();
422
+ });
423
+
424
+ test('stops, the tracker, items and proofs come from their real sources', function () {
425
+ $db = TrackingPageDatabase::boot();
426
+ TrackingPageDatabase::insert($db, 'entities', ['uuid' => 'e-a', 'payload_uuid' => 'payload-1', 'destination_uuid' => 'place-a', 'name' => 'Lamp']);
427
+ TrackingPageDatabase::insert($db, 'entities', ['uuid' => 'e-none', 'payload_uuid' => 'payload-1', 'name' => 'Shade']);
428
+ TrackingPageDatabase::insert($db, 'entities', ['uuid' => 'e-b', 'payload_uuid' => 'payload-1', 'destination_uuid' => 'place-b', 'name' => 'Chair']);
429
+ TrackingPageDatabase::insert($db, 'proofs', ['uuid' => 'p-1', 'public_id' => 'proof_one', 'subject_uuid' => 'wp-a', 'created_at' => '2026-10-08 09:00:00']);
430
+
431
+ $context = new Fleetbase\FleetOps\Tracking\TrackingContext(trackingViewOrder(), null, null, null, null, trackingViewStops(), collect(), collect(), null, null, null);
432
+ app()->instance(TrackingContextBuilder::class, new class($context) extends TrackingContextBuilder {
433
+ public function __construct(private $context)
434
+ {
435
+ }
436
+
437
+ public function build(Order $order, Fleetbase\FleetOps\Tracking\TrackingOptions $options): Fleetbase\FleetOps\Tracking\TrackingContext
438
+ {
439
+ return $this->context;
440
+ }
441
+ });
442
+ app()->instance(TrackingIntelligenceService::class, new class extends TrackingIntelligenceService {
443
+ public bool $fail = false;
444
+
445
+ public function __construct()
446
+ {
447
+ }
448
+
449
+ public function track(Order $order, array|Fleetbase\FleetOps\Tracking\TrackingOptions $options = []): array
450
+ {
451
+ if ($this->fail) {
452
+ throw new RuntimeException('provider down');
453
+ }
454
+
455
+ return ['provider' => 'fake'];
456
+ }
457
+ });
458
+
459
+ $view = new TrackingView();
460
+ $call = fn (string $method, ...$args) => (fn () => $this->{$method}(...$args))->call($view);
461
+ $order = trackingViewOrder();
462
+ $scopeA = new TrackingScope('order-1', Contact::class, 'contact-a', $order);
463
+ $shipper = new TrackingScope('order-1', Contact::class, 'contact-shipper', $order);
464
+
465
+ expect($call('contextStopsFor', $order)->pluck('type')->all())->toBe(['pickup', 'waypoint', 'waypoint'])
466
+ ->and($call('stopsFor', trackingViewOrder(['type' => 'storefront']))->pluck('type')->all())->toBe(['pickup', 'pickup', 'pickup'])
467
+ ->and($call('trackerFor', $order))->toBe(['provider' => 'fake'])
468
+ ->and($call('trackerFor', trackingViewOrder(['status' => 'completed'])))->toBe([]);
469
+
470
+ app(TrackingIntelligenceService::class)->fail = true;
471
+ expect($call('trackerFor', $order))->toBe([])
472
+ ->and($call('entitiesFor', $order, ['place-a'], $scopeA)->pluck('uuid')->all())->toBe(['e-a'])
473
+ ->and($call('entitiesFor', $order, [], $shipper)->pluck('uuid')->all())->toBe(['e-none'])
474
+ ->and($call('entitiesFor', trackingViewOrder(['payload_uuid' => null]), [], $shipper)->all())->toBe([])
475
+ ->and($call('proofsFor', ['wp-a'])->pluck('public_id')->all())->toBe(['proof_one'])
476
+ ->and($call('proofsFor', [])->all())->toBe([])
477
+ ->and($call('findProof', 'proof_one')?->uuid)->toBe('p-1')
478
+ ->and($call('updateFor', $order, $scopeA, trackingViewStops(), [])->ownStops()->count())->toBe(1);
479
+ });