@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
|
@@ -14,7 +14,7 @@ use Illuminate\Http\Request;
|
|
|
14
14
|
* through ProfileAccountManager, and the exception-to-error-response
|
|
15
15
|
* branches. Role assignment requires the full spatie permission boot, which
|
|
16
16
|
* this fixture leaves out, so flows settle in the documented error branch
|
|
17
|
-
*
|
|
17
|
+
* and the account created for the failed driver is removed again (see
|
|
18
18
|
* DriverControllerExistingUserAdoptionTest for the full flow).
|
|
19
19
|
*/
|
|
20
20
|
if (!function_exists('Fleetbase\Support\session')) {
|
|
@@ -157,6 +157,17 @@ function fleetopsInternalDriverCreateBoot(): SQLiteConnection
|
|
|
157
157
|
return $connection;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Keep a copy of every removed user row so a test can inspect the account
|
|
162
|
+
* that was created and then cleaned up when the driver failed.
|
|
163
|
+
*/
|
|
164
|
+
function fleetopsInternalDriverCreateTrackRemovedUsers(SQLiteConnection $connection): void
|
|
165
|
+
{
|
|
166
|
+
$columns = $connection->getSchemaBuilder()->getColumnListing('users');
|
|
167
|
+
$connection->statement('create table removed_users as select * from users where 0');
|
|
168
|
+
$connection->statement('create trigger keep_removed_users after delete on users begin insert into removed_users (' . implode(', ', $columns) . ') values (' . implode(', ', array_map(fn ($column) => 'old.' . $column, $columns)) . '); end');
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
function fleetopsInternalDriverCreateRequest(array $driver): Request
|
|
161
172
|
{
|
|
162
173
|
return Request::create('/int/v1/drivers', 'POST', ['driver' => $driver]);
|
|
@@ -164,6 +175,7 @@ function fleetopsInternalDriverCreateRequest(array $driver): Request
|
|
|
164
175
|
|
|
165
176
|
test('create record provisions a driver-typed user before role assignment', function () {
|
|
166
177
|
$connection = fleetopsInternalDriverCreateBoot();
|
|
178
|
+
fleetopsInternalDriverCreateTrackRemovedUsers($connection);
|
|
167
179
|
|
|
168
180
|
$result = (new DriverController())->createRecord(fleetopsInternalDriverCreateRequest([
|
|
169
181
|
'name' => 'New Driver',
|
|
@@ -172,17 +184,18 @@ test('create record provisions a driver-typed user before role assignment', func
|
|
|
172
184
|
]));
|
|
173
185
|
|
|
174
186
|
// The user record is created and typed before role assignment fails in
|
|
175
|
-
// the harness; the exception surfaces through the error response branch
|
|
187
|
+
// the harness; the exception surfaces through the error response branch
|
|
188
|
+
// and the account is removed so no orphan login is left behind.
|
|
176
189
|
expect($result)->toBeInstanceOf(JsonResponse::class)
|
|
177
190
|
->and($result->getData(true))->toHaveKey('error')
|
|
178
|
-
->and($connection->table('users')->count())->toBe(
|
|
179
|
-
->and($connection->table('
|
|
180
|
-
->and($connection->table('
|
|
181
|
-
->and($connection->table('company_users')->count())->toBeGreaterThanOrEqual(0);
|
|
191
|
+
->and($connection->table('users')->count())->toBe(0)
|
|
192
|
+
->and($connection->table('removed_users')->value('type'))->toBe('driver')
|
|
193
|
+
->and($connection->table('removed_users')->value('email'))->toBe('newdriver@example.com');
|
|
182
194
|
});
|
|
183
195
|
|
|
184
196
|
test('create record ignores a picked user account and resolves the login from the email', function () {
|
|
185
197
|
$connection = fleetopsInternalDriverCreateBoot();
|
|
198
|
+
fleetopsInternalDriverCreateTrackRemovedUsers($connection);
|
|
186
199
|
$connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'name' => 'Existing', 'email' => 'existing@example.com', 'type' => 'driver']);
|
|
187
200
|
|
|
188
201
|
(new DriverController())->createRecord(fleetopsInternalDriverCreateRequest([
|
|
@@ -193,13 +206,14 @@ test('create record ignores a picked user account and resolves the login from th
|
|
|
193
206
|
|
|
194
207
|
// The login account is managed by the profile: a new driver account is
|
|
195
208
|
// created for the email rather than the picked account being taken over
|
|
196
|
-
expect($connection->table('users')->count())->toBe(
|
|
197
|
-
->and($connection->table('
|
|
209
|
+
expect($connection->table('users')->count())->toBe(1)
|
|
210
|
+
->and($connection->table('removed_users')->where('email', 'someone@example.com')->value('type'))->toBe('driver')
|
|
198
211
|
->and($connection->table('users')->where('uuid', '11111111-1111-4111-8111-111111111111')->value('name'))->toBe('Existing');
|
|
199
212
|
});
|
|
200
213
|
|
|
201
214
|
test('create record applies photo avatars to the new driver account', function () {
|
|
202
215
|
$connection = fleetopsInternalDriverCreateBoot();
|
|
216
|
+
fleetopsInternalDriverCreateTrackRemovedUsers($connection);
|
|
203
217
|
|
|
204
218
|
$result = (new DriverController())->createRecord(fleetopsInternalDriverCreateRequest([
|
|
205
219
|
'name' => 'Photo Driver',
|
|
@@ -208,7 +222,7 @@ test('create record applies photo avatars to the new driver account', function (
|
|
|
208
222
|
|
|
209
223
|
// Avatar is set when the account is created, before the harness
|
|
210
224
|
// role-assignment limitation.
|
|
211
|
-
expect($connection->table('
|
|
225
|
+
expect($connection->table('removed_users')->value('avatar_uuid'))->toBe('22222222-2222-4222-8222-222222222222')
|
|
212
226
|
->and($result)->toBeInstanceOf(JsonResponse::class)
|
|
213
227
|
->and($result->getData(true))->toHaveKey('error');
|
|
214
228
|
});
|
|
@@ -327,6 +327,9 @@ function fleetopsOrchestrationOrder(string $publicId, ?string $dropoffUuid = 'dr
|
|
|
327
327
|
$payload = new Payload();
|
|
328
328
|
$payload->setRawAttributes(['uuid' => 'payload-' . $publicId], true);
|
|
329
329
|
$payload->setRelation('dropoff', $dropoff);
|
|
330
|
+
// the run resolves route stops, so every stop relation is set up front
|
|
331
|
+
$payload->setRelation('pickup', null);
|
|
332
|
+
$payload->setRelation('waypoints', collect());
|
|
330
333
|
|
|
331
334
|
$order = new FleetOpsOrchestrationCommitOrderFake();
|
|
332
335
|
$order->setRawAttributes([
|
|
@@ -518,8 +521,17 @@ test('orchestration optimize routes hydrates missing vehicle relations before se
|
|
|
518
521
|
'mode' => 'optimize_routes',
|
|
519
522
|
]));
|
|
520
523
|
|
|
524
|
+
$payload = $response->getData(true);
|
|
525
|
+
|
|
526
|
+
// greedy (the probe's configured engine) cannot sequence stops, so the
|
|
527
|
+
// built-in sequencer runs and the summary says so without raising a warning
|
|
521
528
|
expect($response->getStatusCode())->toBe(200)
|
|
522
|
-
->and($
|
|
529
|
+
->and($payload['summary'])->toMatchArray([
|
|
530
|
+
'engine' => 'route_sequence_fake',
|
|
531
|
+
'requested_engine' => 'greedy',
|
|
532
|
+
])
|
|
533
|
+
->and($payload['summary']['fallback_reason'])->toContain('does not sequence stops')
|
|
534
|
+
->and($payload)->not->toHaveKey('warning')
|
|
523
535
|
->and($order->relationLoaded('vehicle'))->toBeTrue()
|
|
524
536
|
->and($order->vehicle->driver->public_id)->toBe('driver_one')
|
|
525
537
|
->and($controller->routeEngine->calls)->toHaveCount(1);
|
|
@@ -926,3 +938,161 @@ test('assign drivers skips orders without a prior assignment entry', function ()
|
|
|
926
938
|
expect($matched->vehicle_assigned_uuid)->toBe('vehicle_one-uuid')
|
|
927
939
|
->and($unmatched->vehicle_assigned_uuid)->toBeNull();
|
|
928
940
|
});
|
|
941
|
+
|
|
942
|
+
class FleetOpsSolverEngineFake implements OrchestrationEngineInterface
|
|
943
|
+
{
|
|
944
|
+
public array $calls = [];
|
|
945
|
+
|
|
946
|
+
public function allocate(Collection $orders, Collection $vehicles, array $options = []): array
|
|
947
|
+
{
|
|
948
|
+
$this->calls[] = compact('orders', 'vehicles', 'options');
|
|
949
|
+
$vehicle = $vehicles->first();
|
|
950
|
+
|
|
951
|
+
return [
|
|
952
|
+
'assignments' => $orders->values()->map(fn ($order, $index) => [
|
|
953
|
+
'order_id' => $order->public_id,
|
|
954
|
+
'vehicle_id' => $vehicle->public_id,
|
|
955
|
+
'driver_id' => null,
|
|
956
|
+
'sequence' => $index + 1,
|
|
957
|
+
'arrival' => 1778918400 + $index,
|
|
958
|
+
'duration' => 600,
|
|
959
|
+
'distance' => 4200,
|
|
960
|
+
'route_distance' => 4200,
|
|
961
|
+
'route_duration' => 600,
|
|
962
|
+
])->all(),
|
|
963
|
+
'unassigned' => [],
|
|
964
|
+
'summary' => ['cost' => 600],
|
|
965
|
+
];
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
public function getName(): string
|
|
969
|
+
{
|
|
970
|
+
return 'VROOM';
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
public function getIdentifier(): string
|
|
974
|
+
{
|
|
975
|
+
return 'vroom';
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function fleetopsOrchestrationAssignedOrder(string $publicId, ?string $vehicleUuid, ?Vehicle $vehicle = null): FleetOpsOrchestrationCommitOrderFake
|
|
980
|
+
{
|
|
981
|
+
$order = fleetopsOrchestrationOrder($publicId);
|
|
982
|
+
$order->vehicle_assigned_uuid = $vehicleUuid;
|
|
983
|
+
if ($vehicle) {
|
|
984
|
+
$order->setRelation('vehicle', $vehicle);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return $order;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
test('orchestration run falls back to greedy and reports why when the selected engine is unavailable', function () {
|
|
991
|
+
$registry = new OrchestrationEngineRegistry();
|
|
992
|
+
$registry->register(new GreedyOrchestrationEngine());
|
|
993
|
+
$registry->register(new FleetOpsThrowingOrchestrationEngineFake());
|
|
994
|
+
|
|
995
|
+
$vehicle = fleetopsOrchestrationVehicle('vehicle_one');
|
|
996
|
+
$vehicle->setRelation('driver', fleetopsOrchestrationDriver('driver_one'));
|
|
997
|
+
|
|
998
|
+
$controller = new FleetOpsOrchestrationCommitControllerProbe($registry);
|
|
999
|
+
$controller->engineSetting = 'throwing';
|
|
1000
|
+
$controller->orderQuery = new FleetOpsOrchestrationQueryFake(collect([fleetopsOrchestrationOrder('order_one'), fleetopsOrchestrationOrder('order_two')]));
|
|
1001
|
+
$controller->vehicleQuery = new FleetOpsOrchestrationQueryFake(collect([$vehicle]));
|
|
1002
|
+
|
|
1003
|
+
// an empty engine option defers to the organization's selected engine
|
|
1004
|
+
$response = $controller->run(Request::create('/orchestrator/run', 'POST', [
|
|
1005
|
+
'mode' => 'assign_vehicles',
|
|
1006
|
+
'options' => ['engine' => ''],
|
|
1007
|
+
]));
|
|
1008
|
+
|
|
1009
|
+
$payload = $response->getData(true);
|
|
1010
|
+
|
|
1011
|
+
expect($response->getStatusCode())->toBe(200)
|
|
1012
|
+
->and($payload['summary'])->toMatchArray([
|
|
1013
|
+
'engine' => 'greedy',
|
|
1014
|
+
'requested_engine' => 'throwing',
|
|
1015
|
+
'fallback_reason' => 'orchestration unavailable',
|
|
1016
|
+
'metrics' => 'estimated',
|
|
1017
|
+
'distance' => 0,
|
|
1018
|
+
'duration' => 0,
|
|
1019
|
+
])
|
|
1020
|
+
->and($payload['warning'])->toBe('The "greedy" engine was used instead of "throwing": orchestration unavailable')
|
|
1021
|
+
// the fallback still routes every order onto the one vehicle
|
|
1022
|
+
->and(array_column($payload['assignments'], 'order_id'))->toBe(['order_one', 'order_two'])
|
|
1023
|
+
->and($payload['assignments'][0])->toHaveKeys(['arrival', 'route_distance', 'route_duration']);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test('orchestration optimize routes runs the selected solver once per vehicle', function () {
|
|
1027
|
+
$solver = new FleetOpsSolverEngineFake();
|
|
1028
|
+
$registry = new OrchestrationEngineRegistry();
|
|
1029
|
+
$registry->register(new GreedyOrchestrationEngine());
|
|
1030
|
+
$registry->register($solver);
|
|
1031
|
+
|
|
1032
|
+
$vehicle = fleetopsOrchestrationVehicle('vehicle_one');
|
|
1033
|
+
$vehicle->uuid = 'vehicle-one-uuid';
|
|
1034
|
+
|
|
1035
|
+
$controller = new FleetOpsOrchestrationCommitControllerProbe($registry);
|
|
1036
|
+
$controller->engineSetting = 'vroom';
|
|
1037
|
+
$controller->orderQuery = new FleetOpsOrchestrationQueryFake(collect([
|
|
1038
|
+
fleetopsOrchestrationAssignedOrder('order_one', 'vehicle-one-uuid', $vehicle),
|
|
1039
|
+
fleetopsOrchestrationAssignedOrder('order_two', 'vehicle-one-uuid', $vehicle),
|
|
1040
|
+
// a vehicle that can no longer be found, and an order with no vehicle
|
|
1041
|
+
fleetopsOrchestrationAssignedOrder('order_ghost', 'ghost-uuid'),
|
|
1042
|
+
fleetopsOrchestrationAssignedOrder('order_loose', null),
|
|
1043
|
+
]));
|
|
1044
|
+
$controller->vehicleQuery = new FleetOpsOrchestrationQueryFake(collect([$vehicle]));
|
|
1045
|
+
|
|
1046
|
+
$response = $controller->run(Request::create('/orchestrator/run', 'POST', [
|
|
1047
|
+
'mode' => 'optimize_routes',
|
|
1048
|
+
'options' => ['allocation_strategy' => 'capacity_only'],
|
|
1049
|
+
]));
|
|
1050
|
+
|
|
1051
|
+
$payload = $response->getData(true);
|
|
1052
|
+
|
|
1053
|
+
expect($response->getStatusCode())->toBe(200)
|
|
1054
|
+
->and($solver->calls)->toHaveCount(1)
|
|
1055
|
+
->and($solver->calls[0]['vehicles']->all())->toBe([$vehicle])
|
|
1056
|
+
->and($solver->calls[0]['orders']->pluck('public_id')->all())->toBe(['order_one', 'order_two'])
|
|
1057
|
+
// sequencing always needs a routed solve, whatever the phase asked for
|
|
1058
|
+
->and($solver->calls[0]['options']['allocation_strategy'])->toBe('route_aware')
|
|
1059
|
+
->and(array_column($payload['assignments'], 'arrival'))->toBe([1778918400, 1778918401])
|
|
1060
|
+
->and($payload['unassigned'])->toBe(['order_ghost', 'order_loose'])
|
|
1061
|
+
->and($payload['summary'])->toBe([
|
|
1062
|
+
'engine' => 'vroom',
|
|
1063
|
+
'assigned' => 2,
|
|
1064
|
+
'unassigned' => 2,
|
|
1065
|
+
'distance' => 4200,
|
|
1066
|
+
'duration' => 600,
|
|
1067
|
+
'metrics' => 'engine',
|
|
1068
|
+
])
|
|
1069
|
+
->and($payload)->not->toHaveKey('warning');
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
test('orchestration optimize routes falls back to the route sequencer with a warning', function (string $engineSetting, string $reason) {
|
|
1073
|
+
$registry = new OrchestrationEngineRegistry();
|
|
1074
|
+
$registry->register(new GreedyOrchestrationEngine());
|
|
1075
|
+
$registry->register(new FleetOpsThrowingOrchestrationEngineFake());
|
|
1076
|
+
|
|
1077
|
+
$vehicle = fleetopsOrchestrationVehicle('vehicle_one');
|
|
1078
|
+
$vehicle->uuid = 'vehicle-one-uuid';
|
|
1079
|
+
$vehicle->setRelation('driver', fleetopsOrchestrationDriver('driver_one'));
|
|
1080
|
+
|
|
1081
|
+
$controller = new FleetOpsOrchestrationCommitControllerProbe($registry);
|
|
1082
|
+
$controller->engineSetting = $engineSetting;
|
|
1083
|
+
$controller->orderQuery = new FleetOpsOrchestrationQueryFake(collect([fleetopsOrchestrationAssignedOrder('order_one', 'vehicle-one-uuid', $vehicle)]));
|
|
1084
|
+
$controller->vehicleQuery = new FleetOpsOrchestrationQueryFake(collect([$vehicle]));
|
|
1085
|
+
|
|
1086
|
+
$payload = $controller->run(Request::create('/orchestrator/run', 'POST', ['mode' => 'optimize_routes']))->getData(true);
|
|
1087
|
+
|
|
1088
|
+
expect($controller->routeEngine->calls)->toHaveCount(1)
|
|
1089
|
+
->and($payload['summary'])->toMatchArray([
|
|
1090
|
+
'engine' => 'route_sequence_fake',
|
|
1091
|
+
'requested_engine' => $engineSetting,
|
|
1092
|
+
'fallback_reason' => $reason,
|
|
1093
|
+
])
|
|
1094
|
+
->and($payload['warning'])->toContain($reason);
|
|
1095
|
+
})->with([
|
|
1096
|
+
'unavailable solver' => ['throwing', 'orchestration unavailable'],
|
|
1097
|
+
'unregistered' => ['missing', 'No orchestration engine is registered as "missing".'],
|
|
1098
|
+
]);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
|
+
use Fleetbase\FleetOps\Exceptions\OrderEditLockedException;
|
|
3
4
|
use Fleetbase\FleetOps\Flow\Activity;
|
|
4
5
|
use Fleetbase\FleetOps\Http\Controllers\Internal\v1\OrderController;
|
|
5
6
|
use Fleetbase\FleetOps\Http\Requests\BulkDispatchRequest;
|
|
6
7
|
use Fleetbase\FleetOps\Http\Requests\CancelOrderRequest;
|
|
8
|
+
use Fleetbase\FleetOps\Http\Resources\v1\PublicOrderTracking;
|
|
7
9
|
use Fleetbase\FleetOps\Models\Driver;
|
|
8
10
|
use Fleetbase\FleetOps\Models\Entity;
|
|
9
11
|
use Fleetbase\FleetOps\Models\Order;
|
|
@@ -13,6 +15,8 @@ use Fleetbase\FleetOps\Models\Place;
|
|
|
13
15
|
use Fleetbase\FleetOps\Models\Proof;
|
|
14
16
|
use Fleetbase\FleetOps\Models\TrackingStatus;
|
|
15
17
|
use Fleetbase\FleetOps\Models\Waypoint;
|
|
18
|
+
use Fleetbase\FleetOps\Support\OrderEdit;
|
|
19
|
+
use Fleetbase\FleetOps\Support\OrderEditPolicy;
|
|
16
20
|
use Fleetbase\FleetOps\Support\OrderTracker;
|
|
17
21
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
18
22
|
use Fleetbase\Models\Type;
|
|
@@ -37,6 +41,43 @@ if (!class_exists('Fleetbase\Http\Requests\ExportRequest', false)) {
|
|
|
37
41
|
eval('namespace Fleetbase\Http\Requests; class ExportRequest extends \Illuminate\Http\Request {}');
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Records the route edit guard and commit instead of reading and announcing through the database.
|
|
46
|
+
*/
|
|
47
|
+
class FleetOpsInternalOrderEditFake extends OrderEdit
|
|
48
|
+
{
|
|
49
|
+
public array $guarded = [];
|
|
50
|
+
public array $lockWith = [];
|
|
51
|
+
public int $commits = 0;
|
|
52
|
+
|
|
53
|
+
public function __construct(public Order $order)
|
|
54
|
+
{
|
|
55
|
+
$this->policy = OrderEditPolicy::for($order);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public function requestedChanges(array $input, string $entitiesMode = self::ENTITIES_APPEND): array
|
|
59
|
+
{
|
|
60
|
+
return $input;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public function guard(array $changes): static
|
|
64
|
+
{
|
|
65
|
+
$this->guarded[] = $changes;
|
|
66
|
+
if ($this->lockWith) {
|
|
67
|
+
throw new OrderEditLockedException($this->lockWith);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return $this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public function commit(array $extra = [], bool $notifyDriver = true): array
|
|
74
|
+
{
|
|
75
|
+
$this->commits++;
|
|
76
|
+
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
class FleetOpsInternalOrderLifecycleControllerProbe extends OrderController
|
|
41
82
|
{
|
|
42
83
|
public Collection $orders;
|
|
@@ -68,6 +109,7 @@ class FleetOpsInternalOrderLifecycleControllerProbe extends OrderController
|
|
|
68
109
|
public Collection $proofResults;
|
|
69
110
|
public array $exportDownloads = [];
|
|
70
111
|
public ?FleetOpsInternalOrderLifecycleOrderFake $trackingOrder = null;
|
|
112
|
+
public array $trackingLookups = [];
|
|
71
113
|
public ?FleetOpsInternalOrderLifecycleOrderFake $scheduleOrder = null;
|
|
72
114
|
public ?FleetOpsInternalOrderLifecycleDriverFake $scheduleDriver = null;
|
|
73
115
|
|
|
@@ -218,6 +260,7 @@ class FleetOpsInternalOrderLifecycleControllerProbe extends OrderController
|
|
|
218
260
|
|
|
219
261
|
protected function findOrderByTrackingNumber(string $trackingNumber): ?Order
|
|
220
262
|
{
|
|
263
|
+
$this->trackingLookups[] = $trackingNumber;
|
|
221
264
|
$this->trackingOrder?->setAttribute('tracking_lookup', $trackingNumber);
|
|
222
265
|
|
|
223
266
|
return $this->trackingOrder;
|
|
@@ -254,6 +297,18 @@ class FleetOpsInternalOrderLifecycleControllerProbe extends OrderController
|
|
|
254
297
|
return $this->order;
|
|
255
298
|
}
|
|
256
299
|
|
|
300
|
+
public ?FleetOpsInternalOrderEditFake $edit = null;
|
|
301
|
+
|
|
302
|
+
public array $editLockedWith = [];
|
|
303
|
+
|
|
304
|
+
protected function beginOrderEdit(Order $order): OrderEdit
|
|
305
|
+
{
|
|
306
|
+
$this->edit ??= new FleetOpsInternalOrderEditFake($order);
|
|
307
|
+
$this->edit->lockWith = $this->editLockedWith;
|
|
308
|
+
|
|
309
|
+
return $this->edit;
|
|
310
|
+
}
|
|
311
|
+
|
|
257
312
|
protected function orderResponse(Order $order): array
|
|
258
313
|
{
|
|
259
314
|
return ['order' => $order];
|
|
@@ -413,6 +468,15 @@ class FleetOpsInternalOrderLifecycleOrderFake extends Order
|
|
|
413
468
|
return $this;
|
|
414
469
|
}
|
|
415
470
|
|
|
471
|
+
public ?string $cancelReasonForTest = null;
|
|
472
|
+
|
|
473
|
+
public function cancelWithReason(?string $reason = null)
|
|
474
|
+
{
|
|
475
|
+
$this->cancelReasonForTest = $reason;
|
|
476
|
+
|
|
477
|
+
return $this->cancel();
|
|
478
|
+
}
|
|
479
|
+
|
|
416
480
|
public function dispatch(bool $save = true): Order
|
|
417
481
|
{
|
|
418
482
|
$this->dispatchedForTest = true;
|
|
@@ -908,10 +972,29 @@ test('internal order controller edits order routes and refreshes current destina
|
|
|
908
972
|
->and($payload->calls)->toContain(['setDropoff', ['name' => 'Dropoff'], ['save' => true]])
|
|
909
973
|
->and($payload->calls)->toContain(['setReturn', ['name' => 'Return'], ['save' => true]])
|
|
910
974
|
->and($payload->calls)->toContain(['updateWaypoints', [['name' => 'Waypoint']]])
|
|
911
|
-
|
|
975
|
+
// the current waypoint is reconciled by the committed edit, not reset to the first stop here
|
|
976
|
+
->and(collect($payload->calls)->where(0, 'setCurrentWaypoint')->all())->toBe([])
|
|
977
|
+
->and($controller->edit->guarded)->toBe([['pickup' => true, 'dropoff' => true, 'return' => true, 'waypoints' => true]])
|
|
978
|
+
->and($controller->edit->commits)->toBe(1)
|
|
912
979
|
->and($order->loadedRelations)->toBe([['payload.pickup', 'payload.dropoff', 'payload.return', 'payload.waypoints']]);
|
|
913
980
|
});
|
|
914
981
|
|
|
982
|
+
test('internal order controller refuses route edits on finished orders with a 422', function () {
|
|
983
|
+
$payload = fleetopsInternalOrderLifecyclePayload(fleetopsInternalOrderLifecyclePlace('starting-place'));
|
|
984
|
+
$order = fleetopsInternalOrderLifecycleOrder('order-route', 'completed');
|
|
985
|
+
$order->setRelation('payload', $payload);
|
|
986
|
+
$controller = fleetopsInternalOrderLifecycleController([$order]);
|
|
987
|
+
$controller->editLockedWith = ['dropoff' => 'The order is completed and can no longer be changed.'];
|
|
988
|
+
|
|
989
|
+
$response = $controller->editOrderRoute('order-route', new Request(['dropoff' => ['name' => 'Dropoff']]));
|
|
990
|
+
|
|
991
|
+
// the harness response factory keeps only the message; the app's error macro adds `locked`
|
|
992
|
+
expect($response->getStatusCode())->toBe(422)
|
|
993
|
+
->and($response->getData(true)['error'])->toBe('The order is completed and can no longer be changed.')
|
|
994
|
+
->and($payload->calls)->toBe([])
|
|
995
|
+
->and($controller->edit->commits)->toBe(0);
|
|
996
|
+
});
|
|
997
|
+
|
|
915
998
|
test('internal order controller clears route endpoints and falls back to dropoff destination', function () {
|
|
916
999
|
$fallbackDestination = fleetopsInternalOrderLifecyclePlace('fallback-place');
|
|
917
1000
|
$payload = fleetopsInternalOrderLifecyclePayload(null, $fallbackDestination);
|
|
@@ -930,7 +1013,7 @@ test('internal order controller clears route endpoints and falls back to dropoff
|
|
|
930
1013
|
->and($payload->calls)->toContain(['removePlace', 'dropoff', ['save' => true]])
|
|
931
1014
|
->and($payload->calls)->toContain(['removePlace', 'return', ['save' => true]])
|
|
932
1015
|
->and($payload->calls)->toContain(['removeWaypoints'])
|
|
933
|
-
->and($
|
|
1016
|
+
->and($controller->edit->commits)->toBe(1);
|
|
934
1017
|
});
|
|
935
1018
|
|
|
936
1019
|
test('internal order controller reports missing order route edits', function () {
|
|
@@ -1054,7 +1137,15 @@ test('internal order controller cancels and dispatches individual orders', funct
|
|
|
1054
1137
|
'order' => 'order-uuid',
|
|
1055
1138
|
])
|
|
1056
1139
|
->and($order->lookup_uuid)->toBe('order-uuid')
|
|
1057
|
-
->and($order->canceledForTest)->toBeTrue()
|
|
1140
|
+
->and($order->canceledForTest)->toBeTrue()
|
|
1141
|
+
->and($order->cancelReasonForTest)->toBeNull();
|
|
1142
|
+
|
|
1143
|
+
$order = fleetopsInternalOrderLifecycleOrder('order-uuid', 'created');
|
|
1144
|
+
$controller = fleetopsInternalOrderLifecycleController([$order]);
|
|
1145
|
+
$controller->cancel(fleetopsCancelOrderRequest(['order' => 'order-uuid', 'reason' => 'Customer asked to cancel']));
|
|
1146
|
+
|
|
1147
|
+
expect($order->canceledForTest)->toBeTrue()
|
|
1148
|
+
->and($order->cancelReasonForTest)->toBe('Customer asked to cancel');
|
|
1058
1149
|
|
|
1059
1150
|
$order = fleetopsInternalOrderLifecycleOrder('order-dispatch', 'created');
|
|
1060
1151
|
$controller = fleetopsInternalOrderLifecycleController([$order]);
|
|
@@ -1549,24 +1640,29 @@ test('internal order controller export lookup and schedule endpoints use resolve
|
|
|
1549
1640
|
->and($export['selections'])->toBe(['order-one', 'order-two'])
|
|
1550
1641
|
->and($controller->exportDownloads[0][0])->toBe(['order-one', 'order-two']);
|
|
1551
1642
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
]
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1643
|
+
// Missing, malformed and unknown tracking numbers all get the one answer,
|
|
1644
|
+
// and malformed input never reaches the query.
|
|
1645
|
+
$notFound = ['error' => OrderController::TRACKING_LOOKUP_ERROR];
|
|
1646
|
+
foreach ([[], ['tracking' => ''], ['tracking' => ' '], ['tracking' => ['TN-1']], ['tracking' => '../etc/passwd'], ['tracking' => "TN-1' OR 1=1"], ['tracking' => str_repeat('A', 101)], ['tracking' => '-TN']] as $input) {
|
|
1647
|
+
expect($controller->lookup(new Request($input))->getData(true))->toBe($notFound);
|
|
1648
|
+
}
|
|
1649
|
+
expect($controller->trackingLookups)->toBe([]);
|
|
1650
|
+
|
|
1651
|
+
expect($controller->lookup(new Request(['tracking' => 'TN-404']))->getData(true))->toBe($notFound)
|
|
1652
|
+
->and($controller->trackingLookups)->toBe(['TN-404']);
|
|
1558
1653
|
|
|
1559
1654
|
$trackedOrder = fleetopsInternalOrderLifecycleOrder('tracked-order');
|
|
1560
1655
|
$trackedOrder->trackerForTest = new FleetOpsInternalOrderLifecycleTrackerFake($trackedOrder);
|
|
1561
1656
|
$controller->trackingOrder = $trackedOrder;
|
|
1562
1657
|
|
|
1563
|
-
$lookup = $controller->lookup(new Request(['tracking' => 'TN-100']));
|
|
1658
|
+
$lookup = $controller->lookup(new Request(['tracking' => ' TN-100.a_b ']));
|
|
1564
1659
|
|
|
1565
|
-
expect($lookup
|
|
1566
|
-
->and($
|
|
1567
|
-
->and($trackedOrder->
|
|
1660
|
+
expect($lookup)->toBeInstanceOf(PublicOrderTracking::class)
|
|
1661
|
+
->and($lookup->resource)->toBe($trackedOrder)
|
|
1662
|
+
->and($trackedOrder->tracking_lookup)->toBe('TN-100.a_b')
|
|
1663
|
+
->and($trackedOrder->loadedMissing)->toBe([['trackingNumber', 'trackingStatuses', 'payload.pickup', 'payload.dropoff', 'payload.waypoints', 'payload.entities']])
|
|
1568
1664
|
->and($trackedOrder->tracker_data)->toBe(['tracker' => 'info', 'options' => []])
|
|
1569
|
-
->and($trackedOrder->
|
|
1665
|
+
->and($trackedOrder->trackerForTest->etaOptions)->toBe([]);
|
|
1570
1666
|
|
|
1571
1667
|
expect($controller->scheduleOrder(new Request(['order' => 'missing-order']))->getData(true))->toBe([
|
|
1572
1668
|
'error' => 'No order found to schedule.',
|
|
@@ -157,18 +157,20 @@ test('bulk assign driver validation enforces uuid contracts', function () {
|
|
|
157
157
|
expect($valid['driver'])->toBe('22222222-2222-4222-8222-222222222222');
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
test('assign driver to orders updates the assignment column', function () {
|
|
160
|
+
test('assign driver to orders updates the assignment column and tells the drivers it replaces', function () {
|
|
161
161
|
$connection = fleetopsInternalOrderHelpersBoot();
|
|
162
162
|
$connection->table('orders')->insert([
|
|
163
|
-
['uuid' => 'order-1', 'company_uuid' => 'company-1'],
|
|
164
|
-
['uuid' => 'order-2', 'company_uuid' => 'company-1'],
|
|
163
|
+
['uuid' => 'order-1', 'company_uuid' => 'company-1', 'driver_assigned_uuid' => null],
|
|
164
|
+
['uuid' => 'order-2', 'company_uuid' => 'company-1', 'driver_assigned_uuid' => null],
|
|
165
|
+
// already with another driver, who no longer exists: nothing to tell, nothing to fail
|
|
166
|
+
['uuid' => 'order-3', 'company_uuid' => 'company-1', 'driver_assigned_uuid' => 'driver-gone'],
|
|
165
167
|
]);
|
|
166
168
|
$driver = new Driver();
|
|
167
169
|
$driver->setRawAttributes(['uuid' => 'driver-1'], true);
|
|
168
170
|
|
|
169
|
-
(new FleetOpsInternalOrderHelpersProbe())->callProtected('assignDriverToOrders', ['order-1', 'order-2'], $driver);
|
|
171
|
+
(new FleetOpsInternalOrderHelpersProbe())->callProtected('assignDriverToOrders', ['order-1', 'order-2', 'order-3'], $driver);
|
|
170
172
|
|
|
171
|
-
expect($connection->table('orders')->where('driver_assigned_uuid', 'driver-1')->count())->toBe(
|
|
173
|
+
expect($connection->table('orders')->where('driver_assigned_uuid', 'driver-1')->count())->toBe(3);
|
|
172
174
|
});
|
|
173
175
|
|
|
174
176
|
test('dispatch domain event helper emits and returns the event', function () {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Controllers\Internal\v1\PositionController;
|
|
4
|
+
use Fleetbase\Models\User;
|
|
5
|
+
use Fleetbase\Support\SocketCluster\ChannelAuthorizer;
|
|
6
|
+
use Fleetbase\Support\SocketCluster\ChannelDecision;
|
|
7
|
+
use Fleetbase\Support\SocketCluster\SocketPrincipal;
|
|
8
|
+
use Fleetbase\TestSupport\DispatchRecorder;
|
|
9
|
+
use Illuminate\Database\ConnectionResolver;
|
|
10
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
11
|
+
use Illuminate\Database\SQLiteConnection;
|
|
12
|
+
use Illuminate\Http\Request;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Covers the positions/replay channel check: with socket authentication on, a replay may only
|
|
16
|
+
* target a channel the requesting user could subscribe to; with it off the endpoint is unchanged.
|
|
17
|
+
*/
|
|
18
|
+
class PositionReplayChannelAuthorizerFake
|
|
19
|
+
{
|
|
20
|
+
public array $checked = [];
|
|
21
|
+
|
|
22
|
+
public function __construct(public array $allowed = [])
|
|
23
|
+
{
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public function authorize(?SocketPrincipal $principal, string $channel): ChannelDecision
|
|
27
|
+
{
|
|
28
|
+
$this->checked[] = [$principal, $channel];
|
|
29
|
+
|
|
30
|
+
return in_array($channel, $this->allowed, true) ? ChannelDecision::allowed('resolver') : ChannelDecision::denied('forbidden');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function positionReplayChannelBoot(bool $enabled): PositionReplayChannelAuthorizerFake
|
|
35
|
+
{
|
|
36
|
+
// Socket authentication needs both the switch and the signing key.
|
|
37
|
+
config([
|
|
38
|
+
'broadcasting.connections.socketcluster.auth_enabled' => $enabled,
|
|
39
|
+
'broadcasting.connections.socketcluster.auth_key' => $enabled ? 'fleetops-position-replay-test-key-0123456789' : null,
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
$pdo = new PDO('sqlite::memory:');
|
|
43
|
+
$pdo->sqliteCreateFunction('ST_PointFromText', fn ($wkt, $srid = 0, $axisOrder = null) => $wkt);
|
|
44
|
+
$connection = new SQLiteConnection($pdo);
|
|
45
|
+
$resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
46
|
+
$resolver->setDefaultConnection('mysql');
|
|
47
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
48
|
+
|
|
49
|
+
$schema = $connection->getSchemaBuilder();
|
|
50
|
+
foreach (['positions' => ['uuid', 'company_uuid', 'subject_uuid', 'coordinates', 'speed'], 'companies' => ['uuid', 'public_id']] as $table => $columns) {
|
|
51
|
+
$schema->create($table, function ($blueprint) use ($columns) {
|
|
52
|
+
$blueprint->increments('id');
|
|
53
|
+
foreach ($columns as $column) {
|
|
54
|
+
$blueprint->string($column)->nullable();
|
|
55
|
+
}
|
|
56
|
+
$blueprint->timestamps();
|
|
57
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
$connection->table('companies')->insert(['uuid' => 'company-1', 'public_id' => 'company_one']);
|
|
61
|
+
$connection->table('positions')->insert(['uuid' => 'pos-1', 'company_uuid' => 'company-1', 'subject_uuid' => 'vehicle-1', 'created_at' => '2026-10-06 08:00:00']);
|
|
62
|
+
|
|
63
|
+
session(['company' => 'company-1']);
|
|
64
|
+
DispatchRecorder::$dispatched = [];
|
|
65
|
+
|
|
66
|
+
$authorizer = new PositionReplayChannelAuthorizerFake(['vehicle.vehicle-1']);
|
|
67
|
+
app()->instance(ChannelAuthorizer::class, $authorizer);
|
|
68
|
+
|
|
69
|
+
return $authorizer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function positionReplayChannelRequest(mixed $channel, ?User $user = null): Request
|
|
73
|
+
{
|
|
74
|
+
$request = Request::create('/int/v1/positions/replay', 'POST', ['channel_id' => $channel, 'position_ids' => ['pos-1']]);
|
|
75
|
+
$request->setUserResolver(fn () => $user);
|
|
76
|
+
|
|
77
|
+
return $request;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function positionReplayChannelUser(): User
|
|
81
|
+
{
|
|
82
|
+
$user = (new ReflectionClass(User::class))->newInstanceWithoutConstructor();
|
|
83
|
+
$user->setRawAttributes(['uuid' => 'user-1', 'public_id' => 'user_one', 'company_uuid' => 'company-1', 'type' => 'user'], true);
|
|
84
|
+
|
|
85
|
+
return $user;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
afterEach(function () {
|
|
89
|
+
config(['broadcasting.connections.socketcluster.auth_enabled' => false, 'broadcasting.connections.socketcluster.auth_key' => null]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a replay to a channel the user may subscribe to is started', function () {
|
|
93
|
+
$authorizer = positionReplayChannelBoot(true);
|
|
94
|
+
|
|
95
|
+
$response = (new PositionController())->replay(positionReplayChannelRequest('vehicle.vehicle-1', positionReplayChannelUser()));
|
|
96
|
+
|
|
97
|
+
[$principal, $channel] = $authorizer->checked[0];
|
|
98
|
+
expect($response->getData(true)['status'])->toBe('ok')
|
|
99
|
+
->and(DispatchRecorder::$dispatched)->toHaveCount(1)
|
|
100
|
+
->and($channel)->toBe('vehicle.vehicle-1')
|
|
101
|
+
->and($principal->kind)->toBe('user')
|
|
102
|
+
->and($principal->sub)->toBe('user-1')
|
|
103
|
+
->and($principal->cid)->toBe('company-1')
|
|
104
|
+
->and($principal->adm)->toBeFalse();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('a replay to any other channel is refused before anything is queued', function () {
|
|
108
|
+
$authorizer = positionReplayChannelBoot(true);
|
|
109
|
+
$controller = new PositionController();
|
|
110
|
+
$user = positionReplayChannelUser();
|
|
111
|
+
|
|
112
|
+
foreach ([
|
|
113
|
+
positionReplayChannelRequest('chat_channel.someone-elses', $user),
|
|
114
|
+
positionReplayChannelRequest('vehicle.vehicle-1', null),
|
|
115
|
+
positionReplayChannelRequest('vehicle. vehicle-1', $user),
|
|
116
|
+
positionReplayChannelRequest(str_repeat('x', 256), $user),
|
|
117
|
+
positionReplayChannelRequest(['vehicle.vehicle-1'], $user),
|
|
118
|
+
] as $request) {
|
|
119
|
+
$response = $controller->replay($request);
|
|
120
|
+
expect($response->getStatusCode())->toBe(403)
|
|
121
|
+
->and($response->getData(true)['error'])->toContain('not allowed');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
expect(DispatchRecorder::$dispatched)->toBe([])
|
|
125
|
+
->and(array_column($authorizer->checked, 1))->toBe(['chat_channel.someone-elses']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('without socket authentication the replay endpoint does not check the channel', function () {
|
|
129
|
+
$authorizer = positionReplayChannelBoot(false);
|
|
130
|
+
|
|
131
|
+
$response = (new PositionController())->replay(positionReplayChannelRequest('anything.at-all'));
|
|
132
|
+
|
|
133
|
+
expect($response->getData(true)['status'])->toBe('ok')
|
|
134
|
+
->and($authorizer->checked)->toBe([]);
|
|
135
|
+
});
|