@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,543 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\TrackingPage;
4
+
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\Support\TrackingScope;
10
+ use Fleetbase\FleetOps\Support\TrackingUpdate;
11
+ use Fleetbase\FleetOps\Tracking\TrackingContextBuilder;
12
+ use Fleetbase\FleetOps\Tracking\TrackingIntelligenceService;
13
+ use Fleetbase\FleetOps\Tracking\TrackingOptions;
14
+ use Fleetbase\FleetOps\Tracking\TrackingStop;
15
+ use Illuminate\Support\Carbon;
16
+ use Illuminate\Support\Collection;
17
+
18
+ /**
19
+ * What the public tracking page receives, per access level.
20
+ *
21
+ * Level 0 (a tracking number alone) carries branding, a coarse stage and how to verify.
22
+ * Level 1 (a verified device) and level 2 (a signed-in customer) carry one customer's
23
+ * stops within the order, built on TrackingUpdate, which already filters stops, timelines,
24
+ * the driver and the vehicle position to the scope. Staff of the owning company see what
25
+ * the customer sees.
26
+ *
27
+ * Never sent at any level: internal uuids, notes, order meta, custom fields, files other
28
+ * than proof of delivery, rates or transactions, another customer's stops, items or
29
+ * people, and the driver's surname or phone. Pickups are where the order comes from
30
+ * (a store, a warehouse) and show by name.
31
+ */
32
+ class TrackingView
33
+ {
34
+ public const VIEWER_VERIFIED = 'verified';
35
+ public const VIEWER_ACCOUNT = 'account';
36
+ public const VIEWER_STAFF = 'staff';
37
+
38
+ /**
39
+ * Status codes that show as a problem with the delivery.
40
+ */
41
+ public const ISSUE_CODES = ['failed', 'attempt_failed', 'delivery_failed', 'returned', 'return_to_sender'];
42
+
43
+ public const STOP_PRECISION = 5;
44
+
45
+ /**
46
+ * Status codes a stop's own tracking number is given when it is set up: bookkeeping,
47
+ * not news for the customer.
48
+ */
49
+ public const SETUP_CODES = ['created'];
50
+
51
+ /**
52
+ * Branding as the page shows it, from the company's config with its own details as fallbacks.
53
+ */
54
+ public function branding(array $config, mixed $company = null, ?string $instanceLogo = null): array
55
+ {
56
+ $branding = (array) data_get($config, 'branding', []);
57
+ $accent = $branding['accent'] ?? TrackingPageConfig::DEFAULT_ACCENT;
58
+
59
+ return [
60
+ 'name' => $branding['display_name'] ?? data_get($company, 'name'),
61
+ 'logo_url' => data_get($company, 'logo_uuid') ? data_get($company, 'logo_url') : $instanceLogo,
62
+ 'accent' => $accent,
63
+ 'accent_dark' => $branding['accent_dark'] ?? null,
64
+ 'ink' => TrackingPageConfig::inkFor($accent),
65
+ 'support_phone' => $branding['support_phone'] ?? data_get($company, 'phone'),
66
+ 'support_email' => $branding['support_email'] ?? null,
67
+ 'website' => $branding['website'] ?? data_get($company, 'website_url'),
68
+ 'powered_by' => (bool) ($branding['powered_by'] ?? true),
69
+ 'theme' => $branding['theme'] ?? 'system',
70
+ 'default_locale' => $branding['default_locale'] ?? 'en-us',
71
+ 'locales' => $branding['locales'] ?? TrackingPageConfig::LOCALES,
72
+ ];
73
+ }
74
+
75
+ /**
76
+ * Level 0: what a tracking number alone shows.
77
+ *
78
+ * @param array $channels the masked channels a code can go out on
79
+ */
80
+ public function publicView(TrackingTarget $target, array $branding, array $channels, bool $signIn): array
81
+ {
82
+ $order = $target->order;
83
+ $public = (bool) data_get($target->config, 'access.public_status', true);
84
+ $update = $target->scope ? new TrackingUpdate($order, $target->scope, $this->stopsFor($order)) : null;
85
+
86
+ return [
87
+ 'level' => 0,
88
+ 'tracking_number' => $target->trackingNumber->tracking_number,
89
+ 'company' => $branding,
90
+ 'stage' => $public ? $this->stage($order, $update) : null,
91
+ 'last_update_on' => $public && $order->updated_at ? Carbon::parse($order->updated_at)->toDateString() : null,
92
+ 'verify' => ['channels' => $channels],
93
+ 'sign_in' => $signIn,
94
+ ];
95
+ }
96
+
97
+ /**
98
+ * Levels 1 and 2: one customer's view of the order.
99
+ */
100
+ public function fullView(TrackingTarget $target, array $branding, string $viewer, ?string $expiresAt = null): array
101
+ {
102
+ $order = $target->order;
103
+ $config = $target->config;
104
+ $scope = $target->scope ?? new TrackingScope((string) $order->uuid, Order::class, (string) $order->uuid, $order);
105
+ $stops = $this->stopsFor($order);
106
+ $tracker = $this->trackerFor($order);
107
+ $update = $this->updateFor($order, $scope, $stops, $tracker);
108
+ $data = $update->toArray()['data'];
109
+ $own = $update->ownStops();
110
+ $placeUuids = $own->map(fn (TrackingStop $stop) => $stop->place?->uuid)->filter()->values()->all();
111
+ $entities = $this->entitiesFor($order, $placeUuids, $scope);
112
+ $showMap = (bool) data_get($config, 'visibility.map', true);
113
+ $location = $showMap ? $data['location'] : null;
114
+ $stopsView = $this->stops($own, collect($data['stops']), $stops);
115
+
116
+ return [
117
+ 'level' => $viewer === self::VIEWER_ACCOUNT ? 2 : 1,
118
+ 'viewer' => $viewer,
119
+ 'tracking_number' => $target->trackingNumber->tracking_number,
120
+ 'company' => $branding,
121
+ 'stage' => $this->stage($order, $update),
122
+ 'status' => [
123
+ 'code' => $data['order']['status'],
124
+ 'delayed' => (bool) data_get($tracker, 'insights.is_delayed', false),
125
+ 'confidence' => data_get($tracker, 'confidence'),
126
+ ],
127
+ 'scheduled_at' => $this->iso($order->scheduled_at),
128
+ 'time_window' => ['start' => $this->iso($order->time_window_start), 'end' => $this->iso($order->time_window_end)],
129
+ 'route' => $data['route'],
130
+ 'stops' => $stopsView,
131
+ 'focus_stop' => $target->waypoint?->public_id,
132
+ 'eta' => $this->eta($stopsView, $tracker, $update),
133
+ 'timeline' => $this->timeline($data, $stopsView, $own),
134
+ 'driver' => $this->driver($data['driver'], $config),
135
+ 'location' => $location,
136
+ 'map' => $showMap ? $this->map($stops, $own, $tracker) : null,
137
+ 'items' => data_get($config, 'visibility.items', true) ? $this->items($entities, $own, $viewer === self::VIEWER_ACCOUNT && data_get($config, 'visibility.item_prices', false) && $scope->isOrderCustomer($order)) : [],
138
+ 'proofs' => $this->proofs($order, $own, $entities, $scope, $config),
139
+ 'instructions' => [
140
+ 'text' => $this->instructionsFor($order, $scope),
141
+ 'editable' => (bool) data_get($config, 'visibility.instructions_edit', false),
142
+ ],
143
+ 'actions' => [
144
+ 'report' => (bool) data_get($config, 'visibility.report_problem', true),
145
+ 'driver_contact' => data_get($config, 'visibility.driver_contact', 'company'),
146
+ ],
147
+ 'session' => ['expires_at' => $expiresAt],
148
+ 'stale_after' => $this->staleAfter($order),
149
+ ];
150
+ }
151
+
152
+ /**
153
+ * The coarse stage: preparing, scheduled, dispatched, in_transit, delivered, issue or canceled.
154
+ */
155
+ public function stage(Order $order, ?TrackingUpdate $update): string
156
+ {
157
+ $status = strtolower((string) $order->status);
158
+ if ($status === 'canceled') {
159
+ return 'canceled';
160
+ }
161
+
162
+ if (in_array($status, self::ISSUE_CODES, true)) {
163
+ return 'issue';
164
+ }
165
+
166
+ $own = $update ? $update->ownStops()->filter(fn (TrackingStop $stop) => $stop->type !== 'pickup') : collect();
167
+ if ($status === 'completed' || ($own->isNotEmpty() && $own->every(fn (TrackingStop $stop) => $stop->completed))) {
168
+ return 'delivered';
169
+ }
170
+
171
+ if ($update?->isEnRoute() || $order->started) {
172
+ return 'in_transit';
173
+ }
174
+
175
+ if ($order->dispatched) {
176
+ return 'dispatched';
177
+ }
178
+
179
+ return $order->scheduled_at && Carbon::parse($order->scheduled_at)->isFuture() ? 'scheduled' : 'preparing';
180
+ }
181
+
182
+ /**
183
+ * The customer's stops: where, when and their own status, never the pickup's place.
184
+ */
185
+ protected function stops(Collection $own, Collection $data, Collection $all): array
186
+ {
187
+ $byId = $data->keyBy(fn ($stop) => $stop['sequence']);
188
+
189
+ return $own->filter(fn (TrackingStop $stop) => $stop->type !== 'pickup')->map(function (TrackingStop $stop) use ($byId, $all) {
190
+ $point = $stop->point();
191
+ $entry = $byId->get($stop->sequence, []);
192
+
193
+ return [
194
+ 'id' => static::stopId($stop),
195
+ 'type' => $stop->type,
196
+ 'marker' => static::markerFor($stop, $all),
197
+ 'sequence' => $stop->sequence,
198
+ 'status' => $stop->status,
199
+ 'complete' => $stop->completed,
200
+ 'eta' => $entry['eta'] ?? null,
201
+ 'timeline' => $entry['timeline'] ?? [],
202
+ 'place' => [
203
+ 'name' => $stop->place?->name,
204
+ 'address' => $stop->place?->address,
205
+ 'latitude' => $point ? round($point->getLat(), self::STOP_PRECISION) : null,
206
+ 'longitude' => $point ? round($point->getLng(), self::STOP_PRECISION) : null,
207
+ ],
208
+ 'window' => [
209
+ 'start' => $this->iso($stop->waypoint?->time_window_start),
210
+ 'end' => $this->iso($stop->waypoint?->time_window_end),
211
+ ],
212
+ ];
213
+ })->values()->all();
214
+ }
215
+
216
+ /**
217
+ * The arrival estimate for the customer's next stop.
218
+ */
219
+ protected function eta(array $stops, array $tracker, TrackingUpdate $update): ?array
220
+ {
221
+ if (!$update->showsVehicle()) {
222
+ return null;
223
+ }
224
+
225
+ foreach ($stops as $stop) {
226
+ if (!$stop['complete'] && !empty($stop['eta'])) {
227
+ return ['at' => $stop['eta']['at'], 'seconds' => $stop['eta']['seconds'], 'confidence' => data_get($tracker, 'confidence')];
228
+ }
229
+ }
230
+
231
+ $at = data_get($tracker, 'eta.completion_at');
232
+
233
+ return $at ? ['at' => $at, 'seconds' => data_get($tracker, 'eta.completion_seconds'), 'confidence' => data_get($tracker, 'confidence')] : null;
234
+ }
235
+
236
+ /**
237
+ * The order's and the customer's stops' status entries, newest first. A stop's
238
+ * entries name the stop (`stop`, `place`), so the page can show the selected stop's
239
+ * alongside the order's; the setup entry each stop's tracking number starts with is
240
+ * left out.
241
+ */
242
+ protected function timeline(array $data, array $stopsView, Collection $own): array
243
+ {
244
+ $entries = collect($data['order']['timeline']);
245
+ $ids = collect($stopsView)->keyBy('sequence');
246
+ $places = $own->keyBy(fn (TrackingStop $stop) => $stop->sequence);
247
+
248
+ foreach ($data['stops'] as $stop) {
249
+ $id = $ids->get($stop['sequence'])['id'] ?? null;
250
+ $place = $places->get($stop['sequence'])?->place?->name;
251
+
252
+ $entries = $entries->merge(collect($stop['timeline'] ?? [])
253
+ ->reject(fn ($entry) => in_array($entry['code'] ?? null, self::SETUP_CODES, true))
254
+ ->map(fn ($entry) => [...$entry, 'stop' => $id, 'place' => $place]));
255
+ }
256
+
257
+ return $entries
258
+ ->unique(fn ($entry) => ($entry['code'] ?? '') . '|' . ($entry['created_at'] ?? '') . '|' . ($entry['status'] ?? '') . '|' . ($entry['stop'] ?? ''))
259
+ // Entries arrive oldest first; reversed before the (stable) sort, two at the
260
+ // same second keep their order rather than swapping.
261
+ ->reverse()
262
+ ->sortByDesc(fn ($entry) => $entry['created_at'] ?? '')
263
+ ->values()
264
+ ->all();
265
+ }
266
+
267
+ protected function driver(?array $driver, array $config): ?array
268
+ {
269
+ if (!$driver) {
270
+ return null;
271
+ }
272
+
273
+ return [
274
+ 'name' => data_get($config, 'visibility.driver_name', true) ? $driver['name'] : null,
275
+ 'vehicle' => data_get($config, 'visibility.vehicle', true) ? $driver['vehicle'] : null,
276
+ 'online' => $driver['online'],
277
+ ];
278
+ }
279
+
280
+ /**
281
+ * Where the order is collected and, when every stop it delivers to is this
282
+ * customer's, the route line. On an order with other customers the line would trace
283
+ * their stops, so it is left out. The line is the tracker's: from the vehicle once
284
+ * it reports, from the first pickup before that.
285
+ */
286
+ protected function map(Collection $stops, Collection $own, array $tracker): array
287
+ {
288
+ $mine = $stops->every(fn (TrackingStop $stop) => $stop->type === 'pickup' || $own->contains(fn (TrackingStop $owned) => $owned === $stop));
289
+
290
+ return [
291
+ 'pickups' => $stops->filter(fn (TrackingStop $stop) => $stop->type === 'pickup')->map(function (TrackingStop $stop) use ($stops) {
292
+ $point = $stop->point();
293
+
294
+ return $point ? [
295
+ 'name' => $stop->place?->name,
296
+ 'marker' => static::markerFor($stop, $stops),
297
+ 'store' => $this->storeFor($stop->place),
298
+ 'latitude' => round($point->getLat(), self::STOP_PRECISION),
299
+ 'longitude' => round($point->getLng(), self::STOP_PRECISION),
300
+ 'complete' => $stop->completed,
301
+ ] : null;
302
+ })->filter()->values()->all(),
303
+ 'polyline' => $mine ? data_get($tracker, 'route.polyline') : null,
304
+ ];
305
+ }
306
+
307
+ /**
308
+ * The customer's items: those going to their stops, and on an order with no stop
309
+ * customers, the order customer's.
310
+ */
311
+ protected function items(Collection $entities, Collection $own, bool $prices): array
312
+ {
313
+ $stops = static::stopIdsByPlace($own);
314
+
315
+ return $entities->map(fn (Entity $entity) => array_filter([
316
+ 'id' => $entity->public_id,
317
+ 'stop' => $entity->destination_uuid ? ($stops[$entity->destination_uuid] ?? null) : null,
318
+ 'name' => $entity->name,
319
+ 'description' => $entity->description,
320
+ 'quantity' => data_get($entity, 'meta.quantity'),
321
+ 'photo_url' => $entity->photo_uuid ? $entity->photo_url : null,
322
+ 'tracking' => data_get($entity, 'tracking'),
323
+ 'price' => $prices ? $entity->price : null,
324
+ 'currency' => $prices ? $entity->currency : null,
325
+ ], fn ($value) => $value !== null))->values()->all();
326
+ }
327
+
328
+ /**
329
+ * Proof of delivery for the customer's completed stops and items.
330
+ */
331
+ protected function proofs(Order $order, Collection $own, Collection $entities, TrackingScope $scope, array $config): array
332
+ {
333
+ $subjects = $own->filter(fn (TrackingStop $stop) => $stop->completed)->map(fn (TrackingStop $stop) => $stop->waypoint?->uuid)->filter()->values()->all();
334
+ $subjects = array_merge($subjects, $entities->map(fn (Entity $entity) => $entity->uuid)->all());
335
+ if ($scope->isOrderCustomer($order) && strtolower((string) $order->status) === 'completed') {
336
+ $subjects[] = $order->uuid;
337
+ }
338
+
339
+ $stops = [];
340
+ foreach ($own as $stop) {
341
+ if ($stop->type !== 'pickup' && $stop->waypoint?->uuid) {
342
+ $stops[$stop->waypoint->uuid] = static::stopId($stop);
343
+ }
344
+ }
345
+
346
+ $byPlace = static::stopIdsByPlace($own);
347
+ foreach ($entities as $entity) {
348
+ $stops[$entity->uuid] = $entity->destination_uuid ? ($byPlace[$entity->destination_uuid] ?? null) : null;
349
+ }
350
+
351
+ return $this->proofsFor($subjects)
352
+ ->map(fn (Proof $proof) => ['id' => $proof->public_id, 'type' => static::proofType($proof), 'stop' => $stops[$proof->subject_uuid] ?? null, 'created_at' => $this->iso($proof->created_at)])
353
+ ->filter(fn ($proof) => ($proof['type'] !== 'photo' || data_get($config, 'visibility.pod_photo', true)) && ($proof['type'] !== 'signature' || data_get($config, 'visibility.pod_signature', true)))
354
+ ->values()
355
+ ->all();
356
+ }
357
+
358
+ /**
359
+ * signature, photo or scan.
360
+ */
361
+ public static function proofType(Proof $proof): string
362
+ {
363
+ $fileType = strtolower((string) data_get($proof, 'file.type'));
364
+ if ($fileType === 'signature' || (!empty($proof->raw_data) && $fileType === '')) {
365
+ return 'signature';
366
+ }
367
+
368
+ return $fileType === 'photo' || $proof->file_uuid ? 'photo' : 'scan';
369
+ }
370
+
371
+ /**
372
+ * Proofs the scope may open, by public id.
373
+ */
374
+ public function proofFor(TrackingTarget $target, string $publicId): ?Proof
375
+ {
376
+ $view = $this->fullView($target, [], self::VIEWER_VERIFIED);
377
+
378
+ if (!collect($view['proofs'])->contains(fn ($proof) => $proof['id'] === $publicId)) {
379
+ return null;
380
+ }
381
+
382
+ return $this->findProof($publicId);
383
+ }
384
+
385
+ protected function iso(mixed $value): ?string
386
+ {
387
+ return $value ? Carbon::parse($value)->toISOString() : null;
388
+ }
389
+
390
+ /**
391
+ * The id the page knows a stop by: its waypoint's public id, or its place's for an
392
+ * order with a single dropoff.
393
+ */
394
+ public static function stopId(TrackingStop $stop): ?string
395
+ {
396
+ return $stop->waypoint?->public_id ?? $stop->place?->public_id;
397
+ }
398
+
399
+ /**
400
+ * The stop's label on the map, as the console's order maps draw it: P for the
401
+ * payload's pickup, D for its dropoff, and each waypoint its number in the route.
402
+ */
403
+ public static function markerFor(TrackingStop $stop, Collection $stops): string
404
+ {
405
+ $isWaypoint = fn (TrackingStop $candidate) => $candidate->waypoint !== null || $candidate->type === 'waypoint';
406
+ if ($isWaypoint($stop)) {
407
+ return (string) ($stops->filter($isWaypoint)->values()->search(fn (TrackingStop $candidate) => $candidate === $stop) + 1);
408
+ }
409
+
410
+ return $stop->type === 'pickup' ? 'P' : 'D';
411
+ }
412
+
413
+ /**
414
+ * The store a pickup is, when it is a storefront store's location: its name and logo
415
+ * draw the marker.
416
+ */
417
+ protected function storeFor(?Place $place): ?array
418
+ {
419
+ if (!$place || !str_contains((string) $place->owner_type, 'Storefront')) {
420
+ return null;
421
+ }
422
+
423
+ try {
424
+ $store = $place->owner;
425
+ } catch (\Throwable) {
426
+ $store = null;
427
+ }
428
+
429
+ return ['logo_url' => data_get($store, 'logo_uuid') ? data_get($store, 'logo_url') : null];
430
+ }
431
+
432
+ /**
433
+ * @return array<string, ?string> the customer's stop ids, by place uuid
434
+ */
435
+ protected static function stopIdsByPlace(Collection $own): array
436
+ {
437
+ $ids = [];
438
+ foreach ($own as $stop) {
439
+ if ($stop->type !== 'pickup' && $stop->place?->uuid) {
440
+ $ids[$stop->place->uuid] = static::stopId($stop);
441
+ }
442
+ }
443
+
444
+ return $ids;
445
+ }
446
+
447
+ /**
448
+ * Whether a stop is where the order is collected rather than delivered: the payload's
449
+ * pickup, a waypoint marked as one, or a waypoint on a storefront order, where the
450
+ * waypoints are the other stores the order is collected from (storefront creates them
451
+ * without a type, so they default to dropoffs).
452
+ */
453
+ public static function isPickup(TrackingStop $stop, ?string $payloadType): bool
454
+ {
455
+ if ($stop->type === 'pickup') {
456
+ return true;
457
+ }
458
+
459
+ if (!$stop->waypoint) {
460
+ return false;
461
+ }
462
+
463
+ return strtolower((string) $stop->waypoint->type) === 'pickup' || ($stop->type === 'waypoint' && strtolower((string) $payloadType) === 'storefront');
464
+ }
465
+
466
+ /**
467
+ * The order's stops, with every pickup typed as one, so the customer's stops, the
468
+ * stage and the route counts leave them out.
469
+ */
470
+ protected function stopsFor(Order $order): Collection
471
+ {
472
+ $payloadType = $order->payload?->type ?? $order->type;
473
+
474
+ return $this->contextStopsFor($order)->each(function (TrackingStop $stop) use ($payloadType) {
475
+ if (static::isPickup($stop, $payloadType)) {
476
+ $stop->type = 'pickup';
477
+ }
478
+ });
479
+ }
480
+
481
+ protected function contextStopsFor(Order $order): Collection
482
+ {
483
+ return app(TrackingContextBuilder::class)->build($order, TrackingOptions::fromArray([], $order->company_uuid))->stops;
484
+ }
485
+
486
+ protected function trackerFor(Order $order): array
487
+ {
488
+ if (in_array(strtolower((string) $order->status), TrackingUpdate::TERMINAL_STATUSES, true)) {
489
+ return [];
490
+ }
491
+
492
+ try {
493
+ return app(TrackingIntelligenceService::class)->track($order);
494
+ } catch (\Throwable) {
495
+ return [];
496
+ }
497
+ }
498
+
499
+ protected function updateFor(Order $order, TrackingScope $scope, Collection $stops, array $tracker): TrackingUpdate
500
+ {
501
+ return TrackingUpdate::for($order, $scope, $stops, $tracker);
502
+ }
503
+
504
+ protected function staleAfter(Order $order): int
505
+ {
506
+ return TrackingOptions::fromArray([], $order->company_uuid)->staleLocationThresholdSeconds;
507
+ }
508
+
509
+ /**
510
+ * @param array<int, string> $placeUuids the customer's stop places
511
+ */
512
+ protected function entitiesFor(Order $order, array $placeUuids, TrackingScope $scope): Collection
513
+ {
514
+ if (!$order->payload_uuid) {
515
+ return collect();
516
+ }
517
+
518
+ $orderCustomer = $scope->isOrderCustomer($order);
519
+
520
+ return Entity::withoutGlobalScopes()
521
+ ->where('payload_uuid', $order->payload_uuid)
522
+ ->get()
523
+ ->filter(fn (Entity $entity) => $entity->destination_uuid ? in_array($entity->destination_uuid, $placeUuids, true) : $orderCustomer)
524
+ ->values();
525
+ }
526
+
527
+ protected function proofsFor(array $subjectUuids): Collection
528
+ {
529
+ return $subjectUuids === [] ? collect() : Proof::withoutGlobalScopes()->with('file')->whereIn('subject_uuid', $subjectUuids)->orderBy('created_at')->get();
530
+ }
531
+
532
+ protected function findProof(string $publicId): ?Proof
533
+ {
534
+ return Proof::withoutGlobalScopes()->with('file')->where('public_id', $publicId)->first();
535
+ }
536
+
537
+ protected function instructionsFor(Order $order, TrackingScope $scope): ?string
538
+ {
539
+ $instructions = data_get($order->getMeta('recipient_instructions', []), sha1($scope->key()));
540
+
541
+ return is_string($instructions) && $instructions !== '' ? $instructions : null;
542
+ }
543
+ }