@fleetbase/fleetops-engine 0.6.70 → 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 +18 -47
- 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/details/activity.hbs +8 -6
- package/addon/components/order/details/activity.js +9 -0
- package/addon/components/order/details.hbs +29 -0
- package/addon/components/order/details.js +26 -0
- package/addon/components/order/form/details.hbs +30 -4
- package/addon/components/order/form/details.js +46 -0
- package/addon/components/order/form/route.hbs +5 -3
- package/addon/components/order/form/route.js +24 -1
- package/addon/components/order/form.hbs +12 -0
- package/addon/components/order/form.js +21 -1
- package/addon/components/order/kanban.js +72 -0
- package/addon/components/order-config-manager/activity-flow.js +45 -10
- package/addon/components/order-config-manager/lifecycle.hbs +250 -0
- package/addon/components/order-config-manager/lifecycle.js +358 -0
- package/addon/components/order-config-manager.js +5 -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/analytics/reports/index.js +10 -1
- package/addon/controllers/connectivity/devices/index/details.js +1 -0
- package/addon/controllers/connectivity/devices/index.js +8 -2
- package/addon/controllers/connectivity/events/index.js +31 -1
- package/addon/controllers/connectivity/sensors/index/details.js +1 -1
- package/addon/controllers/connectivity/sensors/index.js +37 -1
- package/addon/controllers/maintenance/equipment/index/details.js +2 -2
- package/addon/controllers/maintenance/equipment/index.js +28 -9
- package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
- package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
- package/addon/controllers/maintenance/inspection-submissions/index.js +49 -9
- package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
- package/addon/controllers/maintenance/maintenances/index.js +20 -1
- package/addon/controllers/maintenance/parts/index/details.js +2 -2
- package/addon/controllers/maintenance/parts/index.js +28 -8
- package/addon/controllers/maintenance/schedules/index.js +33 -4
- package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
- package/addon/controllers/maintenance/work-orders/index.js +39 -8
- package/addon/controllers/management/contacts/customers.js +13 -0
- package/addon/controllers/management/contacts/index.js +43 -1
- package/addon/controllers/management/drivers/index/details.js +1 -0
- package/addon/controllers/management/drivers/index.js +32 -2
- package/addon/controllers/management/fleets/index/details.js +1 -0
- package/addon/controllers/management/fleets/index.js +39 -1
- package/addon/controllers/management/fuel-reports/index.js +22 -2
- package/addon/controllers/management/fuel-transactions/index.js +43 -7
- package/addon/controllers/management/issues/index/details.js +1 -0
- package/addon/controllers/management/issues/index.js +22 -2
- package/addon/controllers/management/places/index.js +36 -1
- package/addon/controllers/management/trailers/index/details.js +1 -0
- package/addon/controllers/management/trailers/index.js +50 -2
- package/addon/controllers/management/vehicles/index/details.js +1 -0
- package/addon/controllers/management/vehicles/index.js +33 -2
- package/addon/controllers/management/vendors/index.js +26 -2
- package/addon/controllers/operations/orders/index/details.js +50 -7
- package/addon/controllers/operations/orders/index.js +59 -4
- package/addon/controllers/operations/scheduler/index.js +45 -14
- package/addon/controllers/operations/service-rates/index.js +15 -1
- 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 +97 -1
- package/addon/helpers/parcel-box-image.js +16 -0
- package/addon/routes/operations/orders/index/details.js +1 -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/contact-actions.js +1 -0
- package/addon/services/customer-actions.js +2 -1
- package/addon/services/device-actions.js +1 -0
- package/addon/services/device-event-actions.js +1 -0
- package/addon/services/driver-actions.js +4 -13
- package/addon/services/equipment-actions.js +1 -0
- package/addon/services/fleet-actions.js +1 -0
- package/addon/services/fuel-report-actions.js +1 -0
- package/addon/services/inspection-form-actions.js +1 -0
- package/addon/services/inspection-submission-actions.js +1 -0
- package/addon/services/integrated-vendor-actions.js +1 -0
- package/addon/services/issue-actions.js +1 -0
- package/addon/services/location.js +69 -21
- package/addon/services/maintenance-actions.js +1 -0
- package/addon/services/maintenance-schedule-actions.js +2 -1
- package/addon/services/map-settings.js +28 -0
- package/addon/services/order-actions.js +7 -0
- package/addon/services/order-allocation.js +1 -1
- package/addon/services/order-presentation.js +129 -0
- package/addon/services/part-actions.js +1 -0
- package/addon/services/place-actions.js +1 -0
- package/addon/services/sensor-actions.js +1 -0
- package/addon/services/service-area-actions.js +1 -0
- package/addon/services/service-rate-actions.js +1 -0
- package/addon/services/telematic-actions.js +1 -0
- package/addon/services/trailer-actions.js +4 -14
- package/addon/services/vehicle-actions.js +4 -13
- package/addon/services/vendor-actions.js +1 -0
- package/addon/services/work-order-actions.js +1 -0
- package/addon/services/zone-actions.js +1 -0
- package/addon/styles/fleetops-engine.css +1391 -0
- package/addon/templates/analytics/reports/index/details.hbs +1 -0
- package/addon/templates/analytics/reports/index.hbs +1 -0
- package/addon/templates/connectivity/devices/index/details.hbs +1 -0
- package/addon/templates/connectivity/devices/index.hbs +1 -0
- package/addon/templates/connectivity/events/index.hbs +1 -0
- package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
- package/addon/templates/connectivity/sensors/index.hbs +1 -0
- package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
- package/addon/templates/maintenance/equipment/index.hbs +2 -0
- package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
- package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
- package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
- package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
- package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
- package/addon/templates/maintenance/maintenances/index.hbs +1 -0
- package/addon/templates/maintenance/parts/index/details.hbs +1 -0
- package/addon/templates/maintenance/parts/index.hbs +2 -0
- package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
- package/addon/templates/maintenance/schedules/index.hbs +2 -0
- package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
- package/addon/templates/maintenance/work-orders/index.hbs +1 -0
- package/addon/templates/management/contacts/customers/details.hbs +1 -0
- package/addon/templates/management/contacts/customers.hbs +1 -0
- package/addon/templates/management/contacts/index/details.hbs +1 -0
- package/addon/templates/management/contacts/index.hbs +1 -0
- package/addon/templates/management/contacts.hbs +1 -0
- package/addon/templates/management/drivers/index/details.hbs +1 -0
- package/addon/templates/management/drivers/index.hbs +2 -0
- package/addon/templates/management/fleets/index/details.hbs +1 -0
- package/addon/templates/management/fleets/index.hbs +1 -0
- package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
- package/addon/templates/management/fuel-reports/index.hbs +1 -0
- package/addon/templates/management/fuel-transactions/index.hbs +1 -0
- package/addon/templates/management/issues/index/details.hbs +1 -0
- package/addon/templates/management/issues/index.hbs +1 -0
- package/addon/templates/management/places/index/details.hbs +1 -0
- package/addon/templates/management/places/index.hbs +1 -0
- package/addon/templates/management/trailers/index/details.hbs +1 -0
- package/addon/templates/management/trailers/index.hbs +2 -0
- package/addon/templates/management/vehicles/index/details.hbs +1 -0
- package/addon/templates/management/vehicles/index.hbs +2 -0
- package/addon/templates/management/vendors/index/details.hbs +1 -0
- package/addon/templates/management/vendors/index.hbs +2 -0
- package/addon/templates/management/vendors/integrated.hbs +1 -0
- package/addon/templates/operations/orders/index/details.hbs +1 -0
- package/addon/templates/operations/orders/index.hbs +1 -0
- package/addon/templates/operations/service-rates/index/details.hbs +1 -0
- package/addon/templates/operations/service-rates/index.hbs +1 -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/normalize-order-config-flow.js +6 -1
- 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/services/order-presentation.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 +118 -28
- 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 +234 -11
- package/server/src/Models/OrderConfig.php +129 -0
- 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 +109 -6
- 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/OrderControllerActivityFlowsTest.php +72 -0
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
- package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
- 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/OrderConfigLifecycleTest.php +120 -0
- 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/Events/OrderDispatchFailedTest.php +2 -1
- 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 +347 -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,264 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\TrackingPage;
|
|
4
|
+
|
|
5
|
+
use Illuminate\Support\Str;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The shape, defaults and sanitizing of a company's customer tracking page settings.
|
|
9
|
+
*
|
|
10
|
+
* A company keeps its config in the company setting `tracking-page`. Instance-wide
|
|
11
|
+
* defaults for the generic page live in the system setting `fleet-ops.tracking-page`.
|
|
12
|
+
* Everything here is pure, so the controller and the public tracking API read one shape.
|
|
13
|
+
*
|
|
14
|
+
* Privacy rules are not settings: the map and driver hide after delivery, and the driver
|
|
15
|
+
* shows only while en route to the viewer's stops, whatever is saved here.
|
|
16
|
+
*/
|
|
17
|
+
class TrackingPageConfig
|
|
18
|
+
{
|
|
19
|
+
public const SETTING_KEY = 'tracking-page';
|
|
20
|
+
public const ADMIN_SETTING_KEY = 'fleet-ops.tracking-page';
|
|
21
|
+
public const SLUG_INDEX_PREFIX = 'fleet-ops.tracking-page-slugs.';
|
|
22
|
+
public const DEFAULT_ACCENT = '#1F5FA8';
|
|
23
|
+
public const DARK_INK = '#14191A';
|
|
24
|
+
public const LIGHT_INK = '#FFFFFF';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The FleetOps locales a tracking page can be shown in.
|
|
28
|
+
*/
|
|
29
|
+
public const LOCALES = ['en-us', 'ar-ae', 'bg-bg', 'es-pa', 'fr-fr', 'mn-mn', 'pt-br', 'ru-ru', 'uk-ua', 'vi-vn'];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Slugs a company can't take: they would read as part of the page's own URLs.
|
|
33
|
+
*/
|
|
34
|
+
public const RESERVED_SLUGS = [
|
|
35
|
+
'admin', 'api', 'auth', 'console', 'customer-access', 'customer-portal', 'fleetbase', 'help',
|
|
36
|
+
'int', 'login', 'new', 'portal', 'public', 'settings', 'support', 'track', 'tracking', 'www',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Instance-wide defaults for the generic page, as an administrator saved them.
|
|
41
|
+
*/
|
|
42
|
+
public static function sanitizeAdmin(array $input): array
|
|
43
|
+
{
|
|
44
|
+
return [
|
|
45
|
+
'generic_page' => [
|
|
46
|
+
'enabled' => static::bool(data_get($input, 'generic_page.enabled'), true),
|
|
47
|
+
],
|
|
48
|
+
'branding' => [
|
|
49
|
+
'display_name' => static::text(data_get($input, 'branding.display_name'), 80),
|
|
50
|
+
'accent' => static::color(data_get($input, 'branding.accent')) ?? static::DEFAULT_ACCENT,
|
|
51
|
+
'support_phone' => static::text(data_get($input, 'branding.support_phone'), 32),
|
|
52
|
+
'support_email' => static::email(data_get($input, 'branding.support_email')),
|
|
53
|
+
'website' => static::url(data_get($input, 'branding.website')),
|
|
54
|
+
'powered_by' => static::bool(data_get($input, 'branding.powered_by'), true),
|
|
55
|
+
],
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A company's config, with every key present and every value valid.
|
|
61
|
+
*
|
|
62
|
+
* @param array $admin the sanitized instance defaults
|
|
63
|
+
* @param string|null $companyName used for the default slug
|
|
64
|
+
*/
|
|
65
|
+
public static function sanitize(array $input, array $admin = [], ?string $companyName = null): array
|
|
66
|
+
{
|
|
67
|
+
$admin = static::sanitizeAdmin($admin);
|
|
68
|
+
$locale = static::oneOf(data_get($input, 'branding.default_locale'), static::LOCALES, 'en-us');
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
'org_page' => [
|
|
72
|
+
'enabled' => static::bool(data_get($input, 'org_page.enabled'), false),
|
|
73
|
+
'slug' => static::normalizeSlug(data_get($input, 'org_page.slug')) ?: static::slugify((string) $companyName),
|
|
74
|
+
],
|
|
75
|
+
'generic_page' => [
|
|
76
|
+
'allowed' => static::bool(data_get($input, 'generic_page.allowed'), true),
|
|
77
|
+
],
|
|
78
|
+
'links' => [
|
|
79
|
+
'target' => static::oneOf(data_get($input, 'links.target'), ['auto', 'org', 'generic'], 'auto'),
|
|
80
|
+
],
|
|
81
|
+
'branding' => [
|
|
82
|
+
'display_name' => static::text(data_get($input, 'branding.display_name'), 80),
|
|
83
|
+
'logo_uuid' => static::text(data_get($input, 'branding.logo_uuid'), 64),
|
|
84
|
+
'accent' => static::color(data_get($input, 'branding.accent')) ?? $admin['branding']['accent'],
|
|
85
|
+
'accent_dark' => static::color(data_get($input, 'branding.accent_dark')),
|
|
86
|
+
'support_phone' => static::text(data_get($input, 'branding.support_phone'), 32),
|
|
87
|
+
'support_email' => static::email(data_get($input, 'branding.support_email')),
|
|
88
|
+
'website' => static::url(data_get($input, 'branding.website')),
|
|
89
|
+
'powered_by' => static::bool(data_get($input, 'branding.powered_by'), $admin['branding']['powered_by']),
|
|
90
|
+
'theme' => static::oneOf(data_get($input, 'branding.theme'), ['system', 'light'], 'system'),
|
|
91
|
+
'default_locale' => $locale,
|
|
92
|
+
'locales' => static::locales(data_get($input, 'branding.locales'), $locale),
|
|
93
|
+
],
|
|
94
|
+
'access' => [
|
|
95
|
+
'public_status' => static::bool(data_get($input, 'access.public_status'), true),
|
|
96
|
+
'channels' => [
|
|
97
|
+
'sms' => static::bool(data_get($input, 'access.channels.sms'), true),
|
|
98
|
+
'email' => static::bool(data_get($input, 'access.channels.email'), true),
|
|
99
|
+
],
|
|
100
|
+
'session_hours' => static::clampInt(data_get($input, 'access.session_hours'), 1, 168, 24),
|
|
101
|
+
'account_sign_in' => static::bool(data_get($input, 'access.account_sign_in'), true),
|
|
102
|
+
'account_upsell' => static::bool(data_get($input, 'access.account_upsell'), true),
|
|
103
|
+
],
|
|
104
|
+
'visibility' => [
|
|
105
|
+
'map' => static::bool(data_get($input, 'visibility.map'), true),
|
|
106
|
+
'driver_name' => static::bool(data_get($input, 'visibility.driver_name'), true),
|
|
107
|
+
'vehicle' => static::bool(data_get($input, 'visibility.vehicle'), true),
|
|
108
|
+
'driver_contact' => static::oneOf(data_get($input, 'visibility.driver_contact'), ['company', 'driver'], 'company'),
|
|
109
|
+
'items' => static::bool(data_get($input, 'visibility.items'), true),
|
|
110
|
+
'item_prices' => static::bool(data_get($input, 'visibility.item_prices'), false),
|
|
111
|
+
'pod_photo' => static::bool(data_get($input, 'visibility.pod_photo'), true),
|
|
112
|
+
'pod_signature' => static::bool(data_get($input, 'visibility.pod_signature'), true),
|
|
113
|
+
'instructions_edit' => static::bool(data_get($input, 'visibility.instructions_edit'), false),
|
|
114
|
+
'report_problem' => static::bool(data_get($input, 'visibility.report_problem'), true),
|
|
115
|
+
],
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Why a slug can't be used, or null when its shape is fine. Availability is the caller's check.
|
|
121
|
+
*
|
|
122
|
+
* @return string|null `invalid` or `reserved`
|
|
123
|
+
*/
|
|
124
|
+
public static function slugError(?string $slug): ?string
|
|
125
|
+
{
|
|
126
|
+
if (!is_string($slug) || preg_match('/^[a-z0-9](?:[a-z0-9-]{1,38})[a-z0-9]$/', $slug) !== 1 || str_contains($slug, '--')) {
|
|
127
|
+
return 'invalid';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return in_array($slug, static::RESERVED_SLUGS, true) ? 'reserved' : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A slug made from a company name: lowercase, hyphenated, 3 to 40 characters.
|
|
135
|
+
*/
|
|
136
|
+
public static function slugify(string $name): string
|
|
137
|
+
{
|
|
138
|
+
$slug = trim(Str::limit(Str::slug($name), 40, ''), '-');
|
|
139
|
+
|
|
140
|
+
if (strlen($slug) >= 3) {
|
|
141
|
+
return $slug;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return $slug === '' ? 'company-tracking' : $slug . '-tracking';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The text colour readable on an accent: white when it reaches 4.5:1, otherwise dark.
|
|
149
|
+
*/
|
|
150
|
+
public static function inkFor(string $accent): string
|
|
151
|
+
{
|
|
152
|
+
return static::contrastRatio($accent, static::LIGHT_INK) >= 4.5 ? static::LIGHT_INK : static::DARK_INK;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The WCAG contrast ratio of two `#RRGGBB` colours.
|
|
157
|
+
*/
|
|
158
|
+
public static function contrastRatio(string $a, string $b): float
|
|
159
|
+
{
|
|
160
|
+
$lighter = max(static::luminance($a), static::luminance($b));
|
|
161
|
+
$darker = min(static::luminance($a), static::luminance($b));
|
|
162
|
+
|
|
163
|
+
return round(($lighter + 0.05) / ($darker + 0.05), 2);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
protected static function luminance(string $hex): float
|
|
167
|
+
{
|
|
168
|
+
$channels = array_map(function (string $pair) {
|
|
169
|
+
$value = hexdec($pair) / 255;
|
|
170
|
+
|
|
171
|
+
return $value <= 0.03928 ? $value / 12.92 : (($value + 0.055) / 1.055) ** 2.4;
|
|
172
|
+
}, str_split(ltrim($hex, '#'), 2));
|
|
173
|
+
|
|
174
|
+
return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
protected static function normalizeSlug(mixed $value): string
|
|
178
|
+
{
|
|
179
|
+
return is_string($value) ? strtolower(trim($value)) : '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
protected static function bool(mixed $value, bool $default): bool
|
|
183
|
+
{
|
|
184
|
+
if (is_bool($value)) {
|
|
185
|
+
return $value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// filter_var() reads null as false, so a missing value must fall back explicitly.
|
|
189
|
+
if ($value === null || $value === '') {
|
|
190
|
+
return $default;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $default;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
protected static function text(mixed $value, int $max): ?string
|
|
197
|
+
{
|
|
198
|
+
if (!is_string($value) && !is_numeric($value)) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
$value = trim((string) $value);
|
|
203
|
+
|
|
204
|
+
return $value === '' ? null : mb_substr($value, 0, $max);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
protected static function email(mixed $value): ?string
|
|
208
|
+
{
|
|
209
|
+
$value = static::text($value, 191);
|
|
210
|
+
|
|
211
|
+
return $value !== null && filter_var($value, FILTER_VALIDATE_EMAIL) ? $value : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
protected static function url(mixed $value): ?string
|
|
215
|
+
{
|
|
216
|
+
$value = static::text($value, 255);
|
|
217
|
+
|
|
218
|
+
return $value !== null && preg_match('#^https?://#i', $value) === 1 && filter_var($value, FILTER_VALIDATE_URL) ? $value : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* `#RGB` or `#RRGGBB` as uppercase `#RRGGBB`, or null.
|
|
223
|
+
*/
|
|
224
|
+
protected static function color(mixed $value): ?string
|
|
225
|
+
{
|
|
226
|
+
if (!is_string($value) || preg_match('/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i', trim($value), $match) !== 1) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
$hex = $match[1];
|
|
231
|
+
if (strlen($hex) === 3) {
|
|
232
|
+
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return '#' . strtoupper($hex);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
protected static function oneOf(mixed $value, array $allowed, string $default): string
|
|
239
|
+
{
|
|
240
|
+
return is_string($value) && in_array($value, $allowed, true) ? $value : $default;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
protected static function clampInt(mixed $value, int $min, int $max, int $default): int
|
|
244
|
+
{
|
|
245
|
+
if (!is_numeric($value)) {
|
|
246
|
+
return $default;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return max($min, min($max, (int) $value));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The enabled locales, always including the default one; every locale when none are valid.
|
|
254
|
+
*/
|
|
255
|
+
protected static function locales(mixed $value, string $default): array
|
|
256
|
+
{
|
|
257
|
+
$locales = array_values(array_intersect(static::LOCALES, is_array($value) ? $value : []));
|
|
258
|
+
if ($locales === []) {
|
|
259
|
+
return static::LOCALES;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return in_array($default, $locales, true) ? $locales : array_merge([$default], $locales);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\TrackingPage;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Contact;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Vendor;
|
|
7
|
+
use Fleetbase\FleetOps\Support\TrackingScope;
|
|
8
|
+
use Illuminate\Database\Eloquent\Model;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Who a one-time code goes to for a tracking scope, and how their contact details are
|
|
12
|
+
* shown on the page: masked, and never editable, so a code can only reach details the
|
|
13
|
+
* company already holds.
|
|
14
|
+
*/
|
|
15
|
+
class TrackingRecipient
|
|
16
|
+
{
|
|
17
|
+
public const CHANNEL_SMS = 'sms';
|
|
18
|
+
public const CHANNEL_EMAIL = 'email';
|
|
19
|
+
|
|
20
|
+
public function __construct(
|
|
21
|
+
public readonly ?Model $customer,
|
|
22
|
+
public readonly ?string $email,
|
|
23
|
+
public readonly ?string $phone,
|
|
24
|
+
) {
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The scope's customer (a Contact or a Vendor) and the contact details a code can use.
|
|
29
|
+
*
|
|
30
|
+
* @param string|null $fallbackPhone the stop place's phone, used when the customer has none
|
|
31
|
+
*/
|
|
32
|
+
public static function for(?TrackingScope $scope, ?string $fallbackPhone = null, ?Model $customer = null): self
|
|
33
|
+
{
|
|
34
|
+
$customer ??= $scope ? static::loadCustomer($scope) : null;
|
|
35
|
+
|
|
36
|
+
$email = static::validEmail(data_get($customer, 'email'));
|
|
37
|
+
$phone = static::validPhone(data_get($customer, 'phone')) ?? static::validPhone($fallbackPhone);
|
|
38
|
+
|
|
39
|
+
return new self($customer, $email, $phone);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected static function loadCustomer(TrackingScope $scope): ?Model
|
|
43
|
+
{
|
|
44
|
+
$class = match (true) {
|
|
45
|
+
is_a($scope->customer_type, Contact::class, true) => Contact::class,
|
|
46
|
+
is_a($scope->customer_type, Vendor::class, true) => Vendor::class,
|
|
47
|
+
default => null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return $class ? $class::withoutGlobalScopes()->where('uuid', $scope->customer_uuid)->first() : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The channels a code can go out on, given the company's settings and the instance.
|
|
55
|
+
*
|
|
56
|
+
* @return array<int, array{type: string, masked: string}>
|
|
57
|
+
*/
|
|
58
|
+
public function channels(array $config, bool $smsAvailable): array
|
|
59
|
+
{
|
|
60
|
+
$channels = [];
|
|
61
|
+
if ($this->phone && $smsAvailable && data_get($config, 'access.channels.sms', true)) {
|
|
62
|
+
$channels[] = ['type' => self::CHANNEL_SMS, 'masked' => static::maskPhone($this->phone)];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if ($this->email && data_get($config, 'access.channels.email', true)) {
|
|
66
|
+
$channels[] = ['type' => self::CHANNEL_EMAIL, 'masked' => static::maskEmail($this->email)];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return $channels;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public function destination(string $channel): ?string
|
|
73
|
+
{
|
|
74
|
+
return $channel === self::CHANNEL_SMS ? $this->phone : ($channel === self::CHANNEL_EMAIL ? $this->email : null);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `•••• •• 4821`: only the last four digits.
|
|
79
|
+
*/
|
|
80
|
+
public static function maskPhone(string $phone): string
|
|
81
|
+
{
|
|
82
|
+
$digits = preg_replace('/\D/', '', $phone);
|
|
83
|
+
|
|
84
|
+
return '•••• •• ' . substr($digits, -4);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `m•••@g•••.com`: the first letter of each part and the top-level domain.
|
|
89
|
+
*/
|
|
90
|
+
public static function maskEmail(string $email): string
|
|
91
|
+
{
|
|
92
|
+
[$local, $domain] = explode('@', $email, 2);
|
|
93
|
+
$tld = str_contains($domain, '.') ? substr($domain, strrpos($domain, '.')) : '';
|
|
94
|
+
|
|
95
|
+
return mb_substr($local, 0, 1) . '•••@' . mb_substr($domain, 0, 1) . '•••' . $tld;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected static function validEmail(mixed $value): ?string
|
|
99
|
+
{
|
|
100
|
+
$value = is_string($value) ? trim($value) : '';
|
|
101
|
+
|
|
102
|
+
return $value !== '' && filter_var($value, FILTER_VALIDATE_EMAIL) ? $value : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
protected static function validPhone(mixed $value): ?string
|
|
106
|
+
{
|
|
107
|
+
$value = is_string($value) ? trim($value) : '';
|
|
108
|
+
|
|
109
|
+
return strlen(preg_replace('/\D/', '', $value)) >= 6 ? $value : null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\TrackingPage;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Entity;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Payload;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Place;
|
|
9
|
+
use Fleetbase\FleetOps\Models\TrackingNumber;
|
|
10
|
+
use Fleetbase\FleetOps\Models\Waypoint;
|
|
11
|
+
use Fleetbase\FleetOps\Support\TrackingScope;
|
|
12
|
+
use Fleetbase\Models\Setting;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Turns what a visitor typed into the order and customer it may open, or null.
|
|
16
|
+
*
|
|
17
|
+
* Null covers everything a visitor must not be able to tell apart: a malformed number, an
|
|
18
|
+
* unknown one, one from a company that opted out of the shared page, and, on a company's
|
|
19
|
+
* own page, one belonging to another company. The caller answers all of them the same way.
|
|
20
|
+
*
|
|
21
|
+
* Only the `tracking_number` column is matched, never a uuid or public id.
|
|
22
|
+
*/
|
|
23
|
+
class TrackingResolver
|
|
24
|
+
{
|
|
25
|
+
public const NUMBER_PATTERN = '/^[A-Za-z0-9][A-Za-z0-9._\-]{0,99}$/';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The tracking number as typed, trimmed, or null when it can't be one.
|
|
29
|
+
*/
|
|
30
|
+
public static function normalize(mixed $input): ?string
|
|
31
|
+
{
|
|
32
|
+
if (!is_string($input)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
$input = trim($input);
|
|
37
|
+
|
|
38
|
+
return preg_match(static::NUMBER_PATTERN, $input) === 1 ? $input : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param string|null $slug a company page's slug; null for the shared page
|
|
43
|
+
*/
|
|
44
|
+
public function resolve(mixed $input, ?string $slug = null): ?TrackingTarget
|
|
45
|
+
{
|
|
46
|
+
$number = static::normalize($input);
|
|
47
|
+
if ($number === null) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
$companyUuid = null;
|
|
52
|
+
if ($slug !== null) {
|
|
53
|
+
$companyUuid = $this->companyForSlug($slug);
|
|
54
|
+
if ($companyUuid === null) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
$trackingNumber = $this->findTrackingNumber($number, $companyUuid);
|
|
60
|
+
$target = $trackingNumber ? $this->targetFor($trackingNumber) : null;
|
|
61
|
+
if ($target === null) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$config = $this->config($target->companyUuid());
|
|
66
|
+
if ($slug === null && (!$config['generic_page']['allowed'] || !$this->adminConfig()['generic_page']['enabled'])) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ($companyUuid !== null && $target->companyUuid() !== $companyUuid) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return $target->withConfig($config);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The company whose enabled page lives at this slug.
|
|
79
|
+
*/
|
|
80
|
+
public function companyForSlug(?string $slug): ?string
|
|
81
|
+
{
|
|
82
|
+
$slug = strtolower(trim((string) $slug));
|
|
83
|
+
if (TrackingPageConfig::slugError($slug) !== null) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$companyUuid = $this->lookupSetting(TrackingPageConfig::SLUG_INDEX_PREFIX . $slug);
|
|
88
|
+
if (!is_string($companyUuid) || $companyUuid === '') {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$config = $this->config($companyUuid);
|
|
93
|
+
|
|
94
|
+
return $config['org_page']['enabled'] && $config['org_page']['slug'] === $slug ? $companyUuid : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The order and customer a tracking number opens, from whatever owns it.
|
|
99
|
+
*/
|
|
100
|
+
public function targetFor(TrackingNumber $trackingNumber): ?TrackingTarget
|
|
101
|
+
{
|
|
102
|
+
$owner = $this->ownerOf($trackingNumber);
|
|
103
|
+
|
|
104
|
+
if ($owner instanceof Order) {
|
|
105
|
+
return new TrackingTarget($owner, TrackingScope::forOrder($owner), $trackingNumber);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ($owner instanceof Waypoint) {
|
|
109
|
+
$order = $this->orderForPayload($owner->payload_uuid);
|
|
110
|
+
|
|
111
|
+
return $order ? new TrackingTarget($order, TrackingScope::forStop($order, $owner), $trackingNumber, TrackingTarget::KIND_STOP, $owner) : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if ($owner instanceof Entity) {
|
|
115
|
+
$order = $this->orderForPayload($owner->payload_uuid);
|
|
116
|
+
if (!$order) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$waypoint = $this->waypointForEntity($owner);
|
|
121
|
+
|
|
122
|
+
return new TrackingTarget($order, TrackingScope::forStop($order, $waypoint), $trackingNumber, TrackingTarget::KIND_ITEM, $waypoint);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ($owner instanceof Place) {
|
|
126
|
+
// A service stop's pickup or dropoff number: the stop belongs to the order's customer.
|
|
127
|
+
$order = $this->orderForPlaceTrackingNumber($trackingNumber->uuid);
|
|
128
|
+
|
|
129
|
+
return $order ? new TrackingTarget($order, TrackingScope::forOrder($order), $trackingNumber, TrackingTarget::KIND_STOP) : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The slug to put in a company's tracking links, or null to link the shared page.
|
|
137
|
+
*/
|
|
138
|
+
public function linkSlugFor(string $companyUuid): ?string
|
|
139
|
+
{
|
|
140
|
+
$config = $this->config($companyUuid);
|
|
141
|
+
|
|
142
|
+
return $config['links']['target'] !== 'generic' && $config['org_page']['enabled'] ? $config['org_page']['slug'] : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public function config(?string $companyUuid): array
|
|
146
|
+
{
|
|
147
|
+
return TrackingPageConfig::sanitize(
|
|
148
|
+
(array) $this->lookupSetting('company.' . $companyUuid . '.' . TrackingPageConfig::SETTING_KEY, []),
|
|
149
|
+
$this->adminConfig()
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public function adminConfig(): array
|
|
154
|
+
{
|
|
155
|
+
return TrackingPageConfig::sanitizeAdmin((array) $this->lookupSetting(TrackingPageConfig::ADMIN_SETTING_KEY, []));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
protected function findTrackingNumber(string $number, ?string $companyUuid): ?TrackingNumber
|
|
159
|
+
{
|
|
160
|
+
$query = TrackingNumber::withoutGlobalScopes()->where('tracking_number', $number);
|
|
161
|
+
if ($companyUuid !== null) {
|
|
162
|
+
$query->where('company_uuid', $companyUuid);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return $query->first();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected function ownerOf(TrackingNumber $trackingNumber): mixed
|
|
169
|
+
{
|
|
170
|
+
return $trackingNumber->owner;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
protected function orderForPayload(?string $payloadUuid): ?Order
|
|
174
|
+
{
|
|
175
|
+
return $payloadUuid ? Order::withoutGlobalScopes()->where('payload_uuid', $payloadUuid)->first() : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
protected function waypointForEntity(Entity $entity): ?Waypoint
|
|
179
|
+
{
|
|
180
|
+
if (!$entity->payload_uuid || !$entity->destination_uuid) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return Waypoint::withoutGlobalScopes()->where('payload_uuid', $entity->payload_uuid)->where('place_uuid', $entity->destination_uuid)->first();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
protected function orderForPlaceTrackingNumber(string $trackingNumberUuid): ?Order
|
|
188
|
+
{
|
|
189
|
+
$payloadUuid = Payload::withoutGlobalScopes()
|
|
190
|
+
->where('pickup_tracking_number_uuid', $trackingNumberUuid)
|
|
191
|
+
->orWhere('dropoff_tracking_number_uuid', $trackingNumberUuid)
|
|
192
|
+
->value('uuid');
|
|
193
|
+
|
|
194
|
+
return $this->orderForPayload($payloadUuid);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
protected function lookupSetting(string $key, mixed $default = null): mixed
|
|
198
|
+
{
|
|
199
|
+
return Setting::lookup($key, $default);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\TrackingPage;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
6
|
+
use Fleetbase\FleetOps\Models\TrackingNumber;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Waypoint;
|
|
8
|
+
use Fleetbase\FleetOps\Support\TrackingScope;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What a tracking number opened on the public page: the order, the customer within it
|
|
12
|
+
* whose stops the visitor may see (null when the order names nobody), the stop the number
|
|
13
|
+
* belongs to when it is a stop's or an item's, and the owning company's page config.
|
|
14
|
+
*/
|
|
15
|
+
final class TrackingTarget
|
|
16
|
+
{
|
|
17
|
+
public const KIND_ORDER = 'order';
|
|
18
|
+
public const KIND_STOP = 'stop';
|
|
19
|
+
public const KIND_ITEM = 'item';
|
|
20
|
+
|
|
21
|
+
public function __construct(
|
|
22
|
+
public readonly Order $order,
|
|
23
|
+
public readonly ?TrackingScope $scope,
|
|
24
|
+
public readonly TrackingNumber $trackingNumber,
|
|
25
|
+
public readonly string $kind = self::KIND_ORDER,
|
|
26
|
+
public readonly ?Waypoint $waypoint = null,
|
|
27
|
+
public readonly array $config = [],
|
|
28
|
+
) {
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public function withConfig(array $config): self
|
|
32
|
+
{
|
|
33
|
+
return new self($this->order, $this->scope, $this->trackingNumber, $this->kind, $this->waypoint, $config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public function companyUuid(): ?string
|
|
37
|
+
{
|
|
38
|
+
return $this->order->company_uuid;
|
|
39
|
+
}
|
|
40
|
+
}
|