@fleetbase/fleetops-engine 0.6.71 → 0.6.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/RELEASE.md +15 -18
  2. package/addon/components/admin/tracking-page-settings.hbs +56 -0
  3. package/addon/components/admin/tracking-page-settings.js +67 -0
  4. package/addon/components/device/form.hbs +2 -2
  5. package/addon/components/driver/details.hbs +1 -1
  6. package/addon/components/driver/form.hbs +1 -1
  7. package/addon/components/equipment/form.hbs +1 -1
  8. package/addon/components/layout/fleet-ops-sidebar.js +11 -3
  9. package/addon/components/maintenance/form.hbs +3 -3
  10. package/addon/components/map/google-live-map.js +2 -2
  11. package/addon/components/map/leaflet-live-map.js +45 -32
  12. package/addon/components/orchestrator/phase-builder.js +2 -1
  13. package/addon/components/orchestrator-workbench.hbs +7 -1
  14. package/addon/components/orchestrator-workbench.js +16 -2
  15. package/addon/components/order/form/route.hbs +1 -1
  16. package/addon/components/order/form/route.js +13 -1
  17. package/addon/components/service-rate/details.hbs +1 -1
  18. package/addon/components/service-rate/form.hbs +1 -1
  19. package/addon/components/tracking-page/full-view.hbs +276 -0
  20. package/addon/components/tracking-page/full-view.js +474 -0
  21. package/addon/components/tracking-page/locale-selector.js +30 -0
  22. package/addon/components/tracking-page/map.hbs +44 -0
  23. package/addon/components/tracking-page/map.js +328 -0
  24. package/addon/components/tracking-page/order-list.hbs +35 -0
  25. package/addon/components/tracking-page/public-view.hbs +60 -0
  26. package/addon/components/tracking-page/settings-preview.hbs +21 -0
  27. package/addon/components/tracking-page/settings-preview.js +23 -0
  28. package/addon/components/tracking-page/verify-panel.hbs +109 -0
  29. package/addon/components/tracking-page.hbs +142 -0
  30. package/addon/components/tracking-page.js +564 -0
  31. package/addon/components/trailer/form.hbs +2 -2
  32. package/addon/components/vehicle/form.hbs +3 -3
  33. package/addon/components/warranty/form.hbs +2 -2
  34. package/addon/components/widget/live-fleet.js +54 -6
  35. package/addon/components/widget/proof-of-delivery.hbs +49 -0
  36. package/addon/components/widget/proof-of-delivery.js +62 -0
  37. package/addon/components/work-order/form.hbs +3 -3
  38. package/addon/controllers/operations/orders/index/details.js +19 -3
  39. package/addon/controllers/operations/scheduler/index.js +45 -14
  40. package/addon/controllers/settings/index.js +6 -0
  41. package/addon/controllers/settings/map.js +75 -0
  42. package/addon/controllers/settings/orchestrator.js +8 -6
  43. package/addon/controllers/settings/tracking-page.js +212 -0
  44. package/addon/extension.js +65 -1
  45. package/addon/helpers/parcel-box-image.js +16 -0
  46. package/addon/routes/operations/orders/index/new.js +5 -4
  47. package/addon/routes/operations/scheduler/index.js +1 -1
  48. package/addon/routes/settings/tracking-page.js +31 -0
  49. package/addon/routes.js +1 -0
  50. package/addon/services/location.js +69 -21
  51. package/addon/services/map-settings.js +28 -0
  52. package/addon/services/order-allocation.js +1 -1
  53. package/addon/styles/fleetops-engine.css +1391 -0
  54. package/addon/templates/settings/map.hbs +46 -0
  55. package/addon/templates/settings/tracking-page.hbs +318 -0
  56. package/addon/utils/identity-cell-resource.js +2 -56
  57. package/addon/utils/map-default-view.js +132 -0
  58. package/addon/utils/placeholder-images.js +4 -69
  59. package/addon/utils/resource-descriptors/assets.js +2 -57
  60. package/addon/utils/resource-descriptors/helpers.js +23 -122
  61. package/addon/utils/resource-descriptors/operations.js +2 -61
  62. package/addon/utils/resource-descriptors/people.js +3 -142
  63. package/addon/utils/resource-descriptors/shared.js +18 -0
  64. package/addon/utils/tracking-live-source.js +123 -0
  65. package/addon/utils/tracking-page-theme.js +123 -0
  66. package/app/components/admin/tracking-page-settings.js +1 -0
  67. package/app/components/tracking-page/full-view.js +1 -0
  68. package/app/components/tracking-page/locale-selector.js +1 -0
  69. package/app/components/tracking-page/map.js +1 -0
  70. package/app/components/tracking-page/order-list.js +1 -0
  71. package/app/components/tracking-page/public-view.js +1 -0
  72. package/app/components/tracking-page/settings-preview.js +1 -0
  73. package/app/components/tracking-page/verify-panel.js +1 -0
  74. package/app/components/tracking-page.js +1 -0
  75. package/app/components/widget/proof-of-delivery.js +1 -0
  76. package/app/controllers/settings/tracking-page.js +1 -0
  77. package/app/helpers/parcel-box-image.js +1 -0
  78. package/app/routes/settings/tracking-page.js +1 -0
  79. package/app/templates/settings/tracking-page.js +1 -0
  80. package/app/utils/map-default-view.js +1 -0
  81. package/app/utils/tracking-live-source.js +1 -0
  82. package/app/utils/tracking-page-theme.js +1 -0
  83. package/composer.json +2 -2
  84. package/extension.json +1 -1
  85. package/package.json +4 -4
  86. package/scripts/pest-bootstrap.php +4 -0
  87. package/server/config/fleetops.php +4 -0
  88. package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
  89. package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
  90. package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
  91. package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
  92. package/server/resources/views/labels/default.php +2 -2
  93. package/server/resources/views/labels/entity-label.php +2 -2
  94. package/server/resources/views/labels/waypoint-label.php +2 -2
  95. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  96. package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
  97. package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
  98. package/server/src/Events/OrderChanged.php +257 -0
  99. package/server/src/Events/OrderDriverUnassigned.php +68 -0
  100. package/server/src/Exceptions/OrderEditLockedException.php +32 -0
  101. package/server/src/Expansions/CompanyExpansion.php +3 -2
  102. package/server/src/Expansions/UserExpansion.php +12 -10
  103. package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
  104. package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
  105. package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
  106. package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
  107. package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
  108. package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
  109. package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
  110. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
  111. package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
  112. package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
  113. package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
  114. package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
  115. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
  116. package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
  117. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
  118. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
  119. package/server/src/Http/Controllers/Internal/v1/OrderController.php +84 -24
  120. package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
  121. package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
  122. package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
  123. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
  124. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
  125. package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
  126. package/server/src/Http/Filter/ContactFilter.php +1 -1
  127. package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
  128. package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
  129. package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
  130. package/server/src/Http/Filter/OrderFilter.php +39 -4
  131. package/server/src/Http/Filter/ProofFilter.php +18 -0
  132. package/server/src/Http/Filter/RouteFilter.php +18 -0
  133. package/server/src/Http/Filter/TelematicFilter.php +23 -0
  134. package/server/src/Http/Filter/WarrantyFilter.php +23 -0
  135. package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
  136. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
  137. package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
  138. package/server/src/Http/Resources/v1/Index/Order.php +11 -2
  139. package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
  140. package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
  141. package/server/src/Http/Resources/v1/Zone.php +31 -23
  142. package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
  143. package/server/src/Jobs/ProcessAllocationJob.php +16 -6
  144. package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
  145. package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
  146. package/server/src/Listeners/HandleOrderCanceled.php +9 -1
  147. package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
  148. package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
  149. package/server/src/Mail/CustomerCredentialsMail.php +4 -2
  150. package/server/src/Models/AssetConnection.php +3 -0
  151. package/server/src/Models/Contact.php +3 -0
  152. package/server/src/Models/Device.php +28 -0
  153. package/server/src/Models/DeviceEvent.php +7 -0
  154. package/server/src/Models/Driver.php +20 -7
  155. package/server/src/Models/Entity.php +12 -4
  156. package/server/src/Models/Fleet.php +11 -32
  157. package/server/src/Models/FleetDriver.php +3 -0
  158. package/server/src/Models/FleetVehicle.php +3 -0
  159. package/server/src/Models/FuelReport.php +5 -13
  160. package/server/src/Models/Issue.php +4 -16
  161. package/server/src/Models/Order.php +200 -9
  162. package/server/src/Models/Payload.php +227 -35
  163. package/server/src/Models/Place.php +7 -1
  164. package/server/src/Models/ServiceArea.php +7 -0
  165. package/server/src/Models/ServiceRate.php +7 -17
  166. package/server/src/Models/TrackingNumber.php +101 -2
  167. package/server/src/Models/TrackingSession.php +33 -0
  168. package/server/src/Models/TrackingSessionGrant.php +22 -0
  169. package/server/src/Models/TrackingStatus.php +6 -0
  170. package/server/src/Models/Vehicle.php +21 -2
  171. package/server/src/Models/Vendor.php +7 -0
  172. package/server/src/Models/Waypoint.php +13 -4
  173. package/server/src/Notifications/OrderAssigned.php +2 -1
  174. package/server/src/Notifications/OrderCanceled.php +7 -1
  175. package/server/src/Notifications/OrderChanged.php +87 -0
  176. package/server/src/Notifications/OrderCompleted.php +2 -1
  177. package/server/src/Notifications/OrderDispatchFailed.php +2 -1
  178. package/server/src/Notifications/OrderDispatched.php +2 -1
  179. package/server/src/Notifications/OrderFailed.php +2 -1
  180. package/server/src/Notifications/OrderUnassigned.php +66 -0
  181. package/server/src/Notifications/WaypointCompleted.php +2 -1
  182. package/server/src/Observers/CompanyUserObserver.php +8 -7
  183. package/server/src/Observers/DriverObserver.php +6 -1
  184. package/server/src/Observers/OrderObserver.php +141 -1
  185. package/server/src/Observers/TrackingNumberObserver.php +18 -6
  186. package/server/src/Observers/UserObserver.php +2 -1
  187. package/server/src/Observers/VehicleObserver.php +8 -3
  188. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
  189. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
  190. package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
  191. package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
  192. package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
  193. package/server/src/Providers/EventServiceProvider.php +11 -0
  194. package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
  195. package/server/src/Support/ApiCacheThrottle.php +76 -0
  196. package/server/src/Support/LiveOrderQuery.php +3 -1
  197. package/server/src/Support/OrderEdit.php +365 -0
  198. package/server/src/Support/OrderEditPolicy.php +292 -0
  199. package/server/src/Support/ProfileAccountManager.php +54 -1
  200. package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
  201. package/server/src/Support/SocketChannels.php +346 -0
  202. package/server/src/Support/Telematics/TelematicService.php +7 -2
  203. package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
  204. package/server/src/Support/TrackingChannel.php +55 -0
  205. package/server/src/Support/TrackingCode.php +242 -0
  206. package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
  207. package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
  208. package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
  209. package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
  210. package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
  211. package/server/src/Support/TrackingPage/TrackingView.php +543 -0
  212. package/server/src/Support/TrackingPublisher.php +261 -0
  213. package/server/src/Support/TrackingScope.php +182 -0
  214. package/server/src/Support/TrackingUpdate.php +402 -0
  215. package/server/src/Support/Utils.php +15 -3
  216. package/server/src/Traits/HasTrackingNumber.php +2 -0
  217. package/server/src/routes.php +39 -7
  218. package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
  219. package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
  220. package/server/tests/ApiEntityControllerContractsTest.php +84 -0
  221. package/server/tests/ApiOrderControllerContractsTest.php +245 -0
  222. package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
  223. package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
  224. package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
  225. package/server/tests/CompactResourceSerializationTest.php +23 -59
  226. package/server/tests/ControllerFilterContractsTest.php +1 -1
  227. package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
  228. package/server/tests/EventContractsTest.php +26 -60
  229. package/server/tests/ExpansionContractsTest.php +15 -2
  230. package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
  231. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
  232. package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
  233. package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
  234. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
  235. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
  236. package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
  237. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
  238. package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
  239. package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
  240. package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
  241. package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
  242. package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
  243. package/server/tests/IndexCustomerPhotoTest.php +151 -0
  244. package/server/tests/ModelAccessorContractsTest.php +4 -3
  245. package/server/tests/NotificationAndMailContractsTest.php +5 -8
  246. package/server/tests/ObserverContractsTest.php +72 -10
  247. package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
  248. package/server/tests/OrderFilterExecutionTest.php +167 -2
  249. package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
  250. package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
  251. package/server/tests/QueueJobContractsTest.php +2 -1
  252. package/server/tests/RequestContractsTest.php +1 -1
  253. package/server/tests/RouteRegistrationExecutionTest.php +26 -0
  254. package/server/tests/RouteSequencingEngineTest.php +17 -0
  255. package/server/tests/SettingControllerContractsTest.php +110 -0
  256. package/server/tests/Support/TrackingPageDatabase.php +200 -0
  257. package/server/tests/TenantScopingTest.php +206 -0
  258. package/server/tests/TrackingPageSettingsTest.php +343 -0
  259. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
  260. package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
  261. package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
  262. package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
  263. package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
  264. package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
  265. package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
  266. package/server/tests/Unit/Models/FleetTest.php +4 -3
  267. package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
  268. package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
  269. package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
  270. package/server/tests/Unit/Models/PayloadTest.php +18 -4
  271. package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
  272. package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
  273. package/server/tests/Unit/Models/VehicleTest.php +16 -2
  274. package/server/tests/Unit/Models/WaypointTest.php +4 -3
  275. package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
  276. package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
  277. package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
  278. package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
  279. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
  280. package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
  281. package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
  282. package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
  283. package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
  284. package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
  285. package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
  286. package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
  287. package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
  288. package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
  289. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
  290. package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
  291. package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
  292. package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
  293. package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
  294. package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
  295. package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
  296. package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
  297. package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
  298. package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
  299. package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
  300. package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
  301. package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
  302. package/server/tests/VehicleResourceContractsTest.php +46 -0
  303. package/server/tests/VroomOrchestrationEngineTest.php +34 -1
  304. package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
  305. package/translations/ar-ae.yml +274 -0
  306. package/translations/bg-bg.yaml +274 -0
  307. package/translations/en-us.yaml +287 -0
  308. package/translations/es-pa.yaml +274 -0
  309. package/translations/fr-fr.yaml +274 -0
  310. package/translations/mn-mn.yaml +274 -0
  311. package/translations/pt-br.yaml +274 -0
  312. package/translations/ru-ru.yaml +276 -2
  313. package/translations/uk-ua.yaml +275 -1
  314. package/translations/vi-vn.yaml +274 -0
  315. package/addon/components/cell/contact-identity.hbs +0 -1
  316. package/addon/components/cell/customer-identity.hbs +0 -1
  317. package/addon/components/cell/driver-identity.hbs +0 -1
  318. package/addon/components/cell/fleet-identity.hbs +0 -1
  319. package/addon/components/cell/order-identity.hbs +0 -1
  320. package/addon/components/cell/place-identity.hbs +0 -1
  321. package/addon/components/cell/vehicle-identity.hbs +0 -1
  322. package/addon/components/cell/vendor-identity.hbs +0 -1
  323. package/addon/components/contact/pill.hbs +0 -24
  324. package/addon/components/contact/summary.hbs +0 -1
  325. package/addon/components/customer/pill.hbs +0 -24
  326. package/addon/components/customer/summary.hbs +0 -1
  327. package/addon/components/driver/pill.hbs +0 -24
  328. package/addon/components/driver/summary.hbs +0 -1
  329. package/addon/components/fleet/pill.hbs +0 -24
  330. package/addon/components/fleet/summary.hbs +0 -1
  331. package/addon/components/order/pill.hbs +0 -24
  332. package/addon/components/order/summary.hbs +0 -1
  333. package/addon/components/place/pill.hbs +0 -24
  334. package/addon/components/place/summary.hbs +0 -1
  335. package/addon/components/select-option/contact.hbs +0 -12
  336. package/addon/components/select-option/customer.hbs +0 -12
  337. package/addon/components/select-option/driver.hbs +0 -12
  338. package/addon/components/select-option/fleet.hbs +0 -12
  339. package/addon/components/select-option/order.hbs +0 -12
  340. package/addon/components/select-option/place.hbs +0 -12
  341. package/addon/components/select-option/vehicle.hbs +0 -12
  342. package/addon/components/select-option/vendor.hbs +0 -12
  343. package/addon/components/vehicle/pill.hbs +0 -24
  344. package/addon/components/vehicle/summary.hbs +0 -1
  345. package/addon/components/vendor/pill.hbs +0 -24
  346. package/addon/components/vendor/summary.hbs +0 -1
  347. package/app/components/cell/contact-identity.js +0 -1
  348. package/app/components/cell/customer-identity.js +0 -1
  349. package/app/components/cell/driver-identity.js +0 -1
  350. package/app/components/cell/fleet-identity.js +0 -1
  351. package/app/components/cell/order-identity.js +0 -1
  352. package/app/components/cell/place-identity.js +0 -1
  353. package/app/components/cell/vehicle-identity.js +0 -1
  354. package/app/components/cell/vendor-identity.js +0 -1
  355. package/app/components/contact/pill.js +0 -1
  356. package/app/components/contact/summary.js +0 -1
  357. package/app/components/customer/pill.js +0 -1
  358. package/app/components/customer/summary.js +0 -1
  359. package/app/components/driver/pill.js +0 -1
  360. package/app/components/driver/summary.js +0 -1
  361. package/app/components/fleet/pill.js +0 -1
  362. package/app/components/fleet/summary.js +0 -1
  363. package/app/components/order/pill.js +0 -1
  364. package/app/components/order/summary.js +0 -1
  365. package/app/components/place/pill.js +0 -1
  366. package/app/components/place/summary.js +0 -1
  367. package/app/components/select-option/contact.js +0 -1
  368. package/app/components/select-option/customer.js +0 -1
  369. package/app/components/select-option/driver.js +0 -1
  370. package/app/components/select-option/fleet.js +0 -1
  371. package/app/components/select-option/order.js +0 -1
  372. package/app/components/select-option/place.js +0 -1
  373. package/app/components/select-option/vehicle.js +0 -1
  374. package/app/components/select-option/vendor.js +0 -1
  375. package/app/components/vehicle/pill.js +0 -1
  376. package/app/components/vehicle/summary.js +0 -1
  377. package/app/components/vendor/pill.js +0 -1
  378. package/app/components/vendor/summary.js +0 -1
@@ -0,0 +1,158 @@
1
+ <?php
2
+
3
+ if (!function_exists('Fleetbase\\Observers\\event')) {
4
+ // The webhook observer announces each deletion; only the cleanup matters here
5
+ eval('namespace Fleetbase\Observers; function event($event = null, $payload = []) { return []; }');
6
+ }
7
+
8
+ // The lifecycle event the webhook observer builds names the model with a core-api macro
9
+ if (!Illuminate\Support\Str::hasMacro('humanize')) {
10
+ Illuminate\Support\Str::macro('humanize', fn ($value, $uppercase = true) => str_replace('_', ' ', Illuminate\Support\Str::snake((string) $value)));
11
+ }
12
+
13
+ use Fleetbase\Events\UserRemovedFromCompany;
14
+ use Fleetbase\FleetOps\Listeners\HandleUserRemovedFromCompany;
15
+ use Fleetbase\FleetOps\Models\Driver;
16
+ use Fleetbase\FleetOps\Observers\CompanyUserObserver;
17
+ use Fleetbase\FleetOps\Observers\DriverObserver;
18
+ use Fleetbase\FleetOps\Observers\UserObserver;
19
+ use Fleetbase\Models\Company;
20
+ use Fleetbase\Models\CompanyUser;
21
+ use Fleetbase\Models\User;
22
+ use Fleetbase\Tests\Support\TrackingPageDatabase;
23
+ use Illuminate\Cache\ArrayStore;
24
+ use Illuminate\Cache\Repository;
25
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
26
+ use Illuminate\Database\SQLiteConnection;
27
+ use Illuminate\Events\Dispatcher;
28
+ use Illuminate\Support\Facades\Cache;
29
+
30
+ /**
31
+ * Drivers removed along with their user or company membership go through
32
+ * Driver::delete(), so DriverObserver unassigns their orders and releases
33
+ * their managed login account.
34
+ */
35
+ beforeEach(function () {
36
+ Cache::swap(new Repository(new ArrayStore()));
37
+ session(['company' => 'co-1']);
38
+ });
39
+
40
+ afterEach(function () {
41
+ EloquentModel::unsetConnectionResolver();
42
+ EloquentModel::unsetEventDispatcher();
43
+ });
44
+
45
+ function driverDeletionCleanupDatabase(): SQLiteConnection
46
+ {
47
+ $db = TrackingPageDatabase::boot();
48
+ $schema = $db->getSchemaBuilder();
49
+
50
+ $schema->create('users', function ($table) {
51
+ $table->increments('id');
52
+ $table->string('uuid')->nullable();
53
+ $table->string('public_id')->nullable();
54
+ $table->string('company_uuid')->nullable();
55
+ $table->string('type')->nullable();
56
+ $table->timestamps();
57
+ $table->softDeletes();
58
+ });
59
+ $schema->create('company_users', function ($table) {
60
+ $table->increments('id');
61
+ $table->string('uuid')->nullable();
62
+ $table->string('company_uuid')->nullable();
63
+ $table->string('user_uuid')->nullable();
64
+ $table->string('status')->nullable();
65
+ $table->timestamps();
66
+ $table->softDeletes();
67
+ });
68
+ $schema->create('drivers', function ($table) {
69
+ $table->increments('id');
70
+ $table->string('uuid')->nullable();
71
+ $table->string('public_id')->nullable();
72
+ $table->string('company_uuid')->nullable();
73
+ $table->string('user_uuid')->nullable();
74
+ $table->string('vehicle_uuid')->nullable();
75
+ $table->timestamps();
76
+ $table->softDeletes();
77
+ });
78
+
79
+ // Model hooks bind to the dispatcher present when a class boots
80
+ EloquentModel::setEventDispatcher(new Dispatcher());
81
+ EloquentModel::clearBootedModels();
82
+ Driver::observe(DriverObserver::class);
83
+ // Core models clear the HTTP response cache on writes, and log activity
84
+ app()->instance('responsecache', new class {
85
+ public function __call($method, $arguments)
86
+ {
87
+ return null;
88
+ }
89
+ });
90
+ config()->set('activitylog.enabled', false);
91
+ app()->bind(Illuminate\Contracts\Config\Repository::class, fn () => config());
92
+
93
+ TrackingPageDatabase::insert($db, 'users', ['uuid' => 'user-1', 'type' => 'driver']);
94
+ TrackingPageDatabase::insert($db, 'users', ['uuid' => 'user-2', 'type' => 'driver']);
95
+ TrackingPageDatabase::insert($db, 'company_users', ['uuid' => 'cu-1', 'company_uuid' => 'co-1', 'user_uuid' => 'user-1', 'status' => 'active']);
96
+ TrackingPageDatabase::insert($db, 'drivers', ['uuid' => 'driver-1', 'company_uuid' => 'co-1', 'user_uuid' => 'user-1']);
97
+ TrackingPageDatabase::insert($db, 'drivers', ['uuid' => 'driver-2', 'company_uuid' => 'co-1', 'user_uuid' => 'user-2']);
98
+ TrackingPageDatabase::insert($db, 'orders', ['uuid' => 'order-1', 'company_uuid' => 'co-1', 'driver_assigned_uuid' => 'driver-1']);
99
+ TrackingPageDatabase::insert($db, 'orders', ['uuid' => 'order-2', 'company_uuid' => 'co-1', 'driver_assigned_uuid' => 'driver-2']);
100
+
101
+ return $db;
102
+ }
103
+
104
+ function driverDeletionCleanupState(SQLiteConnection $db): array
105
+ {
106
+ return [
107
+ 'drivers' => $db->table('drivers')->whereNull('deleted_at')->orderBy('uuid')->pluck('uuid')->all(),
108
+ 'orders' => $db->table('orders')->orderBy('uuid')->pluck('driver_assigned_uuid', 'uuid')->all(),
109
+ 'users' => $db->table('users')->whereNull('deleted_at')->orderBy('uuid')->pluck('uuid')->all(),
110
+ ];
111
+ }
112
+
113
+ test('deleting a user deletes their drivers through the model so their orders are unassigned', function () {
114
+ $db = driverDeletionCleanupDatabase();
115
+ TrackingPageDatabase::insert($db, 'drivers', ['uuid' => 'driver-1-other', 'company_uuid' => 'co-2', 'user_uuid' => 'user-1']);
116
+
117
+ // The user is already soft-deleted when its deleted event fires
118
+ $db->table('users')->where('uuid', 'user-1')->update(['deleted_at' => now()]);
119
+ $user = User::withTrashed()->where('uuid', 'user-1')->first();
120
+
121
+ (new UserObserver())->deleted($user);
122
+
123
+ expect(driverDeletionCleanupState($db))->toBe([
124
+ 'drivers' => ['driver-2'],
125
+ 'orders' => ['order-1' => null, 'order-2' => 'driver-2'],
126
+ 'users' => ['user-2'],
127
+ ]);
128
+ });
129
+
130
+ test('deleting a company user deletes its driver through the model and releases the managed account', function () {
131
+ $db = driverDeletionCleanupDatabase();
132
+
133
+ $companyUser = CompanyUser::where('uuid', 'cu-1')->first();
134
+ $companyUser->delete();
135
+
136
+ (new CompanyUserObserver())->deleted($companyUser);
137
+
138
+ expect(driverDeletionCleanupState($db))->toBe([
139
+ 'drivers' => ['driver-2'],
140
+ 'orders' => ['order-1' => null, 'order-2' => 'driver-2'],
141
+ 'users' => ['user-2'],
142
+ ]);
143
+ });
144
+
145
+ test('removing a user from a company deletes its driver through the model', function () {
146
+ $db = driverDeletionCleanupDatabase();
147
+
148
+ $company = new Company();
149
+ $company->setRawAttributes(['uuid' => 'co-1'], true);
150
+
151
+ (new HandleUserRemovedFromCompany())->handle(new UserRemovedFromCompany(User::where('uuid', 'user-1')->first(), $company));
152
+
153
+ expect(driverDeletionCleanupState($db))->toBe([
154
+ 'drivers' => ['driver-2'],
155
+ 'orders' => ['order-1' => null, 'order-2' => 'driver-2'],
156
+ 'users' => ['user-2'],
157
+ ]);
158
+ });
@@ -104,6 +104,26 @@ test('company user status changes sync onto the driver record', function () {
104
104
  expect($connection->table('drivers')->value('status'))->toBe('suspended');
105
105
  });
106
106
 
107
+ test('company user changes only touch the driver profile in that company', function () {
108
+ $connection = fleetopsObserversBoot();
109
+ $connection->table('users')->insert(['uuid' => 'user-1', 'company_uuid' => 'company-1']);
110
+ $connection->table('drivers')->insert([
111
+ ['uuid' => 'driver-other', 'company_uuid' => 'company-2', 'user_uuid' => 'user-1', 'status' => 'active'],
112
+ ['uuid' => 'driver-1', 'company_uuid' => 'company-1', 'user_uuid' => 'user-1', 'status' => 'active'],
113
+ ]);
114
+
115
+ $companyUser = fleetopsObserversCompanyUser();
116
+ $companyUser->status = 'suspended';
117
+ $companyUser->syncChanges();
118
+ (new CompanyUserObserver())->updated($companyUser);
119
+
120
+ expect($connection->table('drivers')->pluck('status', 'uuid')->all())->toBe(['driver-other' => 'active', 'driver-1' => 'suspended']);
121
+
122
+ (new CompanyUserObserver())->deleted($companyUser);
123
+
124
+ expect($connection->table('drivers')->whereNull('deleted_at')->pluck('uuid')->all())->toBe(['driver-other']);
125
+ });
126
+
107
127
  test('service area deletion cascades to zones and border derives from country', function () {
108
128
  $connection = fleetopsObserversBoot();
109
129
  $connection->table('zones')->insert(['uuid' => 'zone-1', 'company_uuid' => 'company-1', 'service_area_uuid' => 'sa-1', 'name' => 'Zone 1']);
@@ -142,12 +142,12 @@ test('vehicle observer seams resolve drivers and release assignments', function
142
142
  expect(fleetopsObserverSeamInvoke(Fleetbase\FleetOps\Observers\VehicleObserver::class, 'findDriver', ['driver-veh-1'])?->uuid)->toBe('driver-veh-1')
143
143
  ->and(fleetopsObserverSeamInvoke(Fleetbase\FleetOps\Observers\VehicleObserver::class, 'findDriver', ['driver-missing']))->toBeNull();
144
144
 
145
- // Deleting a vehicle releases only the drivers assigned to it
145
+ // Deleting a vehicle unassigns only the drivers assigned to it, and keeps them
146
146
  $vehicle = new Vehicle();
147
147
  $vehicle->setRawAttributes(['uuid' => 'vehicle-seam-1'], true);
148
- fleetopsObserverSeamInvoke(Fleetbase\FleetOps\Observers\VehicleObserver::class, 'deleteDriversAssignedTo', [$vehicle]);
148
+ fleetopsObserverSeamInvoke(Fleetbase\FleetOps\Observers\VehicleObserver::class, 'unassignDriversFrom', [$vehicle]);
149
149
 
150
- expect($connection->table('drivers')->whereNull('deleted_at')->pluck('uuid')->all())->toBe(['driver-veh-2']);
150
+ expect($connection->table('drivers')->whereNull('deleted_at')->pluck('vehicle_uuid', 'uuid')->all())->toBe(['driver-veh-1' => null, 'driver-veh-2' => 'vehicle-other']);
151
151
  });
152
152
 
153
153
  test('fleet observer clears the parent reference from child fleets', function () {
@@ -0,0 +1,100 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Order;
4
+ use Fleetbase\FleetOps\Observers\OrderObserver;
5
+ use Fleetbase\FleetOps\Support\TrackingPublisher;
6
+ use Illuminate\Cache\ArrayStore;
7
+ use Illuminate\Cache\Repository;
8
+ use Illuminate\Support\Facades\Cache;
9
+
10
+ /**
11
+ * Covers how the order observer follows driver assignment: a replaced driver is
12
+ * told they were unassigned, and the new driver is notified exactly once (and not
13
+ * at all for a silent assignment).
14
+ */
15
+ class FleetOpsOrderAssignmentObserverOrder extends Order
16
+ {
17
+ public array $events = [];
18
+ public array $originalData = [];
19
+ public bool $driverChanged = true;
20
+ public bool $driverDirty = false;
21
+
22
+ public function getOriginal($key = null, $default = null): mixed
23
+ {
24
+ return $key === null ? $this->originalData : ($this->originalData[$key] ?? $default);
25
+ }
26
+
27
+ public function wasChanged($attributes = null): bool
28
+ {
29
+ return $attributes === 'driver_assigned_uuid' && $this->driverChanged;
30
+ }
31
+
32
+ public function isDirty($attributes = null): bool
33
+ {
34
+ return $attributes === 'driver_assigned_uuid' && $this->driverDirty;
35
+ }
36
+
37
+ public function setDriverLocationAsPickup($force = false)
38
+ {
39
+ }
40
+
41
+ public function notifyDriverAssigned()
42
+ {
43
+ $this->events[] = 'assigned:' . $this->driver_assigned_uuid;
44
+ }
45
+
46
+ public function notifyDriverUnassigned($driver): bool
47
+ {
48
+ $this->events[] = 'unassigned:' . $driver;
49
+
50
+ return true;
51
+ }
52
+ }
53
+
54
+ beforeEach(function () {
55
+ Cache::swap(new Repository(new ArrayStore()));
56
+ session(['company' => 'company-uuid']);
57
+
58
+ // public tracking is published elsewhere; it is not what these tests cover
59
+ app()->instance(TrackingPublisher::class, new class extends TrackingPublisher {
60
+ public function orderChanged(Order $order): bool
61
+ {
62
+ return false;
63
+ }
64
+ });
65
+ });
66
+
67
+ test('reassigning an order tells the replaced driver and notifies the new one once', function () {
68
+ $order = new FleetOpsOrderAssignmentObserverOrder();
69
+ $order->setRawAttributes(['uuid' => 'order-uuid', 'driver_assigned_uuid' => 'driver-new'], true);
70
+ $order->originalData = ['driver_assigned_uuid' => 'driver-old'];
71
+
72
+ (new OrderObserver())->updated($order);
73
+
74
+ expect($order->events)->toBe(['unassigned:driver-old', 'assigned:driver-new']);
75
+ });
76
+
77
+ test('unassigning tells the driver and a silent assignment sends no assignment notification', function () {
78
+ $unassigned = new FleetOpsOrderAssignmentObserverOrder();
79
+ $unassigned->setRawAttributes(['uuid' => 'order-uuid', 'driver_assigned_uuid' => null], true);
80
+ $unassigned->originalData = ['driver_assigned_uuid' => 'driver-old'];
81
+
82
+ (new OrderObserver())->updated($unassigned);
83
+
84
+ $silent = new FleetOpsOrderAssignmentObserverOrder();
85
+ $silent->setRawAttributes(['uuid' => 'order-uuid', 'driver_assigned_uuid' => 'driver-new'], true);
86
+ $silent->silentDriverAssignment = true;
87
+
88
+ (new OrderObserver())->updated($silent);
89
+
90
+ expect($unassigned->events)->toBe(['unassigned:driver-old', 'assigned:'])
91
+ ->and($silent->events)->toBe([]);
92
+ });
93
+
94
+ test('assign driver relies on the observer instead of notifying before the save', function () {
95
+ $source = file_get_contents(dirname(__DIR__, 3) . '/src/Models/Order.php');
96
+ preg_match('/public function assignDriver\(.*?\n }\n/s', $source, $matches);
97
+
98
+ expect($matches[0] ?? '')->not->toContain('notifyDriverAssigned()')
99
+ ->toContain('$this->silentDriverAssignment = (bool) $silent;');
100
+ });
@@ -0,0 +1,213 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Entity;
4
+ use Fleetbase\FleetOps\Models\Order;
5
+ use Fleetbase\FleetOps\Models\Waypoint;
6
+ use Fleetbase\FleetOps\Observers\OrderObserver;
7
+ use Fleetbase\Tests\Support\TrackingPageDatabase;
8
+ use Illuminate\Cache\ArrayStore;
9
+ use Illuminate\Cache\Repository;
10
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
11
+ use Illuminate\Database\SQLiteConnection;
12
+ use Illuminate\Support\Carbon;
13
+ use Illuminate\Support\Facades\Cache;
14
+
15
+ class OrderDeleteCascadeBulkProbe extends Order
16
+ {
17
+ protected $table = 'orders';
18
+
19
+ public static array $deleted = [];
20
+
21
+ public function delete()
22
+ {
23
+ static::$deleted[] = $this->uuid;
24
+
25
+ return parent::delete();
26
+ }
27
+ }
28
+
29
+ beforeEach(function () {
30
+ Cache::swap(new Repository(new ArrayStore()));
31
+ session(['company' => 'co-1']);
32
+ OrderDeleteCascadeBulkProbe::$deleted = [];
33
+ });
34
+
35
+ afterEach(function () {
36
+ Carbon::setTestNow();
37
+ EloquentModel::unsetConnectionResolver();
38
+ });
39
+
40
+ function orderDeleteCascadeFixture(): SQLiteConnection
41
+ {
42
+ $db = TrackingPageDatabase::boot();
43
+
44
+ $rows = [
45
+ 'orders' => [
46
+ ['uuid' => 'order-1', 'public_id' => 'order_one', 'company_uuid' => 'co-1', 'payload_uuid' => 'payload-1', 'tracking_number_uuid' => 'tn-order'],
47
+ ['uuid' => 'order-shared-a', 'public_id' => 'order_shared_a', 'company_uuid' => 'co-1', 'payload_uuid' => 'payload-shared', 'tracking_number_uuid' => 'tn-shared-a'],
48
+ ['uuid' => 'order-shared-b', 'public_id' => 'order_shared_b', 'company_uuid' => 'co-1', 'payload_uuid' => 'payload-shared'],
49
+ ['uuid' => 'order-bare', 'public_id' => 'order_bare', 'company_uuid' => 'co-1'],
50
+ ['uuid' => 'order-other', 'public_id' => 'order_other', 'company_uuid' => 'co-2', 'payload_uuid' => 'payload-other'],
51
+ ],
52
+ 'payloads' => [
53
+ ['uuid' => 'payload-1', 'dropoff_tracking_number_uuid' => 'tn-dropoff'],
54
+ ['uuid' => 'payload-shared'],
55
+ ['uuid' => 'payload-other'],
56
+ ],
57
+ 'entities' => [
58
+ ['uuid' => 'entity-1', 'payload_uuid' => 'payload-1', 'tracking_number_uuid' => 'tn-entity'],
59
+ ['uuid' => 'entity-owned', 'payload_uuid' => 'payload-1'],
60
+ ['uuid' => 'entity-gone', 'payload_uuid' => 'payload-1', 'deleted_at' => '2026-01-01 00:00:00'],
61
+ ['uuid' => 'entity-shared', 'payload_uuid' => 'payload-shared'],
62
+ ],
63
+ 'waypoints' => [
64
+ ['uuid' => 'wp-1', 'payload_uuid' => 'payload-1', 'tracking_number_uuid' => 'tn-waypoint'],
65
+ ],
66
+ 'tracking_numbers' => [
67
+ ['uuid' => 'tn-order', 'owner_uuid' => 'order-1', 'owner_type' => Order::class],
68
+ ['uuid' => 'tn-entity', 'owner_uuid' => 'entity-1', 'owner_type' => Entity::class],
69
+ ['uuid' => 'tn-entity-owned', 'owner_uuid' => 'entity-owned', 'owner_type' => Entity::class],
70
+ ['uuid' => 'tn-waypoint', 'owner_uuid' => 'wp-1', 'owner_type' => Waypoint::class],
71
+ ['uuid' => 'tn-dropoff', 'owner_uuid' => 'place-dropoff'],
72
+ ['uuid' => 'tn-shared-a', 'owner_uuid' => 'order-shared-a', 'owner_type' => Order::class],
73
+ ['uuid' => 'tn-unrelated', 'owner_uuid' => 'order-other', 'owner_type' => Order::class],
74
+ ],
75
+ 'tracking_statuses' => [
76
+ ['uuid' => 'ts-order', 'tracking_number_uuid' => 'tn-order'],
77
+ ['uuid' => 'ts-entity', 'tracking_number_uuid' => 'tn-entity-owned'],
78
+ ['uuid' => 'ts-shared-a', 'tracking_number_uuid' => 'tn-shared-a'],
79
+ ['uuid' => 'ts-unrelated', 'tracking_number_uuid' => 'tn-unrelated'],
80
+ ],
81
+ ];
82
+
83
+ foreach ($rows as $table => $records) {
84
+ foreach ($records as $record) {
85
+ TrackingPageDatabase::insert($db, $table, $record);
86
+ }
87
+ }
88
+
89
+ return $db;
90
+ }
91
+
92
+ function orderDeleteCascadeDeletedAt(SQLiteConnection $db, string $table, string $uuid): ?string
93
+ {
94
+ return $db->table($table)->where('uuid', $uuid)->value('deleted_at');
95
+ }
96
+
97
+ function orderDeleteCascadeSoftDelete(SQLiteConnection $db, string $uuid): Order
98
+ {
99
+ $db->table('orders')->where('uuid', $uuid)->update(['deleted_at' => now()->toDateTimeString()]);
100
+
101
+ return Order::withTrashed()->where('uuid', $uuid)->first();
102
+ }
103
+
104
+ test('deleting an order soft-deletes its payload tree and tracking data with the order timestamp', function () {
105
+ Carbon::setTestNow('2026-10-09 12:00:00');
106
+ $db = orderDeleteCascadeFixture();
107
+ $order = orderDeleteCascadeSoftDelete($db, 'order-1');
108
+
109
+ (new OrderObserver())->deleted($order);
110
+
111
+ $stamp = '2026-10-09 12:00:00';
112
+ foreach ([
113
+ ['payloads', 'payload-1'],
114
+ ['entities', 'entity-1'],
115
+ ['entities', 'entity-owned'],
116
+ ['waypoints', 'wp-1'],
117
+ ['tracking_numbers', 'tn-order'],
118
+ ['tracking_numbers', 'tn-entity'],
119
+ ['tracking_numbers', 'tn-entity-owned'],
120
+ ['tracking_numbers', 'tn-waypoint'],
121
+ ['tracking_numbers', 'tn-dropoff'],
122
+ ['tracking_statuses', 'ts-order'],
123
+ ['tracking_statuses', 'ts-entity'],
124
+ ] as [$table, $uuid]) {
125
+ expect(orderDeleteCascadeDeletedAt($db, $table, $uuid))->toBe($stamp, "{$table}.{$uuid}");
126
+ }
127
+
128
+ // Rows outside the order, and children deleted earlier, keep their state.
129
+ expect(orderDeleteCascadeDeletedAt($db, 'entities', 'entity-gone'))->toBe('2026-01-01 00:00:00')
130
+ ->and(orderDeleteCascadeDeletedAt($db, 'tracking_numbers', 'tn-unrelated'))->toBeNull()
131
+ ->and(orderDeleteCascadeDeletedAt($db, 'tracking_statuses', 'ts-unrelated'))->toBeNull()
132
+ ->and(orderDeleteCascadeDeletedAt($db, 'payloads', 'payload-other'))->toBeNull()
133
+ ->and(Cache::get('live:co-1:orders:version'))->toBe(1);
134
+ });
135
+
136
+ test('restoring an order brings back only the children deleted with it', function () {
137
+ Carbon::setTestNow('2026-10-09 12:00:00');
138
+ $db = orderDeleteCascadeFixture();
139
+ $order = orderDeleteCascadeSoftDelete($db, 'order-1');
140
+ $observer = new OrderObserver();
141
+ $observer->deleted($order);
142
+
143
+ // An entity deleted on its own after the order keeps its own timestamp.
144
+ $db->table('entities')->insert(['uuid' => 'entity-later', 'payload_uuid' => 'payload-1', 'deleted_at' => '2026-10-10 08:00:00']);
145
+
146
+ $observer->restoring($order);
147
+ $db->table('orders')->where('uuid', 'order-1')->update(['deleted_at' => null]);
148
+ $order->deleted_at = null;
149
+ $observer->restored($order);
150
+
151
+ foreach ([
152
+ ['payloads', 'payload-1'],
153
+ ['entities', 'entity-1'],
154
+ ['entities', 'entity-owned'],
155
+ ['waypoints', 'wp-1'],
156
+ ['tracking_numbers', 'tn-order'],
157
+ ['tracking_numbers', 'tn-entity'],
158
+ ['tracking_numbers', 'tn-entity-owned'],
159
+ ['tracking_numbers', 'tn-waypoint'],
160
+ ['tracking_numbers', 'tn-dropoff'],
161
+ ['tracking_statuses', 'ts-order'],
162
+ ['tracking_statuses', 'ts-entity'],
163
+ ] as [$table, $uuid]) {
164
+ expect(orderDeleteCascadeDeletedAt($db, $table, $uuid))->toBeNull("{$table}.{$uuid}");
165
+ }
166
+
167
+ expect(orderDeleteCascadeDeletedAt($db, 'entities', 'entity-gone'))->toBe('2026-01-01 00:00:00')
168
+ ->and(orderDeleteCascadeDeletedAt($db, 'entities', 'entity-later'))->toBe('2026-10-10 08:00:00');
169
+
170
+ // A second "restored" without a matching "restoring" has nothing to restore.
171
+ $db->table('tracking_numbers')->where('uuid', 'tn-order')->update(['deleted_at' => '2026-10-09 12:00:00']);
172
+ $observer->restored($order);
173
+ $observer->restoring($order);
174
+
175
+ expect(orderDeleteCascadeDeletedAt($db, 'tracking_numbers', 'tn-order'))->toBe('2026-10-09 12:00:00');
176
+ });
177
+
178
+ test('a payload shared with another live order is left alone', function () {
179
+ Carbon::setTestNow('2026-10-09 12:00:00');
180
+ $db = orderDeleteCascadeFixture();
181
+ $order = orderDeleteCascadeSoftDelete($db, 'order-shared-a');
182
+
183
+ (new OrderObserver())->deleted($order);
184
+
185
+ expect(orderDeleteCascadeDeletedAt($db, 'payloads', 'payload-shared'))->toBeNull()
186
+ ->and(orderDeleteCascadeDeletedAt($db, 'entities', 'entity-shared'))->toBeNull()
187
+ ->and(orderDeleteCascadeDeletedAt($db, 'tracking_numbers', 'tn-shared-a'))->toBe('2026-10-09 12:00:00')
188
+ ->and(orderDeleteCascadeDeletedAt($db, 'tracking_statuses', 'ts-shared-a'))->toBe('2026-10-09 12:00:00');
189
+
190
+ // An order without a payload or deletion timestamp (force delete) still cascades cleanly.
191
+ $bare = Order::where('uuid', 'order-bare')->first();
192
+ (new OrderObserver())->deleted($bare);
193
+
194
+ expect(orderDeleteCascadeDeletedAt($db, 'payloads', 'payload-other'))->toBeNull();
195
+ });
196
+
197
+ test('bulk removal deletes each matched order of the session company one by one', function () {
198
+ $db = orderDeleteCascadeFixture();
199
+
200
+ $count = (new OrderDeleteCascadeBulkProbe())->bulkRemove(['order-1', 'order_bare', 'order-other']);
201
+
202
+ expect($count)->toBe(2)
203
+ ->and(OrderDeleteCascadeBulkProbe::$deleted)->toBe(['order-1', 'order-bare'])
204
+ ->and(orderDeleteCascadeDeletedAt($db, 'orders', 'order-1'))->not->toBeNull()
205
+ ->and(orderDeleteCascadeDeletedAt($db, 'orders', 'order-other'))->toBeNull();
206
+
207
+ // Without a session company the lookup is not tenant constrained, like core-api's.
208
+ session(['company' => null]);
209
+ OrderDeleteCascadeBulkProbe::$deleted = [];
210
+
211
+ expect((new OrderDeleteCascadeBulkProbe())->bulkRemove(['order-other']))->toBe(1)
212
+ ->and(OrderDeleteCascadeBulkProbe::$deleted)->toBe(['order-other']);
213
+ });
@@ -0,0 +1,67 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Vehicle;
4
+ use Fleetbase\FleetOps\Observers\VehicleObserver;
5
+ use Fleetbase\Tests\Support\TrackingPageDatabase;
6
+ use Illuminate\Cache\ArrayStore;
7
+ use Illuminate\Cache\Repository;
8
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
9
+ use Illuminate\Database\SQLiteConnection;
10
+ use Illuminate\Support\Facades\Cache;
11
+
12
+ beforeEach(function () {
13
+ Cache::swap(new Repository(new ArrayStore()));
14
+ session(['company' => 'co-1']);
15
+ });
16
+
17
+ afterEach(function () {
18
+ EloquentModel::unsetConnectionResolver();
19
+ });
20
+
21
+ function vehicleObserverDeleteDatabase(): SQLiteConnection
22
+ {
23
+ $db = TrackingPageDatabase::boot();
24
+
25
+ $db->getSchemaBuilder()->create('users', function ($table) {
26
+ $table->increments('id');
27
+ $table->string('uuid')->nullable();
28
+ $table->string('company_uuid')->nullable();
29
+ $table->timestamps();
30
+ $table->softDeletes();
31
+ });
32
+ $db->getSchemaBuilder()->create('drivers', function ($table) {
33
+ $table->increments('id');
34
+ $table->string('uuid')->nullable();
35
+ $table->string('company_uuid')->nullable();
36
+ $table->string('user_uuid')->nullable();
37
+ $table->string('vehicle_uuid')->nullable();
38
+ $table->timestamps();
39
+ $table->softDeletes();
40
+ });
41
+
42
+ foreach (['user-1', 'user-2', 'user-3'] as $user) {
43
+ TrackingPageDatabase::insert($db, 'users', ['uuid' => $user, 'company_uuid' => 'co-1']);
44
+ }
45
+ TrackingPageDatabase::insert($db, 'drivers', ['uuid' => 'driver-1', 'company_uuid' => 'co-1', 'user_uuid' => 'user-1', 'vehicle_uuid' => 'vehicle-1']);
46
+ TrackingPageDatabase::insert($db, 'drivers', ['uuid' => 'driver-2', 'company_uuid' => 'co-1', 'user_uuid' => 'user-2', 'vehicle_uuid' => 'vehicle-1']);
47
+ TrackingPageDatabase::insert($db, 'drivers', ['uuid' => 'driver-3', 'company_uuid' => 'co-1', 'user_uuid' => 'user-3', 'vehicle_uuid' => 'vehicle-2']);
48
+
49
+ return $db;
50
+ }
51
+
52
+ test('deleting a vehicle unassigns its drivers without deleting them', function () {
53
+ $db = vehicleObserverDeleteDatabase();
54
+
55
+ $vehicle = new Vehicle();
56
+ $vehicle->setRawAttributes(['uuid' => 'vehicle-1', 'company_uuid' => 'co-1'], true);
57
+
58
+ (new VehicleObserver())->deleted($vehicle);
59
+
60
+ $drivers = $db->table('drivers')->orderBy('uuid')->get(['uuid', 'vehicle_uuid', 'deleted_at'])->map(fn ($row) => (array) $row)->all();
61
+
62
+ expect($drivers)->toBe([
63
+ ['uuid' => 'driver-1', 'vehicle_uuid' => null, 'deleted_at' => null],
64
+ ['uuid' => 'driver-2', 'vehicle_uuid' => null, 'deleted_at' => null],
65
+ ['uuid' => 'driver-3', 'vehicle_uuid' => 'vehicle-2', 'deleted_at' => null],
66
+ ]);
67
+ });