@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,495 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Controllers\Public\TrackingController;
|
|
4
|
+
use Fleetbase\FleetOps\Models\Contact;
|
|
5
|
+
use Fleetbase\FleetOps\Models\Entity;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Place;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Proof;
|
|
9
|
+
use Fleetbase\FleetOps\Models\TrackingNumber;
|
|
10
|
+
use Fleetbase\FleetOps\Models\Waypoint;
|
|
11
|
+
use Fleetbase\FleetOps\Support\TrackingPage\TrackingAccess;
|
|
12
|
+
use Fleetbase\FleetOps\Support\TrackingPage\TrackingPageConfig;
|
|
13
|
+
use Fleetbase\FleetOps\Support\TrackingPage\TrackingResolver;
|
|
14
|
+
use Fleetbase\FleetOps\Support\TrackingPage\TrackingTarget;
|
|
15
|
+
use Fleetbase\FleetOps\Support\TrackingPage\TrackingView;
|
|
16
|
+
use Fleetbase\FleetOps\Support\TrackingScope;
|
|
17
|
+
use Fleetbase\Models\VerificationCode;
|
|
18
|
+
use Fleetbase\Tests\Support\TrackingPageDatabase;
|
|
19
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
20
|
+
use Illuminate\Http\Request;
|
|
21
|
+
use Symfony\Component\HttpFoundation\Cookie;
|
|
22
|
+
|
|
23
|
+
class TrackingControllerResolverFake extends TrackingResolver
|
|
24
|
+
{
|
|
25
|
+
public ?TrackingTarget $target = null;
|
|
26
|
+
public ?string $slugCompany = null;
|
|
27
|
+
public array $company = [];
|
|
28
|
+
public array $admin = [];
|
|
29
|
+
public array $resolved = [];
|
|
30
|
+
|
|
31
|
+
public function resolve(mixed $input, ?string $slug = null): ?TrackingTarget
|
|
32
|
+
{
|
|
33
|
+
$this->resolved[] = [$input, $slug];
|
|
34
|
+
|
|
35
|
+
return $this->target;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public function companyForSlug(?string $slug): ?string
|
|
39
|
+
{
|
|
40
|
+
return $this->slugCompany;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public function config(?string $companyUuid): array
|
|
44
|
+
{
|
|
45
|
+
return TrackingPageConfig::sanitize($this->company);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public function adminConfig(): array
|
|
49
|
+
{
|
|
50
|
+
return TrackingPageConfig::sanitizeAdmin($this->admin);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class TrackingControllerAccessFake extends TrackingAccess
|
|
55
|
+
{
|
|
56
|
+
public bool $lookup = true;
|
|
57
|
+
public bool $verify = true;
|
|
58
|
+
public bool $reportOk = true;
|
|
59
|
+
public int $paused = 0;
|
|
60
|
+
public array $claim = ['status' => self::SEND_SENT, 'retry_after' => 30];
|
|
61
|
+
public array $check = ['status' => VerificationCode::CHECK_VALID, 'channel' => 'sms'];
|
|
62
|
+
public bool $granted = false;
|
|
63
|
+
public array $issued = [];
|
|
64
|
+
public array $grants = [];
|
|
65
|
+
public array $revoked = [];
|
|
66
|
+
|
|
67
|
+
public function allowLookup(Request $request): bool
|
|
68
|
+
{
|
|
69
|
+
return $this->lookup;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public function allowVerify(Request $request): bool
|
|
73
|
+
{
|
|
74
|
+
return $this->verify;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public function allowReport(Request $request, string $orderUuid): bool
|
|
78
|
+
{
|
|
79
|
+
return $this->reportOk;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public function pausedFor(TrackingScope $scope): int
|
|
83
|
+
{
|
|
84
|
+
return $this->paused;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public function claimSend(TrackingScope $scope, Request $request): array
|
|
88
|
+
{
|
|
89
|
+
return $this->claim;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public function issueCode(TrackingScope $scope, mixed $recipient, string $channel): VerificationCode
|
|
93
|
+
{
|
|
94
|
+
$this->issued[] = [$scope->key(), $recipient?->uuid, $channel];
|
|
95
|
+
$code = new VerificationCode();
|
|
96
|
+
$code->plainCode = '482193';
|
|
97
|
+
|
|
98
|
+
return $code;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public function checkCode(TrackingScope $scope, string $code): array
|
|
102
|
+
{
|
|
103
|
+
return $this->check + ['code' => $code];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public function hasGrant(Request $request, ?TrackingScope $scope): bool
|
|
107
|
+
{
|
|
108
|
+
return $this->granted && $scope !== null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public function grantExpiresAt(Request $request, TrackingScope $scope): ?string
|
|
112
|
+
{
|
|
113
|
+
return '2026-10-09T09:00:00.000000Z';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public function grant(Request $request, TrackingScope $scope, ?string $channel, int $hours): ?Cookie
|
|
117
|
+
{
|
|
118
|
+
$this->grants[] = [$scope->key(), $channel, $hours];
|
|
119
|
+
|
|
120
|
+
return $this->cookie('token-value-token-value-token-value-xx', $hours * 60);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public function revoke(Request $request, ?TrackingScope $scope = null): ?Cookie
|
|
124
|
+
{
|
|
125
|
+
$this->revoked[] = $scope?->key();
|
|
126
|
+
|
|
127
|
+
return $scope ? null : $this->cookie('', -1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
class TrackingControllerViewFake extends TrackingView
|
|
132
|
+
{
|
|
133
|
+
public ?Proof $proof = null;
|
|
134
|
+
public array $calls = [];
|
|
135
|
+
|
|
136
|
+
public function publicView(TrackingTarget $target, array $branding, array $channels, bool $signIn): array
|
|
137
|
+
{
|
|
138
|
+
return ['level' => 0, 'branding' => $branding, 'channels' => $channels, 'sign_in' => $signIn];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public function fullView(TrackingTarget $target, array $branding, string $viewer, ?string $expiresAt = null): array
|
|
142
|
+
{
|
|
143
|
+
$this->calls[] = $viewer;
|
|
144
|
+
|
|
145
|
+
return ['level' => $viewer === self::VIEWER_ACCOUNT ? 2 : 1, 'viewer' => $viewer, 'expires_at' => $expiresAt, 'company' => $branding];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public function proofFor(TrackingTarget $target, string $publicId): ?Proof
|
|
149
|
+
{
|
|
150
|
+
return $this->proof;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class TrackingControllerProbe extends TrackingController
|
|
155
|
+
{
|
|
156
|
+
public mixed $user = null;
|
|
157
|
+
public array $sms = [];
|
|
158
|
+
public array $mails = [];
|
|
159
|
+
public array $providers = [];
|
|
160
|
+
public bool $portal = true;
|
|
161
|
+
public bool $sockets = false;
|
|
162
|
+
public ?array $contactsOver = null;
|
|
163
|
+
|
|
164
|
+
public function __construct(public TrackingControllerResolverFake $fakeResolver, public TrackingControllerAccessFake $fakeAccess, public TrackingControllerViewFake $fakeView)
|
|
165
|
+
{
|
|
166
|
+
parent::__construct($fakeResolver, $fakeAccess, $fakeView);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
protected function requestUser(Request $request): mixed
|
|
170
|
+
{
|
|
171
|
+
return $this->user;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
protected function smsProviders(): array
|
|
175
|
+
{
|
|
176
|
+
return $this->providers;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
protected function customerPortalInstalled(): bool
|
|
180
|
+
{
|
|
181
|
+
return $this->portal;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
protected function instanceLogo(): ?string
|
|
185
|
+
{
|
|
186
|
+
return 'https://instance.example/logo.png';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
protected function smsService(): Fleetbase\Services\SmsService
|
|
190
|
+
{
|
|
191
|
+
$probe = $this;
|
|
192
|
+
|
|
193
|
+
return new class($probe) extends Fleetbase\Services\SmsService {
|
|
194
|
+
public function __construct(private $probe)
|
|
195
|
+
{
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public function send(string $to, string $text, array $options = [], ?string $provider = null): array
|
|
199
|
+
{
|
|
200
|
+
$this->probe->sms[] = [$to, $text, $options];
|
|
201
|
+
|
|
202
|
+
return ['success' => true];
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
protected function mail(string $to, string $subject, string $body): void
|
|
208
|
+
{
|
|
209
|
+
$this->mails[] = [$to, $subject, $body];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected function socketsEnabled(): bool
|
|
213
|
+
{
|
|
214
|
+
return $this->sockets && parent::socketsEnabled() === false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
protected function socketTokenFor(TrackingScope $scope): array
|
|
218
|
+
{
|
|
219
|
+
return ['token' => 'jwt', 'expires_in' => 1800, 'expires_at' => '2026-10-08T09:30:00Z'];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
class TrackingControllerOrder extends Order
|
|
224
|
+
{
|
|
225
|
+
public bool $saved = false;
|
|
226
|
+
|
|
227
|
+
public function saveQuietly(array $options = [])
|
|
228
|
+
{
|
|
229
|
+
$this->saved = true;
|
|
230
|
+
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
afterEach(function () {
|
|
236
|
+
EloquentModel::unsetConnectionResolver();
|
|
237
|
+
config(['services.twilio.sid' => null, 'services.twilio.token' => null]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
function trackingController(): TrackingControllerProbe
|
|
241
|
+
{
|
|
242
|
+
TrackingPageDatabase::boot();
|
|
243
|
+
|
|
244
|
+
return new TrackingControllerProbe(new TrackingControllerResolverFake(), new TrackingControllerAccessFake(), new TrackingControllerViewFake());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function trackingControllerTarget(array $config = [], bool $scoped = true, ?Waypoint $waypoint = null): TrackingTarget
|
|
248
|
+
{
|
|
249
|
+
$order = new TrackingControllerOrder();
|
|
250
|
+
$order->setRawAttributes(['uuid' => 'order-1', 'company_uuid' => 'co-1', 'customer_uuid' => 'contact-1', 'customer_type' => Contact::class, 'meta' => json_encode([])], true);
|
|
251
|
+
|
|
252
|
+
$trackingNumber = new TrackingNumber();
|
|
253
|
+
$trackingNumber->setRawAttributes(['uuid' => 'tn-1', 'tracking_number' => 'NOR1000000001US'], true);
|
|
254
|
+
|
|
255
|
+
$scope = $scoped ? new TrackingScope('order-1', Contact::class, 'contact-1', $order) : null;
|
|
256
|
+
|
|
257
|
+
return new TrackingTarget($order, $scope, $trackingNumber, TrackingTarget::KIND_ORDER, $waypoint, TrackingPageConfig::sanitize($config));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function trackingControllerRequest(array $input = []): Request
|
|
261
|
+
{
|
|
262
|
+
return Request::create('/public/track/NOR1000000001US', 'GET', $input, [], [], ['REMOTE_ADDR' => '203.0.113.7']);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
test('the page config shows a company page or the shared page, and nothing for a page that is off', function () {
|
|
266
|
+
$controller = trackingController();
|
|
267
|
+
TrackingPageDatabase::insert(Illuminate\Database\Eloquent\Model::getConnectionResolver()->connection(), 'companies', ['uuid' => 'co-1', 'name' => 'Northwind Couriers', 'phone' => '+15105550142']);
|
|
268
|
+
|
|
269
|
+
expect($controller->config(trackingControllerRequest(['org' => 'northwind']))->getStatusCode())->toBe(404);
|
|
270
|
+
|
|
271
|
+
$controller->fakeResolver->slugCompany = 'co-1';
|
|
272
|
+
$controller->fakeResolver->company = ['access' => ['session_hours' => 12]];
|
|
273
|
+
$page = $controller->config(trackingControllerRequest(['org' => ' Northwind ']))->getData(true);
|
|
274
|
+
expect($page['company']['name'])->toBe('Northwind Couriers')
|
|
275
|
+
->and($page['org'])->toBe('northwind')
|
|
276
|
+
->and($page['sign_in'])->toBeTrue()
|
|
277
|
+
->and($page['session_hours'])->toBe(12);
|
|
278
|
+
|
|
279
|
+
$shared = $controller->config(trackingControllerRequest(['org' => ' ']))->getData(true);
|
|
280
|
+
expect($shared['org'])->toBeNull()
|
|
281
|
+
->and($shared['company']['logo_url'])->toBe('https://instance.example/logo.png')
|
|
282
|
+
->and($shared['session_hours'])->toBe(24);
|
|
283
|
+
|
|
284
|
+
$controller->fakeResolver->admin = ['generic_page' => ['enabled' => false]];
|
|
285
|
+
expect($controller->config(trackingControllerRequest())->getStatusCode())->toBe(404);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('lookups answer unknown and throttled numbers alike, and show what the viewer may see', function () {
|
|
289
|
+
$controller = trackingController();
|
|
290
|
+
|
|
291
|
+
$notFound = $controller->show(trackingControllerRequest(), 'NOR1000000001US');
|
|
292
|
+
expect($notFound->getStatusCode())->toBe(404)
|
|
293
|
+
->and($notFound->getData(true))->toBe(['error' => TrackingController::NOT_FOUND, 'code' => 'not_found']);
|
|
294
|
+
|
|
295
|
+
$controller->fakeResolver->target = trackingControllerTarget();
|
|
296
|
+
$controller->fakeAccess->lookup = false;
|
|
297
|
+
expect($controller->show(trackingControllerRequest(), 'NOR1000000001US')->getData(true))->toBe($notFound->getData(true));
|
|
298
|
+
|
|
299
|
+
// A tracking number alone: the public view, with the channels a code can use.
|
|
300
|
+
$controller->fakeAccess->lookup = true;
|
|
301
|
+
$controller->providers = ['vonage' => ['available' => true]];
|
|
302
|
+
TrackingPageDatabase::insert(Illuminate\Database\Eloquent\Model::getConnectionResolver()->connection(), 'contacts', ['uuid' => 'contact-1', 'email' => 'maya@example.com', 'phone' => '+15105554821']);
|
|
303
|
+
$public = $controller->show(trackingControllerRequest(['org' => 'Northwind']), 'NOR1000000001US')->getData(true);
|
|
304
|
+
expect($public['level'])->toBe(0)
|
|
305
|
+
->and(array_column($public['channels'], 'type'))->toBe(['sms', 'email'])
|
|
306
|
+
->and($public['sign_in'])->toBeTrue()
|
|
307
|
+
->and($controller->fakeResolver->resolved[1])->toBe(['NOR1000000001US', 'northwind']);
|
|
308
|
+
|
|
309
|
+
$controller->fakeResolver->target = trackingControllerTarget(['access' => ['account_sign_in' => false]], false);
|
|
310
|
+
$unscoped = $controller->show(trackingControllerRequest(), 'NOR1000000001US')->getData(true);
|
|
311
|
+
expect($unscoped['channels'])->toBe([])->and($unscoped['sign_in'])->toBeFalse();
|
|
312
|
+
|
|
313
|
+
// A verified device, a signed-in customer and staff of the owning company see the full view.
|
|
314
|
+
$controller->fakeResolver->target = trackingControllerTarget();
|
|
315
|
+
$controller->fakeAccess->granted = true;
|
|
316
|
+
expect($controller->show(trackingControllerRequest(), 'NOR1000000001US')->getData(true))->toMatchArray(['viewer' => 'verified', 'expires_at' => '2026-10-09T09:00:00.000000Z']);
|
|
317
|
+
|
|
318
|
+
$controller->fakeAccess->granted = false;
|
|
319
|
+
$controller->user = (object) ['uuid' => 'user-1', 'type' => 'customer', 'company_uuid' => 'co-1'];
|
|
320
|
+
TrackingPageDatabase::insert(Illuminate\Database\Eloquent\Model::getConnectionResolver()->connection(), 'contacts', ['uuid' => 'contact-x', 'user_uuid' => 'user-1']);
|
|
321
|
+
expect($controller->show(trackingControllerRequest(), 'NOR1000000001US')->getData(true)['level'])->toBe(0);
|
|
322
|
+
|
|
323
|
+
Illuminate\Database\Eloquent\Model::getConnectionResolver()->connection()->table('contacts')->where('uuid', 'contact-1')->update(['user_uuid' => 'user-1']);
|
|
324
|
+
expect($controller->show(trackingControllerRequest(), 'NOR1000000001US')->getData(true))->toMatchArray(['viewer' => 'account', 'level' => 2, 'expires_at' => null]);
|
|
325
|
+
|
|
326
|
+
$controller->user = (object) ['uuid' => 'user-2', 'type' => 'user', 'company_uuid' => 'co-1'];
|
|
327
|
+
expect($controller->show(trackingControllerRequest(), 'NOR1000000001US')->getData(true)['viewer'])->toBe('staff');
|
|
328
|
+
|
|
329
|
+
// Staff of another company, or a user with no id, get the public view.
|
|
330
|
+
$controller->user = (object) ['uuid' => 'user-3', 'type' => 'user', 'company_uuid' => 'co-2'];
|
|
331
|
+
expect($controller->show(trackingControllerRequest(), 'NOR1000000001US')->getData(true)['level'])->toBe(0);
|
|
332
|
+
$controller->user = (object) ['type' => 'customer'];
|
|
333
|
+
expect($controller->show(trackingControllerRequest(), 'NOR1000000001US')->getData(true)['level'])->toBe(0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('codes go out on an available channel, within the limits', function () {
|
|
337
|
+
$controller = trackingController();
|
|
338
|
+
$db = Illuminate\Database\Eloquent\Model::getConnectionResolver()->connection();
|
|
339
|
+
TrackingPageDatabase::insert($db, 'contacts', ['uuid' => 'contact-1', 'email' => 'maya@example.com']);
|
|
340
|
+
TrackingPageDatabase::insert($db, 'companies', ['uuid' => 'co-1', 'name' => 'Northwind', 'options' => json_encode(['alpha_numeric_sender_id_enabled' => true, 'alpha_numeric_sender_id' => 'NORTHWIND'])]);
|
|
341
|
+
|
|
342
|
+
expect($controller->sendCode(trackingControllerRequest(['channel' => 'email']), 'X')->getStatusCode())->toBe(404);
|
|
343
|
+
|
|
344
|
+
$place = new Place();
|
|
345
|
+
$place->setRawAttributes(['uuid' => 'place-1', 'phone' => '+15105554821'], true);
|
|
346
|
+
$waypoint = new Waypoint();
|
|
347
|
+
$waypoint->setRelation('place', $place);
|
|
348
|
+
$controller->fakeResolver->target = trackingControllerTarget([], true, $waypoint);
|
|
349
|
+
|
|
350
|
+
// SMS needs a provider; the stop's place phone stands in for the contact's missing one.
|
|
351
|
+
expect($controller->sendCode(trackingControllerRequest(['channel' => 'sms']), 'NOR1')->getData(true))->toBe(['error' => 'channel_unavailable']);
|
|
352
|
+
|
|
353
|
+
config(['services.twilio.sid' => 'AC1', 'services.twilio.token' => 'secret']);
|
|
354
|
+
$sent = $controller->sendCode(trackingControllerRequest(['channel' => 'sms']), 'NOR1');
|
|
355
|
+
expect($sent->getStatusCode())->toBe(202)
|
|
356
|
+
->and($sent->getData(true))->toBe(['channel' => 'sms', 'masked' => '•••• •• 4821', 'expires_in' => 600, 'resend_after' => 30])
|
|
357
|
+
->and($controller->fakeAccess->issued[0])->toBe(['order-1:' . Contact::class . ':contact-1', 'contact-1', 'sms'])
|
|
358
|
+
->and($controller->sms[0][0])->toBe('+15105554821')
|
|
359
|
+
->and($controller->sms[0][1])->toBe('482193 is your Northwind delivery code. It expires in 10 minutes.')
|
|
360
|
+
->and($controller->sms[0][2])->toBe(['twilioParams' => ['from' => 'NORTHWIND']]);
|
|
361
|
+
|
|
362
|
+
$controller->sendCode(trackingControllerRequest(['channel' => 'email']), 'NOR1');
|
|
363
|
+
expect($controller->mails[0])->toBe(['maya@example.com', '482193 is your Northwind delivery code', '482193 is your Northwind delivery code. It expires in 10 minutes.']);
|
|
364
|
+
|
|
365
|
+
$controller->fakeAccess->claim = ['status' => TrackingAccess::SEND_COOLDOWN, 'retry_after' => 12];
|
|
366
|
+
expect($controller->sendCode(trackingControllerRequest(['channel' => 'email']), 'NOR1')->getStatusCode())->toBe(429);
|
|
367
|
+
$controller->fakeAccess->claim = ['status' => TrackingAccess::SEND_PAUSED, 'retry_after' => 200];
|
|
368
|
+
expect($controller->sendCode(trackingControllerRequest(['channel' => 'email']), 'NOR1')->getData(true))->toBe(['error' => 'paused', 'retry_after' => 200]);
|
|
369
|
+
|
|
370
|
+
// A company with no name of its own falls back to a generic sender name, and no alphanumeric sender.
|
|
371
|
+
$db->table('companies')->delete();
|
|
372
|
+
$controller->fakeAccess->claim = ['status' => TrackingAccess::SEND_SENT, 'retry_after' => 30];
|
|
373
|
+
$controller->sendCode(trackingControllerRequest(['channel' => 'sms']), 'NOR1');
|
|
374
|
+
expect($controller->sms[1][1])->toStartWith('482193 is your Your delivery delivery code')
|
|
375
|
+
->and($controller->sms[1][2])->toBe([]);
|
|
376
|
+
|
|
377
|
+
$controller->fakeResolver->target = trackingControllerTarget([], false);
|
|
378
|
+
expect($controller->sendCode(trackingControllerRequest(['channel' => 'email']), 'NOR1')->getStatusCode())->toBe(422);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('checking a code verifies the device or says why it can’t', function () {
|
|
382
|
+
$controller = trackingController();
|
|
383
|
+
|
|
384
|
+
$controller->fakeAccess->verify = false;
|
|
385
|
+
expect($controller->verifyCode(trackingControllerRequest(['code' => '482193']), 'NOR1')->getStatusCode())->toBe(429);
|
|
386
|
+
|
|
387
|
+
$controller->fakeAccess->verify = true;
|
|
388
|
+
expect($controller->verifyCode(trackingControllerRequest(['code' => '482193']), 'NOR1')->getStatusCode())->toBe(404);
|
|
389
|
+
|
|
390
|
+
$controller->fakeResolver->target = trackingControllerTarget(['access' => ['session_hours' => 6]]);
|
|
391
|
+
$verified = $controller->verifyCode(trackingControllerRequest(['code' => '48 21 93']), 'NOR1');
|
|
392
|
+
expect($verified->getStatusCode())->toBe(200)
|
|
393
|
+
->and($verified->getData(true)['viewer'])->toBe('verified')
|
|
394
|
+
->and($verified->headers->getCookies()[0]->getName())->toBe(TrackingAccess::COOKIE)
|
|
395
|
+
->and($controller->fakeAccess->grants[0])->toBe(['order-1:' . Contact::class . ':contact-1', 'sms', 6]);
|
|
396
|
+
|
|
397
|
+
$controller->fakeAccess->check = ['status' => VerificationCode::CHECK_INVALID, 'attempts_left' => 2];
|
|
398
|
+
expect($controller->verifyCode(trackingControllerRequest(['code' => '111111']), 'NOR1')->getData(true))->toBe(['error' => 'invalid', 'attempts_left' => 2]);
|
|
399
|
+
|
|
400
|
+
$controller->fakeAccess->check = ['status' => VerificationCode::CHECK_LOCKED];
|
|
401
|
+
expect($controller->verifyCode(trackingControllerRequest(['code' => '111111']), 'NOR1')->getData(true))->toBe(['error' => 'paused', 'retry_after' => TrackingAccess::PAUSE_SECONDS]);
|
|
402
|
+
|
|
403
|
+
$controller->fakeAccess->check = ['status' => VerificationCode::CHECK_EXPIRED];
|
|
404
|
+
expect($controller->verifyCode(trackingControllerRequest(['code' => '111111']), 'NOR1')->getStatusCode())->toBe(410);
|
|
405
|
+
|
|
406
|
+
$controller->fakeAccess->paused = 120;
|
|
407
|
+
expect($controller->verifyCode(trackingControllerRequest(['code' => '111111']), 'NOR1')->getData(true))->toBe(['error' => 'paused', 'retry_after' => 120]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('signing out drops one delivery or the whole device', function () {
|
|
411
|
+
$controller = trackingController();
|
|
412
|
+
$controller->fakeResolver->target = trackingControllerTarget();
|
|
413
|
+
|
|
414
|
+
$one = $controller->signOut(trackingControllerRequest(['number' => 'NOR1']));
|
|
415
|
+
expect($one->getData(true))->toBe(['status' => 'signed_out'])
|
|
416
|
+
->and($one->headers->getCookies())->toBe([])
|
|
417
|
+
->and($controller->fakeAccess->revoked[0])->toBe('order-1:' . Contact::class . ':contact-1');
|
|
418
|
+
|
|
419
|
+
$all = $controller->signOut(trackingControllerRequest());
|
|
420
|
+
expect($all->headers->getCookies()[0]->getExpiresTime())->toBe(1)
|
|
421
|
+
->and($controller->fakeAccess->revoked[1])->toBeNull();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('delivery instructions, proofs, reports and socket tokens need a verified viewer', function () {
|
|
425
|
+
$controller = trackingController();
|
|
426
|
+
|
|
427
|
+
foreach (['updateInstructions', 'report', 'socketToken'] as $method) {
|
|
428
|
+
expect($controller->{$method}(trackingControllerRequest(), 'NOR1')->getStatusCode())->toBe(404);
|
|
429
|
+
}
|
|
430
|
+
expect($controller->proof(trackingControllerRequest(), 'NOR1', 'proof_1')->getStatusCode())->toBe(404);
|
|
431
|
+
|
|
432
|
+
$controller->fakeResolver->target = trackingControllerTarget();
|
|
433
|
+
expect($controller->updateInstructions(trackingControllerRequest(), 'NOR1')->getStatusCode())->toBe(404);
|
|
434
|
+
|
|
435
|
+
$controller->fakeAccess->granted = true;
|
|
436
|
+
|
|
437
|
+
// Instructions
|
|
438
|
+
expect($controller->updateInstructions(trackingControllerRequest(['text' => 'Gate 4']), 'NOR1')->getStatusCode())->toBe(403);
|
|
439
|
+
$controller->fakeResolver->target = trackingControllerTarget(['visibility' => ['instructions_edit' => true]]);
|
|
440
|
+
$controller->updateInstructions(trackingControllerRequest(['text' => ' Leave with concierge ']), 'NOR1');
|
|
441
|
+
$order = $controller->fakeResolver->target->order;
|
|
442
|
+
expect($order->saved)->toBeTrue()
|
|
443
|
+
->and($order->getMeta('recipient_instructions'))->toBe([sha1('order-1:' . Contact::class . ':contact-1') => 'Leave with concierge']);
|
|
444
|
+
|
|
445
|
+
// Proofs
|
|
446
|
+
expect($controller->proof(trackingControllerRequest(), 'NOR1', 'proof_1')->getStatusCode())->toBe(404);
|
|
447
|
+
$controller->fakeView->proof = new Proof();
|
|
448
|
+
$controller->fakeView->proof->setRawAttributes(['raw_data' => 'iVBOR'], true);
|
|
449
|
+
$controller->fakeView->proof->setRelation('file', null);
|
|
450
|
+
expect($controller->proof(trackingControllerRequest(), 'NOR1', 'proof_1')->getData(true))->toBe(['url' => 'data:image/png;base64,iVBOR', 'type' => 'signature']);
|
|
451
|
+
$controller->fakeView->proof->setRawAttributes(['raw_data' => 'data:image/svg+xml;base64,PHN2Zz4='], true);
|
|
452
|
+
expect($controller->proof(trackingControllerRequest(), 'NOR1', 'proof_1')->getData(true)['url'])->toBe('data:image/svg+xml;base64,PHN2Zz4=');
|
|
453
|
+
$controller->fakeView->proof->setRawAttributes([], true);
|
|
454
|
+
expect($controller->proof(trackingControllerRequest(), 'NOR1', 'proof_1')->getStatusCode())->toBe(404);
|
|
455
|
+
$controller->fakeView->proof->setRelation('file', (object) ['url' => 'https://bucket.example/signed']);
|
|
456
|
+
expect($controller->proof(trackingControllerRequest(), 'NOR1', 'proof_1')->getData(true)['url'])->toBe('https://bucket.example/signed');
|
|
457
|
+
|
|
458
|
+
// Reports
|
|
459
|
+
expect($controller->report(trackingControllerRequest(['message' => 'Box damaged']), 'NOR1')->getStatusCode())->toBe(422);
|
|
460
|
+
$controller->fakeResolver->target = trackingControllerTarget(['branding' => ['support_email' => 'help@northwind.example']]);
|
|
461
|
+
expect($controller->report(trackingControllerRequest(['message' => ' ']), 'NOR1')->getStatusCode())->toBe(422);
|
|
462
|
+
$controller->fakeAccess->reportOk = false;
|
|
463
|
+
expect($controller->report(trackingControllerRequest(['message' => 'Box damaged']), 'NOR1')->getStatusCode())->toBe(429);
|
|
464
|
+
$controller->fakeAccess->reportOk = true;
|
|
465
|
+
expect($controller->report(trackingControllerRequest(['message' => 'Box damaged']), 'NOR1')->getStatusCode())->toBe(202)
|
|
466
|
+
->and($controller->mails[0])->toBe(['help@northwind.example', 'Problem reported for NOR1000000001US', "A customer reported a problem with delivery NOR1000000001US:\n\nBox damaged"]);
|
|
467
|
+
|
|
468
|
+
// Socket tokens, only when the instance publishes tracking channels.
|
|
469
|
+
expect($controller->socketToken(trackingControllerRequest(), 'NOR1')->getStatusCode())->toBe(404);
|
|
470
|
+
$controller->sockets = true;
|
|
471
|
+
expect($controller->socketToken(trackingControllerRequest(), 'NOR1')->getData(true))->toBe(['token' => 'jwt', 'expires_in' => 1800, 'expires_at' => '2026-10-08T09:30:00Z']);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('a signed-in customer lists their orders with the number that opens their part', function () {
|
|
475
|
+
$controller = trackingController();
|
|
476
|
+
$db = Illuminate\Database\Eloquent\Model::getConnectionResolver()->connection();
|
|
477
|
+
|
|
478
|
+
expect($controller->accountOrders(trackingControllerRequest())->getStatusCode())->toBe(401);
|
|
479
|
+
|
|
480
|
+
$controller->user = (object) ['uuid' => 'user-1', 'type' => 'customer'];
|
|
481
|
+
TrackingPageDatabase::insert($db, 'contacts', ['uuid' => 'contact-1', 'user_uuid' => 'user-1']);
|
|
482
|
+
TrackingPageDatabase::insert($db, 'tracking_numbers', ['uuid' => 'tn-own', 'tracking_number' => 'NOR1']);
|
|
483
|
+
TrackingPageDatabase::insert($db, 'tracking_numbers', ['uuid' => 'tn-stop', 'tracking_number' => 'NOR2-STOP']);
|
|
484
|
+
TrackingPageDatabase::insert($db, 'orders', ['uuid' => 'order-own', 'customer_uuid' => 'contact-1', 'customer_type' => Contact::class, 'tracking_number_uuid' => 'tn-own', 'payload_uuid' => 'p-own', 'status' => 'completed', 'created_at' => '2026-10-07 09:00:00', 'updated_at' => '2026-10-07 10:00:00']);
|
|
485
|
+
TrackingPageDatabase::insert($db, 'orders', ['uuid' => 'order-stop', 'customer_uuid' => 'shipper', 'customer_type' => Contact::class, 'payload_uuid' => 'p-stop', 'status' => 'created', 'created_at' => '2026-10-08 09:00:00']);
|
|
486
|
+
TrackingPageDatabase::insert($db, 'orders', ['uuid' => 'order-none', 'customer_uuid' => 'contact-1', 'customer_type' => Contact::class, 'payload_uuid' => 'p-none', 'created_at' => '2026-10-06 09:00:00']);
|
|
487
|
+
TrackingPageDatabase::insert($db, 'waypoints', ['uuid' => 'wp-1', 'payload_uuid' => 'p-stop', 'customer_uuid' => 'contact-1', 'customer_type' => Contact::class, 'tracking_number_uuid' => 'tn-stop']);
|
|
488
|
+
TrackingPageDatabase::insert($db, 'entities', ['uuid' => 'e-1', 'payload_uuid' => 'p-own', 'name' => 'Ceramic lamp']);
|
|
489
|
+
|
|
490
|
+
$orders = $controller->accountOrders(trackingControllerRequest())->getData(true)['orders'];
|
|
491
|
+
expect($orders)->toBe([
|
|
492
|
+
['tracking_number' => 'NOR2-STOP', 'title' => null, 'status' => 'created', 'stage' => 'preparing', 'updated_at' => null],
|
|
493
|
+
['tracking_number' => 'NOR1', 'title' => 'Ceramic lamp', 'status' => 'completed', 'stage' => 'delivered', 'updated_at' => '2026-10-07T10:00:00.000000Z'],
|
|
494
|
+
]);
|
|
495
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Filter\OrderFilter;
|
|
4
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Index\Customer as IndexCustomerResource;
|
|
5
|
+
use Fleetbase\FleetOps\Models\Contact;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Customer;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Vendor;
|
|
8
|
+
use Fleetbase\FleetOps\Support\LiveOrderQuery;
|
|
9
|
+
use Illuminate\Database\Eloquent\Builder;
|
|
10
|
+
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
11
|
+
use Illuminate\Http\Request;
|
|
12
|
+
|
|
13
|
+
class FleetOpsIndexCustomerPhotoMorphTo extends MorphTo
|
|
14
|
+
{
|
|
15
|
+
public array $morphWith = [];
|
|
16
|
+
|
|
17
|
+
public function __construct()
|
|
18
|
+
{
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public function morphWith(array $with)
|
|
22
|
+
{
|
|
23
|
+
$this->morphWith = $with;
|
|
24
|
+
|
|
25
|
+
return $this;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class FleetOpsIndexCustomerPhotoBuilder extends Builder
|
|
30
|
+
{
|
|
31
|
+
public array $with = [];
|
|
32
|
+
|
|
33
|
+
public function __construct()
|
|
34
|
+
{
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public function __call($method, $parameters)
|
|
38
|
+
{
|
|
39
|
+
return $this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public function where($column, $operator = null, $value = null, $boolean = 'and')
|
|
43
|
+
{
|
|
44
|
+
return $this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public function whereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
|
|
48
|
+
{
|
|
49
|
+
return $this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public function whereNotIn($column, $values, $boolean = 'and')
|
|
53
|
+
{
|
|
54
|
+
return $this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public function whereNull($columns, $boolean = 'and', $not = false)
|
|
58
|
+
{
|
|
59
|
+
return $this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public function with($relations, $callback = null)
|
|
63
|
+
{
|
|
64
|
+
$this->with = array_merge($this->with, $relations);
|
|
65
|
+
|
|
66
|
+
return $this;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class FleetOpsIndexCustomerPhotoLiveQuery extends LiveOrderQuery
|
|
71
|
+
{
|
|
72
|
+
public static FleetOpsIndexCustomerPhotoBuilder $builder;
|
|
73
|
+
|
|
74
|
+
protected static function newOrderQuery(string $companyUuid): Builder
|
|
75
|
+
{
|
|
76
|
+
return static::$builder = new FleetOpsIndexCustomerPhotoBuilder();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fleetopsIndexCustomerPhotoExpectedMorphWith(): array
|
|
81
|
+
{
|
|
82
|
+
return [
|
|
83
|
+
Contact::class => ['photo'],
|
|
84
|
+
Customer::class => ['photo'],
|
|
85
|
+
Vendor::class => ['logo'],
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fleetopsIndexCustomerPhotoResolve(object $customer): array
|
|
90
|
+
{
|
|
91
|
+
return (new IndexCustomerResource($customer))->resolve(Request::create('/', 'GET'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test('index customer resource sends a contact photo url', function () {
|
|
95
|
+
$contact = new Contact();
|
|
96
|
+
$contact->setRawAttributes(['name' => 'Fixture Contact']);
|
|
97
|
+
$contact->setRelation('photo', (object) ['url' => 'https://cdn.example.test/contact.png']);
|
|
98
|
+
|
|
99
|
+
expect(fleetopsIndexCustomerPhotoResolve($contact)['photo_url'])->toBe('https://cdn.example.test/contact.png');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('index customer resource sends a vendor logo as the photo url', function () {
|
|
103
|
+
$vendor = new Vendor();
|
|
104
|
+
$vendor->setRawAttributes(['name' => 'Fixture Vendor']);
|
|
105
|
+
$vendor->setRelation('logo', (object) ['url' => 'https://cdn.example.test/vendor.png']);
|
|
106
|
+
|
|
107
|
+
expect(fleetopsIndexCustomerPhotoResolve($vendor)['photo_url'])->toBe('https://cdn.example.test/vendor.png');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('index customer resource sends a null photo url for a customer type without one', function () {
|
|
111
|
+
$payload = fleetopsIndexCustomerPhotoResolve(new ArrayObject(['name' => 'Fixture'], ArrayObject::ARRAY_AS_PROPS));
|
|
112
|
+
|
|
113
|
+
expect($payload)->toHaveKey('photo_url')
|
|
114
|
+
->and($payload['photo_url'])->toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('index customer eager loads the photo file for each customer type', function () {
|
|
118
|
+
$relation = new FleetOpsIndexCustomerPhotoMorphTo();
|
|
119
|
+
|
|
120
|
+
expect(IndexCustomerResource::withPhoto($relation))->toBe($relation)
|
|
121
|
+
->and($relation->morphWith)->toBe(fleetopsIndexCustomerPhotoExpectedMorphWith());
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('live order query eager loads customer photos', function () {
|
|
125
|
+
FleetOpsIndexCustomerPhotoLiveQuery::make('company-uuid', ['with_relations' => true, 'apply_permissions' => false]);
|
|
126
|
+
|
|
127
|
+
$relation = new FleetOpsIndexCustomerPhotoMorphTo();
|
|
128
|
+
FleetOpsIndexCustomerPhotoLiveQuery::$builder->with['customer']($relation);
|
|
129
|
+
|
|
130
|
+
expect($relation->morphWith)->toBe(fleetopsIndexCustomerPhotoExpectedMorphWith());
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('internal order filter eager loads customer photos', function () {
|
|
134
|
+
$builder = new FleetOpsIndexCustomerPhotoBuilder();
|
|
135
|
+
$request = Request::create('/int/v1/orders', 'GET');
|
|
136
|
+
$session = app('session.store');
|
|
137
|
+
$session->put('company', 'company_test');
|
|
138
|
+
$request->setLaravelSession($session);
|
|
139
|
+
|
|
140
|
+
$filter = new OrderFilter($request);
|
|
141
|
+
$prop = (new ReflectionClass($filter))->getParentClass()->getProperty('builder');
|
|
142
|
+
$prop->setAccessible(true);
|
|
143
|
+
$prop->setValue($filter, $builder);
|
|
144
|
+
|
|
145
|
+
$filter->queryForInternal();
|
|
146
|
+
|
|
147
|
+
$relation = new FleetOpsIndexCustomerPhotoMorphTo();
|
|
148
|
+
$builder->with['customer']($relation);
|
|
149
|
+
|
|
150
|
+
expect($relation->morphWith)->toBe(fleetopsIndexCustomerPhotoExpectedMorphWith());
|
|
151
|
+
});
|
|
@@ -85,7 +85,8 @@ function fleetopsModelAccessorsUseInMemoryRelationConnection(): void
|
|
|
85
85
|
EloquentModel::setConnectionResolver($resolver);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// Extends the relation the model method now declares, so the override stays compatible; the parent constructor is skipped on purpose.
|
|
89
|
+
class FleetOpsCountingRelationFake extends HasManyThrough
|
|
89
90
|
{
|
|
90
91
|
public function __construct(private int $total, private int $online)
|
|
91
92
|
{
|
|
@@ -251,12 +252,12 @@ class FleetOpsDriverOrderAccessorFake extends Order
|
|
|
251
252
|
|
|
252
253
|
class FleetOpsFleetAccessorFake extends Fleet
|
|
253
254
|
{
|
|
254
|
-
public function drivers()
|
|
255
|
+
public function drivers(): HasManyThrough
|
|
255
256
|
{
|
|
256
257
|
return new FleetOpsCountingRelationFake(5, 2);
|
|
257
258
|
}
|
|
258
259
|
|
|
259
|
-
public function vehicles()
|
|
260
|
+
public function vehicles(): HasManyThrough
|
|
260
261
|
{
|
|
261
262
|
return new FleetOpsCountingRelationFake(7, 3);
|
|
262
263
|
}
|