@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.
- package/RELEASE.md +15 -18
- package/addon/components/admin/tracking-page-settings.hbs +56 -0
- package/addon/components/admin/tracking-page-settings.js +67 -0
- package/addon/components/device/form.hbs +2 -2
- package/addon/components/driver/details.hbs +1 -1
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/equipment/form.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +11 -3
- package/addon/components/maintenance/form.hbs +3 -3
- package/addon/components/map/google-live-map.js +2 -2
- package/addon/components/map/leaflet-live-map.js +45 -32
- package/addon/components/orchestrator/phase-builder.js +2 -1
- package/addon/components/orchestrator-workbench.hbs +7 -1
- package/addon/components/orchestrator-workbench.js +16 -2
- package/addon/components/order/form/route.hbs +1 -1
- package/addon/components/order/form/route.js +13 -1
- package/addon/components/service-rate/details.hbs +1 -1
- package/addon/components/service-rate/form.hbs +1 -1
- package/addon/components/tracking-page/full-view.hbs +276 -0
- package/addon/components/tracking-page/full-view.js +474 -0
- package/addon/components/tracking-page/locale-selector.js +30 -0
- package/addon/components/tracking-page/map.hbs +44 -0
- package/addon/components/tracking-page/map.js +328 -0
- package/addon/components/tracking-page/order-list.hbs +35 -0
- package/addon/components/tracking-page/public-view.hbs +60 -0
- package/addon/components/tracking-page/settings-preview.hbs +21 -0
- package/addon/components/tracking-page/settings-preview.js +23 -0
- package/addon/components/tracking-page/verify-panel.hbs +109 -0
- package/addon/components/tracking-page.hbs +142 -0
- package/addon/components/tracking-page.js +564 -0
- package/addon/components/trailer/form.hbs +2 -2
- package/addon/components/vehicle/form.hbs +3 -3
- package/addon/components/warranty/form.hbs +2 -2
- package/addon/components/widget/live-fleet.js +54 -6
- package/addon/components/widget/proof-of-delivery.hbs +49 -0
- package/addon/components/widget/proof-of-delivery.js +62 -0
- package/addon/components/work-order/form.hbs +3 -3
- package/addon/controllers/operations/orders/index/details.js +19 -3
- package/addon/controllers/operations/scheduler/index.js +45 -14
- package/addon/controllers/settings/index.js +6 -0
- package/addon/controllers/settings/map.js +75 -0
- package/addon/controllers/settings/orchestrator.js +8 -6
- package/addon/controllers/settings/tracking-page.js +212 -0
- package/addon/extension.js +65 -1
- package/addon/helpers/parcel-box-image.js +16 -0
- package/addon/routes/operations/orders/index/new.js +5 -4
- package/addon/routes/operations/scheduler/index.js +1 -1
- package/addon/routes/settings/tracking-page.js +31 -0
- package/addon/routes.js +1 -0
- package/addon/services/location.js +69 -21
- package/addon/services/map-settings.js +28 -0
- package/addon/services/order-allocation.js +1 -1
- package/addon/styles/fleetops-engine.css +1391 -0
- package/addon/templates/settings/map.hbs +46 -0
- package/addon/templates/settings/tracking-page.hbs +318 -0
- package/addon/utils/identity-cell-resource.js +2 -56
- package/addon/utils/map-default-view.js +132 -0
- package/addon/utils/placeholder-images.js +4 -69
- package/addon/utils/resource-descriptors/assets.js +2 -57
- package/addon/utils/resource-descriptors/helpers.js +23 -122
- package/addon/utils/resource-descriptors/operations.js +2 -61
- package/addon/utils/resource-descriptors/people.js +3 -142
- package/addon/utils/resource-descriptors/shared.js +18 -0
- package/addon/utils/tracking-live-source.js +123 -0
- package/addon/utils/tracking-page-theme.js +123 -0
- package/app/components/admin/tracking-page-settings.js +1 -0
- package/app/components/tracking-page/full-view.js +1 -0
- package/app/components/tracking-page/locale-selector.js +1 -0
- package/app/components/tracking-page/map.js +1 -0
- package/app/components/tracking-page/order-list.js +1 -0
- package/app/components/tracking-page/public-view.js +1 -0
- package/app/components/tracking-page/settings-preview.js +1 -0
- package/app/components/tracking-page/verify-panel.js +1 -0
- package/app/components/tracking-page.js +1 -0
- package/app/components/widget/proof-of-delivery.js +1 -0
- package/app/controllers/settings/tracking-page.js +1 -0
- package/app/helpers/parcel-box-image.js +1 -0
- package/app/routes/settings/tracking-page.js +1 -0
- package/app/templates/settings/tracking-page.js +1 -0
- package/app/utils/map-default-view.js +1 -0
- package/app/utils/tracking-live-source.js +1 -0
- package/app/utils/tracking-page-theme.js +1 -0
- package/composer.json +2 -2
- package/extension.json +1 -1
- package/package.json +4 -4
- package/scripts/pest-bootstrap.php +4 -0
- package/server/config/fleetops.php +4 -0
- package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
- package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
- package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
- package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
- package/server/resources/views/labels/default.php +2 -2
- package/server/resources/views/labels/entity-label.php +2 -2
- package/server/resources/views/labels/waypoint-label.php +2 -2
- package/server/src/Auth/Schemas/FleetOps.php +5 -0
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
- package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
- package/server/src/Events/OrderChanged.php +257 -0
- package/server/src/Events/OrderDriverUnassigned.php +68 -0
- package/server/src/Exceptions/OrderEditLockedException.php +32 -0
- package/server/src/Expansions/CompanyExpansion.php +3 -2
- package/server/src/Expansions/UserExpansion.php +12 -10
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
- package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
- package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
- package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
- package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
- package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
- package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
- package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
- package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
- package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +84 -24
- package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
- package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
- package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
- package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
- package/server/src/Http/Filter/ContactFilter.php +1 -1
- package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
- package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
- package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
- package/server/src/Http/Filter/OrderFilter.php +39 -4
- package/server/src/Http/Filter/ProofFilter.php +18 -0
- package/server/src/Http/Filter/RouteFilter.php +18 -0
- package/server/src/Http/Filter/TelematicFilter.php +23 -0
- package/server/src/Http/Filter/WarrantyFilter.php +23 -0
- package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
- package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
- package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
- package/server/src/Http/Resources/v1/Index/Order.php +11 -2
- package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
- package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
- package/server/src/Http/Resources/v1/Zone.php +31 -23
- package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
- package/server/src/Jobs/ProcessAllocationJob.php +16 -6
- package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
- package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
- package/server/src/Listeners/HandleOrderCanceled.php +9 -1
- package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
- package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
- package/server/src/Mail/CustomerCredentialsMail.php +4 -2
- package/server/src/Models/AssetConnection.php +3 -0
- package/server/src/Models/Contact.php +3 -0
- package/server/src/Models/Device.php +28 -0
- package/server/src/Models/DeviceEvent.php +7 -0
- package/server/src/Models/Driver.php +20 -7
- package/server/src/Models/Entity.php +12 -4
- package/server/src/Models/Fleet.php +11 -32
- package/server/src/Models/FleetDriver.php +3 -0
- package/server/src/Models/FleetVehicle.php +3 -0
- package/server/src/Models/FuelReport.php +5 -13
- package/server/src/Models/Issue.php +4 -16
- package/server/src/Models/Order.php +200 -9
- package/server/src/Models/Payload.php +227 -35
- package/server/src/Models/Place.php +7 -1
- package/server/src/Models/ServiceArea.php +7 -0
- package/server/src/Models/ServiceRate.php +7 -17
- package/server/src/Models/TrackingNumber.php +101 -2
- package/server/src/Models/TrackingSession.php +33 -0
- package/server/src/Models/TrackingSessionGrant.php +22 -0
- package/server/src/Models/TrackingStatus.php +6 -0
- package/server/src/Models/Vehicle.php +21 -2
- package/server/src/Models/Vendor.php +7 -0
- package/server/src/Models/Waypoint.php +13 -4
- package/server/src/Notifications/OrderAssigned.php +2 -1
- package/server/src/Notifications/OrderCanceled.php +7 -1
- package/server/src/Notifications/OrderChanged.php +87 -0
- package/server/src/Notifications/OrderCompleted.php +2 -1
- package/server/src/Notifications/OrderDispatchFailed.php +2 -1
- package/server/src/Notifications/OrderDispatched.php +2 -1
- package/server/src/Notifications/OrderFailed.php +2 -1
- package/server/src/Notifications/OrderUnassigned.php +66 -0
- package/server/src/Notifications/WaypointCompleted.php +2 -1
- package/server/src/Observers/CompanyUserObserver.php +8 -7
- package/server/src/Observers/DriverObserver.php +6 -1
- package/server/src/Observers/OrderObserver.php +141 -1
- package/server/src/Observers/TrackingNumberObserver.php +18 -6
- package/server/src/Observers/UserObserver.php +2 -1
- package/server/src/Observers/VehicleObserver.php +8 -3
- package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
- package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
- package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
- package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
- package/server/src/Providers/EventServiceProvider.php +11 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
- package/server/src/Support/ApiCacheThrottle.php +76 -0
- package/server/src/Support/LiveOrderQuery.php +3 -1
- package/server/src/Support/OrderEdit.php +365 -0
- package/server/src/Support/OrderEditPolicy.php +292 -0
- package/server/src/Support/ProfileAccountManager.php +54 -1
- package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
- package/server/src/Support/SocketChannels.php +346 -0
- package/server/src/Support/Telematics/TelematicService.php +7 -2
- package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
- package/server/src/Support/TrackingChannel.php +55 -0
- package/server/src/Support/TrackingCode.php +242 -0
- package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
- package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
- package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
- package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
- package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
- package/server/src/Support/TrackingPage/TrackingView.php +543 -0
- package/server/src/Support/TrackingPublisher.php +261 -0
- package/server/src/Support/TrackingScope.php +182 -0
- package/server/src/Support/TrackingUpdate.php +402 -0
- package/server/src/Support/Utils.php +15 -3
- package/server/src/Traits/HasTrackingNumber.php +2 -0
- package/server/src/routes.php +39 -7
- package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
- package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
- package/server/tests/ApiEntityControllerContractsTest.php +84 -0
- package/server/tests/ApiOrderControllerContractsTest.php +245 -0
- package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
- package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
- package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
- package/server/tests/CompactResourceSerializationTest.php +23 -59
- package/server/tests/ControllerFilterContractsTest.php +1 -1
- package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
- package/server/tests/EventContractsTest.php +26 -60
- package/server/tests/ExpansionContractsTest.php +15 -2
- package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
- package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
- package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
- package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
- package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
- package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
- package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
- package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
- package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
- package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
- package/server/tests/IndexCustomerPhotoTest.php +151 -0
- package/server/tests/ModelAccessorContractsTest.php +4 -3
- package/server/tests/NotificationAndMailContractsTest.php +5 -8
- package/server/tests/ObserverContractsTest.php +72 -10
- package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
- package/server/tests/OrderFilterExecutionTest.php +167 -2
- package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
- package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
- package/server/tests/QueueJobContractsTest.php +2 -1
- package/server/tests/RequestContractsTest.php +1 -1
- package/server/tests/RouteRegistrationExecutionTest.php +26 -0
- package/server/tests/RouteSequencingEngineTest.php +17 -0
- package/server/tests/SettingControllerContractsTest.php +110 -0
- package/server/tests/Support/TrackingPageDatabase.php +200 -0
- package/server/tests/TenantScopingTest.php +206 -0
- package/server/tests/TrackingPageSettingsTest.php +343 -0
- package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
- package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
- package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
- package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
- package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
- package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
- package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
- package/server/tests/Unit/Models/FleetTest.php +4 -3
- package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
- package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
- package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
- package/server/tests/Unit/Models/PayloadTest.php +18 -4
- package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
- package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
- package/server/tests/Unit/Models/VehicleTest.php +16 -2
- package/server/tests/Unit/Models/WaypointTest.php +4 -3
- package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
- package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
- package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
- package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
- package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
- package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
- package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
- package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
- package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
- package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
- package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
- package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
- package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
- package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
- package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
- package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
- package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
- package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
- package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
- package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
- package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
- package/server/tests/VehicleResourceContractsTest.php +46 -0
- package/server/tests/VroomOrchestrationEngineTest.php +34 -1
- package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
- package/translations/ar-ae.yml +274 -0
- package/translations/bg-bg.yaml +274 -0
- package/translations/en-us.yaml +287 -0
- package/translations/es-pa.yaml +274 -0
- package/translations/fr-fr.yaml +274 -0
- package/translations/mn-mn.yaml +274 -0
- package/translations/pt-br.yaml +274 -0
- package/translations/ru-ru.yaml +276 -2
- package/translations/uk-ua.yaml +275 -1
- package/translations/vi-vn.yaml +274 -0
- package/addon/components/cell/contact-identity.hbs +0 -1
- package/addon/components/cell/customer-identity.hbs +0 -1
- package/addon/components/cell/driver-identity.hbs +0 -1
- package/addon/components/cell/fleet-identity.hbs +0 -1
- package/addon/components/cell/order-identity.hbs +0 -1
- package/addon/components/cell/place-identity.hbs +0 -1
- package/addon/components/cell/vehicle-identity.hbs +0 -1
- package/addon/components/cell/vendor-identity.hbs +0 -1
- package/addon/components/contact/pill.hbs +0 -24
- package/addon/components/contact/summary.hbs +0 -1
- package/addon/components/customer/pill.hbs +0 -24
- package/addon/components/customer/summary.hbs +0 -1
- package/addon/components/driver/pill.hbs +0 -24
- package/addon/components/driver/summary.hbs +0 -1
- package/addon/components/fleet/pill.hbs +0 -24
- package/addon/components/fleet/summary.hbs +0 -1
- package/addon/components/order/pill.hbs +0 -24
- package/addon/components/order/summary.hbs +0 -1
- package/addon/components/place/pill.hbs +0 -24
- package/addon/components/place/summary.hbs +0 -1
- package/addon/components/select-option/contact.hbs +0 -12
- package/addon/components/select-option/customer.hbs +0 -12
- package/addon/components/select-option/driver.hbs +0 -12
- package/addon/components/select-option/fleet.hbs +0 -12
- package/addon/components/select-option/order.hbs +0 -12
- package/addon/components/select-option/place.hbs +0 -12
- package/addon/components/select-option/vehicle.hbs +0 -12
- package/addon/components/select-option/vendor.hbs +0 -12
- package/addon/components/vehicle/pill.hbs +0 -24
- package/addon/components/vehicle/summary.hbs +0 -1
- package/addon/components/vendor/pill.hbs +0 -24
- package/addon/components/vendor/summary.hbs +0 -1
- package/app/components/cell/contact-identity.js +0 -1
- package/app/components/cell/customer-identity.js +0 -1
- package/app/components/cell/driver-identity.js +0 -1
- package/app/components/cell/fleet-identity.js +0 -1
- package/app/components/cell/order-identity.js +0 -1
- package/app/components/cell/place-identity.js +0 -1
- package/app/components/cell/vehicle-identity.js +0 -1
- package/app/components/cell/vendor-identity.js +0 -1
- package/app/components/contact/pill.js +0 -1
- package/app/components/contact/summary.js +0 -1
- package/app/components/customer/pill.js +0 -1
- package/app/components/customer/summary.js +0 -1
- package/app/components/driver/pill.js +0 -1
- package/app/components/driver/summary.js +0 -1
- package/app/components/fleet/pill.js +0 -1
- package/app/components/fleet/summary.js +0 -1
- package/app/components/order/pill.js +0 -1
- package/app/components/order/summary.js +0 -1
- package/app/components/place/pill.js +0 -1
- package/app/components/place/summary.js +0 -1
- package/app/components/select-option/contact.js +0 -1
- package/app/components/select-option/customer.js +0 -1
- package/app/components/select-option/driver.js +0 -1
- package/app/components/select-option/fleet.js +0 -1
- package/app/components/select-option/order.js +0 -1
- package/app/components/select-option/place.js +0 -1
- package/app/components/select-option/vehicle.js +0 -1
- package/app/components/select-option/vendor.js +0 -1
- package/app/components/vehicle/pill.js +0 -1
- package/app/components/vehicle/summary.js +0 -1
- package/app/components/vendor/pill.js +0 -1
- package/app/components/vendor/summary.js +0 -1
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Support;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Index\Customer as CustomerIndexResource;
|
|
5
6
|
use Fleetbase\FleetOps\Models\Order;
|
|
6
7
|
use Illuminate\Database\Eloquent\Builder;
|
|
8
|
+
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
7
9
|
|
|
8
10
|
class LiveOrderQuery
|
|
9
11
|
{
|
|
@@ -65,7 +67,7 @@ class LiveOrderQuery
|
|
|
65
67
|
'vehicleAssigned' => function ($query) {
|
|
66
68
|
$query->without(['fleets', 'vendor']);
|
|
67
69
|
},
|
|
68
|
-
'customer',
|
|
70
|
+
'customer' => static fn (MorphTo $relation) => CustomerIndexResource::withPhoto($relation),
|
|
69
71
|
'facilitator',
|
|
70
72
|
]);
|
|
71
73
|
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Events\OrderChanged;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Entity;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Payload;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Place;
|
|
10
|
+
use Illuminate\Support\Arr;
|
|
11
|
+
use Illuminate\Support\Carbon;
|
|
12
|
+
use Illuminate\Support\Facades\Log;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One edit of an order, from the request that changes it to the events that announce it.
|
|
16
|
+
*
|
|
17
|
+
* begin() records the order as it is; guard() refuses changes its status locks
|
|
18
|
+
* (see OrderEditPolicy); commit() works out what actually changed and then:
|
|
19
|
+
*
|
|
20
|
+
* - keeps the driver on the right stop when the route changed (reconcileCurrentWaypoint),
|
|
21
|
+
* - recomputes the order's distance and time and republishes public tracking,
|
|
22
|
+
* - flags item changes made while the order is being prepared (`meta.kitchen_changed_at`),
|
|
23
|
+
* - broadcasts `order.changed` and pushes the change to the assigned driver.
|
|
24
|
+
*
|
|
25
|
+
* $edit = OrderEdit::begin($order);
|
|
26
|
+
* $edit->guard($edit->requestedChanges($input));
|
|
27
|
+
* // ...apply the edit...
|
|
28
|
+
* $edit->commit();
|
|
29
|
+
*/
|
|
30
|
+
class OrderEdit
|
|
31
|
+
{
|
|
32
|
+
public const ENTITIES_APPEND = 'append';
|
|
33
|
+
public const ENTITIES_REPLACE = 'replace';
|
|
34
|
+
|
|
35
|
+
protected OrderEditPolicy $policy;
|
|
36
|
+
|
|
37
|
+
protected ?array $routeBefore = null;
|
|
38
|
+
|
|
39
|
+
protected array $entitiesBefore = [];
|
|
40
|
+
|
|
41
|
+
protected mixed $codBefore = null;
|
|
42
|
+
|
|
43
|
+
protected ?string $scheduledBefore = null;
|
|
44
|
+
|
|
45
|
+
protected ?string $notesBefore = null;
|
|
46
|
+
|
|
47
|
+
protected ?string $customerBefore = null;
|
|
48
|
+
|
|
49
|
+
protected array $marked = [];
|
|
50
|
+
|
|
51
|
+
public function __construct(public Order $order)
|
|
52
|
+
{
|
|
53
|
+
$this->policy = OrderEditPolicy::for($order);
|
|
54
|
+
|
|
55
|
+
$payload = $this->currentPayload();
|
|
56
|
+
if ($payload instanceof Payload) {
|
|
57
|
+
$this->routeBefore = $payload->routeSignature();
|
|
58
|
+
$this->entitiesBefore = $this->entitySnapshot($payload);
|
|
59
|
+
$this->codBefore = $payload->cod_amount;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
$this->scheduledBefore = static::timeString($order->scheduled_at);
|
|
63
|
+
$this->notesBefore = $order->notes;
|
|
64
|
+
$this->customerBefore = $order->customer_uuid;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public static function begin(Order $order): static
|
|
68
|
+
{
|
|
69
|
+
return new static($order);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public function policy(): OrderEditPolicy
|
|
73
|
+
{
|
|
74
|
+
return $this->policy;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalize an entities mode from a request value: `replace` (or `sync`) replaces
|
|
79
|
+
* the list, anything else appends as the API always has.
|
|
80
|
+
*/
|
|
81
|
+
public static function entitiesMode(mixed $value): string
|
|
82
|
+
{
|
|
83
|
+
return in_array(strtolower((string) $value), ['replace', 'sync'], true) ? self::ENTITIES_REPLACE : self::ENTITIES_APPEND;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The policy fields an edit request touches, with the context the policy checks.
|
|
88
|
+
*
|
|
89
|
+
* Keys read from $input (presence matters): pickup, dropoff, return, waypoints,
|
|
90
|
+
* entities, scheduled_at, notes, cod_amount, customer.
|
|
91
|
+
*
|
|
92
|
+
* @return array<string, array|bool>
|
|
93
|
+
*/
|
|
94
|
+
public function requestedChanges(array $input, string $entitiesMode = self::ENTITIES_APPEND): array
|
|
95
|
+
{
|
|
96
|
+
$changes = [];
|
|
97
|
+
|
|
98
|
+
foreach (OrderEditPolicy::ROUTE_FIELDS as $field) {
|
|
99
|
+
if (array_key_exists($field, $input)) {
|
|
100
|
+
$changes[$field] = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
$entities = $input['entities'] ?? null;
|
|
105
|
+
if (is_array($entities) && ($entities !== [] || $entitiesMode === self::ENTITIES_REPLACE)) {
|
|
106
|
+
$payload = $this->currentPayload();
|
|
107
|
+
$adds = true;
|
|
108
|
+
if ($entitiesMode === self::ENTITIES_REPLACE && $payload instanceof Payload) {
|
|
109
|
+
$adds = !$payload->entitiesListOnlyNamesExisting($entities);
|
|
110
|
+
}
|
|
111
|
+
$changes[OrderEditPolicy::FIELD_ITEMS] = ['adds' => $adds];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (array_key_exists('scheduled_at', $input) && static::timeString($input['scheduled_at']) !== $this->scheduledBefore) {
|
|
115
|
+
$changes[OrderEditPolicy::FIELD_SCHEDULE] = ['from' => $this->scheduledBefore, 'to' => $input['scheduled_at']];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (array_key_exists('notes', $input) && (string) $input['notes'] !== (string) $this->notesBefore) {
|
|
119
|
+
$changes[OrderEditPolicy::FIELD_NOTES] = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (array_key_exists('cod_amount', $input) && !static::sameAmount($input['cod_amount'], $this->codBefore)) {
|
|
123
|
+
$changes[OrderEditPolicy::FIELD_COD_AMOUNT] = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (array_key_exists('customer', $input) && !empty($input['customer']) && !$this->isCurrentCustomer($input['customer'])) {
|
|
127
|
+
$changes[OrderEditPolicy::FIELD_CONTACT] = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return $changes;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Refuse the edit when its status locks any requested field.
|
|
135
|
+
*
|
|
136
|
+
* @throws \Fleetbase\FleetOps\Exceptions\OrderEditLockedException
|
|
137
|
+
*/
|
|
138
|
+
public function guard(array $changes): static
|
|
139
|
+
{
|
|
140
|
+
$this->policy->assert($changes);
|
|
141
|
+
|
|
142
|
+
return $this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Record a change the snapshot cannot see (for example a total recomputed elsewhere).
|
|
147
|
+
*/
|
|
148
|
+
public function markChanged(string ...$changes): static
|
|
149
|
+
{
|
|
150
|
+
$this->marked = array_values(array_unique(array_merge($this->marked, $changes)));
|
|
151
|
+
|
|
152
|
+
return $this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Work out what changed since begin(), apply the follow-up effects and announce it.
|
|
157
|
+
*
|
|
158
|
+
* @param array $extra order.changed data overrides (summary, total, currency, changed_by, ...)
|
|
159
|
+
* @param bool $notifyDriver push the change to the assigned driver when it concerns them
|
|
160
|
+
*
|
|
161
|
+
* @return string[] the changes announced
|
|
162
|
+
*/
|
|
163
|
+
public function commit(array $extra = [], bool $notifyDriver = true): array
|
|
164
|
+
{
|
|
165
|
+
$order = $this->order;
|
|
166
|
+
$payload = $this->freshPayload();
|
|
167
|
+
$changes = [];
|
|
168
|
+
|
|
169
|
+
$routeChanges = [];
|
|
170
|
+
if ($payload instanceof Payload) {
|
|
171
|
+
$routeAfter = $payload->routeSignature();
|
|
172
|
+
$routeChanges = $this->routeBefore ? Payload::routeChanges($this->routeBefore, $routeAfter) : ['pickup', 'dropoff', 'waypoints'];
|
|
173
|
+
$routeChanges = array_values(array_filter($routeChanges, function ($part) use ($routeAfter) {
|
|
174
|
+
// A route part that was and still is empty did not change.
|
|
175
|
+
return !empty($this->routeBefore[$part] ?? null) || !empty($routeAfter[$part]);
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
if ($routeChanges) {
|
|
179
|
+
$payload->reconcileCurrentWaypoint($this->routeBefore ?? [], $this->isUnderWay());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if ($this->entitySnapshot($payload) !== $this->entitiesBefore) {
|
|
183
|
+
$changes[] = OrderEditPolicy::FIELD_ITEMS;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!static::sameAmount($payload->cod_amount, $this->codBefore)) {
|
|
187
|
+
$changes[] = OrderEditPolicy::FIELD_COD_AMOUNT;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
$order->setRelation('payload', $payload);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
$changes = array_merge($routeChanges, $changes);
|
|
194
|
+
|
|
195
|
+
if (static::timeString($order->scheduled_at) !== $this->scheduledBefore) {
|
|
196
|
+
$changes[] = OrderEditPolicy::FIELD_SCHEDULE;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if ((string) $order->notes !== (string) $this->notesBefore) {
|
|
200
|
+
$changes[] = OrderEditPolicy::FIELD_NOTES;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if ($order->customer_uuid !== $this->customerBefore) {
|
|
204
|
+
$changes[] = OrderEditPolicy::FIELD_CONTACT;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
$changes = array_values(array_unique(array_merge($changes, $this->marked)));
|
|
208
|
+
|
|
209
|
+
if ($routeChanges) {
|
|
210
|
+
$this->afterRouteChanged();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (in_array(OrderEditPolicy::FIELD_ITEMS, $changes, true) && $this->policy->flagsKitchenChanges()) {
|
|
214
|
+
$this->attempt(fn () => $order->updateMeta('kitchen_changed_at', Carbon::now()->toIso8601ZuluString()));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if ($changes) {
|
|
218
|
+
$summary = $extra['summary'] ?? static::summarize($changes, $payload, $order);
|
|
219
|
+
unset($extra['summary']);
|
|
220
|
+
$this->announce($changes, (string) $summary, $extra, $notifyDriver);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return $changes;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* One line describing the changes, e.g. "Drop-off changed to 8 Marina View · Items changed".
|
|
228
|
+
*/
|
|
229
|
+
public static function summarize(array $changes, ?Payload $payload = null, ?Order $order = null): string
|
|
230
|
+
{
|
|
231
|
+
$parts = [];
|
|
232
|
+
|
|
233
|
+
foreach ($changes as $change) {
|
|
234
|
+
$parts[] = match ($change) {
|
|
235
|
+
'dropoff' => static::placeLine('Drop-off', $payload?->dropoff),
|
|
236
|
+
'pickup' => static::placeLine('Pickup', $payload?->pickup),
|
|
237
|
+
'return' => static::placeLine('Return', $payload?->return),
|
|
238
|
+
'waypoints' => 'Stops changed',
|
|
239
|
+
'items' => 'Items changed',
|
|
240
|
+
'schedule' => $order?->scheduled_at ? 'Rescheduled to ' . Carbon::parse($order->scheduled_at)->format('j M H:i') : 'Schedule cleared',
|
|
241
|
+
'notes' => 'Notes updated',
|
|
242
|
+
'cod_amount' => $payload && $payload->cod_amount !== null ? 'Cash to collect changed to ' . trim(($payload->cod_currency ?? '') . ' ' . $payload->cod_amount) : 'Cash to collect changed',
|
|
243
|
+
'contact' => 'Customer changed',
|
|
244
|
+
'total' => 'Total changed',
|
|
245
|
+
default => ucfirst(str_replace('_', ' ', (string) $change)) . ' changed',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return implode(' · ', array_filter($parts));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
protected function afterRouteChanged(): void
|
|
253
|
+
{
|
|
254
|
+
$order = $this->order;
|
|
255
|
+
|
|
256
|
+
$this->attempt(fn () => $order->setDistanceAndTime());
|
|
257
|
+
$this->attempt(fn () => app(TrackingPublisher::class)->routeChanged($order));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
protected function announce(array $changes, string $summary, array $extra, bool $notifyDriver): void
|
|
261
|
+
{
|
|
262
|
+
OrderChanged::dispatchFor($this->order, $changes, $summary, $extra, $notifyDriver);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Whether the driver is already working the order, so a route change must keep their place.
|
|
267
|
+
*/
|
|
268
|
+
protected function isUnderWay(): bool
|
|
269
|
+
{
|
|
270
|
+
return (bool) $this->order->started || $this->policy->stage() === OrderEditPolicy::STAGE_IN_TRANSIT;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
protected function currentPayload(): ?Payload
|
|
274
|
+
{
|
|
275
|
+
$payload = $this->order->relationLoaded('payload') ? $this->order->getRelation('payload') : $this->attempt(fn () => $this->order->payload);
|
|
276
|
+
|
|
277
|
+
return $payload instanceof Payload ? $payload : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
protected function freshPayload(): ?Payload
|
|
281
|
+
{
|
|
282
|
+
if (!$this->order->payload_uuid) {
|
|
283
|
+
return $this->currentPayload();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
$payload = $this->attempt(fn () => Payload::where('uuid', $this->order->payload_uuid)->withoutGlobalScopes()->first());
|
|
287
|
+
|
|
288
|
+
return $payload instanceof Payload ? $payload : $this->currentPayload();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Entity uuid => fingerprint of its stored attributes.
|
|
293
|
+
*/
|
|
294
|
+
protected function entitySnapshot(Payload $payload): array
|
|
295
|
+
{
|
|
296
|
+
if (!$payload->uuid) {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
$entities = $this->attempt(fn () => Entity::where('payload_uuid', $payload->uuid)->get()) ?? collect();
|
|
301
|
+
|
|
302
|
+
return $entities->mapWithKeys(function (Entity $entity) {
|
|
303
|
+
$attributes = Arr::except($entity->getAttributes(), ['id', 'created_at', 'updated_at', 'qr_code', 'barcode']);
|
|
304
|
+
ksort($attributes);
|
|
305
|
+
|
|
306
|
+
return [$entity->uuid => md5((string) json_encode($attributes))];
|
|
307
|
+
})->sortKeys()->all();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
protected function isCurrentCustomer(mixed $customer): bool
|
|
311
|
+
{
|
|
312
|
+
if (!is_string($customer)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if ($customer === $this->customerBefore) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return $this->attempt(fn () => data_get($this->order, 'customer.public_id')) === $customer;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
protected static function placeLine(string $label, mixed $place): string
|
|
324
|
+
{
|
|
325
|
+
if (!$place instanceof Place) {
|
|
326
|
+
return $label . ' changed';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
$address = trim((string) ($place->address ?: $place->name ?: $place->street1));
|
|
330
|
+
|
|
331
|
+
return $address !== '' ? $label . ' changed to ' . $address : $label . ' changed';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
protected static function timeString(mixed $value): ?string
|
|
335
|
+
{
|
|
336
|
+
if (empty($value)) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
return Carbon::parse($value)->toIso8601ZuluString();
|
|
342
|
+
} catch (\Throwable) {
|
|
343
|
+
return (string) $value;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
protected static function sameAmount(mixed $a, mixed $b): bool
|
|
348
|
+
{
|
|
349
|
+
$a = ($a === '' || $a === null) ? null : (is_numeric($a) ? (float) $a : $a);
|
|
350
|
+
$b = ($b === '' || $b === null) ? null : (is_numeric($b) ? (float) $b : $b);
|
|
351
|
+
|
|
352
|
+
return $a === $b;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
protected function attempt(callable $callback): mixed
|
|
356
|
+
{
|
|
357
|
+
try {
|
|
358
|
+
return $callback();
|
|
359
|
+
} catch (\Throwable $exception) {
|
|
360
|
+
Log::debug('Order edit follow-up failed', ['order' => $this->order->public_id, 'error' => $exception->getMessage()]);
|
|
361
|
+
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Exceptions\OrderEditLockedException;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
7
|
+
use Fleetbase\FleetOps\Models\OrderConfig;
|
|
8
|
+
use Illuminate\Support\Carbon;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Decides which parts of an order may still be edited at its current status.
|
|
12
|
+
*
|
|
13
|
+
* The policy groups statuses into four stages:
|
|
14
|
+
*
|
|
15
|
+
* - open (created, pending, accepted/confirmed, preparing): everything is editable.
|
|
16
|
+
* - ready (ready, pickup_ready): route, contact, notes and cash to collect are editable;
|
|
17
|
+
* items may only be removed and the schedule may only move earlier.
|
|
18
|
+
* - in transit (dispatched, started, enroute, driver_*): route (the driver is rerouted),
|
|
19
|
+
* contact, notes and cash to collect are editable; items and schedule are locked.
|
|
20
|
+
* - terminal (the config's complete or terminal activity, canceled, expired): nothing is editable.
|
|
21
|
+
*
|
|
22
|
+
* Fields are named the way edit requests and `order.changed` events name them:
|
|
23
|
+
* pickup, dropoff, return, waypoints, items, schedule, contact, notes, cod_amount.
|
|
24
|
+
*/
|
|
25
|
+
class OrderEditPolicy
|
|
26
|
+
{
|
|
27
|
+
public const STAGE_OPEN = 'open';
|
|
28
|
+
public const STAGE_READY = 'ready';
|
|
29
|
+
public const STAGE_IN_TRANSIT = 'in_transit';
|
|
30
|
+
public const STAGE_TERMINAL = 'terminal';
|
|
31
|
+
|
|
32
|
+
public const FIELD_PICKUP = 'pickup';
|
|
33
|
+
public const FIELD_DROPOFF = 'dropoff';
|
|
34
|
+
public const FIELD_RETURN = 'return';
|
|
35
|
+
public const FIELD_WAYPOINTS = 'waypoints';
|
|
36
|
+
public const FIELD_ITEMS = 'items';
|
|
37
|
+
public const FIELD_SCHEDULE = 'schedule';
|
|
38
|
+
public const FIELD_CONTACT = 'contact';
|
|
39
|
+
public const FIELD_NOTES = 'notes';
|
|
40
|
+
public const FIELD_COD_AMOUNT = 'cod_amount';
|
|
41
|
+
|
|
42
|
+
public const FIELDS = [
|
|
43
|
+
self::FIELD_PICKUP,
|
|
44
|
+
self::FIELD_DROPOFF,
|
|
45
|
+
self::FIELD_RETURN,
|
|
46
|
+
self::FIELD_WAYPOINTS,
|
|
47
|
+
self::FIELD_ITEMS,
|
|
48
|
+
self::FIELD_SCHEDULE,
|
|
49
|
+
self::FIELD_CONTACT,
|
|
50
|
+
self::FIELD_NOTES,
|
|
51
|
+
self::FIELD_COD_AMOUNT,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
public const ROUTE_FIELDS = [self::FIELD_PICKUP, self::FIELD_DROPOFF, self::FIELD_RETURN, self::FIELD_WAYPOINTS];
|
|
55
|
+
|
|
56
|
+
public const OPEN_STATUSES = ['created', 'pending', 'accepted', 'confirmed', 'preparing'];
|
|
57
|
+
public const READY_STATUSES = ['ready', 'pickup_ready', 'ready_for_pickup'];
|
|
58
|
+
public const IN_TRANSIT_STATUSES = ['dispatched', 'started', 'enroute', 'en_route', 'in_progress', 'picked_up', 'arrived', 'out_for_delivery'];
|
|
59
|
+
public const TERMINAL_STATUSES = ['completed', 'canceled', 'cancelled', 'expired'];
|
|
60
|
+
|
|
61
|
+
public const RESTRICTION_REMOVAL_ONLY = 'removal_only';
|
|
62
|
+
public const RESTRICTION_EARLIER_ONLY = 'earlier_only';
|
|
63
|
+
|
|
64
|
+
protected ?string $stage = null;
|
|
65
|
+
|
|
66
|
+
public function __construct(public Order $order)
|
|
67
|
+
{
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public static function for(Order $order): self
|
|
71
|
+
{
|
|
72
|
+
return new static($order);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The order status the policy reads, normalized to snake case.
|
|
77
|
+
*/
|
|
78
|
+
public function status(): string
|
|
79
|
+
{
|
|
80
|
+
return strtolower((string) ($this->order->status ?: 'created'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The edit stage of the order.
|
|
85
|
+
*/
|
|
86
|
+
public function stage(): string
|
|
87
|
+
{
|
|
88
|
+
if ($this->stage !== null) {
|
|
89
|
+
return $this->stage;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$status = $this->status();
|
|
93
|
+
|
|
94
|
+
if ($this->isTerminal()) {
|
|
95
|
+
return $this->stage = self::STAGE_TERMINAL;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (in_array($status, self::READY_STATUSES, true)) {
|
|
99
|
+
return $this->stage = self::STAGE_READY;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if ((bool) $this->order->started || in_array($status, self::IN_TRANSIT_STATUSES, true) || str_starts_with($status, 'driver_')) {
|
|
103
|
+
return $this->stage = self::STAGE_IN_TRANSIT;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (in_array($status, self::OPEN_STATUSES, true)) {
|
|
107
|
+
return $this->stage = self::STAGE_OPEN;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// A custom status after dispatch means the order is out with a driver.
|
|
111
|
+
return $this->stage = (bool) $this->order->dispatched ? self::STAGE_IN_TRANSIT : self::STAGE_OPEN;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether the order is completed, canceled or expired: completed meaning the
|
|
116
|
+
* order config's complete activity or one of its terminal activity codes.
|
|
117
|
+
*/
|
|
118
|
+
public function isTerminal(): bool
|
|
119
|
+
{
|
|
120
|
+
$status = $this->status();
|
|
121
|
+
|
|
122
|
+
if (in_array($status, self::TERMINAL_STATUSES, true)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (in_array($status, array_merge(self::OPEN_STATUSES, self::READY_STATUSES, self::IN_TRANSIT_STATUSES), true)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$config = $this->orderConfig();
|
|
131
|
+
if (!$config) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if ($config->isTerminalActivityCode($status)) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
$activity = $config->getActivityByCode($status);
|
|
141
|
+
|
|
142
|
+
return $activity ? $activity->complete() : false;
|
|
143
|
+
} catch (\Throwable) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fields locked outright at this stage, each with the reason.
|
|
150
|
+
*
|
|
151
|
+
* @return array<string, string>
|
|
152
|
+
*/
|
|
153
|
+
public function locked(): array
|
|
154
|
+
{
|
|
155
|
+
$stage = $this->stage();
|
|
156
|
+
|
|
157
|
+
if ($stage === self::STAGE_TERMINAL) {
|
|
158
|
+
$reason = sprintf('The order is %s and can no longer be changed.', str_replace('_', ' ', $this->status()));
|
|
159
|
+
|
|
160
|
+
return array_fill_keys(self::FIELDS, $reason);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if ($stage === self::STAGE_IN_TRANSIT) {
|
|
164
|
+
return [
|
|
165
|
+
self::FIELD_ITEMS => 'Items cannot be changed once a driver has the order.',
|
|
166
|
+
self::FIELD_SCHEDULE => 'The schedule cannot be changed once a driver has the order.',
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fields that are only partly editable at this stage.
|
|
175
|
+
*
|
|
176
|
+
* @return array<string, string> field => removal_only|earlier_only
|
|
177
|
+
*/
|
|
178
|
+
public function restrictions(): array
|
|
179
|
+
{
|
|
180
|
+
if ($this->stage() === self::STAGE_READY) {
|
|
181
|
+
return [
|
|
182
|
+
self::FIELD_ITEMS => self::RESTRICTION_REMOVAL_ONLY,
|
|
183
|
+
self::FIELD_SCHEDULE => self::RESTRICTION_EARLIER_ONLY,
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Whether the field may be edited at all at this stage.
|
|
192
|
+
*/
|
|
193
|
+
public function allows(string $field): bool
|
|
194
|
+
{
|
|
195
|
+
return !array_key_exists($field, $this->locked());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Why a change to the field is refused, or null when it is allowed.
|
|
200
|
+
*
|
|
201
|
+
* Context keys:
|
|
202
|
+
* - items: `adds` (bool) whether the change adds or changes items rather than only removing them.
|
|
203
|
+
* - schedule: `from` and `to` (the current and requested scheduled time).
|
|
204
|
+
*/
|
|
205
|
+
public function check(string $field, array $context = []): ?string
|
|
206
|
+
{
|
|
207
|
+
$locked = $this->locked();
|
|
208
|
+
if (isset($locked[$field])) {
|
|
209
|
+
return $locked[$field];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
$restriction = $this->restrictions()[$field] ?? null;
|
|
213
|
+
|
|
214
|
+
if ($restriction === self::RESTRICTION_REMOVAL_ONLY && ($context['adds'] ?? true)) {
|
|
215
|
+
return 'Once the order is ready, items can only be removed.';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if ($restriction === self::RESTRICTION_EARLIER_ONLY && !static::isEarlier($context['from'] ?? null, $context['to'] ?? null)) {
|
|
219
|
+
return 'Once the order is ready, the schedule can only be moved earlier.';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Refuse the edit when any requested field is locked.
|
|
227
|
+
*
|
|
228
|
+
* @param array<string, array|bool> $changes field => context (see check()), or field => true
|
|
229
|
+
*
|
|
230
|
+
* @throws OrderEditLockedException
|
|
231
|
+
*/
|
|
232
|
+
public function assert(array $changes): void
|
|
233
|
+
{
|
|
234
|
+
$refused = [];
|
|
235
|
+
|
|
236
|
+
foreach ($changes as $field => $context) {
|
|
237
|
+
$reason = $this->check($field, is_array($context) ? $context : []);
|
|
238
|
+
if ($reason !== null) {
|
|
239
|
+
$refused[$field] = $reason;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if ($refused) {
|
|
244
|
+
throw new OrderEditLockedException($refused);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Whether item changes at this status must be flagged for the kitchen.
|
|
250
|
+
*/
|
|
251
|
+
public function flagsKitchenChanges(): bool
|
|
252
|
+
{
|
|
253
|
+
return $this->status() === 'preparing';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The policy as data, for clients that mirror it.
|
|
258
|
+
*/
|
|
259
|
+
public function toArray(): array
|
|
260
|
+
{
|
|
261
|
+
return [
|
|
262
|
+
'stage' => $this->stage(),
|
|
263
|
+
'status' => $this->status(),
|
|
264
|
+
'locked' => (object) $this->locked(),
|
|
265
|
+
'restrictions' => (object) $this->restrictions(),
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
protected function orderConfig(): ?OrderConfig
|
|
270
|
+
{
|
|
271
|
+
try {
|
|
272
|
+
$config = $this->order->relationLoaded('orderConfig') ? $this->order->orderConfig : $this->order->orderConfig()->first();
|
|
273
|
+
} catch (\Throwable) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return $config instanceof OrderConfig ? $config : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
protected static function isEarlier(mixed $from, mixed $to): bool
|
|
281
|
+
{
|
|
282
|
+
if (empty($from) || empty($to)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
return Carbon::parse($to)->lessThanOrEqualTo(Carbon::parse($from));
|
|
288
|
+
} catch (\Throwable) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|