@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,242 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Illuminate\Support\Str;
6
+
7
+ /**
8
+ * The content printed into a tracking number's QR code and barcode, and the parser that
9
+ * turns any scanned value back into identifiers.
10
+ *
11
+ * QR (v1) is a URL, so a phone camera opens the customer tracking page while scanners
12
+ * and the navigator app read the parameters:
13
+ *
14
+ * https://<console host>/~/track-order?order=<tracking number>&r=<owner public_id>&v=1
15
+ *
16
+ * Only the tracking number (printed on the label in plain text) and the owner's public
17
+ * id go in: no uuids, no addresses, no names, no company. The owner's type is its
18
+ * public_id prefix (order_, waypoint_, entity_, place_), so it is not repeated. Nothing
19
+ * is signed: a label can be photocopied whatever it carries, so authorization belongs
20
+ * to the endpoint that acts on a scan, not to the code.
21
+ *
22
+ * The linear barcode (Code 128) carries the bare tracking number, matching the text
23
+ * printed beneath it, because warehouse handhelds type what they scan into a field.
24
+ *
25
+ * Codes already on parcels encode the owner's bare uuid, and parse() keeps accepting
26
+ * them, alongside a bare tracking number or a bare public_id.
27
+ */
28
+ class TrackingCode
29
+ {
30
+ public const VERSION = 1;
31
+
32
+ /**
33
+ * Where a phone camera lands. The public tracking page is being rebuilt; when its
34
+ * route changes, change these two and nothing else. parse() never depends on the
35
+ * host or the path, since printed labels outlive both.
36
+ *
37
+ * `~/` is the console's public route prefix: it renders the page whether or not the
38
+ * visitor is signed in. A bare `/track-order` falls into the console's own routes, so
39
+ * a signed-in user landed on an empty console page instead of the tracking page.
40
+ */
41
+ public const PATH = '~/track-order';
42
+ public const TRACKING_PARAM = 'order';
43
+
44
+ /**
45
+ * The tracking page link for emails, notifications and the API: the company's own page
46
+ * (`~/t/{slug}/{number}`) when its settings send links there, otherwise the shared page
47
+ * (`~/track/{number}`). QR codes keep {@see PATH}, which labels already printed carry.
48
+ * Without a tracking number it links the page itself, where the number can be entered.
49
+ */
50
+ public static function pageUrl(?string $trackingNumber, ?string $companyUuid = null): string
51
+ {
52
+ try {
53
+ $slug = $companyUuid ? app(TrackingPage\TrackingResolver::class)->linkSlugFor($companyUuid) : null;
54
+ } catch (\Throwable) {
55
+ $slug = null;
56
+ }
57
+
58
+ $base = $slug ? '~/t/' . rawurlencode($slug) : '~/track';
59
+
60
+ return Utils::consoleUrl(filled($trackingNumber) ? $base . '/' . rawurlencode($trackingNumber) : $base, []);
61
+ }
62
+
63
+ /**
64
+ * The QR content for a tracking number.
65
+ */
66
+ public static function qrContent(string $trackingNumber, ?string $ownerPublicId = null): string
67
+ {
68
+ $query = [static::TRACKING_PARAM => $trackingNumber];
69
+
70
+ if (!empty($ownerPublicId)) {
71
+ $query['r'] = $ownerPublicId;
72
+ }
73
+
74
+ $query['v'] = static::VERSION;
75
+
76
+ return static::consoleBaseUrl() . '/' . static::PATH . '?' . http_build_query($query);
77
+ }
78
+
79
+ /**
80
+ * The console origin, from the same config Utils::consoleUrl() reads. It does not
81
+ * call consoleUrl() because that infers http/https from the app environment: a code
82
+ * printed on a parcel should not change scheme with where it was generated, so it
83
+ * is https unless the console is explicitly configured otherwise.
84
+ */
85
+ protected static function consoleBaseUrl(): string
86
+ {
87
+ $host = rtrim((string) config('fleetbase.console.host', ''), '/');
88
+
89
+ if (Str::startsWith($host, 'http')) {
90
+ return $host;
91
+ }
92
+
93
+ $subdomain = config('fleetbase.console.subdomain');
94
+ $scheme = config('fleetbase.console.secure', true) ? 'https://' : 'http://';
95
+
96
+ return $scheme . ($subdomain ? $subdomain . '.' : '') . $host;
97
+ }
98
+
99
+ /**
100
+ * The linear barcode content for a tracking number.
101
+ */
102
+ public static function barcodeContent(string $trackingNumber): string
103
+ {
104
+ return $trackingNumber;
105
+ }
106
+
107
+ /**
108
+ * Break a scanned value into the identifiers it carries.
109
+ *
110
+ * - `uuid`: a legacy code, the owner's uuid.
111
+ * - `tracking_number` and `public_id`: from a URL code.
112
+ * - `reference`: a bare value that is either a tracking number (the barcode, or one
113
+ * typed by hand) or a public_id. Consumers try both.
114
+ *
115
+ * @return array{uuid: ?string, tracking_number: ?string, public_id: ?string, reference: ?string, version: ?int}
116
+ */
117
+ public static function parse(?string $code): array
118
+ {
119
+ $parsed = [
120
+ 'uuid' => null,
121
+ 'tracking_number' => null,
122
+ 'public_id' => null,
123
+ 'reference' => null,
124
+ 'version' => null,
125
+ ];
126
+
127
+ $code = trim((string) $code);
128
+ if ($code === '') {
129
+ return $parsed;
130
+ }
131
+
132
+ if (preg_match('#^https?://#i', $code)) {
133
+ return array_merge($parsed, static::parseUrl($code));
134
+ }
135
+
136
+ if (Str::isUuid($code)) {
137
+ $parsed['uuid'] = $code;
138
+
139
+ return $parsed;
140
+ }
141
+
142
+ $parsed['reference'] = $code;
143
+
144
+ return $parsed;
145
+ }
146
+
147
+ /**
148
+ * Whether a scanned value identifies the given subject (an order, waypoint, entity or
149
+ * place). The value comes straight from a request, so anything but a string, such as
150
+ * a scanner library's whole result object, matches nothing.
151
+ */
152
+ public static function matches($code, $subject): bool
153
+ {
154
+ $subjectUuid = data_get($subject, 'uuid');
155
+ if (!is_string($code) || !$subject || empty($subjectUuid)) {
156
+ return false;
157
+ }
158
+
159
+ // Labels printed before v1 encode the subject's bare uuid; compare it exactly, as
160
+ // scans always have been.
161
+ if (trim($code) === $subjectUuid) {
162
+ return true;
163
+ }
164
+
165
+ $parsed = static::parse($code);
166
+
167
+ if ($parsed['uuid']) {
168
+ return false;
169
+ }
170
+
171
+ $publicId = data_get($subject, 'public_id');
172
+ $trackingNumber = static::subjectTrackingNumber($subject);
173
+
174
+ if ($parsed['reference']) {
175
+ return static::sameTrackingNumber($parsed['reference'], $trackingNumber) || static::samePublicId($parsed['reference'], $publicId);
176
+ }
177
+
178
+ // A url code names its owner by public id, and that decides: an owner can hold more
179
+ // than one tracking number (the API issues extra ones), so the number is compared
180
+ // only when the code names no owner.
181
+ if ($parsed['public_id']) {
182
+ return static::samePublicId($parsed['public_id'], $publicId);
183
+ }
184
+
185
+ return $parsed['tracking_number'] !== null && static::sameTrackingNumber($parsed['tracking_number'], $trackingNumber);
186
+ }
187
+
188
+ /**
189
+ * @return array{tracking_number: ?string, public_id: ?string, version: ?int}
190
+ */
191
+ protected static function parseUrl(string $url): array
192
+ {
193
+ $parts = parse_url($url);
194
+ $query = [];
195
+ parse_str(is_array($parts) ? ($parts['query'] ?? '') : '', $query);
196
+
197
+ $trackingNumber = static::stringParam($query, static::TRACKING_PARAM) ?? static::stringParam($query, 'tn');
198
+
199
+ // A future route may carry the tracking number in the path (/track/<number>). Only
200
+ // a path of two or more segments can, and the tracking page's own path
201
+ // (/track-order, /~/track-order) is never mistaken for one.
202
+ if ($trackingNumber === null && is_array($parts) && isset($parts['path'])) {
203
+ $segments = array_values(array_filter(explode('/', $parts['path']), fn ($segment) => $segment !== ''));
204
+ if (count($segments) >= 2 && end($segments) !== basename(static::PATH)) {
205
+ $trackingNumber = rawurldecode(end($segments));
206
+ }
207
+ }
208
+
209
+ $version = static::stringParam($query, 'v');
210
+
211
+ return [
212
+ 'tracking_number' => $trackingNumber,
213
+ 'public_id' => static::stringParam($query, 'r'),
214
+ 'version' => $version !== null && ctype_digit($version) ? (int) $version : null,
215
+ ];
216
+ }
217
+
218
+ protected static function stringParam(array $query, string $key): ?string
219
+ {
220
+ $value = $query[$key] ?? null;
221
+
222
+ return is_string($value) && trim($value) !== '' ? trim($value) : null;
223
+ }
224
+
225
+ protected static function subjectTrackingNumber($subject): ?string
226
+ {
227
+ $trackingNumber = data_get($subject, 'trackingNumber.tracking_number') ?? data_get($subject, 'tracking_number');
228
+
229
+ return is_string($trackingNumber) && $trackingNumber !== '' ? $trackingNumber : null;
230
+ }
231
+
232
+ protected static function sameTrackingNumber(string $scanned, ?string $trackingNumber): bool
233
+ {
234
+ // The label prints the number upper-cased and handhelds may not preserve case.
235
+ return $trackingNumber !== null && strcasecmp($scanned, $trackingNumber) === 0;
236
+ }
237
+
238
+ protected static function samePublicId(string $scanned, $publicId): bool
239
+ {
240
+ return is_string($publicId) && $publicId !== '' && $scanned === $publicId;
241
+ }
242
+ }
@@ -0,0 +1,356 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\TrackingPage;
4
+
5
+ use Fleetbase\FleetOps\Models\TrackingSession;
6
+ use Fleetbase\FleetOps\Models\TrackingSessionGrant;
7
+ use Fleetbase\FleetOps\Support\TrackingScope;
8
+ use Fleetbase\Models\VerificationCode;
9
+ use Fleetbase\Scopes\ExpiryScope;
10
+ use Illuminate\Cache\RateLimiter;
11
+ use Illuminate\Http\Request;
12
+ use Illuminate\Support\Carbon;
13
+ use Illuminate\Support\Str;
14
+ use Symfony\Component\HttpFoundation\Cookie;
15
+
16
+ /**
17
+ * One-time codes and device sessions for the public tracking page.
18
+ *
19
+ * A code is sent to a scope's recipient and proves the visitor is that customer. A correct
20
+ * code gives the device a grant for that one customer within that one order. Codes are
21
+ * stored hashed (VerificationCode::issue()), three wrong codes pause the scope for every
22
+ * device, and sends are limited per scope and per address.
23
+ */
24
+ class TrackingAccess
25
+ {
26
+ public const COOKIE = 'fb_track';
27
+ public const CODE_PURPOSE = 'fleetops_tracking_access';
28
+ public const CODE_MINUTES = 10;
29
+ public const MAX_ATTEMPTS = 3;
30
+ public const PAUSE_SECONDS = 300;
31
+ public const RESEND_SECONDS = 30;
32
+ public const SENDS_PER_SCOPE = 3;
33
+ public const SENDS_PER_IP = 5;
34
+ public const SEND_WINDOW = 900;
35
+ public const LOOKUPS_PER_IP = 30;
36
+ public const LOOKUP_WINDOW = 60;
37
+ public const VERIFIES_PER_IP = 10;
38
+ public const REPORTS_PER_HOUR = 3;
39
+
40
+ public const SEND_SENT = 'sent';
41
+ public const SEND_COOLDOWN = 'cooldown';
42
+ public const SEND_LIMITED = 'limited';
43
+ public const SEND_PAUSED = 'paused';
44
+
45
+ public function __construct(protected ?RateLimiter $limiter = null)
46
+ {
47
+ }
48
+
49
+ /**
50
+ * Whether this address may make another lookup, counting it.
51
+ */
52
+ public function allowLookup(Request $request): bool
53
+ {
54
+ return $this->attempt('trk-lookup:' . $request->ip(), self::LOOKUPS_PER_IP, self::LOOKUP_WINDOW);
55
+ }
56
+
57
+ /**
58
+ * Whether this address may check another code, counting it.
59
+ */
60
+ public function allowVerify(Request $request): bool
61
+ {
62
+ return $this->attempt('trk-verify:' . $request->ip(), self::VERIFIES_PER_IP, self::LOOKUP_WINDOW);
63
+ }
64
+
65
+ /**
66
+ * Whether this address may report another problem with this order, counting it.
67
+ */
68
+ public function allowReport(Request $request, string $orderUuid): bool
69
+ {
70
+ return $this->attempt('trk-report:' . $orderUuid . ':' . $request->ip(), self::REPORTS_PER_HOUR, 3600);
71
+ }
72
+
73
+ /**
74
+ * Whether a scope's codes are paused after too many wrong ones, and for how long.
75
+ */
76
+ public function pausedFor(TrackingScope $scope): int
77
+ {
78
+ $key = $this->pauseKey($scope);
79
+
80
+ return $this->limiter()->tooManyAttempts($key, 1) ? max(1, $this->limiter()->availableIn($key)) : 0;
81
+ }
82
+
83
+ /**
84
+ * Check the limits for sending a code and, when they allow it, count the send.
85
+ *
86
+ * @return array{status: string, retry_after: int}
87
+ */
88
+ public function claimSend(TrackingScope $scope, Request $request): array
89
+ {
90
+ if ($paused = $this->pausedFor($scope)) {
91
+ return ['status' => self::SEND_PAUSED, 'retry_after' => $paused];
92
+ }
93
+
94
+ $cooldown = 'trk-resend:' . $scope->key();
95
+ if ($this->limiter()->tooManyAttempts($cooldown, 1)) {
96
+ return ['status' => self::SEND_COOLDOWN, 'retry_after' => max(1, $this->limiter()->availableIn($cooldown))];
97
+ }
98
+
99
+ foreach (['trk-send-scope:' . $scope->key() => self::SENDS_PER_SCOPE, 'trk-send-ip:' . $request->ip() => self::SENDS_PER_IP] as $key => $max) {
100
+ if ($this->limiter()->tooManyAttempts($key, $max)) {
101
+ return ['status' => self::SEND_LIMITED, 'retry_after' => max(1, $this->limiter()->availableIn($key))];
102
+ }
103
+ }
104
+
105
+ $this->limiter()->hit($cooldown, self::RESEND_SECONDS);
106
+ $this->limiter()->hit('trk-send-scope:' . $scope->key(), self::SEND_WINDOW);
107
+ $this->limiter()->hit('trk-send-ip:' . $request->ip(), self::SEND_WINDOW);
108
+
109
+ return ['status' => self::SEND_SENT, 'retry_after' => self::RESEND_SECONDS];
110
+ }
111
+
112
+ /**
113
+ * A new code for a scope, voiding the scope's earlier unused ones. The plain code is on `plainCode`.
114
+ */
115
+ public function issueCode(TrackingScope $scope, mixed $recipient, string $channel): VerificationCode
116
+ {
117
+ $this->voidCodes($scope);
118
+
119
+ return $this->createCode($recipient, [
120
+ 'expireAfter' => Carbon::now()->addMinutes(self::CODE_MINUTES),
121
+ 'meta' => [
122
+ 'scope' => $scope->key(),
123
+ 'channel' => $channel,
124
+ 'company_uuid' => $scope->company_uuid,
125
+ ],
126
+ ]);
127
+ }
128
+
129
+ /**
130
+ * Check a code for a scope.
131
+ *
132
+ * @return array{status: string, attempts_left?: int, channel?: string|null}
133
+ */
134
+ public function checkCode(TrackingScope $scope, string $code): array
135
+ {
136
+ $verificationCode = $this->latestCode($scope);
137
+ if (!$verificationCode) {
138
+ return ['status' => VerificationCode::CHECK_EXPIRED];
139
+ }
140
+
141
+ $result = $verificationCode->check($code, self::MAX_ATTEMPTS);
142
+ if ($result === VerificationCode::CHECK_VALID) {
143
+ $channel = $verificationCode->getMeta('channel');
144
+ $this->consumeCode($verificationCode);
145
+
146
+ return ['status' => $result, 'channel' => $channel];
147
+ }
148
+
149
+ if ($result === VerificationCode::CHECK_LOCKED) {
150
+ $this->limiter()->hit($this->pauseKey($scope), self::PAUSE_SECONDS);
151
+ }
152
+
153
+ return ['status' => $result, 'attempts_left' => $verificationCode->attemptsLeft(self::MAX_ATTEMPTS)];
154
+ }
155
+
156
+ /**
157
+ * The device session the request's cookie names, when it is live.
158
+ */
159
+ public function session(Request $request): ?TrackingSession
160
+ {
161
+ $token = $request->cookie(self::COOKIE);
162
+ if (!is_string($token) || strlen($token) < 32) {
163
+ return null;
164
+ }
165
+
166
+ $session = $this->findSession(hash('sha256', $token));
167
+ if (!$session || $session->revoked_at || ($session->expires_at && $session->expires_at->isPast())) {
168
+ return null;
169
+ }
170
+
171
+ return $session;
172
+ }
173
+
174
+ /**
175
+ * Whether the request's device holds a live grant for this scope.
176
+ */
177
+ public function hasGrant(Request $request, ?TrackingScope $scope): bool
178
+ {
179
+ if (!$scope) {
180
+ return false;
181
+ }
182
+
183
+ $session = $this->session($request);
184
+
185
+ return $session !== null && $this->findGrant($session, $scope) !== null;
186
+ }
187
+
188
+ /**
189
+ * When the device's grant for this scope ends.
190
+ */
191
+ public function grantExpiresAt(Request $request, TrackingScope $scope): ?string
192
+ {
193
+ $session = $this->session($request);
194
+ $grant = $session ? $this->findGrant($session, $scope) : null;
195
+
196
+ return $grant?->expires_at?->toISOString();
197
+ }
198
+
199
+ /**
200
+ * Give the device a grant for a scope, starting a session when it has none.
201
+ *
202
+ * @return Cookie|null the cookie to set when a new session was started
203
+ */
204
+ public function grant(Request $request, TrackingScope $scope, ?string $channel, int $hours): ?Cookie
205
+ {
206
+ $expiresAt = Carbon::now()->addHours($hours);
207
+ $session = $this->session($request);
208
+ $cookie = null;
209
+
210
+ if (!$session) {
211
+ $token = Str::random(40);
212
+ $session = $this->createSession([
213
+ 'token_hash' => hash('sha256', $token),
214
+ 'ip' => $request->ip(),
215
+ 'user_agent' => Str::limit((string) $request->userAgent(), 250, ''),
216
+ 'last_seen_at' => Carbon::now(),
217
+ 'expires_at' => $expiresAt,
218
+ ]);
219
+ $cookie = $this->cookie($token, $hours * 60);
220
+ } elseif (!$session->expires_at || $session->expires_at->lt($expiresAt)) {
221
+ $this->saveSession($session, ['expires_at' => $expiresAt, 'last_seen_at' => Carbon::now()]);
222
+ $cookie = $this->cookie((string) $request->cookie(self::COOKIE), $hours * 60);
223
+ }
224
+
225
+ $this->saveGrant($session, $scope, $channel, $expiresAt);
226
+
227
+ return $cookie;
228
+ }
229
+
230
+ /**
231
+ * Sign the device out: of one scope, or of every scope when none is given.
232
+ *
233
+ * @return Cookie|null an expired cookie when the whole session ended
234
+ */
235
+ public function revoke(Request $request, ?TrackingScope $scope = null): ?Cookie
236
+ {
237
+ $session = $this->session($request);
238
+ if (!$session) {
239
+ return $this->cookie('', -1);
240
+ }
241
+
242
+ if ($scope) {
243
+ $this->deleteGrant($session, $scope);
244
+
245
+ return null;
246
+ }
247
+
248
+ $this->saveSession($session, ['revoked_at' => Carbon::now()]);
249
+
250
+ return $this->cookie('', -1);
251
+ }
252
+
253
+ public function cookie(string $value, int $minutes): Cookie
254
+ {
255
+ $sameSite = strtolower((string) config('fleetops.tracking.cookie_same_site', 'lax'));
256
+ $sameSite = in_array($sameSite, ['lax', 'strict', 'none'], true) ? $sameSite : 'lax';
257
+ $secure = $sameSite === 'none' || (bool) config('session.secure', false);
258
+
259
+ return Cookie::create(self::COOKIE, $value, $minutes < 0 ? 1 : Carbon::now()->addMinutes($minutes)->getTimestamp(), '/', config('session.domain'), $secure, true, false, $sameSite);
260
+ }
261
+
262
+ protected function attempt(string $key, int $max, int $decay): bool
263
+ {
264
+ if ($this->limiter()->tooManyAttempts($key, $max)) {
265
+ return false;
266
+ }
267
+
268
+ $this->limiter()->hit($key, $decay);
269
+
270
+ return true;
271
+ }
272
+
273
+ protected function pauseKey(TrackingScope $scope): string
274
+ {
275
+ return 'trk-pause:' . $scope->key();
276
+ }
277
+
278
+ protected function limiter(): RateLimiter
279
+ {
280
+ return $this->limiter ??= app(RateLimiter::class);
281
+ }
282
+
283
+ protected function voidCodes(TrackingScope $scope): void
284
+ {
285
+ VerificationCode::withoutGlobalScope(ExpiryScope::class)->where('for', self::CODE_PURPOSE)->where('meta->scope', $scope->key())->delete();
286
+ }
287
+
288
+ protected function createCode(mixed $recipient, array $options): VerificationCode
289
+ {
290
+ return VerificationCode::issue($recipient, self::CODE_PURPOSE, $options);
291
+ }
292
+
293
+ /**
294
+ * The scope's newest code that has not been used. Read without the expiry scope so an
295
+ * expired code is reported as such rather than as missing; a used code stays on the
296
+ * table for the ledger but never verifies again.
297
+ */
298
+ protected function latestCode(TrackingScope $scope): ?VerificationCode
299
+ {
300
+ return VerificationCode::withoutGlobalScope(ExpiryScope::class)
301
+ ->where('for', self::CODE_PURPOSE)
302
+ ->where('meta->scope', $scope->key())
303
+ ->where(function ($query) {
304
+ $query->whereNull('status')->orWhere('status', '!=', VerificationCode::STATUS_USED);
305
+ })
306
+ ->latest()
307
+ ->first();
308
+ }
309
+
310
+ protected function consumeCode(VerificationCode $verificationCode): void
311
+ {
312
+ $verificationCode->consume();
313
+ }
314
+
315
+ protected function findSession(string $tokenHash): ?TrackingSession
316
+ {
317
+ return TrackingSession::where('token_hash', $tokenHash)->first();
318
+ }
319
+
320
+ protected function createSession(array $attributes): TrackingSession
321
+ {
322
+ return TrackingSession::create($attributes);
323
+ }
324
+
325
+ protected function saveSession(TrackingSession $session, array $attributes): void
326
+ {
327
+ $session->forceFill($attributes)->save();
328
+ }
329
+
330
+ protected function findGrant(TrackingSession $session, TrackingScope $scope): ?TrackingSessionGrant
331
+ {
332
+ return TrackingSessionGrant::where('tracking_session_uuid', $session->uuid)
333
+ ->where('order_uuid', $scope->order_uuid)
334
+ ->where('customer_type', $scope->customer_type)
335
+ ->where('customer_uuid', $scope->customer_uuid)
336
+ ->where('expires_at', '>', Carbon::now())
337
+ ->first();
338
+ }
339
+
340
+ protected function saveGrant(TrackingSession $session, TrackingScope $scope, ?string $channel, Carbon $expiresAt): void
341
+ {
342
+ TrackingSessionGrant::updateOrCreate(
343
+ ['tracking_session_uuid' => $session->uuid, 'order_uuid' => $scope->order_uuid, 'customer_type' => $scope->customer_type, 'customer_uuid' => $scope->customer_uuid],
344
+ ['company_uuid' => $scope->company_uuid, 'channel' => $channel, 'expires_at' => $expiresAt]
345
+ );
346
+ }
347
+
348
+ protected function deleteGrant(TrackingSession $session, TrackingScope $scope): void
349
+ {
350
+ TrackingSessionGrant::where('tracking_session_uuid', $session->uuid)
351
+ ->where('order_uuid', $scope->order_uuid)
352
+ ->where('customer_type', $scope->customer_type)
353
+ ->where('customer_uuid', $scope->customer_uuid)
354
+ ->delete();
355
+ }
356
+ }