@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,347 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\AssetConnection;
4
+ use Fleetbase\FleetOps\Models\Contact;
5
+ use Fleetbase\FleetOps\Models\Device;
6
+ use Fleetbase\FleetOps\Models\DeviceEvent;
7
+ use Fleetbase\FleetOps\Models\Driver;
8
+ use Fleetbase\FleetOps\Models\FleetDriver;
9
+ use Fleetbase\FleetOps\Models\FleetVehicle;
10
+ use Fleetbase\FleetOps\Models\IntegratedVendor;
11
+ use Fleetbase\FleetOps\Models\Order;
12
+ use Fleetbase\FleetOps\Models\Payload;
13
+ use Fleetbase\FleetOps\Models\ServiceArea;
14
+ use Fleetbase\FleetOps\Models\TrackingStatus;
15
+ use Fleetbase\FleetOps\Models\Trailer;
16
+ use Fleetbase\FleetOps\Models\Vehicle;
17
+ use Fleetbase\FleetOps\Models\Vendor;
18
+ use Fleetbase\FleetOps\Models\Waypoint;
19
+ use Fleetbase\FleetOps\Support\ApiCacheThrottle;
20
+ use Fleetbase\Tests\Support\TrackingPageDatabase;
21
+ use Illuminate\Cache\ArrayStore;
22
+ use Illuminate\Cache\Repository;
23
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
24
+ use Illuminate\Database\Eloquent\Relations\MorphTo;
25
+ use Illuminate\Database\Eloquent\Relations\Relation;
26
+ use Illuminate\Database\SQLiteConnection;
27
+ use Illuminate\Events\Dispatcher;
28
+ use Illuminate\Support\Carbon;
29
+ use Illuminate\Support\Facades\Cache;
30
+ use Illuminate\Support\Str;
31
+
32
+ if (!function_exists('Fleetbase\FleetOps\Models\dispatch')) {
33
+ // Order::dispatch() queues its domain event; only the write and its invalidation matter here
34
+ eval('namespace Fleetbase\FleetOps\Models; function dispatch($job) { return new class { public function afterCommit() { return $this; } }; }');
35
+ }
36
+
37
+ beforeEach(function () {
38
+ Cache::swap(new Repository(new ArrayStore()));
39
+ session(['company' => 'co-1']);
40
+ });
41
+
42
+ afterEach(function () {
43
+ Carbon::setTestNow();
44
+ EloquentModel::unsetConnectionResolver();
45
+ EloquentModel::unsetEventDispatcher();
46
+ });
47
+
48
+ /**
49
+ * The model's declared dependencies, read from the property because
50
+ * getApiCacheDependencies() only exists from fleetbase/core-api#301 on.
51
+ */
52
+ function apiCacheDeclaredDependencies(EloquentModel $model): array
53
+ {
54
+ $property = new ReflectionProperty($model, 'apiCacheDependencies');
55
+ $property->setAccessible(true);
56
+
57
+ return $property->getValue($model);
58
+ }
59
+
60
+ /**
61
+ * The relation method a dependency segment names, matched the way core does (as written, camel or snake case).
62
+ */
63
+ function apiCacheRelationMethod(EloquentModel $model, string $segment): ?string
64
+ {
65
+ foreach (array_unique([$segment, Str::camel($segment), Str::snake($segment)]) as $method) {
66
+ if (!method_exists($model, $method)) {
67
+ continue;
68
+ }
69
+
70
+ $reflection = new ReflectionMethod($model, $method);
71
+ if ($reflection->isPublic() && !$reflection->isStatic() && $reflection->getNumberOfRequiredParameters() === 0) {
72
+ return $method;
73
+ }
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ function apiCacheQueryVersion(string $table, ?string $companyUuid): int
80
+ {
81
+ return (int) Cache::get("api_query_version:{$table}:{$companyUuid}", 0);
82
+ }
83
+
84
+ function apiCacheDatabase(): SQLiteConnection
85
+ {
86
+ $db = TrackingPageDatabase::boot();
87
+ $schema = $db->getSchemaBuilder();
88
+
89
+ // Model hooks bind to the dispatcher present when a class boots
90
+ EloquentModel::setEventDispatcher(new Dispatcher());
91
+ EloquentModel::clearBootedModels();
92
+ // Core models clear the HTTP response cache on writes
93
+ app()->instance('responsecache', new class {
94
+ public function __call($method, $arguments)
95
+ {
96
+ return null;
97
+ }
98
+ });
99
+
100
+ foreach (['fleet_drivers' => 'driver_uuid', 'fleet_vehicles' => 'vehicle_uuid'] as $table => $member) {
101
+ $schema->create($table, function ($table) use ($member) {
102
+ $table->increments('id');
103
+ $table->string('uuid')->nullable();
104
+ $table->string('fleet_uuid')->nullable();
105
+ $table->string($member)->nullable();
106
+ $table->timestamps();
107
+ $table->softDeletes();
108
+ });
109
+ }
110
+
111
+ $schema->create('asset_connections', function ($table) {
112
+ $table->increments('id');
113
+ $table->string('uuid')->nullable();
114
+ $table->string('public_id')->nullable();
115
+ $table->string('company_uuid')->nullable();
116
+ foreach (['connector_type', 'connector_uuid', 'connected_type', 'connected_uuid', 'relationship_type'] as $column) {
117
+ $table->string($column)->nullable();
118
+ }
119
+ $table->timestamps();
120
+ $table->softDeletes();
121
+ });
122
+
123
+ $schema->table('orders', function ($table) {
124
+ $table->timestamp('dispatched_at')->nullable();
125
+ });
126
+
127
+ return $db;
128
+ }
129
+
130
+ test('declared api cache dependencies resolve to relations or to the tables of morphTo targets', function (string $modelClass, array $morphTargets) {
131
+ TrackingPageDatabase::boot();
132
+
133
+ $model = new $modelClass();
134
+ $declared = apiCacheDeclaredDependencies($model);
135
+ $named = [];
136
+
137
+ expect($declared)->not->toBeEmpty();
138
+
139
+ foreach ($declared as $path) {
140
+ $current = $model;
141
+ $segments = explode('.', $path);
142
+
143
+ foreach ($segments as $segment) {
144
+ $method = apiCacheRelationMethod($current, $segment);
145
+
146
+ // core treats a single segment naming no method as a table name
147
+ if ($method === null && count($segments) === 1) {
148
+ $named[] = $path;
149
+ continue 2;
150
+ }
151
+
152
+ expect($method)->not->toBeNull("{$modelClass} dependency {$path} has no relation {$segment}");
153
+
154
+ $relation = Relation::noConstraints(fn () => $current->newInstance()->{$method}());
155
+
156
+ expect($relation)->toBeInstanceOf(Relation::class)
157
+ ->and($relation)->not->toBeInstanceOf(MorphTo::class);
158
+
159
+ $current = $relation->getRelated();
160
+ }
161
+ }
162
+
163
+ expect($named)->toEqualCanonicalizing(array_map(fn (string $target) => (new $target())->getTable(), $morphTargets));
164
+ })->with([
165
+ 'order' => [Order::class, [Contact::class, Vendor::class, IntegratedVendor::class]],
166
+ 'driver' => [Driver::class, []],
167
+ 'vehicle' => [Vehicle::class, []],
168
+ 'device' => [Device::class, [Vehicle::class, Trailer::class]],
169
+ 'device event' => [DeviceEvent::class, []],
170
+ 'vendor' => [Vendor::class, []],
171
+ 'service area' => [ServiceArea::class, []],
172
+ ]);
173
+
174
+ test('models only embedded by cached lists invalidate their table on writes', function () {
175
+ $db = apiCacheDatabase();
176
+
177
+ $fleetDriver = FleetDriver::create(['fleet_uuid' => 'fleet-1', 'driver_uuid' => 'driver-1']);
178
+ expect(apiCacheQueryVersion('fleet_drivers', null))->toBe(1);
179
+
180
+ $fleetDriver->delete();
181
+ expect(apiCacheQueryVersion('fleet_drivers', null))->toBe(2);
182
+
183
+ FleetVehicle::create(['fleet_uuid' => 'fleet-1', 'vehicle_uuid' => 'vehicle-1']);
184
+ expect(apiCacheQueryVersion('fleet_vehicles', null))->toBe(1);
185
+
186
+ AssetConnection::create(['company_uuid' => 'co-1', 'connector_type' => Vehicle::class, 'connector_uuid' => 'vehicle-1', 'connected_type' => Trailer::class, 'connected_uuid' => 'trailer-1']);
187
+ expect(apiCacheQueryVersion('asset_connections', 'co-1'))->toBe(1);
188
+
189
+ TrackingPageDatabase::insert($db, 'waypoints', ['uuid' => 'wp-1', 'company_uuid' => 'co-1', 'payload_uuid' => 'payload-1', 'order' => 0]);
190
+ Waypoint::where('uuid', 'wp-1')->first()->update(['order' => 1]);
191
+ expect(apiCacheQueryVersion('waypoints', 'co-1'))->toBe(1);
192
+ });
193
+
194
+ test('writes that skip model events invalidate the tables they write', function () {
195
+ $db = apiCacheDatabase();
196
+
197
+ // Order::dispatch() saves quietly
198
+ TrackingPageDatabase::insert($db, 'orders', ['uuid' => 'order-1', 'company_uuid' => 'co-1', 'dispatched' => false]);
199
+ Order::where('uuid', 'order-1')->first()->dispatch();
200
+ expect(apiCacheQueryVersion('orders', 'co-1'))->toBe(1)
201
+ ->and($db->table('orders')->where('uuid', 'order-1')->value('dispatched'))->toBe(1);
202
+
203
+ // Waypoint::insertGetUuid() inserts with the query builder
204
+ $waypointUuid = Waypoint::insertGetUuid(['place_uuid' => 'place-1', 'order' => 0]);
205
+ expect($waypointUuid)->toBeString()
206
+ ->and(apiCacheQueryVersion('waypoints', 'co-1'))->toBe(1);
207
+
208
+ // Payload::removeWaypoints() deletes with the query builder
209
+ TrackingPageDatabase::insert($db, 'payloads', ['uuid' => 'payload-1', 'company_uuid' => 'co-1']);
210
+ $payload = new Payload();
211
+ $payload->setRawAttributes(['uuid' => 'payload-1', 'company_uuid' => 'co-1'], true);
212
+ $payload->removeWaypoints();
213
+ expect(apiCacheQueryVersion('waypoints', 'co-1'))->toBe(2);
214
+
215
+ // TrackingStatus::insertGetUuid() inserts with the query builder
216
+ expect(TrackingStatus::insertGetUuid(['tracking_number_uuid' => 'tn-1', 'status' => 'Created']))->toBeString()
217
+ ->and(apiCacheQueryVersion('tracking_statuses', 'co-1'))->toBe(1);
218
+ });
219
+
220
+ test('throttled invalidation runs at most once per window and table', function () {
221
+ Carbon::setTestNow('2026-10-12 12:00:00');
222
+
223
+ ApiCacheThrottle::invalidate(new Vehicle(), 'co-1');
224
+ ApiCacheThrottle::invalidate(new Vehicle(), 'co-1');
225
+ ApiCacheThrottle::invalidate(new Driver(), 'co-1');
226
+
227
+ expect(apiCacheQueryVersion('vehicles', 'co-1'))->toBe(1)
228
+ ->and(apiCacheQueryVersion('drivers', 'co-1'))->toBe(1);
229
+
230
+ Carbon::setTestNow(Carbon::now()->addSeconds(ApiCacheThrottle::LOCATION_WINDOW + 1));
231
+ ApiCacheThrottle::invalidate(new Vehicle(), 'co-1');
232
+
233
+ expect(apiCacheQueryVersion('vehicles', 'co-1'))->toBe(2);
234
+ });
235
+
236
+ test('attaching or detaching a device invalidates the attached model lists, and device pings do not', function () {
237
+ $db = apiCacheDatabase();
238
+ $db->getSchemaBuilder()->create('devices', function ($table) {
239
+ $table->increments('id');
240
+ foreach (['uuid', 'public_id', 'company_uuid', 'attachable_type', 'attachable_uuid', 'name', 'status'] as $column) {
241
+ $table->string($column)->nullable();
242
+ }
243
+ $table->text('meta')->nullable();
244
+ $table->timestamps();
245
+ $table->softDeletes();
246
+ });
247
+ TrackingPageDatabase::insert($db, 'devices', ['uuid' => 'device-1', 'company_uuid' => 'co-1']);
248
+ // attachTo() and detach() log activity; only the cache versions matter here
249
+ app()->instance(Spatie\Activitylog\ActivityLogger::class, new class extends Spatie\Activitylog\ActivityLogger {
250
+ public function __construct()
251
+ {
252
+ }
253
+
254
+ public function useLog(?string $logName): static
255
+ {
256
+ return $this;
257
+ }
258
+
259
+ public function setLogStatus(Spatie\Activitylog\ActivityLogStatus $logStatus): static
260
+ {
261
+ return $this;
262
+ }
263
+
264
+ public function performedOn(EloquentModel $model): static
265
+ {
266
+ return $this;
267
+ }
268
+
269
+ public function withProperties(mixed $properties): static
270
+ {
271
+ return $this;
272
+ }
273
+
274
+ public function log(string $description): ?Spatie\Activitylog\Contracts\Activity
275
+ {
276
+ return null;
277
+ }
278
+ });
279
+ app()->instance(Spatie\Activitylog\ActivityLogStatus::class, new Spatie\Activitylog\ActivityLogStatus(new Illuminate\Config\Repository(['activitylog' => ['enabled' => false]])));
280
+
281
+ $vehicle = new Vehicle();
282
+ $vehicle->setRawAttributes(['uuid' => 'vehicle-1', 'company_uuid' => 'co-1'], true);
283
+ $device = Device::where('uuid', 'device-1')->first();
284
+
285
+ expect($device->attachTo($vehicle))->toBeTrue()
286
+ ->and(apiCacheQueryVersion('vehicles', 'co-1'))->toBe(1);
287
+
288
+ // Telemetry saves the device row on every ping; vehicle lists no longer depend on it
289
+ $device->update(['status' => 'online']);
290
+ expect(apiCacheQueryVersion('vehicles', 'co-1'))->toBe(1);
291
+
292
+ expect($device->detach())->toBeTrue()
293
+ ->and(apiCacheQueryVersion('vehicles', 'co-1'))->toBe(2);
294
+
295
+ app()->forgetInstance(Spatie\Activitylog\ActivityLogger::class);
296
+ app()->forgetInstance(Spatie\Activitylog\ActivityLogStatus::class);
297
+ });
298
+
299
+ test('throttled invalidation does nothing while api caching is off', function () {
300
+ config(['api.cache.enabled' => false]);
301
+
302
+ ApiCacheThrottle::invalidate(new Vehicle(), 'co-1');
303
+
304
+ config(['api.cache.enabled' => true]);
305
+
306
+ expect(apiCacheQueryVersion('vehicles', 'co-1'))->toBe(0);
307
+ });
308
+
309
+ test('a cache outage never fails a throttled location invalidation', function () {
310
+ Cache::swap(new Repository(new class extends ArrayStore {
311
+ public function add($key, $value, $seconds)
312
+ {
313
+ throw new RuntimeException('cache unavailable');
314
+ }
315
+ }));
316
+
317
+ ApiCacheThrottle::invalidate(new Vehicle(), 'co-1');
318
+
319
+ expect(true)->toBeTrue();
320
+ });
321
+
322
+ test('telemetry writes fire their model events but invalidate at most once per window', function () {
323
+ Carbon::setTestNow('2026-10-12 12:00:00');
324
+ $vehicle = new Vehicle();
325
+ $vehicle->setRawAttributes(['uuid' => 'vehicle-1', 'company_uuid' => 'co-1'], true);
326
+ $writes = 0;
327
+ // Stands in for an evented save, whose model hooks invalidate the table
328
+ $save = function () use ($vehicle, &$writes) {
329
+ $writes++;
330
+ Fleetbase\Support\ApiModelCache::invalidateModelCache($vehicle, 'co-1');
331
+
332
+ return true;
333
+ };
334
+
335
+ foreach (range(1, 3) as $ping) {
336
+ expect(ApiCacheThrottle::telemetryWrite($vehicle, $save))->toBeTrue();
337
+ }
338
+
339
+ expect($writes)->toBe(3);
340
+ if (method_exists(Fleetbase\Support\ApiModelCache::class, 'withoutInvalidating')) {
341
+ // fleetbase/core-api#301: the saves' own invalidations are coalesced into one per window
342
+ expect(apiCacheQueryVersion('vehicles', 'co-1'))->toBe(1);
343
+ } else {
344
+ // An older core keeps invalidating on every save, as before, with no extra throttled bump
345
+ expect(apiCacheQueryVersion('vehicles', 'co-1'))->toBe(3);
346
+ }
347
+ });
@@ -0,0 +1,156 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Exceptions\OrderEditLockedException;
4
+ use Fleetbase\FleetOps\Models\Order;
5
+ use Fleetbase\FleetOps\Models\OrderConfig;
6
+ use Fleetbase\FleetOps\Support\OrderEditPolicy;
7
+
8
+ /**
9
+ * Covers the order edit lock policy: the stage each status falls into, the fields
10
+ * each stage locks or restricts, and the refusal raised for a locked edit.
11
+ */
12
+ function fleetopsEditPolicyOrder(string $status, array $attributes = [], ?OrderConfig $config = null): Order
13
+ {
14
+ $order = new Order();
15
+ $order->setRawAttributes(array_merge(['uuid' => 'order-uuid', 'public_id' => 'order_policy', 'status' => $status], $attributes), true);
16
+ $order->setRelation('orderConfig', $config);
17
+
18
+ return $order;
19
+ }
20
+
21
+ test('open statuses leave every field editable', function () {
22
+ foreach (['created', 'pending', 'accepted', 'confirmed', 'preparing'] as $status) {
23
+ $policy = OrderEditPolicy::for(fleetopsEditPolicyOrder($status));
24
+
25
+ expect($policy->stage())->toBe(OrderEditPolicy::STAGE_OPEN)
26
+ ->and($policy->locked())->toBe([])
27
+ ->and($policy->restrictions())->toBe([])
28
+ ->and($policy->check('items', ['adds' => true]))->toBeNull()
29
+ ->and($policy->check('schedule', ['from' => '2026-10-13 12:00:00', 'to' => '2026-10-14 12:00:00']))->toBeNull();
30
+ }
31
+
32
+ expect(OrderEditPolicy::for(fleetopsEditPolicyOrder('preparing'))->flagsKitchenChanges())->toBeTrue()
33
+ ->and(OrderEditPolicy::for(fleetopsEditPolicyOrder('created'))->flagsKitchenChanges())->toBeFalse();
34
+ });
35
+
36
+ test('ready orders only allow removing items and moving the schedule earlier', function () {
37
+ $policy = OrderEditPolicy::for(fleetopsEditPolicyOrder('pickup_ready'));
38
+
39
+ expect($policy->stage())->toBe(OrderEditPolicy::STAGE_READY)
40
+ ->and($policy->locked())->toBe([])
41
+ ->and($policy->restrictions())->toBe(['items' => 'removal_only', 'schedule' => 'earlier_only'])
42
+ ->and($policy->check('dropoff'))->toBeNull()
43
+ ->and($policy->check('contact'))->toBeNull()
44
+ ->and($policy->check('items', ['adds' => false]))->toBeNull()
45
+ ->and($policy->check('items', ['adds' => true]))->toBe('Once the order is ready, items can only be removed.')
46
+ ->and($policy->check('schedule', ['from' => '2026-10-13 12:00:00', 'to' => '2026-10-13 11:30:00']))->toBeNull()
47
+ ->and($policy->check('schedule', ['from' => '2026-10-13 12:00:00', 'to' => '2026-10-13 12:30:00']))->toBe('Once the order is ready, the schedule can only be moved earlier.')
48
+ ->and($policy->check('schedule', ['from' => null, 'to' => '2026-10-13 12:30:00']))->not->toBeNull();
49
+ });
50
+
51
+ test('orders a driver has keep route contact and notes editable but lock items and schedule', function () {
52
+ $cases = [
53
+ fleetopsEditPolicyOrder('dispatched'),
54
+ fleetopsEditPolicyOrder('driver_enroute'),
55
+ fleetopsEditPolicyOrder('enroute'),
56
+ fleetopsEditPolicyOrder('created', ['started' => true]),
57
+ fleetopsEditPolicyOrder('at_customs', ['dispatched' => true]),
58
+ ];
59
+
60
+ foreach ($cases as $order) {
61
+ $policy = OrderEditPolicy::for($order);
62
+
63
+ expect($policy->stage())->toBe(OrderEditPolicy::STAGE_IN_TRANSIT)
64
+ ->and(array_keys($policy->locked()))->toBe(['items', 'schedule'])
65
+ ->and($policy->allows('dropoff'))->toBeTrue()
66
+ ->and($policy->allows('waypoints'))->toBeTrue()
67
+ ->and($policy->allows('contact'))->toBeTrue()
68
+ ->and($policy->allows('notes'))->toBeTrue()
69
+ ->and($policy->allows('cod_amount'))->toBeTrue()
70
+ ->and($policy->allows('items'))->toBeFalse();
71
+ }
72
+
73
+ // a custom status before dispatch is still open
74
+ expect(OrderEditPolicy::for(fleetopsEditPolicyOrder('awaiting_stock'))->stage())->toBe(OrderEditPolicy::STAGE_OPEN);
75
+ });
76
+
77
+ test('completed canceled and expired orders and the config terminal codes lock everything', function () {
78
+ foreach (['completed', 'canceled', 'expired'] as $status) {
79
+ $policy = OrderEditPolicy::for(fleetopsEditPolicyOrder($status));
80
+
81
+ expect($policy->stage())->toBe(OrderEditPolicy::STAGE_TERMINAL)
82
+ ->and($policy->isTerminal())->toBeTrue()
83
+ ->and(array_keys($policy->locked()))->toBe(OrderEditPolicy::FIELDS)
84
+ ->and($policy->check('notes'))->toBe(sprintf('The order is %s and can no longer be changed.', $status));
85
+ }
86
+
87
+ $config = new OrderConfig();
88
+ $config->setRawAttributes([
89
+ 'uuid' => 'config-uuid',
90
+ 'meta' => json_encode(['lifecycle' => ['initial' => 'booked', 'completed' => 'delivered', 'terminal' => ['no_show']]]),
91
+ 'flow' => json_encode([
92
+ 'booked' => ['key' => 'booked', 'code' => 'booked', 'status' => 'Booked', 'activities' => ['delivered']],
93
+ 'delivered' => ['key' => 'delivered', 'code' => 'delivered', 'status' => 'Delivered', 'complete' => true],
94
+ 'no_show' => ['key' => 'no_show', 'code' => 'no_show', 'status' => 'No show'],
95
+ ]),
96
+ ], true);
97
+
98
+ expect(OrderEditPolicy::for(fleetopsEditPolicyOrder('delivered', [], $config))->stage())->toBe(OrderEditPolicy::STAGE_TERMINAL)
99
+ ->and(OrderEditPolicy::for(fleetopsEditPolicyOrder('no_show', [], $config))->stage())->toBe(OrderEditPolicy::STAGE_TERMINAL)
100
+ ->and(OrderEditPolicy::for(fleetopsEditPolicyOrder('booked', [], $config))->stage())->toBe(OrderEditPolicy::STAGE_OPEN);
101
+ });
102
+
103
+ test('asserting a locked edit throws with every refused field and the policy serializes', function () {
104
+ $policy = OrderEditPolicy::for(fleetopsEditPolicyOrder('dispatched'));
105
+
106
+ $policy->assert(['dropoff' => true, 'notes' => true]);
107
+
108
+ try {
109
+ $policy->assert(['dropoff' => true, 'items' => ['adds' => false], 'schedule' => ['from' => 'a', 'to' => 'b']]);
110
+ $this->fail('Expected the edit to be refused');
111
+ } catch (OrderEditLockedException $exception) {
112
+ expect($exception->locked)->toBe([
113
+ 'items' => 'Items cannot be changed once a driver has the order.',
114
+ 'schedule' => 'The schedule cannot be changed once a driver has the order.',
115
+ ])
116
+ ->and($exception->getCode())->toBe(422)
117
+ ->and($exception->getMessage())->toContain('Items cannot be changed')
118
+ ->and($exception->toResponseData())->toBe(['locked' => $exception->locked]);
119
+ }
120
+
121
+ $serialized = OrderEditPolicy::for(fleetopsEditPolicyOrder('ready'))->toArray();
122
+
123
+ expect($serialized['stage'])->toBe('ready')
124
+ ->and($serialized['status'])->toBe('ready')
125
+ ->and((array) $serialized['locked'])->toBe([])
126
+ ->and((array) $serialized['restrictions'])->toBe(['items' => 'removal_only', 'schedule' => 'earlier_only'])
127
+ ->and(fleetopsEditPolicyOrder('ready')->editPolicy())->toBeInstanceOf(OrderEditPolicy::class);
128
+ });
129
+
130
+ class FleetOpsEditPolicyBrokenConfig extends OrderConfig
131
+ {
132
+ public function isTerminalActivityCode(?string $code): bool
133
+ {
134
+ throw new RuntimeException('activities unavailable');
135
+ }
136
+ }
137
+
138
+ class FleetOpsEditPolicyOrderWithoutDatabase extends Order
139
+ {
140
+ public function orderConfig(): Illuminate\Database\Eloquent\Relations\BelongsTo
141
+ {
142
+ throw new RuntimeException('no database');
143
+ }
144
+ }
145
+
146
+ test('a config that cannot answer, a config that cannot load and times that do not parse all fail safe', function () {
147
+ $brokenConfig = OrderEditPolicy::for(fleetopsEditPolicyOrder('custom_status', [], new FleetOpsEditPolicyBrokenConfig()));
148
+
149
+ $withoutDatabase = new FleetOpsEditPolicyOrderWithoutDatabase();
150
+ $withoutDatabase->setRawAttributes(['uuid' => 'order-uuid', 'public_id' => 'order_policy', 'status' => 'custom_status'], true);
151
+
152
+ expect($brokenConfig->isTerminal())->toBeFalse()
153
+ ->and(OrderEditPolicy::for($withoutDatabase)->isTerminal())->toBeFalse()
154
+ // "earlier only" cannot be judged from times that do not parse, so the move is refused
155
+ ->and(OrderEditPolicy::for(fleetopsEditPolicyOrder('ready'))->check('schedule', ['from' => 'soon', 'to' => 'later']))->toBeString();
156
+ });