@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,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Console\Commands;
|
|
4
4
|
|
|
5
|
+
use Carbon\Carbon;
|
|
5
6
|
use Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy;
|
|
6
7
|
use Illuminate\Console\Command;
|
|
7
8
|
use Illuminate\Support\Collection;
|
|
@@ -21,6 +22,9 @@ class PruneTelematicsData extends Command
|
|
|
21
22
|
|
|
22
23
|
public const LOCK = 'fleetops:prune-telematics-data';
|
|
23
24
|
|
|
25
|
+
/** Cache key prefix for the per-scope compaction watermark (see pruneDeviceEvents). */
|
|
26
|
+
public const WATERMARK_PREFIX = 'fleetops:prune-telematics-data:compacted:';
|
|
27
|
+
|
|
24
28
|
protected $signature = 'fleetops:prune-telematics-data
|
|
25
29
|
{--company= : Only prune data for this company (uuid or public id)}
|
|
26
30
|
{--orphans-only : Only prune data without an existing company or connection}
|
|
@@ -36,6 +40,9 @@ class PruneTelematicsData extends Command
|
|
|
36
40
|
protected int $maxBatches = 50;
|
|
37
41
|
protected bool $dryRun = false;
|
|
38
42
|
|
|
43
|
+
/** table => company uuids present in that table whose company no longer exists; resolved once per run. */
|
|
44
|
+
protected array $orphanCompaniesByTable = [];
|
|
45
|
+
|
|
39
46
|
public function handle(): int
|
|
40
47
|
{
|
|
41
48
|
$tables = $this->requestedTables();
|
|
@@ -142,7 +149,7 @@ class PruneTelematicsData extends Command
|
|
|
142
149
|
}
|
|
143
150
|
$inbox = $telematics ? fn ($query) => $query->whereIn('telematic_uuid', $telematics) : null;
|
|
144
151
|
|
|
145
|
-
return $this->prune($rows, $inbox, $policy, $tables);
|
|
152
|
+
return $this->prune($rows, $inbox, $policy, $tables, $companyUuid);
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
/**
|
|
@@ -150,9 +157,7 @@ class PruneTelematicsData extends Command
|
|
|
150
157
|
*/
|
|
151
158
|
protected function pruneOrphans(RetentionPolicy $policy, array $tables): array
|
|
152
159
|
{
|
|
153
|
-
$rows
|
|
154
|
-
$where->whereNull('company_uuid')->orWhereNotIn('company_uuid', DB::table('companies')->select('uuid'));
|
|
155
|
-
});
|
|
160
|
+
$rows = fn ($query) => $this->scopeOrphanRows($query);
|
|
156
161
|
$inbox = fn ($query) => $query->whereNotExists(function ($connections) use ($query) {
|
|
157
162
|
$connections->selectRaw('1')
|
|
158
163
|
->from('telematics')
|
|
@@ -160,14 +165,46 @@ class PruneTelematicsData extends Command
|
|
|
160
165
|
->whereColumn('telematics.uuid', $query->from . '.telematic_uuid');
|
|
161
166
|
});
|
|
162
167
|
|
|
163
|
-
return $this->prune($rows, $inbox, $policy, $tables);
|
|
168
|
+
return $this->prune($rows, $inbox, $policy, $tables, 'orphans');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Rows without a company, plus rows whose company uuid no longer exists. The gone
|
|
173
|
+
* companies are resolved once per table from the distinct company uuids it holds, so
|
|
174
|
+
* every batch ranges over the (company_uuid, created_at) index instead of evaluating
|
|
175
|
+
* a NOT IN subquery against every row.
|
|
176
|
+
*/
|
|
177
|
+
protected function scopeOrphanRows($query)
|
|
178
|
+
{
|
|
179
|
+
$table = $query->from;
|
|
180
|
+
$gone = $this->orphanCompaniesByTable[$table] ??= $this->orphanCompanies($table);
|
|
181
|
+
|
|
182
|
+
return $query->where(function ($where) use ($gone) {
|
|
183
|
+
$where->whereNull('company_uuid');
|
|
184
|
+
if ($gone !== []) {
|
|
185
|
+
$where->orWhereIn('company_uuid', $gone);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @return string[]
|
|
192
|
+
*/
|
|
193
|
+
protected function orphanCompanies(string $table): array
|
|
194
|
+
{
|
|
195
|
+
return DB::table($table)
|
|
196
|
+
->whereNotNull('company_uuid')
|
|
197
|
+
->whereNotIn('company_uuid', DB::table('companies')->select('uuid'))
|
|
198
|
+
->groupBy('company_uuid')
|
|
199
|
+
->pluck('company_uuid')
|
|
200
|
+
->all();
|
|
164
201
|
}
|
|
165
202
|
|
|
166
|
-
protected function prune(\Closure $rows, ?\Closure $inbox, RetentionPolicy $policy, array $tables): array
|
|
203
|
+
protected function prune(\Closure $rows, ?\Closure $inbox, RetentionPolicy $policy, array $tables, string $scope): array
|
|
167
204
|
{
|
|
168
205
|
$stats = $this->emptyStats();
|
|
169
206
|
if (in_array('device_events', $tables, true)) {
|
|
170
|
-
$stats['tables']['device_events'] = $this->pruneDeviceEvents($rows, $policy);
|
|
207
|
+
$stats['tables']['device_events'] = $this->pruneDeviceEvents($rows, $policy, $scope);
|
|
171
208
|
}
|
|
172
209
|
if (in_array('positions', $tables, true)) {
|
|
173
210
|
$stats['tables']['positions'] = $this->prunePositions($rows, $policy);
|
|
@@ -188,40 +225,69 @@ class PruneTelematicsData extends Command
|
|
|
188
225
|
return $stats;
|
|
189
226
|
}
|
|
190
227
|
|
|
191
|
-
protected function pruneDeviceEvents(\Closure $rows, RetentionPolicy $policy): array
|
|
228
|
+
protected function pruneDeviceEvents(\Closure $rows, RetentionPolicy $policy, string $scope): array
|
|
192
229
|
{
|
|
193
230
|
$stats = $this->emptyTableStats();
|
|
194
231
|
$cutoff = $policy->cutoff('event_retention_days');
|
|
195
232
|
// Soft-deleted events only grow the table; purge them regardless of age.
|
|
196
|
-
$this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats);
|
|
233
|
+
$this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats, 'deleted_at');
|
|
197
234
|
if ($cutoff) {
|
|
198
|
-
$this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats);
|
|
235
|
+
$this->deleteInBatches('device_events', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats, 'created_at');
|
|
199
236
|
}
|
|
200
237
|
if ($policy->compactsEvents() && ($compactAt = $policy->cutoff('event_compact_after_days'))) {
|
|
201
238
|
// Keep the event (type, severity, location, normalized data) but drop the raw provider blobs.
|
|
202
239
|
// Rows past the delete cutoff are left to the delete sweep rather than rewritten first.
|
|
203
|
-
|
|
240
|
+
// Everything older than the watermark was compacted by an earlier complete sweep, so
|
|
241
|
+
// only the rows that aged into the window since then are visited; without it every
|
|
242
|
+
// run re-scanned the whole history looking for blobs that were already gone.
|
|
243
|
+
$watermark = $this->compactionWatermark($scope);
|
|
244
|
+
$since = $cutoff && (!$watermark || $cutoff->greaterThan($watermark)) ? $cutoff : $watermark;
|
|
245
|
+
$completed = $this->updateInBatches(
|
|
204
246
|
'device_events',
|
|
205
247
|
'id',
|
|
206
|
-
function ($query) use ($rows, $
|
|
248
|
+
function ($query) use ($rows, $since, $compactAt) {
|
|
207
249
|
$query = $rows($query)->where('created_at', '<', $compactAt->toDateTimeString())->where(fn ($where) => $where->whereNotNull('payload')->orWhereNotNull('meta'));
|
|
208
250
|
|
|
209
|
-
return $
|
|
251
|
+
return $since ? $query->where('created_at', '>=', $since->toDateTimeString()) : $query;
|
|
210
252
|
},
|
|
211
253
|
['payload' => null, 'meta' => null],
|
|
212
|
-
$stats
|
|
254
|
+
$stats,
|
|
255
|
+
'created_at'
|
|
213
256
|
);
|
|
257
|
+
if ($completed && !$this->dryRun) {
|
|
258
|
+
$this->rememberCompactionWatermark($scope, $compactAt);
|
|
259
|
+
}
|
|
214
260
|
}
|
|
215
261
|
|
|
216
262
|
return $stats;
|
|
217
263
|
}
|
|
218
264
|
|
|
265
|
+
protected function compactionWatermark(string $scope): ?Carbon
|
|
266
|
+
{
|
|
267
|
+
try {
|
|
268
|
+
$value = Cache::get(self::WATERMARK_PREFIX . $scope);
|
|
269
|
+
} catch (\Throwable) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return $value ? Carbon::parse($value) : null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
protected function rememberCompactionWatermark(string $scope, Carbon $compactedBefore): void
|
|
277
|
+
{
|
|
278
|
+
try {
|
|
279
|
+
Cache::forever(self::WATERMARK_PREFIX . $scope, $compactedBefore->toDateTimeString());
|
|
280
|
+
} catch (\Throwable) {
|
|
281
|
+
// A cache outage only costs the next run a wider scan.
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
219
285
|
protected function prunePositions(\Closure $rows, RetentionPolicy $policy): array
|
|
220
286
|
{
|
|
221
287
|
$stats = $this->emptyTableStats();
|
|
222
|
-
$this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats);
|
|
288
|
+
$this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->whereNotNull('deleted_at'), $stats, 'deleted_at');
|
|
223
289
|
if ($cutoff = $policy->cutoff('position_retention_days')) {
|
|
224
|
-
$this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats);
|
|
290
|
+
$this->deleteInBatches('positions', 'id', fn ($query) => $rows($query)->where('created_at', '<', $cutoff->toDateTimeString()), $stats, 'created_at');
|
|
225
291
|
}
|
|
226
292
|
|
|
227
293
|
return $stats;
|
|
@@ -232,7 +298,7 @@ class PruneTelematicsData extends Command
|
|
|
232
298
|
$stats = $this->emptyTableStats();
|
|
233
299
|
foreach (['processed' => 'processed_retention_hours', 'quarantined' => 'quarantine_retention_days'] as $status => $key) {
|
|
234
300
|
if ($cutoff = $policy->cutoff($key)) {
|
|
235
|
-
$this->deleteInBatches('telematic_deliveries', 'uuid', fn ($query) => $inbox($query)->where('status', $status)->where('updated_at', '<', $cutoff->toDateTimeString()), $stats);
|
|
301
|
+
$this->deleteInBatches('telematic_deliveries', 'uuid', fn ($query) => $inbox($query)->where('status', $status)->where('updated_at', '<', $cutoff->toDateTimeString()), $stats, 'updated_at');
|
|
236
302
|
}
|
|
237
303
|
}
|
|
238
304
|
|
|
@@ -244,46 +310,55 @@ class PruneTelematicsData extends Command
|
|
|
244
310
|
$stats = $this->emptyTableStats();
|
|
245
311
|
if ($cutoff = $policy->cutoff('sync_run_retention_days')) {
|
|
246
312
|
// In-flight runs are recovered by the drain command, never expired here.
|
|
247
|
-
$this->deleteInBatches('telematic_sync_runs', 'uuid', fn ($query) => $inbox($query)->whereNotIn('status', ['fetching', 'ingesting'])->where('updated_at', '<', $cutoff->toDateTimeString()), $stats);
|
|
313
|
+
$this->deleteInBatches('telematic_sync_runs', 'uuid', fn ($query) => $inbox($query)->whereNotIn('status', ['fetching', 'ingesting'])->where('updated_at', '<', $cutoff->toDateTimeString()), $stats, 'updated_at');
|
|
248
314
|
}
|
|
249
315
|
|
|
250
316
|
return $stats;
|
|
251
317
|
}
|
|
252
318
|
|
|
253
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Batches are ordered by the column the scope ranges over (the retention indexes lead
|
|
321
|
+
* with company_uuid and that column), so each pick is an index range rather than a
|
|
322
|
+
* primary-key walk across the whole table. Returns true when the scope was drained.
|
|
323
|
+
*/
|
|
324
|
+
protected function deleteInBatches(string $table, string $key, \Closure $scope, array &$stats, ?string $orderBy = null): bool
|
|
254
325
|
{
|
|
255
326
|
if ($this->dryRun) {
|
|
256
327
|
$stats['deleted'] += $scope(DB::table($table))->count();
|
|
257
328
|
|
|
258
|
-
return;
|
|
329
|
+
return true;
|
|
259
330
|
}
|
|
260
331
|
for ($batch = 0; $batch < $this->maxBatches; $batch++) {
|
|
261
|
-
$ids = $scope(DB::table($table))->orderBy($key)->limit($this->batchSize)->pluck($key);
|
|
332
|
+
$ids = $scope(DB::table($table))->orderBy($orderBy ?? $key)->limit($this->batchSize)->pluck($key);
|
|
262
333
|
if ($ids->isEmpty()) {
|
|
263
|
-
return;
|
|
334
|
+
return true;
|
|
264
335
|
}
|
|
265
336
|
$stats['batches']++;
|
|
266
337
|
$stats['deleted'] += DB::table($table)->whereIn($key, $ids->all())->delete();
|
|
267
338
|
}
|
|
268
339
|
$stats['capped'] = true;
|
|
340
|
+
|
|
341
|
+
return false;
|
|
269
342
|
}
|
|
270
343
|
|
|
271
|
-
protected function updateInBatches(string $table, string $key, \Closure $scope, array $values, array &$stats):
|
|
344
|
+
protected function updateInBatches(string $table, string $key, \Closure $scope, array $values, array &$stats, ?string $orderBy = null): bool
|
|
272
345
|
{
|
|
273
346
|
if ($this->dryRun) {
|
|
274
347
|
$stats['compacted'] += $scope(DB::table($table))->count();
|
|
275
348
|
|
|
276
|
-
return;
|
|
349
|
+
return true;
|
|
277
350
|
}
|
|
278
351
|
for ($batch = 0; $batch < $this->maxBatches; $batch++) {
|
|
279
|
-
$ids = $scope(DB::table($table))->orderBy($key)->limit($this->batchSize)->pluck($key);
|
|
352
|
+
$ids = $scope(DB::table($table))->orderBy($orderBy ?? $key)->limit($this->batchSize)->pluck($key);
|
|
280
353
|
if ($ids->isEmpty()) {
|
|
281
|
-
return;
|
|
354
|
+
return true;
|
|
282
355
|
}
|
|
283
356
|
$stats['batches']++;
|
|
284
357
|
$stats['compacted'] += DB::table($table)->whereIn($key, $ids->all())->update($values);
|
|
285
358
|
}
|
|
286
359
|
$stats['capped'] = true;
|
|
360
|
+
|
|
361
|
+
return false;
|
|
287
362
|
}
|
|
288
363
|
|
|
289
364
|
protected function report(string $label, array $stats): void
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Events;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Place;
|
|
8
|
+
use Fleetbase\FleetOps\Notifications\OrderChanged as OrderChangedNotification;
|
|
9
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
10
|
+
use Fleetbase\Models\User;
|
|
11
|
+
use Illuminate\Broadcasting\Channel;
|
|
12
|
+
use Illuminate\Broadcasting\InteractsWithSockets;
|
|
13
|
+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|
14
|
+
use Illuminate\Foundation\Bus\Dispatchable;
|
|
15
|
+
use Illuminate\Support\Carbon;
|
|
16
|
+
use Illuminate\Support\Facades\Log;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Broadcast as `order.changed` when an order's route, items, contact, schedule,
|
|
20
|
+
* notes or amounts were edited after it was placed.
|
|
21
|
+
*
|
|
22
|
+
* Channels: order.{public_id}, order.{uuid}, driver.{public_id} and driver.{uuid}
|
|
23
|
+
* when a driver is assigned, customer.{public_id} when a customer exists and
|
|
24
|
+
* company.{company_uuid}.
|
|
25
|
+
*
|
|
26
|
+
* Data: { id, changes[], summary, dropoff, cod_amount, total, currency, changed_by, changed_at }.
|
|
27
|
+
*
|
|
28
|
+
* Every value is captured when the event is created, so the broadcast does not
|
|
29
|
+
* depend on the order still looking the same when the queue sends it.
|
|
30
|
+
*/
|
|
31
|
+
class OrderChanged implements ShouldBroadcast
|
|
32
|
+
{
|
|
33
|
+
use Dispatchable;
|
|
34
|
+
use InteractsWithSockets;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Changes that concern the assigned driver: these send them a push.
|
|
38
|
+
*/
|
|
39
|
+
public const DRIVER_CHANGES = ['pickup', 'dropoff', 'return', 'waypoints', 'items', 'cod_amount', 'schedule', 'notes'];
|
|
40
|
+
|
|
41
|
+
public string $eventId;
|
|
42
|
+
|
|
43
|
+
public string $sentAt;
|
|
44
|
+
|
|
45
|
+
public array $channelNames = [];
|
|
46
|
+
|
|
47
|
+
public array $data = [];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param array $changes the changed parts: dropoff, items, contact, schedule, notes, total, cod_amount, ...
|
|
51
|
+
* @param string $summary one human line describing the change
|
|
52
|
+
* @param array $extra overrides for the data: total, currency, cod_amount, changed_by, changed_at, dropoff
|
|
53
|
+
*/
|
|
54
|
+
public function __construct(Order $order, array $changes, string $summary = '', array $extra = [])
|
|
55
|
+
{
|
|
56
|
+
$this->eventId = uniqid('event_');
|
|
57
|
+
$this->sentAt = Carbon::now()->toDateTimeString();
|
|
58
|
+
$this->channelNames = static::channelNamesFor($order);
|
|
59
|
+
$this->data = static::dataFor($order, $changes, $summary, $extra);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Broadcast `order.changed` for the order and, unless told otherwise, push the
|
|
64
|
+
* change to the assigned driver when it concerns them.
|
|
65
|
+
*
|
|
66
|
+
* Storefront and other extensions call this after their own order edits.
|
|
67
|
+
*/
|
|
68
|
+
public static function dispatchFor(Order $order, array $changes, string $summary = '', array $extra = [], bool $notifyDriver = true): ?self
|
|
69
|
+
{
|
|
70
|
+
$changes = array_values(array_unique(array_filter($changes, 'is_string')));
|
|
71
|
+
if ($changes === []) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
$event = new static($order, $changes, $summary, $extra);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
event($event);
|
|
79
|
+
} catch (\Throwable $exception) {
|
|
80
|
+
Log::warning('Unable to broadcast order.changed', ['order' => $order->public_id, 'error' => $exception->getMessage()]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if ($notifyDriver && static::concernsDriver($changes)) {
|
|
84
|
+
static::notifyDriver($order, $event);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return $event;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether any of the changes concerns the driver doing the order.
|
|
92
|
+
*/
|
|
93
|
+
public static function concernsDriver(array $changes): bool
|
|
94
|
+
{
|
|
95
|
+
return (bool) array_intersect($changes, self::DRIVER_CHANGES);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Push the change to the order's assigned driver.
|
|
100
|
+
*/
|
|
101
|
+
public static function notifyDriver(Order $order, self $event): bool
|
|
102
|
+
{
|
|
103
|
+
if (!$order->driver_assigned_uuid) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
$driver = static::assignedDriver($order);
|
|
109
|
+
if (!$driver) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
$driver->notify(new OrderChangedNotification($order, $event->data['changes'], (string) $event->data['summary']));
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
} catch (\Throwable $exception) {
|
|
117
|
+
Log::warning('Unable to notify driver of order change', ['order' => $order->public_id, 'error' => $exception->getMessage()]);
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The order's assigned driver, read past the driver scope as the order listeners do.
|
|
125
|
+
*/
|
|
126
|
+
public static function assignedDriver(Order $order): ?Driver
|
|
127
|
+
{
|
|
128
|
+
if (!$order->driver_assigned_uuid) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$driver = $order->relationLoaded('driverAssigned') ? $order->getRelation('driverAssigned') : null;
|
|
133
|
+
if ($driver instanceof Driver && $driver->uuid === $order->driver_assigned_uuid) {
|
|
134
|
+
return $driver;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
$driver = Driver::where('uuid', $order->driver_assigned_uuid)->withoutGlobalScopes()->first();
|
|
138
|
+
if ($driver) {
|
|
139
|
+
$order->setRelation('driverAssigned', $driver);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return $driver;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public static function channelNamesFor(Order $order): array
|
|
146
|
+
{
|
|
147
|
+
$channels = [
|
|
148
|
+
'order.' . $order->public_id,
|
|
149
|
+
'order.' . $order->uuid,
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
if ($order->driver_assigned_uuid) {
|
|
153
|
+
$driverPublicId = static::attempt(fn () => static::assignedDriver($order)?->public_id);
|
|
154
|
+
if ($driverPublicId) {
|
|
155
|
+
$channels[] = 'driver.' . $driverPublicId;
|
|
156
|
+
}
|
|
157
|
+
$channels[] = 'driver.' . $order->driver_assigned_uuid;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if ($order->customer_uuid) {
|
|
161
|
+
$customerPublicId = data_get($order, 'customer.public_id');
|
|
162
|
+
if ($customerPublicId) {
|
|
163
|
+
$channels[] = 'customer.' . $customerPublicId;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if ($order->company_uuid) {
|
|
168
|
+
$channels[] = 'company.' . $order->company_uuid;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return array_values(array_unique($channels));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public static function dataFor(Order $order, array $changes, string $summary = '', array $extra = []): array
|
|
175
|
+
{
|
|
176
|
+
$payload = $order->relationLoaded('payload') ? $order->payload : static::attempt(fn () => $order->payload);
|
|
177
|
+
|
|
178
|
+
return [
|
|
179
|
+
'id' => $order->public_id,
|
|
180
|
+
'changes' => array_values($changes),
|
|
181
|
+
'summary' => $summary,
|
|
182
|
+
'dropoff' => array_key_exists('dropoff', $extra) ? $extra['dropoff'] : static::placeData($payload?->dropoff ?? null),
|
|
183
|
+
'cod_amount' => array_key_exists('cod_amount', $extra) ? $extra['cod_amount'] : static::numberOrNull($payload?->cod_amount ?? null),
|
|
184
|
+
'total' => array_key_exists('total', $extra) ? $extra['total'] : static::numberOrNull(static::attempt(fn () => $order->transaction_amount)),
|
|
185
|
+
'currency' => array_key_exists('currency', $extra) ? $extra['currency'] : (static::attempt(fn () => $order->transaction_currency) ?? $payload?->cod_currency ?? null),
|
|
186
|
+
'changed_by' => array_key_exists('changed_by', $extra) ? $extra['changed_by'] : static::currentUserName(),
|
|
187
|
+
'changed_at' => $extra['changed_at'] ?? Carbon::now()->toIso8601ZuluString(),
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The drop-off as `{ id, address, location: { type: Point, coordinates: [lng, lat] } }`.
|
|
193
|
+
*/
|
|
194
|
+
public static function placeData(?Place $place): ?array
|
|
195
|
+
{
|
|
196
|
+
if (!$place instanceof Place) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
$location = $place->location;
|
|
201
|
+
|
|
202
|
+
return [
|
|
203
|
+
'id' => $place->public_id,
|
|
204
|
+
'address' => static::attempt(fn () => $place->address),
|
|
205
|
+
'location' => $location instanceof Point ? ['type' => 'Point', 'coordinates' => [$location->getLng(), $location->getLat()]] : null,
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public function broadcastOn(): array
|
|
210
|
+
{
|
|
211
|
+
return array_map(fn (string $name) => new Channel($name), $this->channelNames);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public function broadcastAs(): string
|
|
215
|
+
{
|
|
216
|
+
return 'order.changed';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public function broadcastWith(): array
|
|
220
|
+
{
|
|
221
|
+
return [
|
|
222
|
+
'id' => $this->eventId,
|
|
223
|
+
'api_version' => config('api.version'),
|
|
224
|
+
'event' => $this->broadcastAs(),
|
|
225
|
+
'created_at' => $this->sentAt,
|
|
226
|
+
'data' => $this->data,
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
protected static function currentUserName(): ?string
|
|
231
|
+
{
|
|
232
|
+
$userUuid = session('user');
|
|
233
|
+
if (!$userUuid) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return static::attempt(fn () => User::where('uuid', $userUuid)->value('name'));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
protected static function numberOrNull(mixed $value): int|float|null
|
|
241
|
+
{
|
|
242
|
+
if ($value === null || $value === '' || !is_numeric($value)) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return $value + 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
protected static function attempt(callable $callback): mixed
|
|
250
|
+
{
|
|
251
|
+
try {
|
|
252
|
+
return $callback();
|
|
253
|
+
} catch (\Throwable) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Events;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
7
|
+
use Illuminate\Broadcasting\Channel;
|
|
8
|
+
use Illuminate\Broadcasting\InteractsWithSockets;
|
|
9
|
+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|
10
|
+
use Illuminate\Foundation\Bus\Dispatchable;
|
|
11
|
+
use Illuminate\Support\Carbon;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Broadcast as `order.driver_unassigned` when a driver is taken off an order,
|
|
15
|
+
* whether unassigned or replaced by another driver.
|
|
16
|
+
*
|
|
17
|
+
* Channels: driver.{old driver public_id}, driver.{old driver uuid} and the
|
|
18
|
+
* order's company channel. Data: { id: order public id, driver: old driver public id }.
|
|
19
|
+
*/
|
|
20
|
+
class OrderDriverUnassigned implements ShouldBroadcast
|
|
21
|
+
{
|
|
22
|
+
use Dispatchable;
|
|
23
|
+
use InteractsWithSockets;
|
|
24
|
+
|
|
25
|
+
public string $eventId;
|
|
26
|
+
|
|
27
|
+
public string $sentAt;
|
|
28
|
+
|
|
29
|
+
public array $channelNames = [];
|
|
30
|
+
|
|
31
|
+
public array $data = [];
|
|
32
|
+
|
|
33
|
+
public function __construct(Order $order, Driver $driver)
|
|
34
|
+
{
|
|
35
|
+
$this->eventId = uniqid('event_');
|
|
36
|
+
$this->sentAt = Carbon::now()->toDateTimeString();
|
|
37
|
+
$this->channelNames = array_values(array_filter([
|
|
38
|
+
$driver->public_id ? 'driver.' . $driver->public_id : null,
|
|
39
|
+
$driver->uuid ? 'driver.' . $driver->uuid : null,
|
|
40
|
+
$order->company_uuid ? 'company.' . $order->company_uuid : null,
|
|
41
|
+
]));
|
|
42
|
+
$this->data = [
|
|
43
|
+
'id' => $order->public_id,
|
|
44
|
+
'driver' => $driver->public_id,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public function broadcastOn(): array
|
|
49
|
+
{
|
|
50
|
+
return array_map(fn (string $name) => new Channel($name), $this->channelNames);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public function broadcastAs(): string
|
|
54
|
+
{
|
|
55
|
+
return 'order.driver_unassigned';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public function broadcastWith(): array
|
|
59
|
+
{
|
|
60
|
+
return [
|
|
61
|
+
'id' => $this->eventId,
|
|
62
|
+
'api_version' => config('api.version'),
|
|
63
|
+
'event' => $this->broadcastAs(),
|
|
64
|
+
'created_at' => $this->sentAt,
|
|
65
|
+
'data' => $this->data,
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Exceptions;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when an order edit touches a field its current status locks.
|
|
7
|
+
*
|
|
8
|
+
* Carries the refused fields with the reason for each, so a controller can
|
|
9
|
+
* answer 422 with `{ error, locked: { field: reason } }`.
|
|
10
|
+
*/
|
|
11
|
+
class OrderEditLockedException extends \Exception
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* @param array<string, string> $locked the refused fields and why each is locked
|
|
15
|
+
*/
|
|
16
|
+
public function __construct(public array $locked = [], string $message = '', int $code = 422, ?\Throwable $previous = null)
|
|
17
|
+
{
|
|
18
|
+
if ($message === '') {
|
|
19
|
+
$message = $locked ? implode(' ', array_values(array_unique($locked))) : 'This order can no longer be edited.';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
parent::__construct($message, $code, $previous);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The 422 response body: the message plus the refused fields.
|
|
27
|
+
*/
|
|
28
|
+
public function toResponseData(): array
|
|
29
|
+
{
|
|
30
|
+
return ['locked' => $this->locked];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -4,6 +4,7 @@ namespace Fleetbase\FleetOps\Expansions;
|
|
|
4
4
|
|
|
5
5
|
use Fleetbase\Build\Expansion;
|
|
6
6
|
use Fleetbase\FleetOps\Models\Driver;
|
|
7
|
+
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
7
8
|
|
|
8
9
|
class CompanyExpansion implements Expansion
|
|
9
10
|
{
|
|
@@ -18,11 +19,11 @@ class CompanyExpansion implements Expansion
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
|
-
* @return
|
|
22
|
+
* @return HasMany
|
|
22
23
|
*/
|
|
23
24
|
public static function drivers()
|
|
24
25
|
{
|
|
25
|
-
return function () {
|
|
26
|
+
return function (): HasMany {
|
|
26
27
|
/** @var \Illuminate\Database\Eloquent\Model $this */
|
|
27
28
|
return $this->hasMany(Driver::class);
|
|
28
29
|
};
|