@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
@@ -5,6 +5,8 @@ namespace Fleetbase\FleetOps\Expansions;
5
5
  use Fleetbase\Build\Expansion;
6
6
  use Fleetbase\FleetOps\Models\Contact;
7
7
  use Fleetbase\FleetOps\Models\Driver;
8
+ use Illuminate\Database\Eloquent\Relations\HasMany;
9
+ use Illuminate\Database\Eloquent\Relations\HasOne;
8
10
 
9
11
  class UserExpansion implements Expansion
10
12
  {
@@ -19,55 +21,55 @@ class UserExpansion implements Expansion
19
21
  }
20
22
 
21
23
  /**
22
- * @return \Illuminate\Database\Eloquent\Relations\HasOne
24
+ * @return HasOne
23
25
  */
24
26
  public static function driver()
25
27
  {
26
- return function () {
28
+ return function (): HasOne {
27
29
  /** @var \Illuminate\Database\Eloquent\Model $this */
28
30
  return $this->hasOne(Driver::class)->without('user');
29
31
  };
30
32
  }
31
33
 
32
34
  /**
33
- * @return \Illuminate\Database\Eloquent\Relations\HasOne
35
+ * @return HasOne
34
36
  */
35
37
  public static function currentDriverSession()
36
38
  {
37
- return function () {
39
+ return function (): HasOne {
38
40
  /** @var \Illuminate\Database\Eloquent\Model $this */
39
41
  return $this->driver()->where('company_uuid', session('company'));
40
42
  };
41
43
  }
42
44
 
43
45
  /**
44
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
46
+ * @return HasMany
45
47
  */
46
48
  public static function driverProfiles()
47
49
  {
48
- return function () {
50
+ return function (): HasMany {
49
51
  /** @var \Illuminate\Database\Eloquent\Model $this */
50
52
  return $this->hasMany(Driver::class);
51
53
  };
52
54
  }
53
55
 
54
56
  /**
55
- * @return \Illuminate\Database\Eloquent\Relations\HasOne
57
+ * @return HasOne
56
58
  */
57
59
  public static function customer()
58
60
  {
59
- return function () {
61
+ return function (): HasOne {
60
62
  /** @var \Illuminate\Database\Eloquent\Model $this */
61
63
  return $this->hasOne(Contact::class, 'user_uuid', 'uuid')->where('type', 'customer')->without('user');
62
64
  };
63
65
  }
64
66
 
65
67
  /**
66
- * @return \Illuminate\Database\Eloquent\Relations\HasOne
68
+ * @return HasOne
67
69
  */
68
70
  public static function contact()
69
71
  {
70
- return function () {
72
+ return function (): HasOne {
71
73
  /** @var \Illuminate\Database\Eloquent\Model $this */
72
74
  return $this->hasOne(Contact::class, 'user_uuid', 'uuid')->without('user');
73
75
  };
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
4
4
 
5
+ use Fleetbase\Exceptions\SmsBlockedException;
5
6
  use Fleetbase\FleetOps\Exceptions\UserAlreadyExistsException;
6
7
  use Fleetbase\FleetOps\Http\Requests\CreateCustomerOrderRequest;
7
8
  use Fleetbase\FleetOps\Http\Requests\CreateCustomerRequest;
@@ -28,6 +29,7 @@ use Illuminate\Http\JsonResponse;
28
29
  use Illuminate\Http\Request;
29
30
  use Illuminate\Support\Facades\Hash;
30
31
  use Illuminate\Support\Str;
32
+ use Symfony\Component\HttpFoundation\Response;
31
33
 
32
34
  /**
33
35
  * Customer-facing Fleet-Ops API.
@@ -46,6 +48,11 @@ class CustomerController extends Controller
46
48
 
47
49
  public const DEACTIVATED_LOGIN_MESSAGE = 'This customer login has been deactivated.';
48
50
 
51
+ /**
52
+ * Wrong guesses a single code tolerates before it locks itself (core's check()).
53
+ */
54
+ public const CODE_CHECK_ATTEMPTS = 5;
55
+
49
56
  /* ============================================================
50
57
  | Public auth flows (API credential only, no Customer-Token)
51
58
  * ============================================================ */
@@ -80,23 +87,21 @@ class CustomerController extends Controller
80
87
  $providedName = trim((string) $request->input('name', ''));
81
88
  $providedPhone = $request->filled('phone') ? static::phone($request->input('phone')) : null;
82
89
 
83
- // The verification code needs a persisted subject so the mail renderer
84
- // can resolve the polymorphic `subject` relation (the verification blade
85
- // template references `$user->name`). Look up — or create — the User
86
- // before sending. `create()` later backfills password + remaining fields
87
- // on this same row when the customer confirms the code.
90
+ // The code's subject is the person signing up: an existing user (a prior
91
+ // incomplete signup, or an account in another organization) as it stands, else
92
+ // an unsaved User carrying the name and contact details the message needs.
93
+ // Nothing is persisted until create() verifies the code — a users row per
94
+ // unverified identity was a way to fill the table from a leaked credential.
88
95
  $subject = $this->findActiveUserByIdentity($identity, $isEmail ? 'email' : 'phone');
89
96
 
90
97
  if (!$subject) {
91
- // `password` and `type` are guarded on User; assign type after create
92
- // via setUserType (saves the row).
93
- $subject = $this->createUser([
98
+ $subject = new User();
99
+ $subject->forceFill([
94
100
  'company_uuid' => $sessionCompany,
95
101
  'name' => $providedName !== '' ? $providedName : $identity,
96
102
  'email' => $isEmail ? $identity : null,
97
103
  'phone' => $isEmail ? $providedPhone : $identity,
98
104
  ]);
99
- $subject->setUserType('customer');
100
105
  } elseif ($providedName !== '' && (!$subject->name || $subject->name === $subject->email)) {
101
106
  // Existing stub user from a prior incomplete signup — refresh the
102
107
  // greeting name if the client supplied one.
@@ -118,10 +123,13 @@ class CustomerController extends Controller
118
123
  ]);
119
124
  } else {
120
125
  $this->generateSmsVerification($subject, 'fleetops_create_customer', [
126
+ 'hashed' => true,
121
127
  'messageCallback' => fn ($verification) => 'Your ' . config('app.name') . ' verification code is ' . $verification->code,
122
128
  'meta' => $meta,
123
129
  ]);
124
130
  }
131
+ } catch (SmsBlockedException $e) {
132
+ return static::smsBlockedResponse($e);
125
133
  } catch (\Twilio\Exceptions\RestException $e) {
126
134
  return response()->apiError($e->getMessage());
127
135
  } catch (\Exception $e) {
@@ -391,13 +399,21 @@ class CustomerController extends Controller
391
399
  return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
392
400
  }
393
401
 
402
+ // A guard refusal (paused, over budget, unusable number) falls back to email like
403
+ // a provider failure does; with no email to fall back to, the refusal itself is the
404
+ // answer, with its status, not the generic error.
405
+ $smsFailure = null;
406
+
394
407
  try {
395
408
  $this->generateSmsVerification($user, 'fleetops_customer_login', [
409
+ 'hashed' => true,
396
410
  'messageCallback' => fn ($verification) => 'Your ' . config('app.name') . ' verification code is ' . $verification->code,
397
411
  ]);
398
412
 
399
413
  return response()->json(['status' => 'ok', 'method' => 'sms']);
400
414
  } catch (\Throwable $e) {
415
+ $smsFailure = $e;
416
+
401
417
  if ($user->email) {
402
418
  try {
403
419
  $this->generateEmailVerification($user, 'fleetops_customer_login', [
@@ -407,12 +423,12 @@ class CustomerController extends Controller
407
423
 
408
424
  return response()->json(['status' => 'ok', 'method' => 'email']);
409
425
  } catch (\Throwable $inner) {
410
- return response()->apiError('Unable to send verification code.');
426
+ return static::sendFailureResponse($smsFailure, 'Unable to send verification code.');
411
427
  }
412
428
  }
413
429
  }
414
430
 
415
- return response()->apiError('Unable to send verification code.');
431
+ return static::sendFailureResponse($smsFailure, 'Unable to send verification code.');
416
432
  }
417
433
 
418
434
  /**
@@ -494,10 +510,13 @@ class CustomerController extends Controller
494
510
  ]);
495
511
  } else {
496
512
  $this->generateSmsVerification($user, 'fleetops_customer_password_reset', [
513
+ 'hashed' => true,
497
514
  'messageCallback' => fn ($v) => 'Your ' . config('app.name') . ' password reset code is ' . $v->code,
498
515
  'meta' => $meta,
499
516
  ]);
500
517
  }
518
+ } catch (SmsBlockedException $e) {
519
+ return static::smsBlockedResponse($e);
501
520
  } catch (\Throwable $e) {
502
521
  return response()->apiError(app()->hasDebugModeEnabled() ? $e->getMessage() : 'Unable to send reset code.');
503
522
  }
@@ -542,12 +561,9 @@ class CustomerController extends Controller
542
561
  // setPasswordAttribute mutator hashes plaintext; don't pre-hash here.
543
562
  $user->password = $password;
544
563
  $user->save();
545
- // Invalidate all existing sessions for this user after a password reset.
564
+ // Invalidate all existing sessions for this user after a password reset. The
565
+ // code itself was used up when it verified (findVerificationCode).
546
566
  $this->deleteUserTokens($user);
547
- // Null on the testing-bypass path — there is no row to consume.
548
- if ($verificationCode) {
549
- $verificationCode->delete();
550
- }
551
567
 
552
568
  return response()->json(['status' => 'ok']);
553
569
  }
@@ -948,6 +964,36 @@ class CustomerController extends Controller
948
964
  return VerificationCode::generateSmsVerificationFor($user, $for, $options);
949
965
  }
950
966
 
967
+ /**
968
+ * Answer a refusal from the SMS guard with the status it carries: 422 for a number that
969
+ * can never be texted, 429 with Retry-After for a cooldown or budget, 423 while texting
970
+ * is paused. The sends here sit inside catch-alls so a provider outage becomes an email
971
+ * fallback or a generic error; a refusal must not be flattened into that generic error,
972
+ * or an app can neither explain it to the person nor back off.
973
+ */
974
+ protected static function smsBlockedResponse(SmsBlockedException $e)
975
+ {
976
+ $response = response()->apiError($e->getMessage(), $e->status());
977
+ if ($response instanceof Response) {
978
+ $response->headers->add($e->headers());
979
+ }
980
+
981
+ return $response;
982
+ }
983
+
984
+ /**
985
+ * The response when a code could not be sent: the guard's refusal when that is why,
986
+ * otherwise the flow's own generic message.
987
+ */
988
+ protected static function sendFailureResponse(?\Throwable $failure, string $message)
989
+ {
990
+ if ($failure instanceof SmsBlockedException) {
991
+ return static::smsBlockedResponse($failure);
992
+ }
993
+
994
+ return response()->apiError($message);
995
+ }
996
+
951
997
  /**
952
998
  * Whether the supplied code matches the configured testing bypass code.
953
999
  *
@@ -978,14 +1024,58 @@ class CustomerController extends Controller
978
1024
  );
979
1025
  }
980
1026
 
1027
+ /**
1028
+ * Whether the submitted code verifies against a live code for the purpose and
1029
+ * subject or identity in `$attributes`, using the code up when it does.
1030
+ */
981
1031
  protected function verificationCodeExists(array $attributes): bool
982
1032
  {
983
- return VerificationCode::where($attributes)->exists();
1033
+ return $this->findVerificationCode($attributes) !== null;
984
1034
  }
985
1035
 
1036
+ /**
1037
+ * The live code the submitted code verified against, used up so it cannot verify
1038
+ * again, or null. `$attributes` carries `code`, `for` and either `subject_uuid` or
1039
+ * `meta->identity`. Live codes are checked newest first; a wrong guess counts against
1040
+ * each until it locks (core's check()). Plaintext rows written before codes were hashed
1041
+ * verify the same way.
1042
+ */
986
1043
  protected function findVerificationCode(array $attributes): mixed
987
1044
  {
988
- return VerificationCode::where($attributes)->first();
1045
+ $code = (string) ($attributes['code'] ?? '');
1046
+
1047
+ foreach ($this->liveVerificationCodes($attributes) as $verificationCode) {
1048
+ if ($verificationCode->check($code, static::CODE_CHECK_ATTEMPTS) === VerificationCode::CHECK_VALID) {
1049
+ $verificationCode->consume();
1050
+
1051
+ return $verificationCode;
1052
+ }
1053
+ }
1054
+
1055
+ return null;
1056
+ }
1057
+
1058
+ /**
1059
+ * Unexpired, unused, unlocked codes for the purpose and subject or identity in
1060
+ * `$attributes`, newest first.
1061
+ *
1062
+ * @return \Illuminate\Support\Collection<int, VerificationCode>
1063
+ */
1064
+ protected function liveVerificationCodes(array $attributes)
1065
+ {
1066
+ $query = VerificationCode::query()
1067
+ ->where('for', (string) ($attributes['for'] ?? ''))
1068
+ ->where(function ($query) {
1069
+ $query->whereNull('status')->orWhereNotIn('status', [VerificationCode::STATUS_USED, VerificationCode::STATUS_LOCKED]);
1070
+ });
1071
+
1072
+ foreach (['subject_uuid', 'meta->identity'] as $column) {
1073
+ if (array_key_exists($column, $attributes)) {
1074
+ $query->where($column, $attributes[$column]);
1075
+ }
1076
+ }
1077
+
1078
+ return $query->orderByDesc('created_at')->get();
989
1079
  }
990
1080
 
991
1081
  protected function findFileByPublicId(string $publicId): mixed
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
4
4
 
5
+ use Fleetbase\Exceptions\SmsBlockedException;
5
6
  use Fleetbase\FleetOps\Events\DriverLocationChanged;
6
7
  use Fleetbase\FleetOps\Events\GeofenceEntered;
7
8
  use Fleetbase\FleetOps\Events\GeofenceExited;
@@ -21,9 +22,11 @@ use Fleetbase\FleetOps\Models\Driver;
21
22
  use Fleetbase\FleetOps\Models\Order;
22
23
  use Fleetbase\FleetOps\Models\Vehicle;
23
24
  use Fleetbase\FleetOps\Models\Vendor;
25
+ use Fleetbase\FleetOps\Support\ApiCacheThrottle;
24
26
  use Fleetbase\FleetOps\Support\GeofenceIntersectionService;
25
27
  use Fleetbase\FleetOps\Support\OSRM;
26
28
  use Fleetbase\FleetOps\Support\ProfileAccountManager;
29
+ use Fleetbase\FleetOps\Support\TrackingPublisher;
27
30
  use Fleetbase\FleetOps\Support\Utils;
28
31
  use Fleetbase\Http\Controllers\Controller;
29
32
  use Fleetbase\Http\Requests\SwitchOrganizationRequest;
@@ -42,6 +45,7 @@ use Illuminate\Support\Carbon;
42
45
  use Illuminate\Support\Facades\DB;
43
46
  use Illuminate\Support\Facades\Hash;
44
47
  use Illuminate\Support\Str;
48
+ use Symfony\Component\HttpFoundation\Response;
45
49
 
46
50
  class DriverController extends Controller
47
51
  {
@@ -54,6 +58,11 @@ class DriverController extends Controller
54
58
  */
55
59
  public const DEACTIVATED_LOGIN_MESSAGE = 'This driver login has been deactivated.';
56
60
 
61
+ /**
62
+ * Wrong guesses a single code tolerates before it locks itself (core's check()).
63
+ */
64
+ public const CODE_CHECK_ATTEMPTS = 5;
65
+
57
66
  /**
58
67
  * Public expansion name => Eloquent relation name.
59
68
  *
@@ -373,7 +382,14 @@ class DriverController extends Controller
373
382
  }
374
383
  }
375
384
 
385
+ // Location writes are quiet and frequent: invalidate cached lists at most once a window
386
+ ApiCacheThrottle::invalidate($driver, $driver->company_uuid);
387
+ if ($vehicle) {
388
+ ApiCacheThrottle::invalidate($vehicle, $vehicle->company_uuid);
389
+ }
390
+
376
391
  broadcast(new DriverLocationChanged($driver));
392
+ app(TrackingPublisher::class)->driverMoved($driver);
377
393
 
378
394
  // ----------------------------------------------------------------
379
395
  // Geofence intersection detection
@@ -431,11 +447,13 @@ class DriverController extends Controller
431
447
 
432
448
  // Perform a single update call
433
449
  $driver->updateQuietly(['online' => $onlineValue]);
450
+ $driver->invalidateApiCache();
434
451
 
435
452
  // Update vehicle online too
436
453
  $driver->loadMissing('vehicle');
437
454
  if ($vehicle = $driver->vehicle) {
438
455
  $vehicle->updateQuietly(['online' => $onlineValue]);
456
+ $vehicle->invalidateApiCache();
439
457
  }
440
458
 
441
459
  // Return the updated resource
@@ -574,9 +592,15 @@ class DriverController extends Controller
574
592
  // Get the user's company for this driver profile
575
593
  $company = static::getDriverCompanyFromUser($user);
576
594
 
595
+ // A guard refusal (paused, over budget, unusable number) falls back to email like
596
+ // a provider failure does; with no email to fall back to, the refusal itself is the
597
+ // answer, with its status, not the generic error.
598
+ $smsFailure = null;
599
+
577
600
  // generate verification token
578
601
  try {
579
602
  VerificationCode::generateSmsVerificationFor($user, 'driver_login', [
603
+ 'hashed' => true,
580
604
  'company_uuid' => $company->uuid,
581
605
  'messageCallback' => function ($verification) use ($company) {
582
606
  return 'Your ' . data_get($company, 'name', config('app.name')) . ' verification code is ' . $verification->code;
@@ -585,6 +609,8 @@ class DriverController extends Controller
585
609
 
586
610
  return response()->json(['status' => 'OK', 'method' => 'sms']);
587
611
  } catch (\Throwable $e) {
612
+ $smsFailure = $e;
613
+
588
614
  if (app()->bound('sentry')) {
589
615
  app('sentry')->captureException($e);
590
616
  }
@@ -608,7 +634,7 @@ class DriverController extends Controller
608
634
  }
609
635
  }
610
636
 
611
- return response()->apiError('Unable to send SMS Verification code.');
637
+ return static::sendFailureResponse($smsFailure, 'Unable to send SMS Verification code.');
612
638
  }
613
639
 
614
640
  /**
@@ -641,8 +667,8 @@ class DriverController extends Controller
641
667
  return response()->apiError(static::DEACTIVATED_LOGIN_MESSAGE, 403);
642
668
  }
643
669
 
644
- // find and verify code
645
- $verificationCode = VerificationCode::where(['subject_uuid' => $user->uuid, 'code' => $code, 'for' => $for])->exists();
670
+ // find and verify code: the user's newest live code for this purpose, used up
671
+ $verificationCode = static::spendCode($user, $for, (string) $code);
646
672
  if (!$verificationCode && !static::verificationBypassMatches($identity, $code)) {
647
673
  return response()->apiError('Invalid verification code!');
648
674
  }
@@ -1336,6 +1362,8 @@ class DriverController extends Controller
1336
1362
 
1337
1363
  try {
1338
1364
  static::sendResetCode($user, $identity);
1365
+ } catch (SmsBlockedException $e) {
1366
+ return static::smsBlockedResponse($e);
1339
1367
  } catch (\Throwable $e) {
1340
1368
  return response()->apiError(static::debugEnabled() ? $e->getMessage() : 'Unable to send reset code.');
1341
1369
  }
@@ -1376,7 +1404,7 @@ class DriverController extends Controller
1376
1404
  $user->password = $password;
1377
1405
  $user->save();
1378
1406
 
1379
- $verification->delete();
1407
+ // The code was used up when it verified (findResetCode); every session ends.
1380
1408
  $user->tokens()->delete();
1381
1409
 
1382
1410
  return response()->json(['status' => 'ok']);
@@ -1411,14 +1439,40 @@ class DriverController extends Controller
1411
1439
  return Hash::check($plain, $user->password);
1412
1440
  }
1413
1441
 
1414
- /** The unexpired reset code for this user, if the one supplied matches. */
1442
+ /**
1443
+ * The live reset code the supplied one verified against, used up so it cannot
1444
+ * verify again, or null.
1445
+ */
1415
1446
  protected static function findResetCode(User $user, string $code)
1416
1447
  {
1417
- return VerificationCode::where([
1418
- 'subject_uuid' => $user->uuid,
1419
- 'code' => $code,
1420
- 'for' => 'driver_password_reset',
1421
- ])->where('expires_at', '>', now())->first();
1448
+ return static::spendCode($user, 'driver_password_reset', $code);
1449
+ }
1450
+
1451
+ /**
1452
+ * Verify a code against the user's live codes for a purpose and use it up. Codes are
1453
+ * checked newest first; a wrong guess counts against each until it locks (core's
1454
+ * check()). Plaintext rows written before codes were hashed verify the same way.
1455
+ */
1456
+ protected static function spendCode(User $user, string $for, string $code): ?VerificationCode
1457
+ {
1458
+ $live = VerificationCode::query()
1459
+ ->where('for', $for)
1460
+ ->where('subject_uuid', $user->uuid)
1461
+ ->where(function ($query) {
1462
+ $query->whereNull('status')->orWhereNotIn('status', [VerificationCode::STATUS_USED, VerificationCode::STATUS_LOCKED]);
1463
+ })
1464
+ ->orderByDesc('created_at')
1465
+ ->get();
1466
+
1467
+ foreach ($live as $verificationCode) {
1468
+ if ($verificationCode->check($code, static::CODE_CHECK_ATTEMPTS) === VerificationCode::CHECK_VALID) {
1469
+ $verificationCode->consume();
1470
+
1471
+ return $verificationCode;
1472
+ }
1473
+ }
1474
+
1475
+ return null;
1422
1476
  }
1423
1477
 
1424
1478
  /** Sends the reset code by whichever channel the identity names. */
@@ -1433,11 +1487,42 @@ class DriverController extends Controller
1433
1487
  ]);
1434
1488
  } else {
1435
1489
  VerificationCode::generateSmsVerificationFor($user, 'driver_password_reset', [
1490
+ 'hashed' => true,
1436
1491
  'messageCallback' => fn ($v) => 'Your ' . config('app.name') . ' password reset code is ' . $v->code,
1437
1492
  ]);
1438
1493
  }
1439
1494
  }
1440
1495
 
1496
+ /**
1497
+ * Answer a refusal from the SMS guard with the status it carries: 422 for a number that
1498
+ * can never be texted, 429 with Retry-After for a cooldown or budget, 423 while texting
1499
+ * is paused. The sends here sit inside catch-alls so a provider outage becomes an email
1500
+ * fallback or a generic error; a refusal must not be flattened into that generic error,
1501
+ * or an app can neither explain it to the person nor back off.
1502
+ */
1503
+ protected static function smsBlockedResponse(SmsBlockedException $e)
1504
+ {
1505
+ $response = response()->apiError($e->getMessage(), $e->status());
1506
+ if ($response instanceof Response) {
1507
+ $response->headers->add($e->headers());
1508
+ }
1509
+
1510
+ return $response;
1511
+ }
1512
+
1513
+ /**
1514
+ * The response when a code could not be sent: the guard's refusal when that is why,
1515
+ * otherwise the flow's own generic message.
1516
+ */
1517
+ protected static function sendFailureResponse(?\Throwable $failure, string $message)
1518
+ {
1519
+ if ($failure instanceof SmsBlockedException) {
1520
+ return static::smsBlockedResponse($failure);
1521
+ }
1522
+
1523
+ return response()->apiError($message);
1524
+ }
1525
+
1441
1526
  /** Resolve a driver's user account from an email address or a phone number. */
1442
1527
  protected static function findDriverUserByIdentity(string $identity): ?User
1443
1528
  {
@@ -2,13 +2,17 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
4
4
 
5
+ use Fleetbase\FleetOps\Exceptions\OrderEditLockedException;
5
6
  use Fleetbase\FleetOps\Http\Requests\CreateEntityRequest;
6
7
  use Fleetbase\FleetOps\Http\Requests\UpdateEntityRequest;
7
8
  use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource;
8
9
  use Fleetbase\FleetOps\Http\Resources\v1\Entity as EntityResource;
9
10
  use Fleetbase\FleetOps\Models\Entity;
11
+ use Fleetbase\FleetOps\Models\Order;
10
12
  use Fleetbase\FleetOps\Models\Payload;
11
13
  use Fleetbase\FleetOps\Models\Place;
14
+ use Fleetbase\FleetOps\Support\OrderEdit;
15
+ use Fleetbase\FleetOps\Support\OrderEditPolicy;
12
16
  use Fleetbase\FleetOps\Support\Utils;
13
17
  use Fleetbase\Http\Controllers\Controller;
14
18
  use Illuminate\Http\Request;
@@ -79,8 +83,15 @@ class EntityController extends Controller
79
83
  // make sure company is set
80
84
  $input['company_uuid'] = session('company');
81
85
 
86
+ // adding an item to an order is refused once the order's status locks its items
87
+ $edit = $this->orderEditForPayload($input['payload_uuid'] ?? null);
88
+ if ($locked = $this->guardItemsEdit($edit, true)) {
89
+ return $locked;
90
+ }
91
+
82
92
  // create the entity
83
93
  $entity = $this->createEntity($input);
94
+ $this->commitItemsEdit($edit);
84
95
 
85
96
  // response the driver resource
86
97
  return $this->entityResource($entity);
@@ -159,8 +170,15 @@ class EntityController extends Controller
159
170
  }
160
171
  }
161
172
 
173
+ // changing an order's item is refused once the order's status locks its items
174
+ $edit = $this->orderEditForPayload($entity->payload_uuid);
175
+ if ($locked = $this->guardItemsEdit($edit, true)) {
176
+ return $locked;
177
+ }
178
+
162
179
  // update the entity
163
180
  $entity->update($input);
181
+ $this->commitItemsEdit($edit);
164
182
 
165
183
  // response the entity resource
166
184
  return $this->entityResource($entity);
@@ -220,13 +238,61 @@ class EntityController extends Controller
220
238
  );
221
239
  }
222
240
 
241
+ // removing an order's item is refused once the order's status locks its items
242
+ $edit = $this->orderEditForPayload($entity->payload_uuid);
243
+ if ($locked = $this->guardItemsEdit($edit, false)) {
244
+ return $locked;
245
+ }
246
+
223
247
  // delete the entity
224
248
  $entity->delete();
249
+ $this->commitItemsEdit($edit);
225
250
 
226
251
  // response the entity resource
227
252
  return $this->deletedEntityResource($entity);
228
253
  }
229
254
 
255
+ /**
256
+ * Begin an edit of the order the payload belongs to, if it belongs to one.
257
+ */
258
+ protected function orderEditForPayload(?string $payloadUuid): ?OrderEdit
259
+ {
260
+ if (!$payloadUuid) {
261
+ return null;
262
+ }
263
+
264
+ try {
265
+ $order = Order::where('payload_uuid', $payloadUuid)->first();
266
+ } catch (\Throwable) {
267
+ return null;
268
+ }
269
+
270
+ return $order ? OrderEdit::begin($order) : null;
271
+ }
272
+
273
+ /**
274
+ * A 422 response when the order's status locks the item change, otherwise null.
275
+ */
276
+ protected function guardItemsEdit(?OrderEdit $edit, bool $adds)
277
+ {
278
+ if (!$edit) {
279
+ return null;
280
+ }
281
+
282
+ try {
283
+ $edit->guard([OrderEditPolicy::FIELD_ITEMS => ['adds' => $adds]]);
284
+ } catch (OrderEditLockedException $exception) {
285
+ return response()->apiError($exception->getMessage(), 422, $exception->toResponseData());
286
+ }
287
+
288
+ return null;
289
+ }
290
+
291
+ protected function commitItemsEdit(?OrderEdit $edit): void
292
+ {
293
+ $edit?->commit();
294
+ }
295
+
230
296
  protected function entityInputFromRequest(Request $request): array
231
297
  {
232
298
  return $request->only([
@@ -434,10 +434,13 @@ class FleetController extends Controller
434
434
 
435
435
  protected function removeVehicleFromFleet(Fleet $fleet, Vehicle $vehicle): void
436
436
  {
437
- FleetVehicle::where([
437
+ // Deleted without model events; fleet_vehicles rows carry no company
438
+ if (FleetVehicle::where([
438
439
  'fleet_uuid' => $fleet->uuid,
439
440
  'vehicle_uuid' => $vehicle->uuid,
440
- ])->delete();
441
+ ])->delete()) {
442
+ FleetVehicle::invalidateApiCacheManually();
443
+ }
441
444
  }
442
445
 
443
446
  protected function assignDriverToFleet(Fleet $fleet, Driver $driver): void
@@ -450,10 +453,13 @@ class FleetController extends Controller
450
453
 
451
454
  protected function removeDriverFromFleet(Fleet $fleet, Driver $driver): void
452
455
  {
453
- FleetDriver::where([
456
+ // Deleted without model events; fleet_drivers rows carry no company
457
+ if (FleetDriver::where([
454
458
  'fleet_uuid' => $fleet->uuid,
455
459
  'driver_uuid' => $driver->uuid,
456
- ])->delete();
460
+ ])->delete()) {
461
+ FleetDriver::invalidateApiCacheManually();
462
+ }
457
463
  }
458
464
 
459
465
  protected function vehicleMembershipPayload(Fleet $fleet, Vehicle $vehicle, bool $assigned): array