@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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
4
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
5
|
+
use Fleetbase\FleetOps\Models\Place;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Waypoint;
|
|
8
|
+
use Fleetbase\FleetOps\Support\TrackingScope;
|
|
9
|
+
use Fleetbase\FleetOps\Support\TrackingUpdate;
|
|
10
|
+
use Fleetbase\FleetOps\Tracking\TrackingStop;
|
|
11
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
12
|
+
use Fleetbase\Models\User;
|
|
13
|
+
use Illuminate\Database\ConnectionResolver;
|
|
14
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
15
|
+
use Illuminate\Database\SQLiteConnection;
|
|
16
|
+
use Illuminate\Support\Carbon;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Covers the per-customer public tracking payload: only the viewing customer's stops, ETAs and
|
|
20
|
+
* timelines; whole-route counts; the driver's first name, vehicle label and online flag with no
|
|
21
|
+
* photo or phone; and the rounded vehicle position only while en route to a stop of theirs.
|
|
22
|
+
*/
|
|
23
|
+
class TrackingUpdateExplodingDriver extends Driver
|
|
24
|
+
{
|
|
25
|
+
public function getLocationAttribute()
|
|
26
|
+
{
|
|
27
|
+
throw new RuntimeException('unreadable location');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TRACKING_UPDATE_CONTACT = 'Fleetbase\\FleetOps\\Models\\Contact';
|
|
32
|
+
|
|
33
|
+
function trackingUpdateModel(string $class, array $attributes): EloquentModel
|
|
34
|
+
{
|
|
35
|
+
$model = (new ReflectionClass($class))->newInstanceWithoutConstructor();
|
|
36
|
+
$model->setRawAttributes($attributes, true);
|
|
37
|
+
|
|
38
|
+
return $model;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function trackingUpdatePlace(string $name, string $address): Place
|
|
42
|
+
{
|
|
43
|
+
return trackingUpdateModel(Place::class, ['uuid' => 'place-' . md5($name), 'public_id' => 'place_' . substr(md5($name), 0, 7), 'name' => $name, 'street1' => $address, 'location' => new Point(1.4, 103.9)]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function trackingUpdateWaypoint(string $publicId, ?string $customerUuid): Waypoint
|
|
47
|
+
{
|
|
48
|
+
return trackingUpdateModel(Waypoint::class, [
|
|
49
|
+
'uuid' => 'uuid-' . $publicId,
|
|
50
|
+
'public_id' => $publicId,
|
|
51
|
+
'customer_uuid' => $customerUuid,
|
|
52
|
+
'customer_type' => $customerUuid ? TRACKING_UPDATE_CONTACT : null,
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function trackingUpdateStop(string $type, int $sequence, bool $completed, ?Waypoint $waypoint, string $trackingNumber, string $placeName): TrackingStop
|
|
57
|
+
{
|
|
58
|
+
return new TrackingStop(
|
|
59
|
+
uuid: 'place-' . $sequence,
|
|
60
|
+
publicId: 'place_' . $sequence,
|
|
61
|
+
type: $type,
|
|
62
|
+
status: $completed ? 'completed' : 'enroute',
|
|
63
|
+
place: trackingUpdatePlace($placeName, $placeName . ' Street'),
|
|
64
|
+
waypoint: $waypoint,
|
|
65
|
+
completed: $completed,
|
|
66
|
+
sequence: $sequence,
|
|
67
|
+
trackingNumberUuid: $trackingNumber,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A started order for contact-1111 sharing its route with contact-2222:
|
|
73
|
+
* pickup (theirs, done), Jane's stop, their own drop, Jane's second stop.
|
|
74
|
+
*/
|
|
75
|
+
function trackingUpdateStops(bool $janeDone = false): array
|
|
76
|
+
{
|
|
77
|
+
return [
|
|
78
|
+
trackingUpdateStop('pickup', 1, true, null, 'tn-pickup', 'Acme Warehouse'),
|
|
79
|
+
trackingUpdateStop('waypoint', 2, $janeDone, trackingUpdateWaypoint('waypoint_jane1', 'contact-2222'), 'tn-jane1', 'Jane Home'),
|
|
80
|
+
trackingUpdateStop('waypoint', 3, false, trackingUpdateWaypoint('waypoint_own', null), 'tn-own', 'Own Office'),
|
|
81
|
+
trackingUpdateStop('waypoint', 4, $janeDone, trackingUpdateWaypoint('waypoint_jane2', 'contact-2222'), 'tn-jane2', 'Jane Gym'),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function trackingUpdateDriver(array $attributes = [], string $class = Driver::class): Driver
|
|
86
|
+
{
|
|
87
|
+
$driver = trackingUpdateModel($class, array_merge([
|
|
88
|
+
'uuid' => 'driver-1111',
|
|
89
|
+
'public_id' => 'driver_1111',
|
|
90
|
+
'online' => true,
|
|
91
|
+
'heading' => 87.6,
|
|
92
|
+
'location' => new Point(1.352083, 103.819836),
|
|
93
|
+
'phone' => '+6591234567',
|
|
94
|
+
'updated_at' => Carbon::parse('2026-10-06 10:00:30'),
|
|
95
|
+
], $attributes));
|
|
96
|
+
$driver->setRelation('user', trackingUpdateModel(User::class, ['name' => 'Ana Lopez', 'phone' => '+6591234567', 'avatar_url' => 'https://cdn.example/ana.jpg']));
|
|
97
|
+
$driver->setRelation('vehicle', null);
|
|
98
|
+
|
|
99
|
+
return $driver;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function trackingUpdateVehicle(array $attributes = []): Vehicle
|
|
103
|
+
{
|
|
104
|
+
return trackingUpdateModel(Vehicle::class, array_merge([
|
|
105
|
+
'uuid' => 'vehicle-1111',
|
|
106
|
+
'name' => 'Van 7',
|
|
107
|
+
'plate_number' => 'SGX1234A',
|
|
108
|
+
'heading' => 10,
|
|
109
|
+
'location' => new Point(1.30001, 103.80001),
|
|
110
|
+
'updated_at' => Carbon::parse('2026-10-06 10:00:00'),
|
|
111
|
+
], $attributes));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function trackingUpdateOrder(array $attributes = [], ?Driver $driver = null, ?Vehicle $vehicle = null): Order
|
|
115
|
+
{
|
|
116
|
+
$order = trackingUpdateModel(Order::class, array_merge([
|
|
117
|
+
'uuid' => 'order-1111',
|
|
118
|
+
'company_uuid' => 'company-1111',
|
|
119
|
+
'customer_type' => TRACKING_UPDATE_CONTACT,
|
|
120
|
+
'customer_uuid' => 'contact-1111',
|
|
121
|
+
'tracking_number_uuid' => 'tn-order',
|
|
122
|
+
'status' => 'started',
|
|
123
|
+
'started' => true,
|
|
124
|
+
], $attributes));
|
|
125
|
+
$order->setRelation('driverAssigned', $driver);
|
|
126
|
+
$order->setRelation('vehicleAssigned', $vehicle);
|
|
127
|
+
|
|
128
|
+
return $order;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function trackingUpdateTimelines(): array
|
|
132
|
+
{
|
|
133
|
+
$entry = fn (string $status, string $code) => ['status' => $status, 'code' => $code, 'complete' => false, 'created_at' => '2026-10-06T09:00:00.000000Z'];
|
|
134
|
+
|
|
135
|
+
return [
|
|
136
|
+
'tn-order' => [$entry('Order started', 'started')],
|
|
137
|
+
'tn-pickup' => [$entry('Picked up', 'picked_up')],
|
|
138
|
+
'tn-own' => [$entry('Driver en route', 'enroute')],
|
|
139
|
+
'tn-jane1' => [$entry('Arriving at Jane Home', 'enroute')],
|
|
140
|
+
'tn-jane2' => [$entry('Queued for Jane Gym', 'pending')],
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function trackingUpdateEtas(): array
|
|
145
|
+
{
|
|
146
|
+
return [
|
|
147
|
+
2 => ['seconds' => 300, 'at' => '2026-10-06T10:05:00.000000Z', 'distance_m' => 1200],
|
|
148
|
+
3 => ['seconds' => 600, 'at' => '2026-10-06T10:10:00.000000Z', 'distance_m' => 2500],
|
|
149
|
+
4 => ['seconds' => 900, 'at' => '2026-10-06T10:15:00.000000Z', 'distance_m' => 4100],
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
beforeEach(function () {
|
|
154
|
+
Carbon::setTestNow('2026-10-06 10:01:00');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterEach(function () {
|
|
158
|
+
Carbon::setTestNow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('the order customer sees only their own stops with whole-route counts', function () {
|
|
162
|
+
$order = trackingUpdateOrder([], trackingUpdateDriver(), trackingUpdateVehicle());
|
|
163
|
+
$scope = TrackingScope::forOrder($order);
|
|
164
|
+
$update = new TrackingUpdate($order, $scope, trackingUpdateStops(), trackingUpdateEtas(), trackingUpdateTimelines());
|
|
165
|
+
|
|
166
|
+
$payload = $update->toArray(TrackingUpdate::REASON_STOP);
|
|
167
|
+
|
|
168
|
+
expect($payload['event'])->toBe('tracking.updated')
|
|
169
|
+
->and($payload['reason'])->toBe('stop')
|
|
170
|
+
->and($payload['sent_at'])->toBe('2026-10-06T10:01:00.000000Z')
|
|
171
|
+
->and($payload['data']['order'])->toBe([
|
|
172
|
+
'status' => 'started',
|
|
173
|
+
'en_route' => true,
|
|
174
|
+
'completed' => false,
|
|
175
|
+
'canceled' => false,
|
|
176
|
+
'timeline' => trackingUpdateTimelines()['tn-order'],
|
|
177
|
+
])
|
|
178
|
+
->and($payload['data']['route'])->toBe(['total_stops' => 4, 'completed_stops' => 1, 'stops_before' => 1])
|
|
179
|
+
->and($payload['data']['stops'])->toBe([
|
|
180
|
+
['id' => null, 'type' => 'pickup', 'sequence' => 1, 'status' => 'completed', 'complete' => true, 'eta' => null, 'timeline' => trackingUpdateTimelines()['tn-pickup']],
|
|
181
|
+
['id' => 'waypoint_own', 'type' => 'waypoint', 'sequence' => 3, 'status' => 'enroute', 'complete' => false, 'eta' => trackingUpdateEtas()[3], 'timeline' => trackingUpdateTimelines()['tn-own']],
|
|
182
|
+
])
|
|
183
|
+
->and($payload['data']['driver'])->toBe(['name' => 'Ana', 'vehicle' => 'Van 7', 'online' => true])
|
|
184
|
+
->and($payload['data']['location'])->toBe(['latitude' => 1.3521, 'longitude' => 103.8198, 'heading' => 88, 'seen_at' => '2026-10-06T10:00:30.000000Z'])
|
|
185
|
+
->and($update->timelineTrackingNumbers())->toBe(['tn-pickup', 'tn-own', 'tn-order']);
|
|
186
|
+
|
|
187
|
+
// Nothing of the other customer's stops, places or people, and nothing personal about the driver.
|
|
188
|
+
$json = json_encode($payload);
|
|
189
|
+
foreach (['waypoint_jane1', 'waypoint_jane2', 'Jane', 'contact-2222', 'Acme Warehouse', 'Own Office', 'Street', 'Lopez', '+6591234567', 'ana.jpg', 'SGX1234A', 'tn-'] as $forbidden) {
|
|
190
|
+
expect($json)->not->toContain($forbidden);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('a waypoint customer sees only their stops and never the order timeline', function () {
|
|
195
|
+
$order = trackingUpdateOrder([], trackingUpdateDriver(), trackingUpdateVehicle());
|
|
196
|
+
$scope = TrackingScope::forStop($order, trackingUpdateWaypoint('waypoint_jane1', 'contact-2222'));
|
|
197
|
+
$update = new TrackingUpdate($order, $scope, trackingUpdateStops(), trackingUpdateEtas(), trackingUpdateTimelines());
|
|
198
|
+
|
|
199
|
+
$payload = $update->toArray();
|
|
200
|
+
|
|
201
|
+
expect($payload['reason'])->toBe('status')
|
|
202
|
+
->and(array_column($payload['data']['stops'], 'id'))->toBe(['waypoint_jane1', 'waypoint_jane2'])
|
|
203
|
+
->and(array_column($payload['data']['stops'], 'eta'))->toBe([trackingUpdateEtas()[2], trackingUpdateEtas()[4]])
|
|
204
|
+
->and($payload['data']['stops'][0]['timeline'])->toBe(trackingUpdateTimelines()['tn-jane1'])
|
|
205
|
+
->and($payload['data']['order']['timeline'])->toBe([])
|
|
206
|
+
->and($payload['data']['route'])->toBe(['total_stops' => 4, 'completed_stops' => 1, 'stops_before' => 0])
|
|
207
|
+
->and($update->timelineTrackingNumbers())->toBe(['tn-jane1', 'tn-jane2'])
|
|
208
|
+
->and($payload['data']['location'])->not->toBeNull();
|
|
209
|
+
|
|
210
|
+
$json = json_encode($payload);
|
|
211
|
+
foreach (['waypoint_own', 'contact-1111', 'Order started', 'Picked up', 'Driver en route', 'Acme', 'Own Office', 'Lopez', '+6591234567', 'ana.jpg'] as $forbidden) {
|
|
212
|
+
expect($json)->not->toContain($forbidden);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('the driver and position are shown only while en route to a stop of the viewer', function () {
|
|
217
|
+
$driver = trackingUpdateDriver();
|
|
218
|
+
$vehicle = trackingUpdateVehicle();
|
|
219
|
+
|
|
220
|
+
// Not started yet: no driver and no position.
|
|
221
|
+
$order = trackingUpdateOrder(['status' => 'dispatched', 'started' => false], $driver, $vehicle);
|
|
222
|
+
$update = new TrackingUpdate($order, TrackingScope::forOrder($order), trackingUpdateStops());
|
|
223
|
+
expect($update->isEnRoute())->toBeFalse()
|
|
224
|
+
->and($update->driver())->toBeNull()
|
|
225
|
+
->and($update->location())->toBeNull()
|
|
226
|
+
->and($update->locationPayload())->toBeNull();
|
|
227
|
+
|
|
228
|
+
// Started by status or timestamp alone.
|
|
229
|
+
$byStatus = trackingUpdateOrder(['status' => 'started', 'started' => false], $driver, $vehicle);
|
|
230
|
+
$byTime = trackingUpdateOrder(['status' => 'enroute', 'started' => false, 'started_at' => Carbon::parse('2026-10-06 09:00:00')], $driver, $vehicle);
|
|
231
|
+
expect((new TrackingUpdate($byStatus, TrackingScope::forOrder($byStatus), trackingUpdateStops()))->isEnRoute())->toBeTrue()
|
|
232
|
+
->and((new TrackingUpdate($byTime, TrackingScope::forOrder($byTime), trackingUpdateStops()))->isEnRoute())->toBeTrue();
|
|
233
|
+
|
|
234
|
+
// Delivered or canceled: hidden, and the order state says so.
|
|
235
|
+
foreach (['completed', 'canceled'] as $status) {
|
|
236
|
+
$done = trackingUpdateOrder(['status' => $status], $driver, $vehicle);
|
|
237
|
+
$payload = (new TrackingUpdate($done, TrackingScope::forOrder($done), trackingUpdateStops()))->toArray();
|
|
238
|
+
expect($payload['data']['driver'])->toBeNull()
|
|
239
|
+
->and($payload['data']['location'])->toBeNull()
|
|
240
|
+
->and($payload['data']['order'][$status])->toBeTrue()
|
|
241
|
+
->and($payload['data']['order']['en_route'])->toBeFalse();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// En route, but every stop of this customer is done: hidden for them, shown to the other.
|
|
245
|
+
$order = trackingUpdateOrder([], $driver, $vehicle);
|
|
246
|
+
$jane = new TrackingUpdate($order, TrackingScope::forStop($order, trackingUpdateWaypoint('waypoint_jane1', 'contact-2222')), trackingUpdateStops(true));
|
|
247
|
+
$own = new TrackingUpdate($order, TrackingScope::forOrder($order), trackingUpdateStops(true));
|
|
248
|
+
expect($jane->customerHasIncompleteStop())->toBeFalse()
|
|
249
|
+
->and($jane->locationPayload())->toBeNull()
|
|
250
|
+
->and($jane->route()['stops_before'])->toBe(0)
|
|
251
|
+
->and($own->locationPayload()['event'])->toBe('tracking.location')
|
|
252
|
+
->and($own->locationPayload()['reason'])->toBe('location')
|
|
253
|
+
->and($own->locationPayload()['data'])->toBe([
|
|
254
|
+
'driver' => ['name' => 'Ana', 'vehicle' => 'Van 7', 'online' => true],
|
|
255
|
+
'location' => ['latitude' => 1.3521, 'longitude' => 103.8198, 'heading' => 88, 'seen_at' => '2026-10-06T10:00:30.000000Z'],
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
// A customer with no stops on the route sees no vehicle.
|
|
259
|
+
$stranger = new TrackingUpdate($order, new TrackingScope('order-1111', TRACKING_UPDATE_CONTACT, 'contact-9999'), trackingUpdateStops());
|
|
260
|
+
expect($stranger->ownStops())->toHaveCount(0)
|
|
261
|
+
->and($stranger->location())->toBeNull()
|
|
262
|
+
->and($stranger->toArray()['data']['stops'])->toBe([]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('the position comes from the most recently seen of driver and vehicle', function () {
|
|
266
|
+
$stops = trackingUpdateStops();
|
|
267
|
+
|
|
268
|
+
// The vehicle reported last.
|
|
269
|
+
$order = trackingUpdateOrder([], trackingUpdateDriver(['updated_at' => Carbon::parse('2026-10-06 09:00:00')]), trackingUpdateVehicle(['updated_at' => Carbon::parse('2026-10-06 10:00:50')]));
|
|
270
|
+
expect((new TrackingUpdate($order, TrackingScope::forOrder($order), $stops))->location())
|
|
271
|
+
->toBe(['latitude' => 1.3, 'longitude' => 103.8, 'heading' => 10, 'seen_at' => '2026-10-06T10:00:50.000000Z']);
|
|
272
|
+
|
|
273
|
+
// The driver's position is unusable (null island), so the vehicle's is used.
|
|
274
|
+
$order = trackingUpdateOrder([], trackingUpdateDriver(['location' => new Point(0, 0)]), trackingUpdateVehicle(['heading' => 'n/a', 'updated_at' => null]));
|
|
275
|
+
expect((new TrackingUpdate($order, TrackingScope::forOrder($order), $stops))->location())
|
|
276
|
+
->toBe(['latitude' => 1.3, 'longitude' => 103.8, 'heading' => null, 'seen_at' => null]);
|
|
277
|
+
|
|
278
|
+
// Unreadable or missing positions everywhere: no position, but the driver is still named.
|
|
279
|
+
$order = trackingUpdateOrder([], trackingUpdateDriver([], TrackingUpdateExplodingDriver::class), trackingUpdateVehicle(['location' => null]));
|
|
280
|
+
$update = new TrackingUpdate($order, TrackingScope::forOrder($order), $stops);
|
|
281
|
+
expect($update->location())->toBeNull()
|
|
282
|
+
->and($update->locationPayload())->toBeNull()
|
|
283
|
+
->and($update->driver()['name'])->toBe('Ana');
|
|
284
|
+
|
|
285
|
+
// Without a driver, the assigned vehicle alone gives the position.
|
|
286
|
+
$order = trackingUpdateOrder([], null, trackingUpdateVehicle());
|
|
287
|
+
$update = new TrackingUpdate($order, TrackingScope::forOrder($order), $stops);
|
|
288
|
+
expect($update->driver())->toBeNull()
|
|
289
|
+
->and($update->location()['latitude'])->toBe(1.3);
|
|
290
|
+
|
|
291
|
+
// Without an assigned vehicle the driver's current vehicle is labelled; an unnamed one has no label.
|
|
292
|
+
$driver = trackingUpdateDriver();
|
|
293
|
+
$driver->setRelation('vehicle', trackingUpdateVehicle(['name' => 'Truck 9']));
|
|
294
|
+
$order = trackingUpdateOrder([], $driver, null);
|
|
295
|
+
expect((new TrackingUpdate($order, TrackingScope::forOrder($order), $stops))->driver()['vehicle'])->toBe('Truck 9');
|
|
296
|
+
|
|
297
|
+
$driver->setRelation('vehicle', trackingUpdateVehicle(['name' => null]));
|
|
298
|
+
expect((new TrackingUpdate($order, TrackingScope::forOrder($order), $stops))->driver()['vehicle'])->toBeNull();
|
|
299
|
+
|
|
300
|
+
// Nobody assigned at all.
|
|
301
|
+
$order = trackingUpdateOrder([], null, null);
|
|
302
|
+
expect((new TrackingUpdate($order, TrackingScope::forOrder($order), $stops))->location())->toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('order state, names and etas tolerate missing values', function () {
|
|
306
|
+
$order = trackingUpdateOrder(['status' => null, 'started' => false], null, null);
|
|
307
|
+
$update = new TrackingUpdate($order, TrackingScope::forOrder($order), ['not a stop', ...trackingUpdateStops()]);
|
|
308
|
+
|
|
309
|
+
expect($update->toArray()['data']['order']['status'])->toBeNull()
|
|
310
|
+
->and($update->route()['total_stops'])->toBe(4)
|
|
311
|
+
->and($update->isTerminal())->toBeFalse()
|
|
312
|
+
->and(TrackingUpdate::firstName(null))->toBeNull()
|
|
313
|
+
->and(TrackingUpdate::firstName(' '))->toBeNull()
|
|
314
|
+
->and(TrackingUpdate::firstName('Cher'))->toBe('Cher')
|
|
315
|
+
->and(TrackingUpdate::firstName(' Ana Maria Lopez '))->toBe('Ana');
|
|
316
|
+
|
|
317
|
+
expect(TrackingUpdate::etasFromTracker([]))->toBe([])
|
|
318
|
+
->and(TrackingUpdate::etasFromTracker(['route' => ['legs' => [
|
|
319
|
+
['stop' => null, 'eta_seconds' => 10],
|
|
320
|
+
['stop' => ['sequence' => 2], 'eta_seconds' => 299.6, 'eta_at' => '2026-10-06T10:05:00.000000Z', 'distance_m' => 1199.7],
|
|
321
|
+
['stop' => ['sequence' => '3'], 'eta_seconds' => null, 'eta_at' => null],
|
|
322
|
+
]]]))->toBe([
|
|
323
|
+
2 => ['seconds' => 300, 'at' => '2026-10-06T10:05:00.000000Z', 'distance_m' => 1200],
|
|
324
|
+
3 => ['seconds' => null, 'at' => null, 'distance_m' => null],
|
|
325
|
+
]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('timelines are loaded for the viewer\'s tracking numbers only, without free-text details', function () {
|
|
329
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
330
|
+
$resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
331
|
+
$resolver->setDefaultConnection('mysql');
|
|
332
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
333
|
+
$connection->getSchemaBuilder()->create('tracking_statuses', function ($blueprint) {
|
|
334
|
+
$blueprint->increments('id');
|
|
335
|
+
foreach (['uuid', 'public_id', 'tracking_number_uuid', 'status', 'details', 'code', 'complete'] as $column) {
|
|
336
|
+
$blueprint->string($column)->nullable();
|
|
337
|
+
}
|
|
338
|
+
$blueprint->timestamp('created_at')->nullable();
|
|
339
|
+
$blueprint->timestamp('updated_at')->nullable();
|
|
340
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
341
|
+
});
|
|
342
|
+
$connection->table('tracking_statuses')->insert([
|
|
343
|
+
['uuid' => 's1', 'tracking_number_uuid' => 'tn-order', 'status' => 'Order created', 'details' => 'Created by Jane', 'code' => 'CREATED', 'complete' => '0', 'created_at' => '2026-10-06 08:00:00'],
|
|
344
|
+
['uuid' => 's2', 'tracking_number_uuid' => 'tn-order', 'status' => 'Order started', 'details' => 'Driver left Acme', 'code' => 'started', 'complete' => '0', 'created_at' => '2026-10-06 09:00:00'],
|
|
345
|
+
['uuid' => 's3', 'tracking_number_uuid' => 'tn-own', 'status' => 'Driver en route', 'details' => null, 'code' => null, 'complete' => '0', 'created_at' => null],
|
|
346
|
+
['uuid' => 's4', 'tracking_number_uuid' => 'tn-jane1', 'status' => 'Arriving at Jane Home', 'details' => null, 'code' => 'enroute', 'complete' => '0', 'created_at' => '2026-10-06 09:30:00'],
|
|
347
|
+
]);
|
|
348
|
+
|
|
349
|
+
$order = trackingUpdateOrder([], trackingUpdateDriver(), trackingUpdateVehicle());
|
|
350
|
+
$tracker = ['route' => ['legs' => [['stop' => ['sequence' => 3], 'eta_seconds' => 600, 'eta_at' => '2026-10-06T10:11:00.000000Z', 'distance_m' => 2500]]]];
|
|
351
|
+
$update = TrackingUpdate::for($order, TrackingScope::forOrder($order), trackingUpdateStops(), $tracker);
|
|
352
|
+
$payload = $update->toArray();
|
|
353
|
+
|
|
354
|
+
expect($payload['data']['order']['timeline'])->toBe([
|
|
355
|
+
['status' => 'Order created', 'code' => 'created', 'complete' => false, 'created_at' => '2026-10-06T08:00:00.000000Z'],
|
|
356
|
+
['status' => 'Order started', 'code' => 'started', 'complete' => false, 'created_at' => '2026-10-06T09:00:00.000000Z'],
|
|
357
|
+
])
|
|
358
|
+
->and($payload['data']['stops'][1]['timeline'])->toBe([['status' => 'Driver en route', 'code' => null, 'complete' => false, 'created_at' => null]])
|
|
359
|
+
->and($payload['data']['stops'][1]['eta'])->toBe(['seconds' => 600, 'at' => '2026-10-06T10:11:00.000000Z', 'distance_m' => 2500])
|
|
360
|
+
->and(json_encode($payload))->not->toContain('Jane')
|
|
361
|
+
->and(json_encode($payload))->not->toContain('Acme')
|
|
362
|
+
->and(TrackingUpdate::loadTimelines([]))->toBe([]);
|
|
363
|
+
});
|
|
@@ -70,6 +70,15 @@ function fleetopsUtilsAdditionalPolygon(): Polygon
|
|
|
70
70
|
]);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
test('stored file url signing passes through values that are not absolute urls', function () {
|
|
74
|
+
// Absolute URLs into the media bucket are re-signed by core-api File::signStoredUrl (covered there);
|
|
75
|
+
// everything else, including avatar option keys and empty values, must come back untouched.
|
|
76
|
+
expect(Utils::signStoredFileUrl(null))->toBeNull()
|
|
77
|
+
->and(Utils::signStoredFileUrl(''))->toBe('')
|
|
78
|
+
->and(Utils::signStoredFileUrl('mini_bus'))->toBe('mini_bus')
|
|
79
|
+
->and(Utils::signStoredFileUrl('custom-avatars/vehicles/c/van.png'))->toBe('custom-avatars/vehicles/c/van.png');
|
|
80
|
+
});
|
|
81
|
+
|
|
73
82
|
test('company transaction currency prefers organization currency and falls back to usd', function () {
|
|
74
83
|
$company = new Company();
|
|
75
84
|
$company->setRawAttributes([
|
|
@@ -380,3 +380,28 @@ test('feature geometries strict lookups matrices and centroids cover edge seams'
|
|
|
380
380
|
expect(Utils::getCentroidFromGeosPolygon($brickPolygon)->x())->toBe(103.81)
|
|
381
381
|
->and(Utils::getCentroidFromGeosMultiPolygon(Brick\Geo\MultiPolygon::of($brickPolygon))->y())->toBe(1.31);
|
|
382
382
|
});
|
|
383
|
+
|
|
384
|
+
test('coordsToCircle keeps every vertex on the requested radius', function () {
|
|
385
|
+
$haversine = function (float $lat1, float $lng1, float $lat2, float $lng2): float {
|
|
386
|
+
$dLat = deg2rad($lat2 - $lat1);
|
|
387
|
+
$dLng = deg2rad($lng2 - $lng1);
|
|
388
|
+
$a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
|
|
389
|
+
|
|
390
|
+
return 6378137 * 2 * atan2(sqrt($a), sqrt(1 - $a));
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// 15 km around Seattle, the case from fleetbase/fleetops#360
|
|
394
|
+
foreach (Utils::coordsToCircle(47.6062, -122.3321, 15000) as [$lat, $lng]) {
|
|
395
|
+
expect($haversine(47.6062, -122.3321, $lat, $lng))->toEqualWithDelta(15000, 1);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('coordsToCircle wraps longitudes across the antimeridian', function () {
|
|
400
|
+
$longitudes = array_column(Utils::coordsToCircle(0, -179.99, 5000), 1);
|
|
401
|
+
|
|
402
|
+
expect(min($longitudes))->toBeGreaterThanOrEqual(-180)
|
|
403
|
+
->and(max($longitudes))->toBeLessThanOrEqual(180)
|
|
404
|
+
// the eastern half of the ring crosses to the far side of the antimeridian
|
|
405
|
+
->and(max($longitudes))->toBeGreaterThan(179.9)
|
|
406
|
+
->and(min($longitudes))->toBeLessThan(-179.99);
|
|
407
|
+
});
|
|
@@ -350,3 +350,49 @@ test('requested relations are empty when the resource cannot load any', function
|
|
|
350
350
|
->and(fleetopsVehicleResourceRequestedRelations((object) ['uuid' => 'vehicle-uuid'], ['with' => ['driver']]))->toBe([])
|
|
351
351
|
->and(fleetopsVehicleResourceRequestedRelations(fleetopsVehicleResourceFixture(), ['with' => ['driver']]))->toBe([]);
|
|
352
352
|
});
|
|
353
|
+
|
|
354
|
+
class FleetOpsVehicleResourceRelationModel extends Illuminate\Database\Eloquent\Model
|
|
355
|
+
{
|
|
356
|
+
public static int $saved = 0;
|
|
357
|
+
|
|
358
|
+
protected $table = 'vehicles';
|
|
359
|
+
|
|
360
|
+
public function driver(): Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
361
|
+
{
|
|
362
|
+
return $this->belongsTo(FleetOpsVehicleResourceRelationModel::class, 'driver_id');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
public function parentVehicle(): Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
366
|
+
{
|
|
367
|
+
return $this->belongsTo(FleetOpsVehicleResourceRelationModel::class, 'parent_id');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public function untypedDriver()
|
|
371
|
+
{
|
|
372
|
+
return $this->belongsTo(FleetOpsVehicleResourceRelationModel::class, 'driver_id');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
public function saveInstance(): self
|
|
376
|
+
{
|
|
377
|
+
static::$saved++;
|
|
378
|
+
|
|
379
|
+
return $this;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
test('requested relations drop names that are methods but not declared relations', function () {
|
|
384
|
+
$resolver = new Illuminate\Database\ConnectionResolver(['default' => new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'))]);
|
|
385
|
+
$resolver->setDefaultConnection('default');
|
|
386
|
+
Illuminate\Database\Eloquent\Model::setConnectionResolver($resolver);
|
|
387
|
+
FleetOpsVehicleResourceRelationModel::$saved = 0;
|
|
388
|
+
|
|
389
|
+
$vehicle = new FleetOpsVehicleResourceRelationModel();
|
|
390
|
+
|
|
391
|
+
// `with=saveInstance` used to reach loadMissing(), which calls the method on a
|
|
392
|
+
// blank model. Only methods that declare a Relation return type survive, named
|
|
393
|
+
// as the model declares them, and nested segments are checked the same way.
|
|
394
|
+
expect(fleetopsVehicleResourceRequestedRelations($vehicle, ['with' => ['driver', 'saveInstance', 'untypedDriver', 'delete', 'driver.saveInstance', 'parent_vehicle', 'payload']]))
|
|
395
|
+
->toBe(['driver', 'parentVehicle'])
|
|
396
|
+
->and(fleetopsVehicleResourceRequestedRelations($vehicle, ['with' => 'saveInstance']))->toBe([])
|
|
397
|
+
->and(FleetOpsVehicleResourceRelationModel::$saved)->toBe(0);
|
|
398
|
+
})->skip(fn () => !class_exists(Fleetbase\Support\RequestRelations::class), 'needs fleetbase/core-api with Fleetbase\Support\RequestRelations');
|
|
@@ -112,7 +112,40 @@ test('vroom maps shipment delivery steps back to fleetops assignments', function
|
|
|
112
112
|
'duration' => 900,
|
|
113
113
|
'distance' => 4200,
|
|
114
114
|
]);
|
|
115
|
-
expect($result['summary'])->toBe(['routes' => 1]);
|
|
115
|
+
expect($result['summary'])->toBe(['engine' => 'vroom', 'routes' => 1]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('vroom carries route totals onto assignments and anchors relative arrivals to the run start', function () {
|
|
119
|
+
$engine = new VroomOrchestrationEngine();
|
|
120
|
+
$method = new ReflectionMethod($engine, 'mapVroomResponse');
|
|
121
|
+
|
|
122
|
+
$method->setAccessible(true);
|
|
123
|
+
|
|
124
|
+
$before = now()->timestamp;
|
|
125
|
+
$result = $method->invoke($engine, [
|
|
126
|
+
'routes' => [
|
|
127
|
+
[
|
|
128
|
+
'description' => json_encode(['vehicle_id' => 'vehicle_one', 'driver_id' => 'driver_one']),
|
|
129
|
+
'distance' => 9100,
|
|
130
|
+
'duration' => 1500,
|
|
131
|
+
'steps' => [
|
|
132
|
+
['type' => 'start', 'arrival' => 0],
|
|
133
|
+
['type' => 'job', 'id' => 1001, 'arrival' => 600, 'duration' => 600, 'distance' => 5000],
|
|
134
|
+
['type' => 'job', 'id' => 1002],
|
|
135
|
+
],
|
|
136
|
+
],
|
|
137
|
+
],
|
|
138
|
+
'summary' => ['distance' => 9100, 'duration' => 1500],
|
|
139
|
+
], [
|
|
140
|
+
1001 => 'order_one',
|
|
141
|
+
1002 => 'order_two',
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
expect($result['assignments'][0]['arrival'])->toBeGreaterThanOrEqual($before + 600)
|
|
145
|
+
->and($result['assignments'][0]['arrival'])->toBeLessThanOrEqual(now()->timestamp + 600)
|
|
146
|
+
->and($result['assignments'][0])->toMatchArray(['route_distance' => 9100, 'route_duration' => 1500])
|
|
147
|
+
->and($result['assignments'][1]['arrival'])->toBeNull()
|
|
148
|
+
->and($result['summary'])->toBe(['engine' => 'vroom', 'distance' => 9100, 'duration' => 1500]);
|
|
116
149
|
});
|
|
117
150
|
|
|
118
151
|
test('vroom capacity-only payload uses matrix indexes instead of coordinates', function () {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Resources\v1\ServiceArea as ServiceAreaResource;
|
|
4
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Zone as ZoneResource;
|
|
5
|
+
use Fleetbase\FleetOps\Models\ServiceArea;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Zone;
|
|
7
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
8
|
+
use Illuminate\Http\Request;
|
|
9
|
+
use Illuminate\Routing\Route;
|
|
10
|
+
|
|
11
|
+
class FleetOpsGeofenceSettingsZoneFake extends Zone
|
|
12
|
+
{
|
|
13
|
+
public function getLocationAttribute(): Point
|
|
14
|
+
{
|
|
15
|
+
return new Point(1.3, 103.8);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public function withCustomFields(array $attributes = [], int|string $position = 'middle'): array
|
|
19
|
+
{
|
|
20
|
+
return $attributes;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class FleetOpsGeofenceSettingsServiceAreaFake extends ServiceArea
|
|
25
|
+
{
|
|
26
|
+
public function getLocationAttribute(): Point
|
|
27
|
+
{
|
|
28
|
+
return new Point(1.3, 103.8);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public function withCustomFields(array $attributes = [], int|string $position = 'middle'): array
|
|
32
|
+
{
|
|
33
|
+
return $attributes;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fleetopsGeofenceSettingsRequest(bool $internal): Request
|
|
38
|
+
{
|
|
39
|
+
$uri = $internal ? 'int/v1/zones/zone_public' : 'v1/zones/zone_public';
|
|
40
|
+
$request = Request::create('/' . $uri, 'GET');
|
|
41
|
+
$route = new Route('GET', $internal ? 'int/v1/zones/{id}' : 'v1/zones/{id}', []);
|
|
42
|
+
|
|
43
|
+
$request->setRouteResolver(fn () => $route);
|
|
44
|
+
app()->instance('request', $request);
|
|
45
|
+
|
|
46
|
+
return $request;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Raw attributes shaped the way MySQL hands them back: tinyint/smallint
|
|
51
|
+
* columns arrive as strings, so the resource must rely on the model casts.
|
|
52
|
+
*/
|
|
53
|
+
function fleetopsGeofenceSettingsRawAttributes(): array
|
|
54
|
+
{
|
|
55
|
+
return [
|
|
56
|
+
'uuid' => 'zone-uuid',
|
|
57
|
+
'public_id' => 'zone_public',
|
|
58
|
+
'service_area_uuid' => 'area-uuid',
|
|
59
|
+
'name' => 'Loading Dock',
|
|
60
|
+
'status' => 'active',
|
|
61
|
+
'trigger_on_entry' => '0',
|
|
62
|
+
'trigger_on_exit' => '1',
|
|
63
|
+
'dwell_threshold_minutes' => '15',
|
|
64
|
+
'speed_limit_kmh' => '25',
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test('zone resource returns geofence settings typed on public and internal requests', function (bool $internal) {
|
|
69
|
+
$zone = new FleetOpsGeofenceSettingsZoneFake();
|
|
70
|
+
$zone->setRawAttributes(fleetopsGeofenceSettingsRawAttributes(), true);
|
|
71
|
+
|
|
72
|
+
$payload = (new ZoneResource($zone))->resolve(fleetopsGeofenceSettingsRequest($internal));
|
|
73
|
+
|
|
74
|
+
expect($payload['id'])->toBe($internal ? null : 'zone_public')
|
|
75
|
+
->and($payload['trigger_on_entry'])->toBeFalse()
|
|
76
|
+
->and($payload['trigger_on_exit'])->toBeTrue()
|
|
77
|
+
->and($payload['dwell_threshold_minutes'])->toBe(15)
|
|
78
|
+
->and($payload['speed_limit_kmh'])->toBe(25);
|
|
79
|
+
})->with(['public' => false, 'internal' => true]);
|
|
80
|
+
|
|
81
|
+
test('zone webhook payload includes typed geofence settings', function () {
|
|
82
|
+
$zone = new FleetOpsGeofenceSettingsZoneFake();
|
|
83
|
+
$zone->setRawAttributes(array_merge(fleetopsGeofenceSettingsRawAttributes(), ['dwell_threshold_minutes' => null]), true);
|
|
84
|
+
|
|
85
|
+
expect((new ZoneResource($zone))->toWebhookPayload())->toMatchArray([
|
|
86
|
+
'id' => 'zone_public',
|
|
87
|
+
'trigger_on_entry' => false,
|
|
88
|
+
'trigger_on_exit' => true,
|
|
89
|
+
'dwell_threshold_minutes' => null,
|
|
90
|
+
'speed_limit_kmh' => 25,
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('zone and service area updates fill geofence settings and round-trip through the resource', function (string $modelClass, string $resourceClass) {
|
|
95
|
+
// Request-shaped input, as the update endpoints pass it to fill()/update().
|
|
96
|
+
$input = [
|
|
97
|
+
'trigger_on_entry' => '1',
|
|
98
|
+
'trigger_on_exit' => false,
|
|
99
|
+
'dwell_threshold_minutes' => '30',
|
|
100
|
+
'speed_limit_kmh' => 60,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
$model = new $modelClass();
|
|
104
|
+
$model->setRawAttributes(['public_id' => 'record_public', 'name' => 'Before'], true);
|
|
105
|
+
$model->fill($input);
|
|
106
|
+
|
|
107
|
+
expect($model->isDirty(['trigger_on_entry', 'trigger_on_exit', 'dwell_threshold_minutes', 'speed_limit_kmh']))->toBeTrue();
|
|
108
|
+
|
|
109
|
+
$payload = (new $resourceClass($model))->resolve(fleetopsGeofenceSettingsRequest(false));
|
|
110
|
+
|
|
111
|
+
expect($payload)->toMatchArray([
|
|
112
|
+
'id' => 'record_public',
|
|
113
|
+
'trigger_on_entry' => true,
|
|
114
|
+
'trigger_on_exit' => false,
|
|
115
|
+
'dwell_threshold_minutes' => 30,
|
|
116
|
+
'speed_limit_kmh' => 60,
|
|
117
|
+
]);
|
|
118
|
+
})->with([
|
|
119
|
+
'zone' => [FleetOpsGeofenceSettingsZoneFake::class, ZoneResource::class],
|
|
120
|
+
'service area' => [FleetOpsGeofenceSettingsServiceAreaFake::class, ServiceAreaResource::class],
|
|
121
|
+
]);
|