@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,402 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
7
|
+
use Fleetbase\FleetOps\Models\TrackingStatus;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
9
|
+
use Fleetbase\FleetOps\Tracking\TrackingStop;
|
|
10
|
+
use Illuminate\Support\Carbon;
|
|
11
|
+
use Illuminate\Support\Collection;
|
|
12
|
+
use Illuminate\Support\Str;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The payload published to one customer's public tracking channel.
|
|
16
|
+
*
|
|
17
|
+
* It carries only what that customer may see: their own stops (status, ETA, timeline), whole-route
|
|
18
|
+
* counts, the driver's first name, the vehicle's label and online flag, and the vehicle's rounded
|
|
19
|
+
* position. It never carries another customer's stops, places, items or people, the driver's
|
|
20
|
+
* photo or phone, or free-text activity details. The driver and position are included only while
|
|
21
|
+
* the order is en route and this customer still has a stop to be served.
|
|
22
|
+
*/
|
|
23
|
+
final class TrackingUpdate
|
|
24
|
+
{
|
|
25
|
+
public const EVENT_UPDATED = 'tracking.updated';
|
|
26
|
+
|
|
27
|
+
public const EVENT_LOCATION = 'tracking.location';
|
|
28
|
+
|
|
29
|
+
public const REASON_STATUS = 'status';
|
|
30
|
+
|
|
31
|
+
public const REASON_STOP = 'stop';
|
|
32
|
+
|
|
33
|
+
public const REASON_ASSIGNMENT = 'assignment';
|
|
34
|
+
|
|
35
|
+
public const REASON_ETA = 'eta';
|
|
36
|
+
|
|
37
|
+
public const REASON_LOCATION = 'location';
|
|
38
|
+
|
|
39
|
+
public const REASON_ROUTE = 'route';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Decimal places kept on published coordinates: 4 places is roughly 11 m.
|
|
43
|
+
*/
|
|
44
|
+
public const COORDINATE_PRECISION = 4;
|
|
45
|
+
|
|
46
|
+
public const TERMINAL_STATUSES = ['completed', 'canceled'];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Most timeline entries published per tracking number.
|
|
50
|
+
*/
|
|
51
|
+
public const TIMELINE_LIMIT = 20;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @var Collection<int, TrackingStop>
|
|
55
|
+
*/
|
|
56
|
+
private Collection $stops;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param iterable<TrackingStop> $stops every stop of the order, in route order
|
|
60
|
+
* @param array<int, array> $etas ETA per stop sequence: seconds, at, distance_m
|
|
61
|
+
* @param array<string, array<int, array>> $timelines timeline entries per tracking number uuid
|
|
62
|
+
*/
|
|
63
|
+
public function __construct(
|
|
64
|
+
private Order $order,
|
|
65
|
+
private TrackingScope $scope,
|
|
66
|
+
iterable $stops = [],
|
|
67
|
+
private array $etas = [],
|
|
68
|
+
private array $timelines = [],
|
|
69
|
+
) {
|
|
70
|
+
$this->stops = collect($stops)->filter(fn ($stop) => $stop instanceof TrackingStop)->values();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the update for a scope from the order's stops and, optionally, a tracking intelligence result.
|
|
75
|
+
*
|
|
76
|
+
* @param iterable<TrackingStop> $stops
|
|
77
|
+
* @param array $tracker a TrackingIntelligenceService::track() result; its route legs give the ETAs
|
|
78
|
+
*/
|
|
79
|
+
public static function for(Order $order, TrackingScope $scope, iterable $stops, array $tracker = []): self
|
|
80
|
+
{
|
|
81
|
+
$update = new self($order, $scope, $stops, static::etasFromTracker($tracker));
|
|
82
|
+
|
|
83
|
+
$update->timelines = static::loadTimelines($update->timelineTrackingNumbers(), $order->getConnectionName());
|
|
84
|
+
|
|
85
|
+
return $update;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The full update: order state, route counts, this customer's stops, driver and position.
|
|
90
|
+
*/
|
|
91
|
+
public function toArray(string $reason = self::REASON_STATUS): array
|
|
92
|
+
{
|
|
93
|
+
return $this->envelope(self::EVENT_UPDATED, $reason, [
|
|
94
|
+
'order' => $this->orderState(),
|
|
95
|
+
'route' => $this->route(),
|
|
96
|
+
'stops' => $this->ownStops()->map(fn (TrackingStop $stop) => $this->stop($stop))->values()->all(),
|
|
97
|
+
'driver' => $this->driver(),
|
|
98
|
+
'location' => $this->location(),
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The position-only update, or null when this customer may not see the vehicle right now.
|
|
104
|
+
*/
|
|
105
|
+
public function locationPayload(): ?array
|
|
106
|
+
{
|
|
107
|
+
$location = $this->location();
|
|
108
|
+
if ($location === null) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return $this->envelope(self::EVENT_LOCATION, self::REASON_LOCATION, [
|
|
113
|
+
'driver' => $this->driver(),
|
|
114
|
+
'location' => $location,
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* This customer's stops.
|
|
120
|
+
*
|
|
121
|
+
* @return Collection<int, TrackingStop>
|
|
122
|
+
*/
|
|
123
|
+
public function ownStops(): Collection
|
|
124
|
+
{
|
|
125
|
+
return $this->stops->filter(fn (TrackingStop $stop) => $this->scope->ownsStop($this->order, $stop->waypoint))->values();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public function isTerminal(): bool
|
|
129
|
+
{
|
|
130
|
+
return in_array(strtolower((string) $this->order->status), self::TERMINAL_STATUSES, true);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Whether the order is under way: started and not yet completed or canceled.
|
|
135
|
+
*/
|
|
136
|
+
public function isEnRoute(): bool
|
|
137
|
+
{
|
|
138
|
+
if ($this->isTerminal()) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (bool) $this->order->started || !empty($this->order->started_at) || strtolower((string) $this->order->status) === 'started';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public function customerHasIncompleteStop(): bool
|
|
146
|
+
{
|
|
147
|
+
return $this->ownStops()->contains(fn (TrackingStop $stop) => !$stop->completed);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether the driver and the vehicle position may be shown to this customer now.
|
|
152
|
+
*/
|
|
153
|
+
public function showsVehicle(): bool
|
|
154
|
+
{
|
|
155
|
+
return $this->isEnRoute() && $this->customerHasIncompleteStop();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Whole-route counts: stops in total, stops done, and stops still ahead of this customer's next one.
|
|
160
|
+
*/
|
|
161
|
+
public function route(): array
|
|
162
|
+
{
|
|
163
|
+
$next = $this->ownStops()->first(fn (TrackingStop $stop) => !$stop->completed);
|
|
164
|
+
|
|
165
|
+
return [
|
|
166
|
+
'total_stops' => $this->stops->count(),
|
|
167
|
+
'completed_stops' => $this->stops->filter(fn (TrackingStop $stop) => $stop->completed)->count(),
|
|
168
|
+
'stops_before' => $next ? $this->stops->filter(fn (TrackingStop $stop) => !$stop->completed && (int) $stop->sequence < (int) $next->sequence)->count() : 0,
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The driver as this customer may see them: first name, vehicle label, online flag.
|
|
174
|
+
*/
|
|
175
|
+
public function driver(): ?array
|
|
176
|
+
{
|
|
177
|
+
$driver = $this->assignedDriver();
|
|
178
|
+
if (!$driver || !$this->showsVehicle()) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return [
|
|
183
|
+
'name' => static::firstName($driver->name),
|
|
184
|
+
'vehicle' => $this->vehicleLabel(),
|
|
185
|
+
'online' => (bool) $driver->online,
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The vehicle position rounded to ~10 m, its heading and when it was seen.
|
|
191
|
+
*/
|
|
192
|
+
public function location(): ?array
|
|
193
|
+
{
|
|
194
|
+
if (!$this->showsVehicle()) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
$source = $this->locationSource();
|
|
199
|
+
if (!$source) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
[$subject, $point] = $source;
|
|
204
|
+
|
|
205
|
+
return [
|
|
206
|
+
'latitude' => round($point->getLat(), self::COORDINATE_PRECISION),
|
|
207
|
+
'longitude' => round($point->getLng(), self::COORDINATE_PRECISION),
|
|
208
|
+
'heading' => is_numeric($subject->heading) ? (int) round((float) $subject->heading) : null,
|
|
209
|
+
'seen_at' => $subject->updated_at ? Carbon::parse($subject->updated_at)->toISOString() : null,
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Tracking numbers whose timeline this customer may read: their stops', plus the order's own for the order customer.
|
|
215
|
+
*
|
|
216
|
+
* @return array<int, string>
|
|
217
|
+
*/
|
|
218
|
+
public function timelineTrackingNumbers(): array
|
|
219
|
+
{
|
|
220
|
+
$uuids = $this->ownStops()->map(fn (TrackingStop $stop) => $stop->trackingNumberUuid)->all();
|
|
221
|
+
|
|
222
|
+
if ($this->scope->isOrderCustomer($this->order)) {
|
|
223
|
+
$uuids[] = $this->order->tracking_number_uuid;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return array_values(array_unique(array_filter($uuids, fn ($uuid) => is_string($uuid) && $uuid !== '')));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* ETAs keyed by stop sequence from a tracking intelligence result's route legs.
|
|
231
|
+
*/
|
|
232
|
+
public static function etasFromTracker(array $tracker): array
|
|
233
|
+
{
|
|
234
|
+
$etas = [];
|
|
235
|
+
foreach ((array) data_get($tracker, 'route.legs', []) as $leg) {
|
|
236
|
+
$sequence = data_get($leg, 'stop.sequence');
|
|
237
|
+
if ($sequence === null) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
$etas[(int) $sequence] = [
|
|
242
|
+
'seconds' => is_numeric(data_get($leg, 'eta_seconds')) ? (int) round((float) data_get($leg, 'eta_seconds')) : null,
|
|
243
|
+
'at' => data_get($leg, 'eta_at'),
|
|
244
|
+
'distance_m' => is_numeric(data_get($leg, 'distance_m')) ? (int) round((float) data_get($leg, 'distance_m')) : null,
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return $etas;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Timeline entries per tracking number: status, code, complete and time only.
|
|
253
|
+
*
|
|
254
|
+
* @param array<int, string> $trackingNumberUuids
|
|
255
|
+
*
|
|
256
|
+
* @return array<string, array<int, array>>
|
|
257
|
+
*/
|
|
258
|
+
public static function loadTimelines(array $trackingNumberUuids, ?string $connection = null): array
|
|
259
|
+
{
|
|
260
|
+
if ($trackingNumberUuids === []) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return TrackingStatus::on($connection)
|
|
265
|
+
->whereIn('tracking_number_uuid', $trackingNumberUuids)
|
|
266
|
+
->orderBy('created_at')
|
|
267
|
+
->get(['tracking_number_uuid', 'status', 'code', 'complete', 'created_at'])
|
|
268
|
+
->groupBy('tracking_number_uuid')
|
|
269
|
+
->map(fn (Collection $statuses) => $statuses->take(-self::TIMELINE_LIMIT)->map(fn (TrackingStatus $status) => static::timelineEntry($status))->values()->all())
|
|
270
|
+
->all();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The first word of a name.
|
|
275
|
+
*/
|
|
276
|
+
public static function firstName(?string $name): ?string
|
|
277
|
+
{
|
|
278
|
+
$name = trim((string) $name);
|
|
279
|
+
|
|
280
|
+
return $name === '' ? null : Str::before($name, ' ');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private function envelope(string $event, string $reason, array $data): array
|
|
284
|
+
{
|
|
285
|
+
return [
|
|
286
|
+
'event' => $event,
|
|
287
|
+
'reason' => $reason,
|
|
288
|
+
'sent_at' => Carbon::now()->toISOString(),
|
|
289
|
+
'data' => $data,
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private function orderState(): array
|
|
294
|
+
{
|
|
295
|
+
$status = strtolower((string) $this->order->status);
|
|
296
|
+
|
|
297
|
+
return [
|
|
298
|
+
'status' => $status !== '' ? $status : null,
|
|
299
|
+
'en_route' => $this->isEnRoute(),
|
|
300
|
+
'completed' => $status === 'completed',
|
|
301
|
+
'canceled' => $status === 'canceled',
|
|
302
|
+
'timeline' => $this->scope->isOrderCustomer($this->order) ? $this->timeline($this->order->tracking_number_uuid) : [],
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private function stop(TrackingStop $stop): array
|
|
307
|
+
{
|
|
308
|
+
$eta = $stop->completed ? null : ($this->etas[(int) $stop->sequence] ?? null);
|
|
309
|
+
|
|
310
|
+
return [
|
|
311
|
+
'id' => $stop->waypoint?->public_id,
|
|
312
|
+
'type' => $stop->type,
|
|
313
|
+
'sequence' => $stop->sequence,
|
|
314
|
+
'status' => $stop->status,
|
|
315
|
+
'complete' => $stop->completed,
|
|
316
|
+
'eta' => $eta,
|
|
317
|
+
'timeline' => $this->timeline($stop->trackingNumberUuid),
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private function timeline(?string $trackingNumberUuid): array
|
|
322
|
+
{
|
|
323
|
+
return $trackingNumberUuid ? ($this->timelines[$trackingNumberUuid] ?? []) : [];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private static function timelineEntry(TrackingStatus $status): array
|
|
327
|
+
{
|
|
328
|
+
$createdAt = $status->getRawOriginal('created_at') ?? $status->created_at;
|
|
329
|
+
|
|
330
|
+
return [
|
|
331
|
+
'status' => $status->status,
|
|
332
|
+
'code' => $status->code !== null ? strtolower((string) $status->code) : null,
|
|
333
|
+
'complete' => (bool) $status->complete,
|
|
334
|
+
'created_at' => $createdAt ? Carbon::parse($createdAt)->toISOString() : null,
|
|
335
|
+
];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private function assignedDriver(): ?Driver
|
|
339
|
+
{
|
|
340
|
+
$driver = $this->order->driverAssigned;
|
|
341
|
+
|
|
342
|
+
return $driver instanceof Driver ? $driver : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private function assignedVehicle(): ?Vehicle
|
|
346
|
+
{
|
|
347
|
+
$vehicle = $this->order->vehicleAssigned ?? $this->assignedDriver()?->vehicle;
|
|
348
|
+
|
|
349
|
+
return $vehicle instanceof Vehicle ? $vehicle : null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private function vehicleLabel(): ?string
|
|
353
|
+
{
|
|
354
|
+
$label = trim((string) $this->assignedVehicle()?->display_name);
|
|
355
|
+
|
|
356
|
+
return $label === '' ? null : $label;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The most recently updated of the driver and the vehicle that has a usable position.
|
|
361
|
+
*
|
|
362
|
+
* @return array{0: Driver|Vehicle, 1: \Fleetbase\LaravelMysqlSpatial\Types\Point}|null
|
|
363
|
+
*/
|
|
364
|
+
private function locationSource(): ?array
|
|
365
|
+
{
|
|
366
|
+
$candidates = [];
|
|
367
|
+
foreach ([$this->assignedDriver(), $this->assignedVehicle()] as $subject) {
|
|
368
|
+
$point = $subject ? static::usablePoint($subject) : null;
|
|
369
|
+
if ($point) {
|
|
370
|
+
$candidates[] = [$subject, $point];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
usort($candidates, fn (array $a, array $b) => static::seenAt($b[0]) <=> static::seenAt($a[0]));
|
|
375
|
+
|
|
376
|
+
return $candidates[0] ?? null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private static function seenAt(Driver|Vehicle $subject): int
|
|
380
|
+
{
|
|
381
|
+
return $subject->updated_at ? Carbon::parse($subject->updated_at)->getTimestamp() : 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private static function usablePoint(Driver|Vehicle $subject)
|
|
385
|
+
{
|
|
386
|
+
try {
|
|
387
|
+
$point = $subject->location ? Utils::getPointFromMixed($subject->location) : null;
|
|
388
|
+
} catch (\Throwable) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!$point) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
$lat = (float) $point->getLat();
|
|
397
|
+
$lng = (float) $point->getLng();
|
|
398
|
+
$valid = is_finite($lat) && is_finite($lng) && abs($lat) <= 90 && abs($lng) <= 180 && !(abs($lat) < 0.000001 && abs($lng) < 0.000001);
|
|
399
|
+
|
|
400
|
+
return $valid ? $point : null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -1220,8 +1220,8 @@ class Utils extends FleetbaseUtils
|
|
|
1220
1220
|
{
|
|
1221
1221
|
$latitude = deg2rad($latitude);
|
|
1222
1222
|
$longitude = deg2rad($longitude);
|
|
1223
|
-
//
|
|
1224
|
-
$radius =
|
|
1223
|
+
// angular radius: metres over the earth's equatorial radius in metres
|
|
1224
|
+
$radius = $meters / 6378137;
|
|
1225
1225
|
// create circle coordinates
|
|
1226
1226
|
$coords = collect();
|
|
1227
1227
|
// loop through the array and write path linestrings — stop short of 360
|
|
@@ -1231,7 +1231,8 @@ class Utils extends FleetbaseUtils
|
|
|
1231
1231
|
$radial = deg2rad($i);
|
|
1232
1232
|
$lat_rad = asin(sin($latitude) * cos($radius) + cos($latitude) * sin($radius) * cos($radial));
|
|
1233
1233
|
$dlon_rad = atan2(sin($radial) * sin($radius) * cos($latitude), cos($radius) - sin($latitude) * sin($lat_rad));
|
|
1234
|
-
|
|
1234
|
+
// shift by 3π, not π: fmod keeps the dividend's sign, so a negative sum would not wrap
|
|
1235
|
+
$lon_rad = fmod($longitude + $dlon_rad + 3 * M_PI, 2 * M_PI) - M_PI;
|
|
1235
1236
|
$coords->push([rad2deg($lat_rad), rad2deg($lon_rad)]);
|
|
1236
1237
|
}
|
|
1237
1238
|
|
|
@@ -1603,4 +1604,15 @@ class Utils extends FleetbaseUtils
|
|
|
1603
1604
|
|
|
1604
1605
|
return $phone;
|
|
1605
1606
|
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Re-sign a URL stored as a string (e.g. a legacy `avatar_url` value) when it points into the
|
|
1610
|
+
* private S3 media bucket, so it keeps working once the bucket stops allowing public reads.
|
|
1611
|
+
* Other values pass through unchanged. Falls back to the raw value on core-api releases that
|
|
1612
|
+
* predate File::signStoredUrl().
|
|
1613
|
+
*/
|
|
1614
|
+
public static function signStoredFileUrl(?string $url): ?string
|
|
1615
|
+
{
|
|
1616
|
+
return method_exists(\Fleetbase\Models\File::class, 'signStoredUrl') ? \Fleetbase\Models\File::signStoredUrl($url) : $url;
|
|
1617
|
+
}
|
|
1606
1618
|
}
|
|
@@ -9,6 +9,7 @@ use Fleetbase\FleetOps\Models\TrackingNumber;
|
|
|
9
9
|
use Fleetbase\FleetOps\Models\TrackingStatus;
|
|
10
10
|
use Fleetbase\FleetOps\Support\Utils;
|
|
11
11
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
12
|
+
use Fleetbase\Support\ApiModelCache;
|
|
12
13
|
use Fleetbase\Support\TemplateString;
|
|
13
14
|
use Illuminate\Database\Eloquent\Model;
|
|
14
15
|
use Illuminate\Support\Facades\DB;
|
|
@@ -28,6 +29,7 @@ trait HasTrackingNumber
|
|
|
28
29
|
], $model);
|
|
29
30
|
|
|
30
31
|
DB::table($model->getTable())->where('uuid', $model->uuid)->update(['tracking_number_uuid' => $trackingNumberId]);
|
|
32
|
+
ApiModelCache::invalidateModelCache($model, data_get($model, 'company_uuid', session('company')));
|
|
31
33
|
|
|
32
34
|
if ($model->isFillable('tracking_number_uuid')) {
|
|
33
35
|
$model->tracking_number_uuid = $trackingNumberId;
|
package/server/src/routes.php
CHANGED
|
@@ -20,6 +20,23 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
20
20
|
// limiter has its own prefix so the two do not share one counter.
|
|
21
21
|
// Every answer is JSON, so a refused submission reaches the page with
|
|
22
22
|
// its reasons instead of as a redirect back to it.
|
|
23
|
+
// The public customer tracking page. A tracking number alone gives a coarse status;
|
|
24
|
+
// a one-time code to the customer's own contact details unlocks their stops. Lookups
|
|
25
|
+
// are rate limited inside the controller, so a throttled lookup answers exactly like
|
|
26
|
+
// an unknown number; code sends and checks have limits of their own there too.
|
|
27
|
+
$router->prefix('public/track')->namespace('Public')->middleware([Fleetbase\FleetOps\Http\Middleware\ForceJsonResponse::class])->group(function ($router) {
|
|
28
|
+
$router->get('config', 'TrackingController@config');
|
|
29
|
+
$router->get('account/orders', 'TrackingController@accountOrders');
|
|
30
|
+
$router->delete('session', 'TrackingController@signOut');
|
|
31
|
+
$router->get('{number}', 'TrackingController@show');
|
|
32
|
+
$router->post('{number}/codes', 'TrackingController@sendCode');
|
|
33
|
+
$router->post('{number}/codes/verify', 'TrackingController@verifyCode');
|
|
34
|
+
$router->put('{number}/instructions', 'TrackingController@updateInstructions');
|
|
35
|
+
$router->get('{number}/proofs/{id}', 'TrackingController@proof');
|
|
36
|
+
$router->post('{number}/report', 'TrackingController@report');
|
|
37
|
+
$router->get('{number}/socket-token', 'TrackingController@socketToken');
|
|
38
|
+
});
|
|
39
|
+
|
|
23
40
|
$router->prefix('public')->namespace('Public')->middleware([Fleetbase\FleetOps\Http\Middleware\ForceJsonResponse::class, 'throttle:60,1,inspection-public'])->group(function ($router) {
|
|
24
41
|
$router->get('inspections/forms/{id}', 'PublicInspectionController@show');
|
|
25
42
|
$router->post('inspections/forms/{id}/submit', 'PublicInspectionController@submit');
|
|
@@ -37,14 +54,16 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
37
54
|
// drivers routes
|
|
38
55
|
$router->group(['prefix' => 'drivers', 'middleware' => []], function () use ($router) {
|
|
39
56
|
$router->post('register-device', 'DriverController@registerDevice');
|
|
40
|
-
|
|
57
|
+
// Routes that send a text message carry a small per-address bucket on top
|
|
58
|
+
// of the API credential throttle; the SMS guard in core bounds the spend.
|
|
59
|
+
$router->post('login-with-sms', 'DriverController@loginWithPhone')->middleware('throttle:5,1,fleetops-sms');
|
|
41
60
|
$router->post('verify-code', 'DriverController@verifyCode');
|
|
42
61
|
$router->post('login', 'DriverController@login');
|
|
43
62
|
// Password management. Deliberately separate from the driver
|
|
44
63
|
// update: changing a password requires proving the old one, and
|
|
45
64
|
// resetting it requires a code, neither of which a general PUT
|
|
46
65
|
// can express.
|
|
47
|
-
$router->post('forgot-password', 'DriverController@forgotPassword');
|
|
66
|
+
$router->post('forgot-password', 'DriverController@forgotPassword')->middleware('throttle:5,1,fleetops-sms');
|
|
48
67
|
$router->post('reset-password', 'DriverController@resetPassword');
|
|
49
68
|
$router->post('{id}/change-password', 'DriverController@changePassword');
|
|
50
69
|
$router->post('{id}/simulate', 'DriverController@simulate');
|
|
@@ -80,12 +99,12 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
80
99
|
// - Authenticated endpoints: API key + `Customer-Token` header (Sanctum)
|
|
81
100
|
$router->group(['prefix' => 'customers', 'middleware' => []], function () use ($router) {
|
|
82
101
|
// Public auth flows
|
|
83
|
-
$router->post('request-creation-code', 'CustomerController@requestCreationCode');
|
|
102
|
+
$router->post('request-creation-code', 'CustomerController@requestCreationCode')->middleware('throttle:5,1,fleetops-sms');
|
|
84
103
|
$router->post('/', 'CustomerController@create');
|
|
85
104
|
$router->post('login', 'CustomerController@login');
|
|
86
|
-
$router->post('login-with-sms', 'CustomerController@loginWithPhone');
|
|
105
|
+
$router->post('login-with-sms', 'CustomerController@loginWithPhone')->middleware('throttle:5,1,fleetops-sms');
|
|
87
106
|
$router->post('verify-code', 'CustomerController@verifyCode');
|
|
88
|
-
$router->post('forgot-password', 'CustomerController@forgotPassword');
|
|
107
|
+
$router->post('forgot-password', 'CustomerController@forgotPassword')->middleware('throttle:5,1,fleetops-sms');
|
|
89
108
|
$router->post('reset-password', 'CustomerController@resetPassword');
|
|
90
109
|
|
|
91
110
|
// Authenticated (require Customer-Token)
|
|
@@ -398,7 +417,10 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
398
417
|
$router->group(
|
|
399
418
|
['prefix' => 'v1/fleet-ops', 'namespace' => 'v1'],
|
|
400
419
|
function ($router) {
|
|
401
|
-
|
|
420
|
+
// The public Track Order page: no session, the tracking number is
|
|
421
|
+
// the credential. Rate limited per address so numbers cannot be
|
|
422
|
+
// enumerated, on a limiter of its own.
|
|
423
|
+
$router->get('lookup', 'OrderController@lookup')->middleware('throttle:30,1,tracking-lookup');
|
|
402
424
|
}
|
|
403
425
|
);
|
|
404
426
|
|
|
@@ -562,7 +584,12 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
562
584
|
$router->post('import', $controller('import'));
|
|
563
585
|
}
|
|
564
586
|
);
|
|
565
|
-
$router->fleetbaseRoutes(
|
|
587
|
+
$router->fleetbaseRoutes(
|
|
588
|
+
'proofs',
|
|
589
|
+
function ($router, $controller) {
|
|
590
|
+
$router->get('recent', $controller('recent'));
|
|
591
|
+
}
|
|
592
|
+
);
|
|
566
593
|
$router->fleetbaseRoutes('purchase-rates');
|
|
567
594
|
$router->fleetbaseRoutes('routes');
|
|
568
595
|
$router->fleetbaseRoutes('positions', function ($router, $controller) {
|
|
@@ -806,6 +833,11 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
806
833
|
$router->post('tracking-settings', 'SettingController@saveTrackingSettings');
|
|
807
834
|
$router->get('admin-tracking-settings', 'SettingController@getAdminTrackingSettings');
|
|
808
835
|
$router->post('admin-tracking-settings', 'SettingController@saveAdminTrackingSettings');
|
|
836
|
+
$router->get('tracking-page-settings', 'SettingController@getTrackingPageSettings');
|
|
837
|
+
$router->post('tracking-page-settings', 'SettingController@saveTrackingPageSettings');
|
|
838
|
+
$router->post('tracking-page-settings/validate-slug', 'SettingController@validateTrackingPageSlug');
|
|
839
|
+
$router->get('admin-tracking-page-settings', 'SettingController@getAdminTrackingPageSettings');
|
|
840
|
+
$router->post('admin-tracking-page-settings', 'SettingController@saveAdminTrackingPageSettings');
|
|
809
841
|
$router->get('map', 'SettingController@getMapSettings');
|
|
810
842
|
$router->post('map', 'SettingController@saveMapSettings');
|
|
811
843
|
$router->get('admin-map', 'SettingController@getAdminMapSettings');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
|
+
use Fleetbase\Exceptions\SmsBlockedException;
|
|
3
4
|
use Fleetbase\FleetOps\Exceptions\UserAlreadyExistsException;
|
|
4
5
|
use Fleetbase\FleetOps\Http\Controllers\Api\v1\CustomerController;
|
|
5
6
|
use Fleetbase\FleetOps\Http\Requests\CreateCustomerOrderRequest;
|
|
@@ -14,6 +15,7 @@ use Fleetbase\FleetOps\Models\Place;
|
|
|
14
15
|
use Fleetbase\FleetOps\Models\ServiceQuote;
|
|
15
16
|
use Fleetbase\FleetOps\Models\Waypoint;
|
|
16
17
|
use Fleetbase\Models\User;
|
|
18
|
+
use Fleetbase\Support\Sms\PhoneValidator;
|
|
17
19
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
18
20
|
use Illuminate\Http\Request;
|
|
19
21
|
|
|
@@ -56,6 +58,7 @@ class FleetOpsApiCustomerControllerProbe extends CustomerController
|
|
|
56
58
|
public mixed $placesResult = null;
|
|
57
59
|
public bool $emailVerificationFails = false;
|
|
58
60
|
public bool $smsVerificationFails = false;
|
|
61
|
+
public ?Throwable $smsVerificationThrows = null;
|
|
59
62
|
public bool $verificationExists = true;
|
|
60
63
|
public bool $passwordMatches = true;
|
|
61
64
|
public bool $findOrderFails = false;
|
|
@@ -164,6 +167,9 @@ class FleetOpsApiCustomerControllerProbe extends CustomerController
|
|
|
164
167
|
|
|
165
168
|
protected function generateSmsVerification(User $user, string $for, array $options): mixed
|
|
166
169
|
{
|
|
170
|
+
if ($this->smsVerificationThrows) {
|
|
171
|
+
throw $this->smsVerificationThrows;
|
|
172
|
+
}
|
|
167
173
|
if ($this->smsVerificationFails) {
|
|
168
174
|
throw new RuntimeException('sms failed');
|
|
169
175
|
}
|
|
@@ -692,12 +698,9 @@ test('api customer controller sends creation verification codes and updates stub
|
|
|
692
698
|
]));
|
|
693
699
|
|
|
694
700
|
expect($created)->toBe(['status' => 'ok'])
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
'email' => 'jane@example.test',
|
|
699
|
-
'phone' => '+15551234567',
|
|
700
|
-
])
|
|
701
|
+
// Nothing is persisted for an unverified identity: the code's subject is an unsaved user.
|
|
702
|
+
->and($controller->createdUsers)->toBe([])
|
|
703
|
+
->and($controller->emailVerifications[0][0])->toBeNull()
|
|
701
704
|
->and($controller->emailVerifications[0][1])->toBe('fleetops_create_customer')
|
|
702
705
|
->and($updated)->toBe(['status' => 'ok'])
|
|
703
706
|
->and($stub->name)->toBe('Stub Name')
|
|
@@ -844,7 +847,6 @@ test('api customer controller resets forgotten passwords', function () {
|
|
|
844
847
|
->and(fleetopsApiCustomerJson($reset))->toBe(['status' => 'ok'])
|
|
845
848
|
->and($controller->genericUser->savedForTest)->toBeTrue()
|
|
846
849
|
->and($controller->genericUser->tokensDeleted)->toBeTrue()
|
|
847
|
-
->and($controller->verificationCode->deleted)->toBeTrue()
|
|
848
850
|
->and(fleetopsApiCustomerJson($unknown->forgotPassword(Request::create('/v1/customers/forgot-password', 'POST', [
|
|
849
851
|
'identity' => 'missing@example.test',
|
|
850
852
|
]))))->toBe(['status' => 'ok'])
|
|
@@ -982,6 +984,51 @@ test('api customer controller handles verification delivery fallbacks and profil
|
|
|
982
984
|
]))))->toBe(['error' => 'update failed']);
|
|
983
985
|
});
|
|
984
986
|
|
|
987
|
+
test('api customer controller answers an sms guard refusal with its status, not the generic error', function () {
|
|
988
|
+
// A paused organization: the sign-up code request answers 423 with the wait.
|
|
989
|
+
$paused = fleetopsApiCustomerController();
|
|
990
|
+
$paused->activeUser = null;
|
|
991
|
+
$paused->smsVerificationThrows = SmsBlockedException::paused('company:company-uuid', 120);
|
|
992
|
+
$creation = $paused->requestCreationCode(new VerifyCreateCustomerRequest([
|
|
993
|
+
'mode' => 'sms',
|
|
994
|
+
'identity' => '15550000000',
|
|
995
|
+
]));
|
|
996
|
+
|
|
997
|
+
// Over budget with no email on file: the 429 and its Retry-After reach the app.
|
|
998
|
+
$limited = fleetopsApiCustomerController();
|
|
999
|
+
$limited->activeUser->email = null;
|
|
1000
|
+
$limited->smsVerificationThrows = SmsBlockedException::limited('company_per_hour', 45);
|
|
1001
|
+
$login = $limited->loginWithPhone(Request::create('/v1/customers/login/phone', 'POST', [
|
|
1002
|
+
'phone' => '15551234567',
|
|
1003
|
+
]));
|
|
1004
|
+
|
|
1005
|
+
// Over budget with an email on file: the code still goes by email.
|
|
1006
|
+
$fallback = fleetopsApiCustomerController();
|
|
1007
|
+
$fallback->smsVerificationThrows = SmsBlockedException::limited('company_per_hour', 45);
|
|
1008
|
+
$fallback->activeUser->email = 'jane@example.test';
|
|
1009
|
+
$byEmail = $fallback->loginWithPhone(Request::create('/v1/customers/login/phone', 'POST', [
|
|
1010
|
+
'phone' => '15551234567',
|
|
1011
|
+
]));
|
|
1012
|
+
|
|
1013
|
+
// A number that can never be texted: the reset request answers 422.
|
|
1014
|
+
$invalid = fleetopsApiCustomerController();
|
|
1015
|
+
$invalid->smsVerificationThrows = SmsBlockedException::invalidDestination(PhoneValidator::REASON_TYPE_NOT_ALLOWED);
|
|
1016
|
+
$forgot = $invalid->forgotPassword(Request::create('/v1/customers/forgot-password', 'POST', [
|
|
1017
|
+
'identity' => '15551234567',
|
|
1018
|
+
]));
|
|
1019
|
+
|
|
1020
|
+
expect($creation->getStatusCode())->toBe(423)
|
|
1021
|
+
->and($creation->headers->get('Retry-After'))->toBe('120')
|
|
1022
|
+
->and(fleetopsApiCustomerJson($creation))->toBe(['error' => 'Text messages are temporarily unavailable. Try again later.'])
|
|
1023
|
+
->and($login->getStatusCode())->toBe(429)
|
|
1024
|
+
->and($login->headers->get('Retry-After'))->toBe('45')
|
|
1025
|
+
->and(fleetopsApiCustomerJson($login))->toBe(['error' => 'Unable to send a verification code right now. Try again in a minute.'])
|
|
1026
|
+
->and(fleetopsApiCustomerJson($byEmail))->toBe(['status' => 'ok', 'method' => 'email'])
|
|
1027
|
+
->and($fallback->emailVerifications[0][1])->toBe('fleetops_customer_login')
|
|
1028
|
+
->and($forgot->getStatusCode())->toBe(422)
|
|
1029
|
+
->and(fleetopsApiCustomerJson($forgot))->toBe(['error' => 'The phone number provided cannot receive text messages.']);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
985
1032
|
/**
|
|
986
1033
|
* Run a callback with an app container that supports environment().
|
|
987
1034
|
*
|