@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
@@ -13,12 +13,15 @@ use Fleetbase\FleetOps\Models\Payload;
13
13
  use Fleetbase\FleetOps\Models\Place;
14
14
  use Fleetbase\FleetOps\Models\Vehicle;
15
15
  use Fleetbase\FleetOps\Models\Vendor;
16
+ use Fleetbase\FleetOps\Models\Waypoint;
17
+ use Fleetbase\FleetOps\Orchestration\Contracts\OrchestrationEngineInterface;
16
18
  use Fleetbase\FleetOps\Orchestration\Engines\DriverAssignmentEngine;
17
19
  use Fleetbase\FleetOps\Orchestration\Engines\RouteSequencingEngine;
18
20
  use Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry;
21
+ use Fleetbase\FleetOps\Orchestration\Support\OrchestratorSettings;
22
+ use Fleetbase\FleetOps\Orchestration\Support\RouteMetrics;
19
23
  use Fleetbase\FleetOps\Traits\AuthorizesMethods;
20
24
  use Fleetbase\Http\Controllers\Controller;
21
- use Fleetbase\Models\Setting;
22
25
  use Illuminate\Http\JsonResponse;
23
26
  use Illuminate\Http\Request;
24
27
  use Illuminate\Support\Carbon;
@@ -42,6 +45,11 @@ class OrchestrationController extends Controller
42
45
  {
43
46
  use AuthorizesMethods;
44
47
 
48
+ /**
49
+ * Engines that assign orders to vehicles without producing a stop sequence.
50
+ */
51
+ protected const ASSIGNMENT_ONLY_ENGINES = ['greedy', 'capacity'];
52
+
45
53
  public function __construct(protected OrchestrationEngineRegistry $registry)
46
54
  {
47
55
  $this->authorizeMethods([
@@ -281,7 +289,7 @@ class OrchestrationController extends Controller
281
289
  // ── Run engine ────────────────────────────────────────────────────────
282
290
  $engineId = $mode === 'assign_drivers'
283
291
  ? 'driver_assignment'
284
- : ($request->input('options.engine') ?? $this->orchestratorEngineSetting());
292
+ : ($request->input('options.engine') ?: $this->orchestratorEngineSetting());
285
293
 
286
294
  try {
287
295
  if ($mode === 'assign_drivers') {
@@ -308,14 +316,13 @@ class OrchestrationController extends Controller
308
316
  }
309
317
  }
310
318
  }
311
- $engine = $this->routeSequencingEngine();
312
- $result = $engine->sequence($orders, $options);
319
+ $result = $this->optimizeRoutes($engineId, $orders, $options);
313
320
  } else {
314
- $engine = $this->registry->resolve($engineId);
315
- $result = $engine->allocate($orders, $vehicles, $options);
321
+ $result = $this->registry->allocateWithFallback($engineId, $orders, $vehicles, $options);
316
322
  }
317
323
  } catch (\RuntimeException $e) {
318
- // Engine is unavailable (e.g. VROOM not reachable).
324
+ // Engine is unavailable (e.g. VROOM not reachable) and there is no
325
+ // built-in engine to fall back to.
319
326
  // Return a structured JSON 503 so the frontend can display a
320
327
  // user-friendly message instead of an unhandled exception page.
321
328
  return response()->json([
@@ -325,6 +332,10 @@ class OrchestrationController extends Controller
325
332
  ], 503);
326
333
  }
327
334
 
335
+ if ($mode !== 'assign_drivers') {
336
+ $result = RouteMetrics::annotate($result, $orders, $vehicles);
337
+ }
338
+
328
339
  return response()->json($result);
329
340
  }
330
341
 
@@ -384,7 +395,71 @@ class OrchestrationController extends Controller
384
395
 
385
396
  protected function orchestratorEngineSetting(): string
386
397
  {
387
- return Setting::lookup('fleetops.orchestrator_engine', 'greedy');
398
+ return OrchestratorSettings::engineForCompany($this->companyUuid());
399
+ }
400
+
401
+ /**
402
+ * Re-sequence each vehicle's already-assigned orders.
403
+ *
404
+ * A solver engine (e.g. VROOM) is run once per vehicle, restricted to
405
+ * that vehicle and its orders, so the stop order, arrival times and
406
+ * distances come from the selected engine. Engines that only assign
407
+ * orders (greedy, capacity) cannot sequence stops, and a solver that is
408
+ * missing or unavailable is replaced; in both cases the built-in route
409
+ * sequencer runs and the result says which engine ran and why.
410
+ */
411
+ protected function optimizeRoutes(string $engineId, $orders, array $options): array
412
+ {
413
+ $warn = true;
414
+ if (in_array($engineId, self::ASSIGNMENT_ONLY_ENGINES, true)) {
415
+ $warn = false;
416
+ $reason = sprintf('The "%s" engine assigns orders but does not sequence stops; stops were sequenced by the built-in route sequencer.', $engineId);
417
+ } elseif (!$this->registry->has($engineId)) {
418
+ $reason = sprintf('No orchestration engine is registered as "%s".', $engineId);
419
+ } else {
420
+ try {
421
+ return $this->sequenceWithEngine($this->registry->resolve($engineId), $orders, $options);
422
+ } catch (\RuntimeException $e) {
423
+ $reason = $e->getMessage();
424
+ }
425
+ }
426
+
427
+ $result = $this->routeSequencingEngine()->sequence($orders, $options);
428
+
429
+ return OrchestrationEngineRegistry::markFallback($result, $result['summary']['engine'] ?? 'route_sequencing', $engineId, $reason, $warn);
430
+ }
431
+
432
+ /**
433
+ * Run a solver engine once per vehicle group, keeping every order on its vehicle.
434
+ */
435
+ protected function sequenceWithEngine(OrchestrationEngineInterface $engine, $orders, array $options): array
436
+ {
437
+ $options = array_merge($options, ['allocation_strategy' => 'route_aware']);
438
+ $assignments = [];
439
+ $unassigned = [];
440
+
441
+ foreach ($orders->groupBy(fn ($order) => $order->vehicle_assigned_uuid ?? '') as $vehicleUuid => $vehicleOrders) {
442
+ $vehicle = $vehicleUuid !== '' && $vehicleOrders->first()->relationLoaded('vehicle') ? $vehicleOrders->first()->vehicle : null;
443
+ if (!$vehicle) {
444
+ // Without an assigned vehicle there is no route to sequence.
445
+ array_push($unassigned, ...$vehicleOrders->pluck('public_id')->all());
446
+ continue;
447
+ }
448
+
449
+ $result = $engine->allocate($vehicleOrders->values(), collect([$vehicle]), $options);
450
+ array_push($assignments, ...($result['assignments'] ?? []));
451
+ array_push($unassigned, ...($result['unassigned'] ?? []));
452
+ }
453
+
454
+ return [
455
+ 'assignments' => $assignments,
456
+ 'unassigned' => array_values(array_unique($unassigned)),
457
+ 'summary' => [
458
+ 'engine' => $engine->getIdentifier(),
459
+ 'assigned' => count($assignments),
460
+ 'unassigned' => count(array_unique($unassigned)),
461
+ ],
462
+ ];
388
463
  }
389
464
 
390
465
  protected function driverAssignmentEngine(): DriverAssignmentEngine
@@ -415,9 +490,10 @@ class OrchestrationController extends Controller
415
490
  return response()->json(['error' => 'No assignments provided.'], 422);
416
491
  }
417
492
 
418
- $committed = [];
419
- $failed = [];
420
- $manifests = [];
493
+ $committed = [];
494
+ $failed = [];
495
+ $manifests = [];
496
+ $resequenced = false;
421
497
 
422
498
  $this->beginOrchestrationTransaction();
423
499
  try {
@@ -502,6 +578,7 @@ class OrchestrationController extends Controller
502
578
  if (!empty($assignment['waypoint_sequence']) && $order->payload) {
503
579
  foreach ($assignment['waypoint_sequence'] as $seq => $waypointId) {
504
580
  $this->updateWaypointSequence($order->payload_uuid, $waypointId, $seq);
581
+ $resequenced = true;
505
582
  }
506
583
  }
507
584
 
@@ -523,6 +600,11 @@ class OrchestrationController extends Controller
523
600
  return response()->json(['error' => 'Commit failed: ' . $e->getMessage()], 500);
524
601
  }
525
602
 
603
+ // Waypoint sequences are written without model events
604
+ if ($resequenced) {
605
+ Waypoint::invalidateApiCacheManually($companyUuid);
606
+ }
607
+
526
608
  return response()->json([
527
609
  'committed' => $committed,
528
610
  'failed' => $failed,
@@ -10,6 +10,7 @@ use Fleetbase\FleetOps\Events\OrderDispatchFailed;
10
10
  use Fleetbase\FleetOps\Events\OrderStarted;
11
11
  use Fleetbase\FleetOps\Events\WaypointActivityChanged;
12
12
  use Fleetbase\FleetOps\Events\WaypointCompleted;
13
+ use Fleetbase\FleetOps\Exceptions\OrderEditLockedException;
13
14
  use Fleetbase\FleetOps\Exports\OrderExport;
14
15
  use Fleetbase\FleetOps\Flow\Activity;
15
16
  use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
@@ -19,6 +20,7 @@ use Fleetbase\FleetOps\Http\Requests\Internal\CreateOrderRequest;
19
20
  use Fleetbase\FleetOps\Http\Resources\v1\Index\Order as OrderIndexResource;
20
21
  use Fleetbase\FleetOps\Http\Resources\v1\Order as OrderResource;
21
22
  use Fleetbase\FleetOps\Http\Resources\v1\Proof as ProofResource;
23
+ use Fleetbase\FleetOps\Http\Resources\v1\PublicOrderTracking;
22
24
  use Fleetbase\FleetOps\Imports\OrdersImport;
23
25
  use Fleetbase\FleetOps\Jobs\FinalizeInternalOrderCreation;
24
26
  use Fleetbase\FleetOps\Jobs\NotifyBulkAssignedDriver;
@@ -33,6 +35,7 @@ use Fleetbase\FleetOps\Models\ServiceQuote;
33
35
  use Fleetbase\FleetOps\Models\TrackingStatus;
34
36
  use Fleetbase\FleetOps\Models\Waypoint;
35
37
  use Fleetbase\FleetOps\Notifications\OrderPing;
38
+ use Fleetbase\FleetOps\Support\OrderEdit;
36
39
  use Fleetbase\FleetOps\Support\ResolvesOrderServiceStops;
37
40
  use Fleetbase\FleetOps\Support\Utils;
38
41
  use Fleetbase\Http\Requests\ExportRequest;
@@ -54,6 +57,12 @@ use Maatwebsite\Excel\Facades\Excel;
54
57
  class OrderController extends FleetOpsController
55
58
  {
56
59
  use ResolvesOrderServiceStops;
60
+
61
+ /**
62
+ * The one answer the public tracking lookup gives for any number it cannot resolve.
63
+ */
64
+ public const TRACKING_LOOKUP_ERROR = 'No order found using tracking number provided.';
65
+
57
66
  /**
58
67
  * Permissions for methods AuthorizationGuard cannot map to a schema action (see FleetOpsController).
59
68
  *
@@ -313,6 +322,19 @@ class OrderController extends FleetOpsController
313
322
  return $this->errorResponse('Unable to find order to update route for.');
314
323
  }
315
324
 
325
+ // Refuse a route edit the order's status locks (completed, canceled, expired)
326
+ $edit = $this->beginOrderEdit($order);
327
+ try {
328
+ $edit->guard($edit->requestedChanges(array_filter([
329
+ 'pickup' => $hasPickupInput ? true : null,
330
+ 'dropoff' => $hasDropoffInput ? true : null,
331
+ 'return' => $hasReturnInput ? true : null,
332
+ 'waypoints' => ($hasWaypointsInput || $hasPickupInput || $hasDropoffInput || $hasReturnInput) ? true : null,
333
+ ])));
334
+ } catch (OrderEditLockedException $exception) {
335
+ return $this->errorResponse($exception->getMessage(), 422, $exception->toResponseData());
336
+ }
337
+
316
338
  if ($hasPickupInput) {
317
339
  if ($pickup) {
318
340
  $order->payload->setPickup($pickup, ['save' => true]);
@@ -347,14 +369,9 @@ class OrderController extends FleetOpsController
347
369
  $order->payload->removeWaypoints();
348
370
  }
349
371
 
350
- $startingDestination = $order->payload->getPickupOrFirstWaypoint();
351
- if (!$startingDestination) {
352
- $startingDestination = $order->payload->getDropoffOrLastWaypoint();
353
- }
354
-
355
- if ($startingDestination instanceof Place || $startingDestination instanceof Waypoint) {
356
- $order->payload->setCurrentWaypoint($startingDestination);
357
- }
372
+ // Only a changed route moves the current waypoint, and an order under way keeps
373
+ // the driver's stop; distance, time, tracking and `order.changed` follow the change
374
+ $this->commitOrderEdit($edit);
358
375
 
359
376
  $order->load(['payload.pickup', 'payload.dropoff', 'payload.return', 'payload.waypoints']);
360
377
 
@@ -595,7 +612,12 @@ class OrderController extends FleetOpsController
595
612
  return $this->errorResponse('No order found to cancel.');
596
613
  }
597
614
 
598
- $order->cancel();
615
+ $reason = $request->input('reason');
616
+ if (is_string($reason) && trim($reason) !== '') {
617
+ $order->cancelWithReason($reason);
618
+ } else {
619
+ $order->cancel();
620
+ }
599
621
 
600
622
  return $this->jsonResponse(
601
623
  [
@@ -765,10 +787,21 @@ class OrderController extends FleetOpsController
765
787
 
766
788
  protected function assignDriverToOrders($orderUuids, Driver $driver): void
767
789
  {
790
+ // Drivers being replaced are told once the assignment is written
791
+ $replaced = $this->scopedToCompany(Order::whereIn('uuid', $orderUuids))
792
+ ->whereNotNull('driver_assigned_uuid')
793
+ ->where('driver_assigned_uuid', '!=', $driver->uuid)
794
+ ->get();
795
+
768
796
  $this->scopedToCompany(Order::whereIn('uuid', $orderUuids))->update([
769
797
  'driver_assigned_uuid' => $driver->uuid,
770
798
  'updated_at' => now(),
771
799
  ]);
800
+ Order::invalidateApiCacheManually(session('company'));
801
+
802
+ foreach ($replaced as $order) {
803
+ $order->notifyDriverUnassigned($order->driver_assigned_uuid);
804
+ }
772
805
  }
773
806
 
774
807
  protected function dispatchBulkAssignedDriverNotification(array $orderUuids, Driver $driver): void
@@ -786,9 +819,19 @@ class OrderController extends FleetOpsController
786
819
  return response()->json($payload);
787
820
  }
788
821
 
789
- protected function errorResponse(string $message, int $status = 400)
822
+ protected function errorResponse(string $message, int $status = 400, array $data = [])
823
+ {
824
+ return response()->error($message, $status, $data);
825
+ }
826
+
827
+ protected function beginOrderEdit(Order $order): OrderEdit
790
828
  {
791
- return response()->error($message, $status);
829
+ return OrderEdit::begin($order);
830
+ }
831
+
832
+ protected function commitOrderEdit(OrderEdit $edit): array
833
+ {
834
+ return $edit->commit();
792
835
  }
793
836
 
794
837
  /**
@@ -1905,27 +1948,43 @@ class OrderController extends FleetOpsController
1905
1948
  return response()->json(OrderConfig::default());
1906
1949
  }
1907
1950
 
1951
+ /**
1952
+ * Look up an order for the public Track Order page.
1953
+ *
1954
+ * Answers with `PublicOrderTracking`, never the full order resource: the
1955
+ * tracking number is the only credential here. A missing, malformed or
1956
+ * unknown tracking number all get the same error, so a caller cannot tell
1957
+ * which one they hit.
1958
+ */
1908
1959
  public function lookup(Request $request)
1909
1960
  {
1910
- $trackingNumber = $request->input('tracking');
1911
- if (!$trackingNumber) {
1912
- return response()->error('No tracking number provided for lookup.');
1913
- }
1914
-
1915
- $order = $this->findOrderByTrackingNumber($trackingNumber);
1961
+ $trackingNumber = $this->normalizeTrackingLookup($request->input('tracking'));
1962
+ $order = $trackingNumber ? $this->findOrderByTrackingNumber($trackingNumber) : null;
1916
1963
 
1917
1964
  if (!$order) {
1918
- return response()->error('No order found using tracking number provided.');
1965
+ return response()->error(static::TRACKING_LOOKUP_ERROR);
1919
1966
  }
1920
1967
 
1921
- // load required relations
1922
- $order->loadMissing(['trackingNumber', 'payload', 'trackingStatuses']);
1923
-
1924
- // load tracker data
1968
+ $order->loadMissing(['trackingNumber', 'trackingStatuses', 'payload.pickup', 'payload.dropoff', 'payload.waypoints', 'payload.entities']);
1925
1969
  $order->tracker_data = $order->tracker()->toArray();
1926
- $order->eta = $order->tracker()->eta();
1927
1970
 
1928
- return new OrderResource($order);
1971
+ return new PublicOrderTracking($order);
1972
+ }
1973
+
1974
+ /**
1975
+ * The tracking number to look up, or null when the input cannot be one:
1976
+ * not a string, blank, too long, or outside the characters tracking
1977
+ * numbers use.
1978
+ */
1979
+ protected function normalizeTrackingLookup($input): ?string
1980
+ {
1981
+ if (!is_string($input)) {
1982
+ return null;
1983
+ }
1984
+
1985
+ $input = trim($input);
1986
+
1987
+ return preg_match('/^[A-Za-z0-9][A-Za-z0-9._\-]{0,99}$/', $input) === 1 ? $input : null;
1929
1988
  }
1930
1989
 
1931
1990
  /**
@@ -1982,6 +2041,7 @@ class OrderController extends FleetOpsController
1982
2041
  }
1983
2042
 
1984
2043
  $order->saveQuietly();
2044
+ $order->invalidateApiCache();
1985
2045
 
1986
2046
  return response()->json([
1987
2047
  'status' => 'OK',
@@ -6,6 +6,10 @@ use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
6
6
  use Fleetbase\FleetOps\Jobs\ReplayPositions;
7
7
  use Fleetbase\FleetOps\Models\Position;
8
8
  use Fleetbase\FleetOps\Support\Utils;
9
+ use Fleetbase\Models\User;
10
+ use Fleetbase\Support\SocketCluster\ChannelAuthorizer;
11
+ use Fleetbase\Support\SocketCluster\SocketPrincipal;
12
+ use Fleetbase\Support\SocketCluster\SocketToken;
9
13
  use Illuminate\Http\Request;
10
14
 
11
15
  class PositionController extends FleetOpsController
@@ -37,6 +41,11 @@ class PositionController extends FleetOpsController
37
41
  return response()->error('Position IDs are required');
38
42
  }
39
43
 
44
+ // With socket authentication on, the replay may only publish to a channel the user could subscribe to.
45
+ if (SocketToken::enabled() && !$this->canReplayTo($request, $channelId)) {
46
+ return response()->error('You are not allowed to replay positions to this channel.', 403);
47
+ }
48
+
40
49
  $positions = Position::whereIn('uuid', $positionIds)
41
50
  ->where('company_uuid', session('company'))
42
51
  ->orderBy('created_at')
@@ -57,6 +66,19 @@ class PositionController extends FleetOpsController
57
66
  ]);
58
67
  }
59
68
 
69
+ /**
70
+ * Whether the requesting user may subscribe to, and so receive a replay on, the channel.
71
+ */
72
+ protected function canReplayTo(Request $request, mixed $channelId): bool
73
+ {
74
+ $user = $request->user();
75
+ if (!$user instanceof User || !is_string($channelId) || $channelId === '' || strlen($channelId) > 255 || preg_match('/\s/', $channelId)) {
76
+ return false;
77
+ }
78
+
79
+ return app(ChannelAuthorizer::class)->authorize(SocketPrincipal::forUser($user, session('company')), $channelId)->allow;
80
+ }
81
+
60
82
  /**
61
83
  * Get position statistics/metrics.
62
84
  *
@@ -21,6 +21,53 @@ class ProofController extends FleetOpsController
21
21
  */
22
22
  public $resource = 'proof';
23
23
 
24
+ /**
25
+ * The most a dashboard can ask for in one request.
26
+ */
27
+ public const RECENT_LIMIT_MAX = 24;
28
+
29
+ /**
30
+ * The latest photo proofs captured for the company's orders, newest first,
31
+ * for the dashboard's proof-of-delivery tile.
32
+ */
33
+ public function recent(Request $request)
34
+ {
35
+ $limit = max(1, min(static::RECENT_LIMIT_MAX, (int) $request->input('limit', 12)));
36
+ $proofs = Proof::where('company_uuid', session('company'))
37
+ ->whereNotNull('order_uuid')
38
+ ->whereHas('file', fn ($query) => $query->where('type', 'photo'))
39
+ ->with(['file', 'order.customer'])
40
+ ->orderByDesc('created_at')
41
+ ->limit($limit)
42
+ ->get();
43
+
44
+ return $this->jsonResponse([
45
+ 'proofs' => $proofs->map(fn (Proof $proof) => $this->recentProofPayload($proof))->values()->all(),
46
+ ]);
47
+ }
48
+
49
+ protected function recentProofPayload(Proof $proof): array
50
+ {
51
+ $order = $proof->order;
52
+
53
+ return [
54
+ 'id' => $proof->public_id,
55
+ 'order_id' => $order?->public_id,
56
+ 'customer_name' => data_get($order, 'customer.name'),
57
+ 'remarks' => $proof->remarks,
58
+ 'url' => $this->photoUrl($proof->file),
59
+ 'captured_at' => $proof->created_at?->toIso8601String(),
60
+ ];
61
+ }
62
+
63
+ /**
64
+ * File::url signs the object on s3/gcs, so a private bucket still serves the photo.
65
+ */
66
+ protected function photoUrl(File $file): ?string
67
+ {
68
+ return $file->url;
69
+ }
70
+
24
71
  /**
25
72
  * Verify a QR code.
26
73
  *
@@ -83,8 +130,8 @@ class ProofController extends FleetOpsController
83
130
  // set the signature storage path
84
131
  $path = $this->signatureStoragePath($proof);
85
132
 
86
- // upload signature
87
- $this->storeSignature($path, base64_decode($signature), 'public');
133
+ // upload signature (private object; File::url serves it through a signed URL)
134
+ $this->storeSignature($path, base64_decode($signature));
88
135
 
89
136
  // create file record for upload
90
137
  $file = $this->createSignatureFile($path, $signature, $proof);
@@ -121,9 +168,13 @@ class ProofController extends FleetOpsController
121
168
  return Proof::create($attributes);
122
169
  }
123
170
 
124
- protected function storeSignature(string $path, string|false $contents, string $visibility): void
171
+ /**
172
+ * Writes without an ACL: the media bucket enforces bucket-owner object ownership, so a
173
+ * 'public' visibility (public-read ACL) makes S3 reject the PUT and put() return false.
174
+ */
175
+ protected function storeSignature(string $path, string|false $contents): void
125
176
  {
126
- Storage::disk('s3')->put($path, $contents, $visibility);
177
+ Storage::disk('s3')->put($path, $contents);
127
178
  }
128
179
 
129
180
  /**