@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,518 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Controllers\Public;
4
+
5
+ use Fleetbase\FleetOps\Models\Contact;
6
+ use Fleetbase\FleetOps\Models\Entity;
7
+ use Fleetbase\FleetOps\Models\Order;
8
+ use Fleetbase\FleetOps\Models\Waypoint;
9
+ use Fleetbase\FleetOps\Support\TrackingChannel;
10
+ use Fleetbase\FleetOps\Support\TrackingPage\TrackingAccess;
11
+ use Fleetbase\FleetOps\Support\TrackingPage\TrackingRecipient;
12
+ use Fleetbase\FleetOps\Support\TrackingPage\TrackingResolver;
13
+ use Fleetbase\FleetOps\Support\TrackingPage\TrackingTarget;
14
+ use Fleetbase\FleetOps\Support\TrackingPage\TrackingView;
15
+ use Fleetbase\FleetOps\Support\TrackingPublisher;
16
+ use Fleetbase\FleetOps\Support\TrackingScope;
17
+ use Fleetbase\FleetOps\Support\Utils;
18
+ use Fleetbase\Models\Company;
19
+ use Fleetbase\Models\Setting;
20
+ use Fleetbase\Models\VerificationCode;
21
+ use Fleetbase\Services\SmsService;
22
+ use Illuminate\Http\JsonResponse;
23
+ use Illuminate\Http\Request;
24
+ use Illuminate\Routing\Controller;
25
+ use Illuminate\Support\Facades\Mail;
26
+ use Symfony\Component\HttpFoundation\Cookie;
27
+
28
+ /**
29
+ * The public customer tracking page's API.
30
+ *
31
+ * A tracking number alone gives level 0. A device that entered a correct one-time code
32
+ * gets level 1 for one customer within the order; a signed-in customer gets level 2;
33
+ * staff of the owning company see the customer's view. Unknown, malformed, opted-out,
34
+ * other-company and throttled lookups all get the same 404, so a caller can't tell
35
+ * which one they hit.
36
+ *
37
+ * `?org={slug}` on any route scopes it to that company's own page.
38
+ */
39
+ class TrackingController extends Controller
40
+ {
41
+ public const NOT_FOUND = 'We can\'t show tracking for that number. Check it matches your email or parcel label and try again.';
42
+
43
+ public function __construct(
44
+ protected ?TrackingResolver $resolver = null,
45
+ protected ?TrackingAccess $access = null,
46
+ protected ?TrackingView $view = null,
47
+ ) {
48
+ $this->resolver ??= new TrackingResolver();
49
+ $this->access ??= new TrackingAccess();
50
+ $this->view ??= new TrackingView();
51
+ }
52
+
53
+ /**
54
+ * The page's branding and what it offers, before any lookup.
55
+ */
56
+ public function config(Request $request): JsonResponse
57
+ {
58
+ $slug = $this->slug($request);
59
+ if ($slug !== null) {
60
+ $companyUuid = $this->resolver->companyForSlug($slug);
61
+ if ($companyUuid === null) {
62
+ return $this->json(['error' => 'page_not_found'], 404);
63
+ }
64
+
65
+ $config = $this->resolver->config($companyUuid);
66
+
67
+ return $this->json([
68
+ 'company' => $this->view->branding($config, $this->findCompany($companyUuid)),
69
+ 'org' => $slug,
70
+ 'sign_in' => $this->signInOffered($config),
71
+ 'session_hours' => $config['access']['session_hours'],
72
+ ]);
73
+ }
74
+
75
+ $admin = $this->resolver->adminConfig();
76
+ if (!$admin['generic_page']['enabled']) {
77
+ return $this->json(['error' => 'page_not_found'], 404);
78
+ }
79
+
80
+ return $this->json([
81
+ 'company' => $this->view->branding(['branding' => $admin['branding']], null, $this->instanceLogo()),
82
+ 'org' => null,
83
+ 'sign_in' => $this->customerPortalInstalled(),
84
+ 'session_hours' => 24,
85
+ ]);
86
+ }
87
+
88
+ public function show(Request $request, string $number): JsonResponse
89
+ {
90
+ if (!$this->access->allowLookup($request)) {
91
+ return $this->notFound();
92
+ }
93
+
94
+ $target = $this->resolve($request, $number);
95
+
96
+ return $target ? $this->json($this->payload($request, $target)) : $this->notFound();
97
+ }
98
+
99
+ public function sendCode(Request $request, string $number): JsonResponse
100
+ {
101
+ $target = $this->resolve($request, $number);
102
+ if (!$target) {
103
+ return $this->notFound();
104
+ }
105
+
106
+ $recipient = $this->recipientFor($target);
107
+ $channel = (string) $request->input('channel');
108
+ $channels = collect($recipient->channels($target->config, $this->smsAvailable()));
109
+ $selected = $channels->firstWhere('type', $channel);
110
+ if (!$target->scope || !$selected) {
111
+ return $this->json(['error' => 'channel_unavailable'], 422);
112
+ }
113
+
114
+ $claim = $this->access->claimSend($target->scope, $request);
115
+ if ($claim['status'] !== TrackingAccess::SEND_SENT) {
116
+ return $this->json(['error' => $claim['status'], 'retry_after' => $claim['retry_after']], $claim['status'] === TrackingAccess::SEND_PAUSED ? 423 : 429);
117
+ }
118
+
119
+ $code = $this->access->issueCode($target->scope, $recipient->customer, $channel);
120
+ $this->deliver($channel, (string) $recipient->destination($channel), (string) $code->plainCode, $target);
121
+
122
+ return $this->json([
123
+ 'channel' => $channel,
124
+ 'masked' => $selected['masked'],
125
+ 'expires_in' => TrackingAccess::CODE_MINUTES * 60,
126
+ 'resend_after' => $claim['retry_after'],
127
+ ], 202);
128
+ }
129
+
130
+ public function verifyCode(Request $request, string $number): JsonResponse
131
+ {
132
+ if (!$this->access->allowVerify($request)) {
133
+ return $this->json(['error' => 'limited', 'retry_after' => TrackingAccess::LOOKUP_WINDOW], 429);
134
+ }
135
+
136
+ $target = $this->resolve($request, $number);
137
+ if (!$target || !$target->scope) {
138
+ return $this->notFound();
139
+ }
140
+
141
+ if ($paused = $this->access->pausedFor($target->scope)) {
142
+ return $this->json(['error' => 'paused', 'retry_after' => $paused], 423);
143
+ }
144
+
145
+ $result = $this->access->checkCode($target->scope, preg_replace('/\D/', '', (string) $request->input('code')));
146
+
147
+ return match ($result['status']) {
148
+ VerificationCode::CHECK_VALID => $this->verified($request, $target, $result['channel'] ?? null),
149
+ VerificationCode::CHECK_LOCKED => $this->json(['error' => 'paused', 'retry_after' => TrackingAccess::PAUSE_SECONDS], 423),
150
+ VerificationCode::CHECK_EXPIRED => $this->json(['error' => 'expired'], 410),
151
+ default => $this->json(['error' => 'invalid', 'attempts_left' => $result['attempts_left']], 422),
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Sign this device out: of one delivery with `?number=`, otherwise of every one.
157
+ */
158
+ public function signOut(Request $request): JsonResponse
159
+ {
160
+ $number = $request->input('number');
161
+ $target = $number ? $this->resolve($request, $number) : null;
162
+ $cookie = $this->access->revoke($request, $target?->scope);
163
+
164
+ return $this->json(['status' => 'signed_out'], 200, $cookie);
165
+ }
166
+
167
+ public function updateInstructions(Request $request, string $number): JsonResponse
168
+ {
169
+ $target = $this->resolve($request, $number);
170
+ $viewer = $target ? $this->viewerFor($request, $target) : null;
171
+ if (!$target || !$viewer || !$target->scope) {
172
+ return $this->notFound();
173
+ }
174
+
175
+ if (!data_get($target->config, 'visibility.instructions_edit', false)) {
176
+ return $this->json(['error' => 'not_allowed'], 403);
177
+ }
178
+
179
+ $text = trim(mb_substr((string) $request->input('text', ''), 0, 500));
180
+ $this->saveInstructions($target->order, $target->scope, $text);
181
+
182
+ return $this->json($this->payload($request, $target));
183
+ }
184
+
185
+ public function proof(Request $request, string $number, string $id): JsonResponse
186
+ {
187
+ $target = $this->resolve($request, $number);
188
+ if (!$target || !$this->viewerFor($request, $target)) {
189
+ return $this->notFound();
190
+ }
191
+
192
+ $proof = $this->view->proofFor($target, $id);
193
+ if (!$proof) {
194
+ return $this->json(['error' => 'not_found'], 404);
195
+ }
196
+
197
+ $url = data_get($proof, 'file.url') ?? (is_string($proof->raw_data) && $proof->raw_data !== '' ? $this->dataUrl($proof->raw_data) : null);
198
+
199
+ return $url ? $this->json(['url' => $url, 'type' => TrackingView::proofType($proof)]) : $this->json(['error' => 'not_found'], 404);
200
+ }
201
+
202
+ public function report(Request $request, string $number): JsonResponse
203
+ {
204
+ $target = $this->resolve($request, $number);
205
+ if (!$target || !$this->viewerFor($request, $target)) {
206
+ return $this->notFound();
207
+ }
208
+
209
+ $branding = $this->brandingFor($target);
210
+ $to = $branding['support_email'];
211
+ $message = trim(mb_substr((string) $request->input('message', ''), 0, 1000));
212
+ if (!data_get($target->config, 'visibility.report_problem', true) || !$to || $message === '') {
213
+ return $this->json(['error' => 'not_allowed'], 422);
214
+ }
215
+
216
+ if (!$this->access->allowReport($request, (string) $target->order->uuid)) {
217
+ return $this->json(['error' => 'limited'], 429);
218
+ }
219
+
220
+ $this->sendReport($to, $target, $message);
221
+
222
+ return $this->json(['status' => 'sent'], 202);
223
+ }
224
+
225
+ public function socketToken(Request $request, string $number): JsonResponse
226
+ {
227
+ $target = $this->resolve($request, $number);
228
+ if (!$target || !$target->scope || !$this->viewerFor($request, $target) || !$this->socketsEnabled()) {
229
+ return $this->notFound();
230
+ }
231
+
232
+ return $this->json($this->socketTokenFor($target->scope));
233
+ }
234
+
235
+ /**
236
+ * A signed-in customer's orders, newest first.
237
+ */
238
+ public function accountOrders(Request $request): JsonResponse
239
+ {
240
+ $contacts = $this->contactsFor($this->requestUser($request));
241
+ if ($contacts === []) {
242
+ return $this->json(['error' => 'unauthenticated'], 401);
243
+ }
244
+
245
+ return $this->json(['orders' => $this->ordersForContacts($contacts)]);
246
+ }
247
+
248
+ protected function payload(Request $request, TrackingTarget $target): array
249
+ {
250
+ $branding = $this->brandingFor($target);
251
+ $viewer = $this->viewerFor($request, $target);
252
+
253
+ if ($viewer) {
254
+ $expiresAt = $viewer === TrackingView::VIEWER_VERIFIED ? $this->access->grantExpiresAt($request, $target->scope) : null;
255
+
256
+ return $this->view->fullView($target, $branding, $viewer, $expiresAt);
257
+ }
258
+
259
+ $channels = $target->scope ? $this->recipientFor($target)->channels($target->config, $this->smsAvailable()) : [];
260
+
261
+ return $this->view->publicView($target, $branding, $channels, $this->signInOffered($target->config));
262
+ }
263
+
264
+ protected function verified(Request $request, TrackingTarget $target, ?string $channel): JsonResponse
265
+ {
266
+ $hours = (int) data_get($target->config, 'access.session_hours', 24);
267
+ $cookie = $this->access->grant($request, $target->scope, $channel, $hours);
268
+ $data = $this->view->fullView($target, $this->brandingFor($target), TrackingView::VIEWER_VERIFIED, now()->addHours($hours)->toISOString());
269
+
270
+ return $this->json($data, 200, $cookie);
271
+ }
272
+
273
+ /**
274
+ * Who is looking: staff of the owning company, a signed-in customer of this delivery,
275
+ * or a device verified for it; null for a visitor with only the tracking number.
276
+ */
277
+ protected function viewerFor(Request $request, TrackingTarget $target): ?string
278
+ {
279
+ $user = $this->requestUser($request);
280
+ if ($user && data_get($user, 'type') !== 'customer' && data_get($user, 'company_uuid') === $target->companyUuid()) {
281
+ return TrackingView::VIEWER_STAFF;
282
+ }
283
+
284
+ if ($user && $target->scope && in_array($target->scope->customer_uuid, $this->contactsFor($user), true)) {
285
+ return TrackingView::VIEWER_ACCOUNT;
286
+ }
287
+
288
+ return $this->access->hasGrant($request, $target->scope) ? TrackingView::VIEWER_VERIFIED : null;
289
+ }
290
+
291
+ protected function resolve(Request $request, mixed $number): ?TrackingTarget
292
+ {
293
+ return $this->resolver->resolve($number, $this->slug($request));
294
+ }
295
+
296
+ protected function slug(Request $request): ?string
297
+ {
298
+ $slug = $request->input('org');
299
+
300
+ return is_string($slug) && trim($slug) !== '' ? strtolower(trim($slug)) : null;
301
+ }
302
+
303
+ protected function brandingFor(TrackingTarget $target): array
304
+ {
305
+ return $this->view->branding($target->config, $this->findCompany($target->companyUuid()));
306
+ }
307
+
308
+ protected function signInOffered(array $config): bool
309
+ {
310
+ return (bool) data_get($config, 'access.account_sign_in', true) && $this->customerPortalInstalled();
311
+ }
312
+
313
+ protected function recipientFor(TrackingTarget $target): TrackingRecipient
314
+ {
315
+ return TrackingRecipient::for($target->scope, $this->fallbackPhone($target));
316
+ }
317
+
318
+ protected function fallbackPhone(TrackingTarget $target): ?string
319
+ {
320
+ return data_get($target->waypoint, 'place.phone') ?? data_get($target->order, 'payload.dropoff.phone');
321
+ }
322
+
323
+ protected function deliver(string $channel, string $destination, string $code, TrackingTarget $target): void
324
+ {
325
+ $name = $this->brandingFor($target)['name'] ?? 'Your delivery';
326
+ $message = $code . ' is your ' . $name . ' delivery code. It expires in ' . TrackingAccess::CODE_MINUTES . ' minutes.';
327
+
328
+ if ($channel === TrackingRecipient::CHANNEL_SMS) {
329
+ $this->sendSms($destination, $message, $target);
330
+
331
+ return;
332
+ }
333
+
334
+ $this->sendEmail($destination, $code . ' is your ' . $name . ' delivery code', $message);
335
+ }
336
+
337
+ protected function saveInstructions(Order $order, TrackingScope $scope, string $text): void
338
+ {
339
+ $instructions = (array) $order->getMeta('recipient_instructions', []);
340
+ $instructions[sha1($scope->key())] = $text;
341
+ $order->setMeta('recipient_instructions', $instructions);
342
+ $order->saveQuietly();
343
+ $order->invalidateApiCache();
344
+ }
345
+
346
+ protected function dataUrl(string $raw): string
347
+ {
348
+ return str_starts_with($raw, 'data:') ? $raw : 'data:image/png;base64,' . $raw;
349
+ }
350
+
351
+ protected function notFound(): JsonResponse
352
+ {
353
+ return $this->json(['error' => self::NOT_FOUND, 'code' => 'not_found'], 404);
354
+ }
355
+
356
+ protected function json(array $data, int $status = 200, ?Cookie $cookie = null): JsonResponse
357
+ {
358
+ $response = new JsonResponse($data, $status);
359
+ if ($cookie) {
360
+ $response->headers->setCookie($cookie);
361
+ }
362
+
363
+ return $response;
364
+ }
365
+
366
+ /**
367
+ * Whether the instance can send SMS. Twilio is listed as always available, so it counts
368
+ * only with credentials; other providers report their own configuration.
369
+ */
370
+ protected function smsAvailable(): bool
371
+ {
372
+ if (filled(config('services.twilio.sid')) && filled(config('services.twilio.token'))) {
373
+ return true;
374
+ }
375
+
376
+ return collect($this->smsProviders())
377
+ ->except(SmsService::PROVIDER_TWILIO)
378
+ ->contains(fn ($provider) => (bool) data_get($provider, 'available'));
379
+ }
380
+
381
+ /**
382
+ * The contact uuids a signed-in customer user acts as.
383
+ *
384
+ * @return array<int, string>
385
+ */
386
+ protected function contactsFor(mixed $user): array
387
+ {
388
+ if (!$user || data_get($user, 'type') !== 'customer' || !data_get($user, 'uuid')) {
389
+ return [];
390
+ }
391
+
392
+ return $this->contactUuidsForUser((string) data_get($user, 'uuid'));
393
+ }
394
+
395
+ protected function contactUuidsForUser(string $userUuid): array
396
+ {
397
+ return Contact::withoutGlobalScopes()->where('user_uuid', $userUuid)->pluck('uuid')->all();
398
+ }
399
+
400
+ /**
401
+ * Each order a customer is on, with the tracking number that opens their part of it.
402
+ *
403
+ * @param array<int, string> $contacts
404
+ */
405
+ protected function ordersForContacts(array $contacts): array
406
+ {
407
+ $payloads = Waypoint::withoutGlobalScopes()->whereIn('customer_uuid', $contacts)->pluck('payload_uuid')->filter()->unique()->values()->all();
408
+ $orders = Order::withoutGlobalScopes()
409
+ ->with(['trackingNumber'])
410
+ ->where(fn ($query) => $query->whereIn('customer_uuid', $contacts)->orWhereIn('payload_uuid', $payloads))
411
+ ->latest()
412
+ ->limit(50)
413
+ ->get();
414
+
415
+ return $orders->map(function (Order $order) use ($contacts) {
416
+ $waypoint = in_array($order->customer_uuid, $contacts, true) ? null : Waypoint::withoutGlobalScopes()->with('trackingNumber')->where('payload_uuid', $order->payload_uuid)->whereIn('customer_uuid', $contacts)->first();
417
+ $first = Entity::withoutGlobalScopes()->where('payload_uuid', $order->payload_uuid)->value('name');
418
+
419
+ return [
420
+ 'tracking_number' => $waypoint ? data_get($waypoint, 'trackingNumber.tracking_number') : data_get($order, 'trackingNumber.tracking_number'),
421
+ 'title' => $first,
422
+ 'status' => $order->status,
423
+ 'stage' => $this->view->stage($order, null),
424
+ 'updated_at' => $order->updated_at ? $order->updated_at->toISOString() : null,
425
+ ];
426
+ })->filter(fn ($row) => !empty($row['tracking_number']))->values()->all();
427
+ }
428
+
429
+ protected function findCompany(?string $companyUuid): mixed
430
+ {
431
+ return $companyUuid ? Company::where('uuid', $companyUuid)->first() : null;
432
+ }
433
+
434
+ protected function sendSms(string $phone, string $message, TrackingTarget $target): void
435
+ {
436
+ $company = $this->findCompany($target->companyUuid());
437
+ $options = [];
438
+ if (data_get($company, 'options.alpha_numeric_sender_id_enabled') && data_get($company, 'options.alpha_numeric_sender_id')) {
439
+ $options['twilioParams'] = ['from' => data_get($company, 'options.alpha_numeric_sender_id')];
440
+ }
441
+
442
+ $this->smsService()->send($phone, $message, $options);
443
+ }
444
+
445
+ protected function sendEmail(string $email, string $subject, string $message): void
446
+ {
447
+ $this->mail($email, $subject, $message);
448
+ }
449
+
450
+ protected function sendReport(string $to, TrackingTarget $target, string $message): void
451
+ {
452
+ $number = $target->trackingNumber->tracking_number;
453
+
454
+ $this->mail($to, 'Problem reported for ' . $number, 'A customer reported a problem with delivery ' . $number . ":\n\n" . $message);
455
+ }
456
+
457
+ protected function socketsEnabled(): bool
458
+ {
459
+ return TrackingPublisher::enabled();
460
+ }
461
+
462
+ // Environment seams: tests replace these.
463
+
464
+ protected function socketTokenFor(TrackingScope $scope): array
465
+ {
466
+ // @codeCoverageIgnoreStart
467
+ // Signs with the instance's socket auth key.
468
+ return TrackingChannel::token($scope);
469
+ // @codeCoverageIgnoreEnd
470
+ }
471
+
472
+ protected function mail(string $to, string $subject, string $body): void
473
+ {
474
+ // @codeCoverageIgnoreStart
475
+ Mail::raw($body, fn ($mail) => $mail->to($to)->subject($subject));
476
+ // @codeCoverageIgnoreEnd
477
+ }
478
+
479
+ protected function smsService(): SmsService
480
+ {
481
+ // @codeCoverageIgnoreStart
482
+ return app(SmsService::class);
483
+ // @codeCoverageIgnoreEnd
484
+ }
485
+
486
+ protected function smsProviders(): array
487
+ {
488
+ // @codeCoverageIgnoreStart
489
+ return (new SmsService())->getAvailableProviders();
490
+ // @codeCoverageIgnoreEnd
491
+ }
492
+
493
+ protected function requestUser(Request $request): mixed
494
+ {
495
+ // @codeCoverageIgnoreStart
496
+ // A signed-in console or customer-portal user, from the bearer token the page sends.
497
+ try {
498
+ return auth('sanctum')->user();
499
+ } catch (\Throwable) {
500
+ return null;
501
+ }
502
+ // @codeCoverageIgnoreEnd
503
+ }
504
+
505
+ protected function customerPortalInstalled(): bool
506
+ {
507
+ // @codeCoverageIgnoreStart
508
+ return collect(Utils::getInstalledFleetbaseExtensions())->contains(fn ($package) => data_get($package, 'name') === 'fleetbase/customer-portal-api');
509
+ // @codeCoverageIgnoreEnd
510
+ }
511
+
512
+ protected function instanceLogo(): ?string
513
+ {
514
+ // @codeCoverageIgnoreStart
515
+ return data_get(Setting::getBranding(), 'logo_url');
516
+ // @codeCoverageIgnoreEnd
517
+ }
518
+ }
@@ -64,7 +64,7 @@ class ContactFilter extends Filter
64
64
  public function address(?string $address)
65
65
  {
66
66
  $this->builder->whereHas(
67
- 'addresses',
67
+ 'place',
68
68
  function ($query) use ($address) {
69
69
  $query->search($address);
70
70
  }
@@ -0,0 +1,23 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Filter;
4
+
5
+ use Fleetbase\Http\Filter\Filter;
6
+
7
+ class InspectionFormFilter extends Filter
8
+ {
9
+ public function queryForInternal()
10
+ {
11
+ $this->builder->where('company_uuid', $this->session->get('company'));
12
+ }
13
+
14
+ public function queryForPublic()
15
+ {
16
+ $this->queryForInternal();
17
+ }
18
+
19
+ public function query(?string $searchQuery)
20
+ {
21
+ $this->builder->search($searchQuery);
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Filter;
4
+
5
+ use Fleetbase\Http\Filter\Filter;
6
+
7
+ class InspectionSubmissionFilter extends Filter
8
+ {
9
+ public function queryForInternal()
10
+ {
11
+ $this->builder->where('company_uuid', $this->session->get('company'));
12
+ }
13
+
14
+ public function queryForPublic()
15
+ {
16
+ $this->queryForInternal();
17
+ }
18
+
19
+ public function query(?string $searchQuery)
20
+ {
21
+ $this->builder->search($searchQuery);
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Filter;
4
+
5
+ use Fleetbase\Http\Filter\Filter;
6
+
7
+ class MaintenanceFilter extends Filter
8
+ {
9
+ public function queryForInternal()
10
+ {
11
+ $this->builder->where('company_uuid', $this->session->get('company'));
12
+ }
13
+
14
+ public function queryForPublic()
15
+ {
16
+ $this->queryForInternal();
17
+ }
18
+
19
+ public function query(?string $searchQuery)
20
+ {
21
+ $this->builder->search($searchQuery);
22
+ }
23
+ }
@@ -2,14 +2,22 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Filter;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
6
+ use Fleetbase\FleetOps\Http\Resources\v1\Index\Customer as CustomerIndexResource;
7
+ use Fleetbase\FleetOps\Models\Contact;
8
+ use Fleetbase\FleetOps\Models\IntegratedVendor;
9
+ use Fleetbase\FleetOps\Models\Vendor;
5
10
  use Fleetbase\FleetOps\Support\Utils;
6
11
  use Fleetbase\Http\Filter\Filter;
7
12
  use Fleetbase\Support\Http;
13
+ use Illuminate\Database\Eloquent\Relations\MorphTo;
8
14
  use Illuminate\Support\Arr;
9
15
  use Illuminate\Support\Str;
10
16
 
11
17
  class OrderFilter extends Filter
12
18
  {
19
+ use ResolvesPublicRelationUuids;
20
+
13
21
  public function queryForInternal()
14
22
  {
15
23
  $companyUuid = $this->request->session()->get('company');
@@ -57,7 +65,7 @@ class OrderFilter extends Filter
57
65
  'vehicleAssigned' => function ($query) {
58
66
  $query->without(['fleets', 'vendor']);
59
67
  },
60
- 'customer',
68
+ 'customer' => static fn (MorphTo $relation) => CustomerIndexResource::withPhoto($relation),
61
69
  'facilitator',
62
70
  ]);
63
71
  }
@@ -141,10 +149,17 @@ class OrderFilter extends Filter
141
149
  }
142
150
  }
143
151
 
152
+ /**
153
+ * A customer is a contact or a vendor. The public API names it by public id,
154
+ * so the id is resolved to the uuid `customer_uuid` stores. The customer
155
+ * portal names its own user, which the authenticatable branch still matches.
156
+ */
144
157
  public function customer(string $customer)
145
158
  {
146
- $this->builder->where(function ($query) use ($customer) {
147
- $query->where('customer_uuid', $customer);
159
+ $customerUuids = $this->resolvePublicRelationUuidsAcross([Contact::class, Vendor::class], $customer);
160
+
161
+ $this->builder->where(function ($query) use ($customer, $customerUuids) {
162
+ $query->whereIn('customer_uuid', $customerUuids);
148
163
  $query->orWhereHas('authenticatableCustomer', function ($query) use ($customer) {
149
164
  $query->where('user_uuid', $customer);
150
165
  });
@@ -158,9 +173,29 @@ class OrderFilter extends Filter
158
173
  });
159
174
  }
160
175
 
176
+ /**
177
+ * A facilitator is a vendor, an integrated vendor or a contact. An id that
178
+ * resolves to none of them leaves an empty list, which matches no order.
179
+ */
161
180
  public function facilitator(string $facilitator)
162
181
  {
163
- $this->builder->where('facilitator_uuid', $facilitator);
182
+ $this->builder->whereIn('facilitator_uuid', $this->resolvePublicRelationUuidsAcross([Vendor::class, IntegratedVendor::class, Contact::class], $facilitator));
183
+ }
184
+
185
+ /**
186
+ * Resolve an identifier against every model a polymorphic relation can point at.
187
+ *
188
+ * @param array<int, class-string> $modelClasses
189
+ *
190
+ * @return array<int, string>
191
+ */
192
+ protected function resolvePublicRelationUuidsAcross(array $modelClasses, string $identifier): array
193
+ {
194
+ return collect($modelClasses)
195
+ ->flatMap(fn (string $modelClass) => $this->resolvePublicRelationUuids($modelClass, $identifier))
196
+ ->unique()
197
+ ->values()
198
+ ->all();
164
199
  }
165
200
 
166
201
  public function type(string $type)