@fleetbase/fleetops-engine 0.6.70 → 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 (510) hide show
  1. package/RELEASE.md +18 -47
  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/details/activity.hbs +8 -6
  16. package/addon/components/order/details/activity.js +9 -0
  17. package/addon/components/order/details.hbs +29 -0
  18. package/addon/components/order/details.js +26 -0
  19. package/addon/components/order/form/details.hbs +30 -4
  20. package/addon/components/order/form/details.js +46 -0
  21. package/addon/components/order/form/route.hbs +5 -3
  22. package/addon/components/order/form/route.js +24 -1
  23. package/addon/components/order/form.hbs +12 -0
  24. package/addon/components/order/form.js +21 -1
  25. package/addon/components/order/kanban.js +72 -0
  26. package/addon/components/order-config-manager/activity-flow.js +45 -10
  27. package/addon/components/order-config-manager/lifecycle.hbs +250 -0
  28. package/addon/components/order-config-manager/lifecycle.js +358 -0
  29. package/addon/components/order-config-manager.js +5 -1
  30. package/addon/components/service-rate/details.hbs +1 -1
  31. package/addon/components/service-rate/form.hbs +1 -1
  32. package/addon/components/tracking-page/full-view.hbs +276 -0
  33. package/addon/components/tracking-page/full-view.js +474 -0
  34. package/addon/components/tracking-page/locale-selector.js +30 -0
  35. package/addon/components/tracking-page/map.hbs +44 -0
  36. package/addon/components/tracking-page/map.js +328 -0
  37. package/addon/components/tracking-page/order-list.hbs +35 -0
  38. package/addon/components/tracking-page/public-view.hbs +60 -0
  39. package/addon/components/tracking-page/settings-preview.hbs +21 -0
  40. package/addon/components/tracking-page/settings-preview.js +23 -0
  41. package/addon/components/tracking-page/verify-panel.hbs +109 -0
  42. package/addon/components/tracking-page.hbs +142 -0
  43. package/addon/components/tracking-page.js +564 -0
  44. package/addon/components/trailer/form.hbs +2 -2
  45. package/addon/components/vehicle/form.hbs +3 -3
  46. package/addon/components/warranty/form.hbs +2 -2
  47. package/addon/components/widget/live-fleet.js +54 -6
  48. package/addon/components/widget/proof-of-delivery.hbs +49 -0
  49. package/addon/components/widget/proof-of-delivery.js +62 -0
  50. package/addon/components/work-order/form.hbs +3 -3
  51. package/addon/controllers/analytics/reports/index.js +10 -1
  52. package/addon/controllers/connectivity/devices/index/details.js +1 -0
  53. package/addon/controllers/connectivity/devices/index.js +8 -2
  54. package/addon/controllers/connectivity/events/index.js +31 -1
  55. package/addon/controllers/connectivity/sensors/index/details.js +1 -1
  56. package/addon/controllers/connectivity/sensors/index.js +37 -1
  57. package/addon/controllers/maintenance/equipment/index/details.js +2 -2
  58. package/addon/controllers/maintenance/equipment/index.js +28 -9
  59. package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
  60. package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
  61. package/addon/controllers/maintenance/inspection-submissions/index.js +49 -9
  62. package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
  63. package/addon/controllers/maintenance/maintenances/index.js +20 -1
  64. package/addon/controllers/maintenance/parts/index/details.js +2 -2
  65. package/addon/controllers/maintenance/parts/index.js +28 -8
  66. package/addon/controllers/maintenance/schedules/index.js +33 -4
  67. package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
  68. package/addon/controllers/maintenance/work-orders/index.js +39 -8
  69. package/addon/controllers/management/contacts/customers.js +13 -0
  70. package/addon/controllers/management/contacts/index.js +43 -1
  71. package/addon/controllers/management/drivers/index/details.js +1 -0
  72. package/addon/controllers/management/drivers/index.js +32 -2
  73. package/addon/controllers/management/fleets/index/details.js +1 -0
  74. package/addon/controllers/management/fleets/index.js +39 -1
  75. package/addon/controllers/management/fuel-reports/index.js +22 -2
  76. package/addon/controllers/management/fuel-transactions/index.js +43 -7
  77. package/addon/controllers/management/issues/index/details.js +1 -0
  78. package/addon/controllers/management/issues/index.js +22 -2
  79. package/addon/controllers/management/places/index.js +36 -1
  80. package/addon/controllers/management/trailers/index/details.js +1 -0
  81. package/addon/controllers/management/trailers/index.js +50 -2
  82. package/addon/controllers/management/vehicles/index/details.js +1 -0
  83. package/addon/controllers/management/vehicles/index.js +33 -2
  84. package/addon/controllers/management/vendors/index.js +26 -2
  85. package/addon/controllers/operations/orders/index/details.js +50 -7
  86. package/addon/controllers/operations/orders/index.js +59 -4
  87. package/addon/controllers/operations/scheduler/index.js +45 -14
  88. package/addon/controllers/operations/service-rates/index.js +15 -1
  89. package/addon/controllers/settings/index.js +6 -0
  90. package/addon/controllers/settings/map.js +75 -0
  91. package/addon/controllers/settings/orchestrator.js +8 -6
  92. package/addon/controllers/settings/tracking-page.js +212 -0
  93. package/addon/extension.js +97 -1
  94. package/addon/helpers/parcel-box-image.js +16 -0
  95. package/addon/routes/operations/orders/index/details.js +1 -0
  96. package/addon/routes/operations/orders/index/new.js +5 -4
  97. package/addon/routes/operations/scheduler/index.js +1 -1
  98. package/addon/routes/settings/tracking-page.js +31 -0
  99. package/addon/routes.js +1 -0
  100. package/addon/services/contact-actions.js +1 -0
  101. package/addon/services/customer-actions.js +2 -1
  102. package/addon/services/device-actions.js +1 -0
  103. package/addon/services/device-event-actions.js +1 -0
  104. package/addon/services/driver-actions.js +4 -13
  105. package/addon/services/equipment-actions.js +1 -0
  106. package/addon/services/fleet-actions.js +1 -0
  107. package/addon/services/fuel-report-actions.js +1 -0
  108. package/addon/services/inspection-form-actions.js +1 -0
  109. package/addon/services/inspection-submission-actions.js +1 -0
  110. package/addon/services/integrated-vendor-actions.js +1 -0
  111. package/addon/services/issue-actions.js +1 -0
  112. package/addon/services/location.js +69 -21
  113. package/addon/services/maintenance-actions.js +1 -0
  114. package/addon/services/maintenance-schedule-actions.js +2 -1
  115. package/addon/services/map-settings.js +28 -0
  116. package/addon/services/order-actions.js +7 -0
  117. package/addon/services/order-allocation.js +1 -1
  118. package/addon/services/order-presentation.js +129 -0
  119. package/addon/services/part-actions.js +1 -0
  120. package/addon/services/place-actions.js +1 -0
  121. package/addon/services/sensor-actions.js +1 -0
  122. package/addon/services/service-area-actions.js +1 -0
  123. package/addon/services/service-rate-actions.js +1 -0
  124. package/addon/services/telematic-actions.js +1 -0
  125. package/addon/services/trailer-actions.js +4 -14
  126. package/addon/services/vehicle-actions.js +4 -13
  127. package/addon/services/vendor-actions.js +1 -0
  128. package/addon/services/work-order-actions.js +1 -0
  129. package/addon/services/zone-actions.js +1 -0
  130. package/addon/styles/fleetops-engine.css +1391 -0
  131. package/addon/templates/analytics/reports/index/details.hbs +1 -0
  132. package/addon/templates/analytics/reports/index.hbs +1 -0
  133. package/addon/templates/connectivity/devices/index/details.hbs +1 -0
  134. package/addon/templates/connectivity/devices/index.hbs +1 -0
  135. package/addon/templates/connectivity/events/index.hbs +1 -0
  136. package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
  137. package/addon/templates/connectivity/sensors/index.hbs +1 -0
  138. package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
  139. package/addon/templates/maintenance/equipment/index.hbs +2 -0
  140. package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
  141. package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
  142. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
  143. package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
  144. package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
  145. package/addon/templates/maintenance/maintenances/index.hbs +1 -0
  146. package/addon/templates/maintenance/parts/index/details.hbs +1 -0
  147. package/addon/templates/maintenance/parts/index.hbs +2 -0
  148. package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
  149. package/addon/templates/maintenance/schedules/index.hbs +2 -0
  150. package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
  151. package/addon/templates/maintenance/work-orders/index.hbs +1 -0
  152. package/addon/templates/management/contacts/customers/details.hbs +1 -0
  153. package/addon/templates/management/contacts/customers.hbs +1 -0
  154. package/addon/templates/management/contacts/index/details.hbs +1 -0
  155. package/addon/templates/management/contacts/index.hbs +1 -0
  156. package/addon/templates/management/contacts.hbs +1 -0
  157. package/addon/templates/management/drivers/index/details.hbs +1 -0
  158. package/addon/templates/management/drivers/index.hbs +2 -0
  159. package/addon/templates/management/fleets/index/details.hbs +1 -0
  160. package/addon/templates/management/fleets/index.hbs +1 -0
  161. package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
  162. package/addon/templates/management/fuel-reports/index.hbs +1 -0
  163. package/addon/templates/management/fuel-transactions/index.hbs +1 -0
  164. package/addon/templates/management/issues/index/details.hbs +1 -0
  165. package/addon/templates/management/issues/index.hbs +1 -0
  166. package/addon/templates/management/places/index/details.hbs +1 -0
  167. package/addon/templates/management/places/index.hbs +1 -0
  168. package/addon/templates/management/trailers/index/details.hbs +1 -0
  169. package/addon/templates/management/trailers/index.hbs +2 -0
  170. package/addon/templates/management/vehicles/index/details.hbs +1 -0
  171. package/addon/templates/management/vehicles/index.hbs +2 -0
  172. package/addon/templates/management/vendors/index/details.hbs +1 -0
  173. package/addon/templates/management/vendors/index.hbs +2 -0
  174. package/addon/templates/management/vendors/integrated.hbs +1 -0
  175. package/addon/templates/operations/orders/index/details.hbs +1 -0
  176. package/addon/templates/operations/orders/index.hbs +1 -0
  177. package/addon/templates/operations/service-rates/index/details.hbs +1 -0
  178. package/addon/templates/operations/service-rates/index.hbs +1 -0
  179. package/addon/templates/settings/map.hbs +46 -0
  180. package/addon/templates/settings/tracking-page.hbs +318 -0
  181. package/addon/utils/identity-cell-resource.js +2 -56
  182. package/addon/utils/map-default-view.js +132 -0
  183. package/addon/utils/normalize-order-config-flow.js +6 -1
  184. package/addon/utils/placeholder-images.js +4 -69
  185. package/addon/utils/resource-descriptors/assets.js +2 -57
  186. package/addon/utils/resource-descriptors/helpers.js +23 -122
  187. package/addon/utils/resource-descriptors/operations.js +2 -61
  188. package/addon/utils/resource-descriptors/people.js +3 -142
  189. package/addon/utils/resource-descriptors/shared.js +18 -0
  190. package/addon/utils/tracking-live-source.js +123 -0
  191. package/addon/utils/tracking-page-theme.js +123 -0
  192. package/app/components/admin/tracking-page-settings.js +1 -0
  193. package/app/components/tracking-page/full-view.js +1 -0
  194. package/app/components/tracking-page/locale-selector.js +1 -0
  195. package/app/components/tracking-page/map.js +1 -0
  196. package/app/components/tracking-page/order-list.js +1 -0
  197. package/app/components/tracking-page/public-view.js +1 -0
  198. package/app/components/tracking-page/settings-preview.js +1 -0
  199. package/app/components/tracking-page/verify-panel.js +1 -0
  200. package/app/components/tracking-page.js +1 -0
  201. package/app/components/widget/proof-of-delivery.js +1 -0
  202. package/app/controllers/settings/tracking-page.js +1 -0
  203. package/app/helpers/parcel-box-image.js +1 -0
  204. package/app/routes/settings/tracking-page.js +1 -0
  205. package/app/services/order-presentation.js +1 -0
  206. package/app/templates/settings/tracking-page.js +1 -0
  207. package/app/utils/map-default-view.js +1 -0
  208. package/app/utils/tracking-live-source.js +1 -0
  209. package/app/utils/tracking-page-theme.js +1 -0
  210. package/composer.json +2 -2
  211. package/extension.json +1 -1
  212. package/package.json +4 -4
  213. package/scripts/pest-bootstrap.php +4 -0
  214. package/server/config/fleetops.php +4 -0
  215. package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
  216. package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
  217. package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
  218. package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
  219. package/server/resources/views/labels/default.php +2 -2
  220. package/server/resources/views/labels/entity-label.php +2 -2
  221. package/server/resources/views/labels/waypoint-label.php +2 -2
  222. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  223. package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
  224. package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
  225. package/server/src/Events/OrderChanged.php +257 -0
  226. package/server/src/Events/OrderDriverUnassigned.php +68 -0
  227. package/server/src/Exceptions/OrderEditLockedException.php +32 -0
  228. package/server/src/Expansions/CompanyExpansion.php +3 -2
  229. package/server/src/Expansions/UserExpansion.php +12 -10
  230. package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
  231. package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
  232. package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
  233. package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
  234. package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
  235. package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
  236. package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
  237. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
  238. package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
  239. package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
  240. package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
  241. package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
  242. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
  243. package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
  244. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
  245. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
  246. package/server/src/Http/Controllers/Internal/v1/OrderController.php +118 -28
  247. package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
  248. package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
  249. package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
  250. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
  251. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
  252. package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
  253. package/server/src/Http/Filter/ContactFilter.php +1 -1
  254. package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
  255. package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
  256. package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
  257. package/server/src/Http/Filter/OrderFilter.php +39 -4
  258. package/server/src/Http/Filter/ProofFilter.php +18 -0
  259. package/server/src/Http/Filter/RouteFilter.php +18 -0
  260. package/server/src/Http/Filter/TelematicFilter.php +23 -0
  261. package/server/src/Http/Filter/WarrantyFilter.php +23 -0
  262. package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
  263. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
  264. package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
  265. package/server/src/Http/Resources/v1/Index/Order.php +11 -2
  266. package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
  267. package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
  268. package/server/src/Http/Resources/v1/Zone.php +31 -23
  269. package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
  270. package/server/src/Jobs/ProcessAllocationJob.php +16 -6
  271. package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
  272. package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
  273. package/server/src/Listeners/HandleOrderCanceled.php +9 -1
  274. package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
  275. package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
  276. package/server/src/Mail/CustomerCredentialsMail.php +4 -2
  277. package/server/src/Models/AssetConnection.php +3 -0
  278. package/server/src/Models/Contact.php +3 -0
  279. package/server/src/Models/Device.php +28 -0
  280. package/server/src/Models/DeviceEvent.php +7 -0
  281. package/server/src/Models/Driver.php +20 -7
  282. package/server/src/Models/Entity.php +12 -4
  283. package/server/src/Models/Fleet.php +11 -32
  284. package/server/src/Models/FleetDriver.php +3 -0
  285. package/server/src/Models/FleetVehicle.php +3 -0
  286. package/server/src/Models/FuelReport.php +5 -13
  287. package/server/src/Models/Issue.php +4 -16
  288. package/server/src/Models/Order.php +234 -11
  289. package/server/src/Models/OrderConfig.php +129 -0
  290. package/server/src/Models/Payload.php +227 -35
  291. package/server/src/Models/Place.php +7 -1
  292. package/server/src/Models/ServiceArea.php +7 -0
  293. package/server/src/Models/ServiceRate.php +7 -17
  294. package/server/src/Models/TrackingNumber.php +109 -6
  295. package/server/src/Models/TrackingSession.php +33 -0
  296. package/server/src/Models/TrackingSessionGrant.php +22 -0
  297. package/server/src/Models/TrackingStatus.php +6 -0
  298. package/server/src/Models/Vehicle.php +21 -2
  299. package/server/src/Models/Vendor.php +7 -0
  300. package/server/src/Models/Waypoint.php +13 -4
  301. package/server/src/Notifications/OrderAssigned.php +2 -1
  302. package/server/src/Notifications/OrderCanceled.php +7 -1
  303. package/server/src/Notifications/OrderChanged.php +87 -0
  304. package/server/src/Notifications/OrderCompleted.php +2 -1
  305. package/server/src/Notifications/OrderDispatchFailed.php +2 -1
  306. package/server/src/Notifications/OrderDispatched.php +2 -1
  307. package/server/src/Notifications/OrderFailed.php +2 -1
  308. package/server/src/Notifications/OrderUnassigned.php +66 -0
  309. package/server/src/Notifications/WaypointCompleted.php +2 -1
  310. package/server/src/Observers/CompanyUserObserver.php +8 -7
  311. package/server/src/Observers/DriverObserver.php +6 -1
  312. package/server/src/Observers/OrderObserver.php +141 -1
  313. package/server/src/Observers/TrackingNumberObserver.php +18 -6
  314. package/server/src/Observers/UserObserver.php +2 -1
  315. package/server/src/Observers/VehicleObserver.php +8 -3
  316. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
  317. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
  318. package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
  319. package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
  320. package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
  321. package/server/src/Providers/EventServiceProvider.php +11 -0
  322. package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
  323. package/server/src/Support/ApiCacheThrottle.php +76 -0
  324. package/server/src/Support/LiveOrderQuery.php +3 -1
  325. package/server/src/Support/OrderEdit.php +365 -0
  326. package/server/src/Support/OrderEditPolicy.php +292 -0
  327. package/server/src/Support/ProfileAccountManager.php +54 -1
  328. package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
  329. package/server/src/Support/SocketChannels.php +346 -0
  330. package/server/src/Support/Telematics/TelematicService.php +7 -2
  331. package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
  332. package/server/src/Support/TrackingChannel.php +55 -0
  333. package/server/src/Support/TrackingCode.php +242 -0
  334. package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
  335. package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
  336. package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
  337. package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
  338. package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
  339. package/server/src/Support/TrackingPage/TrackingView.php +543 -0
  340. package/server/src/Support/TrackingPublisher.php +261 -0
  341. package/server/src/Support/TrackingScope.php +182 -0
  342. package/server/src/Support/TrackingUpdate.php +402 -0
  343. package/server/src/Support/Utils.php +15 -3
  344. package/server/src/Traits/HasTrackingNumber.php +2 -0
  345. package/server/src/routes.php +39 -7
  346. package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
  347. package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
  348. package/server/tests/ApiEntityControllerContractsTest.php +84 -0
  349. package/server/tests/ApiOrderControllerContractsTest.php +245 -0
  350. package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
  351. package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
  352. package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
  353. package/server/tests/CompactResourceSerializationTest.php +23 -59
  354. package/server/tests/ControllerFilterContractsTest.php +1 -1
  355. package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
  356. package/server/tests/EventContractsTest.php +26 -60
  357. package/server/tests/ExpansionContractsTest.php +15 -2
  358. package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
  359. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
  360. package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
  361. package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
  362. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
  363. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
  364. package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
  365. package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
  366. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
  367. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
  368. package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
  369. package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
  370. package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
  371. package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
  372. package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
  373. package/server/tests/IndexCustomerPhotoTest.php +151 -0
  374. package/server/tests/ModelAccessorContractsTest.php +4 -3
  375. package/server/tests/NotificationAndMailContractsTest.php +5 -8
  376. package/server/tests/ObserverContractsTest.php +72 -10
  377. package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
  378. package/server/tests/OrderConfigLifecycleTest.php +120 -0
  379. package/server/tests/OrderFilterExecutionTest.php +167 -2
  380. package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
  381. package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
  382. package/server/tests/QueueJobContractsTest.php +2 -1
  383. package/server/tests/RequestContractsTest.php +1 -1
  384. package/server/tests/RouteRegistrationExecutionTest.php +26 -0
  385. package/server/tests/RouteSequencingEngineTest.php +17 -0
  386. package/server/tests/SettingControllerContractsTest.php +110 -0
  387. package/server/tests/Support/TrackingPageDatabase.php +200 -0
  388. package/server/tests/TenantScopingTest.php +206 -0
  389. package/server/tests/TrackingPageSettingsTest.php +343 -0
  390. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
  391. package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
  392. package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
  393. package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
  394. package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
  395. package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
  396. package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
  397. package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
  398. package/server/tests/Unit/Models/FleetTest.php +4 -3
  399. package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
  400. package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
  401. package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
  402. package/server/tests/Unit/Models/PayloadTest.php +18 -4
  403. package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
  404. package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
  405. package/server/tests/Unit/Models/VehicleTest.php +16 -2
  406. package/server/tests/Unit/Models/WaypointTest.php +4 -3
  407. package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
  408. package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
  409. package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
  410. package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
  411. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
  412. package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
  413. package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
  414. package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
  415. package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
  416. package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
  417. package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
  418. package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
  419. package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
  420. package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
  421. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
  422. package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
  423. package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
  424. package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
  425. package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
  426. package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
  427. package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
  428. package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
  429. package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
  430. package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
  431. package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
  432. package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
  433. package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
  434. package/server/tests/VehicleResourceContractsTest.php +46 -0
  435. package/server/tests/VroomOrchestrationEngineTest.php +34 -1
  436. package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
  437. package/translations/ar-ae.yml +274 -0
  438. package/translations/bg-bg.yaml +274 -0
  439. package/translations/en-us.yaml +347 -0
  440. package/translations/es-pa.yaml +274 -0
  441. package/translations/fr-fr.yaml +274 -0
  442. package/translations/mn-mn.yaml +274 -0
  443. package/translations/pt-br.yaml +274 -0
  444. package/translations/ru-ru.yaml +276 -2
  445. package/translations/uk-ua.yaml +275 -1
  446. package/translations/vi-vn.yaml +274 -0
  447. package/addon/components/cell/contact-identity.hbs +0 -1
  448. package/addon/components/cell/customer-identity.hbs +0 -1
  449. package/addon/components/cell/driver-identity.hbs +0 -1
  450. package/addon/components/cell/fleet-identity.hbs +0 -1
  451. package/addon/components/cell/order-identity.hbs +0 -1
  452. package/addon/components/cell/place-identity.hbs +0 -1
  453. package/addon/components/cell/vehicle-identity.hbs +0 -1
  454. package/addon/components/cell/vendor-identity.hbs +0 -1
  455. package/addon/components/contact/pill.hbs +0 -24
  456. package/addon/components/contact/summary.hbs +0 -1
  457. package/addon/components/customer/pill.hbs +0 -24
  458. package/addon/components/customer/summary.hbs +0 -1
  459. package/addon/components/driver/pill.hbs +0 -24
  460. package/addon/components/driver/summary.hbs +0 -1
  461. package/addon/components/fleet/pill.hbs +0 -24
  462. package/addon/components/fleet/summary.hbs +0 -1
  463. package/addon/components/order/pill.hbs +0 -24
  464. package/addon/components/order/summary.hbs +0 -1
  465. package/addon/components/place/pill.hbs +0 -24
  466. package/addon/components/place/summary.hbs +0 -1
  467. package/addon/components/select-option/contact.hbs +0 -12
  468. package/addon/components/select-option/customer.hbs +0 -12
  469. package/addon/components/select-option/driver.hbs +0 -12
  470. package/addon/components/select-option/fleet.hbs +0 -12
  471. package/addon/components/select-option/order.hbs +0 -12
  472. package/addon/components/select-option/place.hbs +0 -12
  473. package/addon/components/select-option/vehicle.hbs +0 -12
  474. package/addon/components/select-option/vendor.hbs +0 -12
  475. package/addon/components/vehicle/pill.hbs +0 -24
  476. package/addon/components/vehicle/summary.hbs +0 -1
  477. package/addon/components/vendor/pill.hbs +0 -24
  478. package/addon/components/vendor/summary.hbs +0 -1
  479. package/app/components/cell/contact-identity.js +0 -1
  480. package/app/components/cell/customer-identity.js +0 -1
  481. package/app/components/cell/driver-identity.js +0 -1
  482. package/app/components/cell/fleet-identity.js +0 -1
  483. package/app/components/cell/order-identity.js +0 -1
  484. package/app/components/cell/place-identity.js +0 -1
  485. package/app/components/cell/vehicle-identity.js +0 -1
  486. package/app/components/cell/vendor-identity.js +0 -1
  487. package/app/components/contact/pill.js +0 -1
  488. package/app/components/contact/summary.js +0 -1
  489. package/app/components/customer/pill.js +0 -1
  490. package/app/components/customer/summary.js +0 -1
  491. package/app/components/driver/pill.js +0 -1
  492. package/app/components/driver/summary.js +0 -1
  493. package/app/components/fleet/pill.js +0 -1
  494. package/app/components/fleet/summary.js +0 -1
  495. package/app/components/order/pill.js +0 -1
  496. package/app/components/order/summary.js +0 -1
  497. package/app/components/place/pill.js +0 -1
  498. package/app/components/place/summary.js +0 -1
  499. package/app/components/select-option/contact.js +0 -1
  500. package/app/components/select-option/customer.js +0 -1
  501. package/app/components/select-option/driver.js +0 -1
  502. package/app/components/select-option/fleet.js +0 -1
  503. package/app/components/select-option/order.js +0 -1
  504. package/app/components/select-option/place.js +0 -1
  505. package/app/components/select-option/vehicle.js +0 -1
  506. package/app/components/select-option/vendor.js +0 -1
  507. package/app/components/vehicle/pill.js +0 -1
  508. package/app/components/vehicle/summary.js +0 -1
  509. package/app/components/vendor/pill.js +0 -1
  510. package/app/components/vendor/summary.js +0 -1
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Console\Commands;
4
4
 
5
+ use Carbon\Carbon;
5
6
  use Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy;
6
7
  use Illuminate\Console\Command;
7
8
  use Illuminate\Support\Collection;
@@ -21,6 +22,9 @@ class PruneTelematicsData extends Command
21
22
 
22
23
  public const LOCK = 'fleetops:prune-telematics-data';
23
24
 
25
+ /** Cache key prefix for the per-scope compaction watermark (see pruneDeviceEvents). */
26
+ public const WATERMARK_PREFIX = 'fleetops:prune-telematics-data:compacted:';
27
+
24
28
  protected $signature = 'fleetops:prune-telematics-data
25
29
  {--company= : Only prune data for this company (uuid or public id)}
26
30
  {--orphans-only : Only prune data without an existing company or connection}
@@ -36,6 +40,9 @@ class PruneTelematicsData extends Command
36
40
  protected int $maxBatches = 50;
37
41
  protected bool $dryRun = false;
38
42
 
43
+ /** table => company uuids present in that table whose company no longer exists; resolved once per run. */
44
+ protected array $orphanCompaniesByTable = [];
45
+
39
46
  public function handle(): int
40
47
  {
41
48
  $tables = $this->requestedTables();
@@ -142,7 +149,7 @@ class PruneTelematicsData extends Command
142
149
  }
143
150
  $inbox = $telematics ? fn ($query) => $query->whereIn('telematic_uuid', $telematics) : null;
144
151
 
145
- return $this->prune($rows, $inbox, $policy, $tables);
152
+ return $this->prune($rows, $inbox, $policy, $tables, $companyUuid);
146
153
  }
147
154
 
148
155
  /**
@@ -150,9 +157,7 @@ class PruneTelematicsData extends Command
150
157
  */
151
158
  protected function pruneOrphans(RetentionPolicy $policy, array $tables): array
152
159
  {
153
- $rows = fn ($query) => $query->where(function ($where) {
154
- $where->whereNull('company_uuid')->orWhereNotIn('company_uuid', DB::table('companies')->select('uuid'));
155
- });
160
+ $rows = fn ($query) => $this->scopeOrphanRows($query);
156
161
  $inbox = fn ($query) => $query->whereNotExists(function ($connections) use ($query) {
157
162
  $connections->selectRaw('1')
158
163
  ->from('telematics')
@@ -160,14 +165,46 @@ class PruneTelematicsData extends Command
160
165
  ->whereColumn('telematics.uuid', $query->from . '.telematic_uuid');
161
166
  });
162
167
 
163
- return $this->prune($rows, $inbox, $policy, $tables);
168
+ return $this->prune($rows, $inbox, $policy, $tables, 'orphans');
169
+ }
170
+
171
+ /**
172
+ * Rows without a company, plus rows whose company uuid no longer exists. The gone
173
+ * companies are resolved once per table from the distinct company uuids it holds, so
174
+ * every batch ranges over the (company_uuid, created_at) index instead of evaluating
175
+ * a NOT IN subquery against every row.
176
+ */
177
+ protected function scopeOrphanRows($query)
178
+ {
179
+ $table = $query->from;
180
+ $gone = $this->orphanCompaniesByTable[$table] ??= $this->orphanCompanies($table);
181
+
182
+ return $query->where(function ($where) use ($gone) {
183
+ $where->whereNull('company_uuid');
184
+ if ($gone !== []) {
185
+ $where->orWhereIn('company_uuid', $gone);
186
+ }
187
+ });
188
+ }
189
+
190
+ /**
191
+ * @return string[]
192
+ */
193
+ protected function orphanCompanies(string $table): array
194
+ {
195
+ return DB::table($table)
196
+ ->whereNotNull('company_uuid')
197
+ ->whereNotIn('company_uuid', DB::table('companies')->select('uuid'))
198
+ ->groupBy('company_uuid')
199
+ ->pluck('company_uuid')
200
+ ->all();
164
201
  }
165
202
 
166
- protected function prune(\Closure $rows, ?\Closure $inbox, RetentionPolicy $policy, array $tables): array
203
+ protected function prune(\Closure $rows, ?\Closure $inbox, RetentionPolicy $policy, array $tables, string $scope): array
167
204
  {
168
205
  $stats = $this->emptyStats();
169
206
  if (in_array('device_events', $tables, true)) {
170
- $stats['tables']['device_events'] = $this->pruneDeviceEvents($rows, $policy);
207
+ $stats['tables']['device_events'] = $this->pruneDeviceEvents($rows, $policy, $scope);
171
208
  }
172
209
  if (in_array('positions', $tables, true)) {
173
210
  $stats['tables']['positions'] = $this->prunePositions($rows, $policy);
@@ -188,40 +225,69 @@ class PruneTelematicsData extends Command
188
225
  return $stats;
189
226
  }
190
227
 
191
- protected function pruneDeviceEvents(\Closure $rows, RetentionPolicy $policy): array
228
+ protected function pruneDeviceEvents(\Closure $rows, RetentionPolicy $policy, string $scope): array
192
229
  {
193
230
  $stats = $this->emptyTableStats();
194
231
  $cutoff = $policy->cutoff('event_retention_days');
195
232
  // Soft-deleted events only grow the table; purge them regardless of age.
196
- $this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats);
233
+ $this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats, 'deleted_at');
197
234
  if ($cutoff) {
198
- $this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats);
235
+ $this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats, 'created_at');
199
236
  }
200
237
  if ($policy->compactsEvents() && ($compactAt = $policy->cutoff('event_compact_after_days'))) {
201
238
  // Keep the event (type, severity, location, normalized data) but drop the raw provider blobs.
202
239
  // Rows past the delete cutoff are left to the delete sweep rather than rewritten first.
203
- $this->updateInBatches(
240
+ // Everything older than the watermark was compacted by an earlier complete sweep, so
241
+ // only the rows that aged into the window since then are visited; without it every
242
+ // run re-scanned the whole history looking for blobs that were already gone.
243
+ $watermark = $this->compactionWatermark($scope);
244
+ $since = $cutoff && (!$watermark || $cutoff->greaterThan($watermark)) ? $cutoff : $watermark;
245
+ $completed = $this->updateInBatches(
204
246
  'device_events',
205
247
  'id',
206
- function ($query) use ($rows, $cutoff, $compactAt) {
248
+ function ($query) use ($rows, $since, $compactAt) {
207
249
  $query = $rows($query)->where('created_at', '<', $compactAt->toDateTimeString())->where(fn ($where) => $where->whereNotNull('payload')->orWhereNotNull('meta'));
208
250
 
209
- return $cutoff ? $query->where('created_at', '>=', $cutoff->toDateTimeString()) : $query;
251
+ return $since ? $query->where('created_at', '>=', $since->toDateTimeString()) : $query;
210
252
  },
211
253
  ['payload' => null, 'meta' => null],
212
- $stats
254
+ $stats,
255
+ 'created_at'
213
256
  );
257
+ if ($completed && !$this->dryRun) {
258
+ $this->rememberCompactionWatermark($scope, $compactAt);
259
+ }
214
260
  }
215
261
 
216
262
  return $stats;
217
263
  }
218
264
 
265
+ protected function compactionWatermark(string $scope): ?Carbon
266
+ {
267
+ try {
268
+ $value = Cache::get(self::WATERMARK_PREFIX . $scope);
269
+ } catch (\Throwable) {
270
+ return null;
271
+ }
272
+
273
+ return $value ? Carbon::parse($value) : null;
274
+ }
275
+
276
+ protected function rememberCompactionWatermark(string $scope, Carbon $compactedBefore): void
277
+ {
278
+ try {
279
+ Cache::forever(self::WATERMARK_PREFIX . $scope, $compactedBefore->toDateTimeString());
280
+ } catch (\Throwable) {
281
+ // A cache outage only costs the next run a wider scan.
282
+ }
283
+ }
284
+
219
285
  protected function prunePositions(\Closure $rows, RetentionPolicy $policy): array
220
286
  {
221
287
  $stats = $this->emptyTableStats();
222
- $this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats);
288
+ $this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats, 'deleted_at');
223
289
  if ($cutoff = $policy->cutoff('position_retention_days')) {
224
- $this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats);
290
+ $this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats, 'created_at');
225
291
  }
226
292
 
227
293
  return $stats;
@@ -232,7 +298,7 @@ class PruneTelematicsData extends Command
232
298
  $stats = $this->emptyTableStats();
233
299
  foreach (['processed' => 'processed_retention_hours', 'quarantined' => 'quarantine_retention_days'] as $status => $key) {
234
300
  if ($cutoff = $policy->cutoff($key)) {
235
- $this->deleteInBatches('telematic_deliveries', 'uuid', fn ($query) => $inbox($query)->where('status', $status)->where('updated_at', '<', $cutoff->toDateTimeString()), $stats);
301
+ $this->deleteInBatches('telematic_deliveries', 'uuid', fn ($query) => $inbox($query)->where('status', $status)->where('updated_at', '<', $cutoff->toDateTimeString()), $stats, 'updated_at');
236
302
  }
237
303
  }
238
304
 
@@ -244,46 +310,55 @@ class PruneTelematicsData extends Command
244
310
  $stats = $this->emptyTableStats();
245
311
  if ($cutoff = $policy->cutoff('sync_run_retention_days')) {
246
312
  // In-flight runs are recovered by the drain command, never expired here.
247
- $this->deleteInBatches('telematic_sync_runs', 'uuid', fn ($query) => $inbox($query)->whereNotIn('status', ['fetching', 'ingesting'])->where('updated_at', '<', $cutoff->toDateTimeString()), $stats);
313
+ $this->deleteInBatches('telematic_sync_runs', 'uuid', fn ($query) => $inbox($query)->whereNotIn('status', ['fetching', 'ingesting'])->where('updated_at', '<', $cutoff->toDateTimeString()), $stats, 'updated_at');
248
314
  }
249
315
 
250
316
  return $stats;
251
317
  }
252
318
 
253
- protected function deleteInBatches(string $table, string $key, \Closure $scope, array &$stats): void
319
+ /**
320
+ * Batches are ordered by the column the scope ranges over (the retention indexes lead
321
+ * with company_uuid and that column), so each pick is an index range rather than a
322
+ * primary-key walk across the whole table. Returns true when the scope was drained.
323
+ */
324
+ protected function deleteInBatches(string $table, string $key, \Closure $scope, array &$stats, ?string $orderBy = null): bool
254
325
  {
255
326
  if ($this->dryRun) {
256
327
  $stats['deleted'] += $scope(DB::table($table))->count();
257
328
 
258
- return;
329
+ return true;
259
330
  }
260
331
  for ($batch = 0; $batch < $this->maxBatches; $batch++) {
261
- $ids = $scope(DB::table($table))->orderBy($key)->limit($this->batchSize)->pluck($key);
332
+ $ids = $scope(DB::table($table))->orderBy($orderBy ?? $key)->limit($this->batchSize)->pluck($key);
262
333
  if ($ids->isEmpty()) {
263
- return;
334
+ return true;
264
335
  }
265
336
  $stats['batches']++;
266
337
  $stats['deleted'] += DB::table($table)->whereIn($key, $ids->all())->delete();
267
338
  }
268
339
  $stats['capped'] = true;
340
+
341
+ return false;
269
342
  }
270
343
 
271
- protected function updateInBatches(string $table, string $key, \Closure $scope, array $values, array &$stats): void
344
+ protected function updateInBatches(string $table, string $key, \Closure $scope, array $values, array &$stats, ?string $orderBy = null): bool
272
345
  {
273
346
  if ($this->dryRun) {
274
347
  $stats['compacted'] += $scope(DB::table($table))->count();
275
348
 
276
- return;
349
+ return true;
277
350
  }
278
351
  for ($batch = 0; $batch < $this->maxBatches; $batch++) {
279
- $ids = $scope(DB::table($table))->orderBy($key)->limit($this->batchSize)->pluck($key);
352
+ $ids = $scope(DB::table($table))->orderBy($orderBy ?? $key)->limit($this->batchSize)->pluck($key);
280
353
  if ($ids->isEmpty()) {
281
- return;
354
+ return true;
282
355
  }
283
356
  $stats['batches']++;
284
357
  $stats['compacted'] += DB::table($table)->whereIn($key, $ids->all())->update($values);
285
358
  }
286
359
  $stats['capped'] = true;
360
+
361
+ return false;
287
362
  }
288
363
 
289
364
  protected function report(string $label, array $stats): void
@@ -0,0 +1,257 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Events;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\Place;
8
+ use Fleetbase\FleetOps\Notifications\OrderChanged as OrderChangedNotification;
9
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
10
+ use Fleetbase\Models\User;
11
+ use Illuminate\Broadcasting\Channel;
12
+ use Illuminate\Broadcasting\InteractsWithSockets;
13
+ use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
14
+ use Illuminate\Foundation\Bus\Dispatchable;
15
+ use Illuminate\Support\Carbon;
16
+ use Illuminate\Support\Facades\Log;
17
+
18
+ /**
19
+ * Broadcast as `order.changed` when an order's route, items, contact, schedule,
20
+ * notes or amounts were edited after it was placed.
21
+ *
22
+ * Channels: order.{public_id}, order.{uuid}, driver.{public_id} and driver.{uuid}
23
+ * when a driver is assigned, customer.{public_id} when a customer exists and
24
+ * company.{company_uuid}.
25
+ *
26
+ * Data: { id, changes[], summary, dropoff, cod_amount, total, currency, changed_by, changed_at }.
27
+ *
28
+ * Every value is captured when the event is created, so the broadcast does not
29
+ * depend on the order still looking the same when the queue sends it.
30
+ */
31
+ class OrderChanged implements ShouldBroadcast
32
+ {
33
+ use Dispatchable;
34
+ use InteractsWithSockets;
35
+
36
+ /**
37
+ * Changes that concern the assigned driver: these send them a push.
38
+ */
39
+ public const DRIVER_CHANGES = ['pickup', 'dropoff', 'return', 'waypoints', 'items', 'cod_amount', 'schedule', 'notes'];
40
+
41
+ public string $eventId;
42
+
43
+ public string $sentAt;
44
+
45
+ public array $channelNames = [];
46
+
47
+ public array $data = [];
48
+
49
+ /**
50
+ * @param array $changes the changed parts: dropoff, items, contact, schedule, notes, total, cod_amount, ...
51
+ * @param string $summary one human line describing the change
52
+ * @param array $extra overrides for the data: total, currency, cod_amount, changed_by, changed_at, dropoff
53
+ */
54
+ public function __construct(Order $order, array $changes, string $summary = '', array $extra = [])
55
+ {
56
+ $this->eventId = uniqid('event_');
57
+ $this->sentAt = Carbon::now()->toDateTimeString();
58
+ $this->channelNames = static::channelNamesFor($order);
59
+ $this->data = static::dataFor($order, $changes, $summary, $extra);
60
+ }
61
+
62
+ /**
63
+ * Broadcast `order.changed` for the order and, unless told otherwise, push the
64
+ * change to the assigned driver when it concerns them.
65
+ *
66
+ * Storefront and other extensions call this after their own order edits.
67
+ */
68
+ public static function dispatchFor(Order $order, array $changes, string $summary = '', array $extra = [], bool $notifyDriver = true): ?self
69
+ {
70
+ $changes = array_values(array_unique(array_filter($changes, 'is_string')));
71
+ if ($changes === []) {
72
+ return null;
73
+ }
74
+
75
+ $event = new static($order, $changes, $summary, $extra);
76
+
77
+ try {
78
+ event($event);
79
+ } catch (\Throwable $exception) {
80
+ Log::warning('Unable to broadcast order.changed', ['order' => $order->public_id, 'error' => $exception->getMessage()]);
81
+ }
82
+
83
+ if ($notifyDriver && static::concernsDriver($changes)) {
84
+ static::notifyDriver($order, $event);
85
+ }
86
+
87
+ return $event;
88
+ }
89
+
90
+ /**
91
+ * Whether any of the changes concerns the driver doing the order.
92
+ */
93
+ public static function concernsDriver(array $changes): bool
94
+ {
95
+ return (bool) array_intersect($changes, self::DRIVER_CHANGES);
96
+ }
97
+
98
+ /**
99
+ * Push the change to the order's assigned driver.
100
+ */
101
+ public static function notifyDriver(Order $order, self $event): bool
102
+ {
103
+ if (!$order->driver_assigned_uuid) {
104
+ return false;
105
+ }
106
+
107
+ try {
108
+ $driver = static::assignedDriver($order);
109
+ if (!$driver) {
110
+ return false;
111
+ }
112
+
113
+ $driver->notify(new OrderChangedNotification($order, $event->data['changes'], (string) $event->data['summary']));
114
+
115
+ return true;
116
+ } catch (\Throwable $exception) {
117
+ Log::warning('Unable to notify driver of order change', ['order' => $order->public_id, 'error' => $exception->getMessage()]);
118
+
119
+ return false;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * The order's assigned driver, read past the driver scope as the order listeners do.
125
+ */
126
+ public static function assignedDriver(Order $order): ?Driver
127
+ {
128
+ if (!$order->driver_assigned_uuid) {
129
+ return null;
130
+ }
131
+
132
+ $driver = $order->relationLoaded('driverAssigned') ? $order->getRelation('driverAssigned') : null;
133
+ if ($driver instanceof Driver && $driver->uuid === $order->driver_assigned_uuid) {
134
+ return $driver;
135
+ }
136
+
137
+ $driver = Driver::where('uuid', $order->driver_assigned_uuid)->withoutGlobalScopes()->first();
138
+ if ($driver) {
139
+ $order->setRelation('driverAssigned', $driver);
140
+ }
141
+
142
+ return $driver;
143
+ }
144
+
145
+ public static function channelNamesFor(Order $order): array
146
+ {
147
+ $channels = [
148
+ 'order.' . $order->public_id,
149
+ 'order.' . $order->uuid,
150
+ ];
151
+
152
+ if ($order->driver_assigned_uuid) {
153
+ $driverPublicId = static::attempt(fn () => static::assignedDriver($order)?->public_id);
154
+ if ($driverPublicId) {
155
+ $channels[] = 'driver.' . $driverPublicId;
156
+ }
157
+ $channels[] = 'driver.' . $order->driver_assigned_uuid;
158
+ }
159
+
160
+ if ($order->customer_uuid) {
161
+ $customerPublicId = data_get($order, 'customer.public_id');
162
+ if ($customerPublicId) {
163
+ $channels[] = 'customer.' . $customerPublicId;
164
+ }
165
+ }
166
+
167
+ if ($order->company_uuid) {
168
+ $channels[] = 'company.' . $order->company_uuid;
169
+ }
170
+
171
+ return array_values(array_unique($channels));
172
+ }
173
+
174
+ public static function dataFor(Order $order, array $changes, string $summary = '', array $extra = []): array
175
+ {
176
+ $payload = $order->relationLoaded('payload') ? $order->payload : static::attempt(fn () => $order->payload);
177
+
178
+ return [
179
+ 'id' => $order->public_id,
180
+ 'changes' => array_values($changes),
181
+ 'summary' => $summary,
182
+ 'dropoff' => array_key_exists('dropoff', $extra) ? $extra['dropoff'] : static::placeData($payload?->dropoff ?? null),
183
+ 'cod_amount' => array_key_exists('cod_amount', $extra) ? $extra['cod_amount'] : static::numberOrNull($payload?->cod_amount ?? null),
184
+ 'total' => array_key_exists('total', $extra) ? $extra['total'] : static::numberOrNull(static::attempt(fn () => $order->transaction_amount)),
185
+ 'currency' => array_key_exists('currency', $extra) ? $extra['currency'] : (static::attempt(fn () => $order->transaction_currency) ?? $payload?->cod_currency ?? null),
186
+ 'changed_by' => array_key_exists('changed_by', $extra) ? $extra['changed_by'] : static::currentUserName(),
187
+ 'changed_at' => $extra['changed_at'] ?? Carbon::now()->toIso8601ZuluString(),
188
+ ];
189
+ }
190
+
191
+ /**
192
+ * The drop-off as `{ id, address, location: { type: Point, coordinates: [lng, lat] } }`.
193
+ */
194
+ public static function placeData(?Place $place): ?array
195
+ {
196
+ if (!$place instanceof Place) {
197
+ return null;
198
+ }
199
+
200
+ $location = $place->location;
201
+
202
+ return [
203
+ 'id' => $place->public_id,
204
+ 'address' => static::attempt(fn () => $place->address),
205
+ 'location' => $location instanceof Point ? ['type' => 'Point', 'coordinates' => [$location->getLng(), $location->getLat()]] : null,
206
+ ];
207
+ }
208
+
209
+ public function broadcastOn(): array
210
+ {
211
+ return array_map(fn (string $name) => new Channel($name), $this->channelNames);
212
+ }
213
+
214
+ public function broadcastAs(): string
215
+ {
216
+ return 'order.changed';
217
+ }
218
+
219
+ public function broadcastWith(): array
220
+ {
221
+ return [
222
+ 'id' => $this->eventId,
223
+ 'api_version' => config('api.version'),
224
+ 'event' => $this->broadcastAs(),
225
+ 'created_at' => $this->sentAt,
226
+ 'data' => $this->data,
227
+ ];
228
+ }
229
+
230
+ protected static function currentUserName(): ?string
231
+ {
232
+ $userUuid = session('user');
233
+ if (!$userUuid) {
234
+ return null;
235
+ }
236
+
237
+ return static::attempt(fn () => User::where('uuid', $userUuid)->value('name'));
238
+ }
239
+
240
+ protected static function numberOrNull(mixed $value): int|float|null
241
+ {
242
+ if ($value === null || $value === '' || !is_numeric($value)) {
243
+ return null;
244
+ }
245
+
246
+ return $value + 0;
247
+ }
248
+
249
+ protected static function attempt(callable $callback): mixed
250
+ {
251
+ try {
252
+ return $callback();
253
+ } catch (\Throwable) {
254
+ return null;
255
+ }
256
+ }
257
+ }
@@ -0,0 +1,68 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Events;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Illuminate\Broadcasting\Channel;
8
+ use Illuminate\Broadcasting\InteractsWithSockets;
9
+ use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
10
+ use Illuminate\Foundation\Bus\Dispatchable;
11
+ use Illuminate\Support\Carbon;
12
+
13
+ /**
14
+ * Broadcast as `order.driver_unassigned` when a driver is taken off an order,
15
+ * whether unassigned or replaced by another driver.
16
+ *
17
+ * Channels: driver.{old driver public_id}, driver.{old driver uuid} and the
18
+ * order's company channel. Data: { id: order public id, driver: old driver public id }.
19
+ */
20
+ class OrderDriverUnassigned implements ShouldBroadcast
21
+ {
22
+ use Dispatchable;
23
+ use InteractsWithSockets;
24
+
25
+ public string $eventId;
26
+
27
+ public string $sentAt;
28
+
29
+ public array $channelNames = [];
30
+
31
+ public array $data = [];
32
+
33
+ public function __construct(Order $order, Driver $driver)
34
+ {
35
+ $this->eventId = uniqid('event_');
36
+ $this->sentAt = Carbon::now()->toDateTimeString();
37
+ $this->channelNames = array_values(array_filter([
38
+ $driver->public_id ? 'driver.' . $driver->public_id : null,
39
+ $driver->uuid ? 'driver.' . $driver->uuid : null,
40
+ $order->company_uuid ? 'company.' . $order->company_uuid : null,
41
+ ]));
42
+ $this->data = [
43
+ 'id' => $order->public_id,
44
+ 'driver' => $driver->public_id,
45
+ ];
46
+ }
47
+
48
+ public function broadcastOn(): array
49
+ {
50
+ return array_map(fn (string $name) => new Channel($name), $this->channelNames);
51
+ }
52
+
53
+ public function broadcastAs(): string
54
+ {
55
+ return 'order.driver_unassigned';
56
+ }
57
+
58
+ public function broadcastWith(): array
59
+ {
60
+ return [
61
+ 'id' => $this->eventId,
62
+ 'api_version' => config('api.version'),
63
+ 'event' => $this->broadcastAs(),
64
+ 'created_at' => $this->sentAt,
65
+ 'data' => $this->data,
66
+ ];
67
+ }
68
+ }
@@ -0,0 +1,32 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Exceptions;
4
+
5
+ /**
6
+ * Thrown when an order edit touches a field its current status locks.
7
+ *
8
+ * Carries the refused fields with the reason for each, so a controller can
9
+ * answer 422 with `{ error, locked: { field: reason } }`.
10
+ */
11
+ class OrderEditLockedException extends \Exception
12
+ {
13
+ /**
14
+ * @param array<string, string> $locked the refused fields and why each is locked
15
+ */
16
+ public function __construct(public array $locked = [], string $message = '', int $code = 422, ?\Throwable $previous = null)
17
+ {
18
+ if ($message === '') {
19
+ $message = $locked ? implode(' ', array_values(array_unique($locked))) : 'This order can no longer be edited.';
20
+ }
21
+
22
+ parent::__construct($message, $code, $previous);
23
+ }
24
+
25
+ /**
26
+ * The 422 response body: the message plus the refused fields.
27
+ */
28
+ public function toResponseData(): array
29
+ {
30
+ return ['locked' => $this->locked];
31
+ }
32
+ }
@@ -4,6 +4,7 @@ namespace Fleetbase\FleetOps\Expansions;
4
4
 
5
5
  use Fleetbase\Build\Expansion;
6
6
  use Fleetbase\FleetOps\Models\Driver;
7
+ use Illuminate\Database\Eloquent\Relations\HasMany;
7
8
 
8
9
  class CompanyExpansion implements Expansion
9
10
  {
@@ -18,11 +19,11 @@ class CompanyExpansion implements Expansion
18
19
  }
19
20
 
20
21
  /**
21
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
22
+ * @return HasMany
22
23
  */
23
24
  public static function drivers()
24
25
  {
25
- return function () {
26
+ return function (): HasMany {
26
27
  /** @var \Illuminate\Database\Eloquent\Model $this */
27
28
  return $this->hasMany(Driver::class);
28
29
  };