booking-agent-cli 0.7.0__tar.gz → 0.8.0__tar.gz
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.
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/PKG-INFO +2 -2
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/README.md +1 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/pyproject.toml +1 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/__init__.py +1 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/namespace.py +1 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/platform_profile.py +11 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/ucp_capabilities.py +6 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/ucp_checkout.py +144 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/ucp_profile.py +22 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/__init__.py +1 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/booking_summary.py +15 -1
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/bundled_skill.md +18 -16
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/catalog.py +26 -5
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/flow.py +217 -8
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/engine.py +2 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/payment_helpers.py +77 -22
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_ucp_skill_flow_e2e.py +154 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_platform_profile.py +17 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/ucp_checkout_store.py +73 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/ucp_server.py +16 -5
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/offline_stack/stack.py +26 -5
- booking_agent_cli-0.8.0/tests/unit/test_pay_at_service.py +249 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/.gitignore +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/authorization.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/backend/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/backend/live.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/filter.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/google.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/oauth_listener.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/calendar/scope.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/circuit_breaker.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/config.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/defaults.env +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/discovery.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/headless.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/http_deadlines.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/http_retry.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/http_trace.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/merchant_pacing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/models/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/models/platform.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/models/wire.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/observability.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/pop.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/profile_cache.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/registry_url.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/rest_client.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/secure_file.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/settings.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/signals.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/step_timing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/ucp_negotiation.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent/usp_methods.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/activity_log.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/booking_poll.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/calendar_connection.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/calendar_event.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/calendar_preference.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/checkout_prereq.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/config.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/lowlevel.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/runtime.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/commands/update.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/config_loader.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/flow_timings.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/geo_eligibility.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/geocode.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/identity.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/install_integrity.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/link_cli.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/main.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/output.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/payment_token.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/provenance.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/reschedule.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/runtime.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/runtime_prepare.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/search_area.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/session.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/skill_bundle.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/skill_integrity.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/support_bundle_schema.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/support_report.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/timezone_context.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/updates.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/.env.tests +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/conftest.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_cancel_reschedule_e2e.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_latency_soak.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_staging_registry_integration.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_timezone_booking_e2e.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/test_ucp_native_e2e.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/e2e/ucp_native.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_activity_log.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_at_rest_hygiene.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_booking_rejection.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_bulk_cancel.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_calendar_connection.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_calendar_connector.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_calendar_flow.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_calendar_preference.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_checkout_signals.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_circuit_breaker.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_config.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_correlation_flow.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_declined_spt.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_discover_registry_rejection.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_flow_evidence.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_flow_gates.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_flow_reschedule.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_flow_step_timings.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_google_calendar_oauth.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_home_visit_address.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_http_trace.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_identity_logout.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_install_integrity.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_lazy_oauth.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_link_auth.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_link_cli_spt.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_namespace.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_oauth_listener.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_oauth_metadata.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_platform_key_pop.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_pop_keystore.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_profile_cache.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_regression_index.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_release_hold_uses_delete.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_request_retry_semantics.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_runtime_prepare.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_search_area.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_session_compat.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_session_retention.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_skill_integrity.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_skill_portability.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_support_report.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_timezone_context.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_cancel_result.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_checkout_pop.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_checkout_proof_wireup.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_checkout_validate.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_ucp_profile.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_update_command.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/integration/test_wheel_contents.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/SOURCE +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/acp_booking_extension.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/availability.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/booking.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/calendar_freebusy.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/catalog.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/paid_bookings.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/profile.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/registry.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/rest_common.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/usp.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/waitlist.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/spec_schemas/webhook_event.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/fixtures/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/fixtures/profiles.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/fixtures/provenance/pypi_attestations-0.0.30-py3-none-any.whl.provenance.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/fixtures/services.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/backend.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/link_cli.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/mock_calendar.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/pop_business.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/mock/pop_transport.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/support/offline_stack/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/__init__.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_authorization_policy.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_booking_poll_credentials.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_booking_request_error.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_calendar_filter.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_catalog.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_check_version.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_conditional_writes.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_delivery_address.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_discovery.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_geo_eligibility.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_geocode.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_headless.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_http_deadlines.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_http_retry.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_live_search_registry.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_merchant_pacing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_mock_support.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_money_path_events.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_nonce_from_pop_error_body.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_observability.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_outbound_member_names.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_package_layering.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_provenance.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_registry_search_filters.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_registry_search_pricing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_registry_search_responses.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_registry_url.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_release_notes.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_rest_client.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_scoped_method_authorization.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_session_credentials.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_spec_conformance.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_step_timing.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_ucp_capabilities.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_ucp_complete_outcome.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_ucp_create_price_guard.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_ucp_negotiation.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_updates.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/unit/test_wire_model_conformance.py +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/001-jcs-nested-buyer.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/002-jcs-meta-only.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/003-jcs-unicode-empty-meta.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/004-proof-issuance-no-ath.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/005-proof-presentation-ath-nonce.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/006-cross-key-replay.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/007-downgrade-cnf-without-proof.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/008-replayed-jti.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/009-aud-mismatch.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/010-alg-none.json +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/README.md +0 -0
- {booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/tests/vectors/pop/SOURCE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: booking-agent-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: The Booking Agent CLI: a stateful USP booking flow with hard gates, plus its USP protocol library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/wix/booking-agent
|
|
6
6
|
Project-URL: Documentation, https://github.com/wix/booking-agent/blob/master/booking-agent/README.md
|
|
@@ -78,7 +78,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
|
|
|
78
78
|
the release workflow, so a harness can pin and verify the exact text it follows:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.
|
|
81
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.56.0/booking-agent/skills/booking-agent/SKILL.md
|
|
82
82
|
gh attestation verify SKILL.md --repo wix/booking-agent \
|
|
83
83
|
--signer-workflow wix/booking-agent/.github/workflows/release.yml
|
|
84
84
|
```
|
|
@@ -36,7 +36,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
|
|
|
36
36
|
the release workflow, so a harness can pin and verify the exact text it follows:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.
|
|
39
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.56.0/booking-agent/skills/booking-agent/SKILL.md
|
|
40
40
|
gh attestation verify SKILL.md --repo wix/booking-agent \
|
|
41
41
|
--signer-workflow wix/booking-agent/.github/workflows/release.yml
|
|
42
42
|
```
|
|
@@ -17,6 +17,7 @@ CAP_CATALOG = "dev.usp-protocol.services.catalog"
|
|
|
17
17
|
CAP_AVAILABILITY = "dev.usp-protocol.services.availability"
|
|
18
18
|
CAP_BOOKINGS = "dev.usp-protocol.services.bookings"
|
|
19
19
|
CAP_PAID_BOOKINGS = "dev.usp-protocol.services.paid_bookings"
|
|
20
|
+
CAP_PAY_AT_SERVICE = "dev.usp-protocol.services.pay_at_service"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class NamespaceOriginError(ValueError):
|
|
@@ -88,6 +88,17 @@ def platform_profile_doc() -> Dict[str, Any]:
|
|
|
88
88
|
"extends": "dev.ucp.shopping.checkout",
|
|
89
89
|
}
|
|
90
90
|
],
|
|
91
|
+
"dev.usp-protocol.services.pay_at_service": [
|
|
92
|
+
{
|
|
93
|
+
"version": "2026-08-20",
|
|
94
|
+
"spec": (
|
|
95
|
+
"https://usp-protocol.dev/specification"
|
|
96
|
+
"#pay-at-service-settlement-extension"
|
|
97
|
+
),
|
|
98
|
+
"schema": "https://usp-protocol.dev/schemas/services/pay_at_service.json",
|
|
99
|
+
"extends": "dev.ucp.shopping.checkout",
|
|
100
|
+
}
|
|
101
|
+
],
|
|
91
102
|
"dev.ucp.common.payment.terms": [
|
|
92
103
|
{
|
|
93
104
|
"version": "2026-08-25",
|
|
@@ -28,6 +28,7 @@ from __future__ import annotations
|
|
|
28
28
|
from typing import Any, Callable, Dict, List
|
|
29
29
|
|
|
30
30
|
from .ucp_negotiation import _capabilities_from_doc
|
|
31
|
+
from .namespace import CAP_PAY_AT_SERVICE
|
|
31
32
|
from .ucp_profile import CAP_PAID_BOOKINGS, UcpProfileValidationError
|
|
32
33
|
|
|
33
34
|
|
|
@@ -91,9 +92,14 @@ def _paid_bookings_handler(echoed_versions: List[str], platform_supported: List[
|
|
|
91
92
|
_validate_versions_supported(CAP_PAID_BOOKINGS, echoed_versions, platform_supported)
|
|
92
93
|
|
|
93
94
|
|
|
95
|
+
def _pay_at_service_handler(echoed_versions: List[str], platform_supported: List[str]) -> None:
|
|
96
|
+
_validate_versions_supported(CAP_PAY_AT_SERVICE, echoed_versions, platform_supported)
|
|
97
|
+
|
|
98
|
+
|
|
94
99
|
# namespace -> handler. Register new capabilities (e.g. AP2) here.
|
|
95
100
|
CAPABILITY_HANDLERS: Dict[str, CapabilityHandler] = {
|
|
96
101
|
CAP_PAID_BOOKINGS: _paid_bookings_handler,
|
|
102
|
+
CAP_PAY_AT_SERVICE: _pay_at_service_handler,
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import uuid
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from typing import Any, Dict, List, Optional
|
|
10
11
|
|
|
@@ -489,6 +490,129 @@ def immediate_schedule_amount(term: Dict[str, Any]) -> Optional[int]:
|
|
|
489
490
|
return amount
|
|
490
491
|
|
|
491
492
|
|
|
493
|
+
PAY_AT_SERVICE_CAPABILITY = "dev.usp-protocol.services.pay_at_service"
|
|
494
|
+
SETTLEMENT_MODE_OFFLINE = "offline"
|
|
495
|
+
#: Disclosure code the business emits for a pay-at-service checkout (USP 11.3.5 S8).
|
|
496
|
+
PAYMENT_DUE_AT_SERVICE_CODE = "payment_due_at_service"
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def is_offline_settled(checkout: Any) -> bool:
|
|
500
|
+
"""True when the checkout declares ``settlement.mode == "offline"`` (USP 11.3.3)."""
|
|
501
|
+
settlement = checkout.get("settlement") if isinstance(checkout, dict) else None
|
|
502
|
+
return isinstance(settlement, dict) and settlement.get("mode") == SETTLEMENT_MODE_OFFLINE
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _instants_equal(a: Any, b: Any) -> bool:
|
|
506
|
+
"""Compare two RFC 3339 instants, tolerating a differing UTC offset spelling."""
|
|
507
|
+
if a == b:
|
|
508
|
+
return True
|
|
509
|
+
try:
|
|
510
|
+
left = datetime.fromisoformat(str(a).replace("Z", "+00:00"))
|
|
511
|
+
right = datetime.fromisoformat(str(b).replace("Z", "+00:00"))
|
|
512
|
+
except ValueError:
|
|
513
|
+
return False
|
|
514
|
+
if left.tzinfo is None or right.tzinfo is None:
|
|
515
|
+
return False
|
|
516
|
+
return left == right
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _is_at_service_term(term: Any) -> bool:
|
|
520
|
+
schedules = term.get("schedules") if isinstance(term, dict) else None
|
|
521
|
+
return (
|
|
522
|
+
isinstance(schedules, list)
|
|
523
|
+
and bool(schedules)
|
|
524
|
+
and all(isinstance(s, dict) and s.get("type") == "at_service" for s in schedules)
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def validate_offline_settlement(
|
|
529
|
+
checkout: Dict[str, Any],
|
|
530
|
+
*,
|
|
531
|
+
negotiated_capabilities: Optional[Dict[str, Any]] = None,
|
|
532
|
+
persisted_term: Optional[Dict[str, Any]] = None,
|
|
533
|
+
slot_start: Optional[str] = None,
|
|
534
|
+
) -> Dict[str, Any]:
|
|
535
|
+
"""Validate a pay-at-service checkout (USP 11.3) and return its selected term.
|
|
536
|
+
|
|
537
|
+
Fails closed on anything the extension does not define: an unknown settlement
|
|
538
|
+
mode, a settlement without the terms it names (S1), more than one offline term
|
|
539
|
+
(S2), a ``due_at`` that is not the slot start (S3), or a selected term other
|
|
540
|
+
than the offline one (S4 applies only when the offline term is selected).
|
|
541
|
+
"""
|
|
542
|
+
settlement = checkout.get("settlement")
|
|
543
|
+
if not isinstance(settlement, dict):
|
|
544
|
+
raise UcpProfileValidationError("Checkout settlement must be an object.")
|
|
545
|
+
if settlement.get("mode") != SETTLEMENT_MODE_OFFLINE:
|
|
546
|
+
raise UcpProfileValidationError(
|
|
547
|
+
f"Checkout settlement mode {settlement.get('mode')!r} is not supported."
|
|
548
|
+
)
|
|
549
|
+
term_id = settlement.get("term_id")
|
|
550
|
+
if not isinstance(term_id, str) or not term_id:
|
|
551
|
+
raise UcpProfileValidationError("Checkout settlement is missing term_id.")
|
|
552
|
+
caps = negotiated_capabilities or {}
|
|
553
|
+
missing = [
|
|
554
|
+
cap for cap in (PAY_AT_SERVICE_CAPABILITY, PAYMENT_TERMS_CAPABILITY) if cap not in caps
|
|
555
|
+
]
|
|
556
|
+
if missing:
|
|
557
|
+
raise UcpProfileValidationError(
|
|
558
|
+
"Checkout declared pay-at-service settlement without negotiated capabilities "
|
|
559
|
+
f"{missing!r}."
|
|
560
|
+
)
|
|
561
|
+
payment = checkout.get("payment") if isinstance(checkout.get("payment"), dict) else {}
|
|
562
|
+
terms = payment.get("terms")
|
|
563
|
+
selected_id = payment.get("selected_term_id")
|
|
564
|
+
if not isinstance(terms, list) or not terms or not selected_id:
|
|
565
|
+
raise UcpProfileValidationError(
|
|
566
|
+
"Checkout settlement requires payment.terms and payment.selected_term_id."
|
|
567
|
+
)
|
|
568
|
+
named = [t for t in terms if isinstance(t, dict) and t.get("id") == term_id]
|
|
569
|
+
if len(named) != 1:
|
|
570
|
+
raise UcpProfileValidationError(
|
|
571
|
+
"Checkout settlement.term_id must match exactly one payment.terms[].id."
|
|
572
|
+
)
|
|
573
|
+
term = named[0]
|
|
574
|
+
offline_terms = [t for t in terms if _is_at_service_term(t)]
|
|
575
|
+
if len(offline_terms) != 1 or offline_terms[0] is not term:
|
|
576
|
+
raise UcpProfileValidationError(
|
|
577
|
+
"A pay-at-service checkout must offer exactly one pay-at-service term, "
|
|
578
|
+
"and settlement.term_id must name it."
|
|
579
|
+
)
|
|
580
|
+
if selected_id != term_id:
|
|
581
|
+
raise UcpProfileValidationError(
|
|
582
|
+
"The pay-at-service term is not the selected payment term, so the booking "
|
|
583
|
+
"cannot be completed without a payment instrument."
|
|
584
|
+
)
|
|
585
|
+
total = checkout_total_minor_units(checkout)
|
|
586
|
+
if total is None:
|
|
587
|
+
raise UcpProfileValidationError("Checkout totals must include a type=total amount.")
|
|
588
|
+
amounts: list[int] = []
|
|
589
|
+
for schedule in term["schedules"]:
|
|
590
|
+
amount = _schedule_amount(schedule)
|
|
591
|
+
if amount is None or amount <= 0:
|
|
592
|
+
raise UcpProfileValidationError(
|
|
593
|
+
"Each pay-at-service schedule must carry a positive integer amount."
|
|
594
|
+
)
|
|
595
|
+
amounts.append(amount)
|
|
596
|
+
due_at = schedule.get("due_at")
|
|
597
|
+
if not due_at:
|
|
598
|
+
raise UcpProfileValidationError(
|
|
599
|
+
"Pay-at-service schedule must set due_at to the slot start."
|
|
600
|
+
)
|
|
601
|
+
if slot_start and not _instants_equal(due_at, slot_start):
|
|
602
|
+
raise UcpProfileValidationError(
|
|
603
|
+
"Pay-at-service due_at must equal the booked slot start."
|
|
604
|
+
)
|
|
605
|
+
if sum(amounts) != int(total):
|
|
606
|
+
raise UcpProfileValidationError(
|
|
607
|
+
"Pay-at-service schedule amounts must sum to the checkout total."
|
|
608
|
+
)
|
|
609
|
+
if persisted_term is not None and persisted_term != term:
|
|
610
|
+
raise UcpProfileValidationError(
|
|
611
|
+
"The payment term changed since it was quoted. Do not complete this booking."
|
|
612
|
+
)
|
|
613
|
+
return term
|
|
614
|
+
|
|
615
|
+
|
|
492
616
|
def validate_payment_terms(
|
|
493
617
|
checkout: Dict[str, Any],
|
|
494
618
|
*,
|
|
@@ -496,13 +620,33 @@ def validate_payment_terms(
|
|
|
496
620
|
require_terms: bool = False,
|
|
497
621
|
persisted_term: Optional[Dict[str, Any]] = None,
|
|
498
622
|
slot_start: Optional[str] = None,
|
|
623
|
+
expect_offline_settlement: bool = False,
|
|
499
624
|
) -> Optional[Dict[str, Any]]:
|
|
500
625
|
"""Validate UCP payment terms on a checkout. Returns the selected term or None.
|
|
501
626
|
|
|
502
627
|
Deposits require the negotiated terms capability, exactly one selected term,
|
|
503
628
|
schedules that sum to checkout total, exactly one positive immediate amount,
|
|
504
629
|
and an at-service balance with due_at.
|
|
630
|
+
|
|
631
|
+
A checkout routed as pay-at-service (``expect_offline_settlement``) must carry
|
|
632
|
+
an offline ``settlement`` and is validated by :func:`validate_offline_settlement`;
|
|
633
|
+
any other route that receives a ``settlement`` fails closed rather than being
|
|
634
|
+
treated as an ordinary charge.
|
|
505
635
|
"""
|
|
636
|
+
has_settlement = "settlement" in checkout
|
|
637
|
+
if has_settlement != expect_offline_settlement:
|
|
638
|
+
raise UcpProfileValidationError(
|
|
639
|
+
"Checkout carries a settlement this booking was not routed for."
|
|
640
|
+
if has_settlement
|
|
641
|
+
else "This booking was routed as pay-at-service but the checkout has no settlement."
|
|
642
|
+
)
|
|
643
|
+
if has_settlement:
|
|
644
|
+
return validate_offline_settlement(
|
|
645
|
+
checkout,
|
|
646
|
+
negotiated_capabilities=negotiated_capabilities,
|
|
647
|
+
persisted_term=persisted_term,
|
|
648
|
+
slot_start=slot_start,
|
|
649
|
+
)
|
|
506
650
|
payment = checkout.get("payment") if isinstance(checkout.get("payment"), dict) else {}
|
|
507
651
|
terms = payment.get("terms") if isinstance(payment, dict) else None
|
|
508
652
|
has_terms = isinstance(terms, list) and len(terms) > 0
|
|
@@ -20,9 +20,16 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
20
20
|
from pydantic.alias_generators import to_camel
|
|
21
21
|
|
|
22
22
|
from .authorization import AuthorizationPolicy, parse_policy
|
|
23
|
-
from .namespace import
|
|
23
|
+
from .namespace import (
|
|
24
|
+
CAP_CATALOG,
|
|
25
|
+
CAP_PAID_BOOKINGS,
|
|
26
|
+
CAP_PAY_AT_SERVICE,
|
|
27
|
+
SVC_SERVICES,
|
|
28
|
+
check_origin_binding,
|
|
29
|
+
)
|
|
24
30
|
|
|
25
31
|
CAP_CHECKOUT = "dev.ucp.shopping.checkout"
|
|
32
|
+
CAP_PAYMENT_TERMS = "dev.ucp.common.payment.terms"
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
class UcpTransportBinding(BaseModel):
|
|
@@ -110,6 +117,20 @@ class UcpNativeContext(BaseModel):
|
|
|
110
117
|
def requires_paid_bookings(self) -> bool:
|
|
111
118
|
return CAP_PAID_BOOKINGS in self.profile.capabilities
|
|
112
119
|
|
|
120
|
+
def declares_pay_at_service(self) -> bool:
|
|
121
|
+
"""True when the business declares everything the pay-at-service checkout path needs.
|
|
122
|
+
|
|
123
|
+
USP 11.3.2: a business routing ``at_service`` through a checkout declares
|
|
124
|
+
``pay_at_service`` *and* ``dev.ucp.common.payment.terms``; declaring the
|
|
125
|
+
first without the second is malformed, so it is not treated as declared.
|
|
126
|
+
"""
|
|
127
|
+
caps = self.profile.capabilities
|
|
128
|
+
return (
|
|
129
|
+
CAP_PAY_AT_SERVICE in caps
|
|
130
|
+
and CAP_PAYMENT_TERMS in caps
|
|
131
|
+
and CAP_CHECKOUT in caps
|
|
132
|
+
)
|
|
133
|
+
|
|
113
134
|
def usp_authorization(self) -> Optional[AuthorizationPolicy]:
|
|
114
135
|
"""The ``config.authorization`` policy on the ``dev.usp-protocol.services`` binding.
|
|
115
136
|
|
{booking_agent_cli-0.7.0 → booking_agent_cli-0.8.0}/src/booking_agent_cli/booking_summary.py
RENAMED
|
@@ -104,7 +104,7 @@ def price_text(session: Any, price_info: Dict[str, Any]) -> Optional[str]:
|
|
|
104
104
|
return "Free"
|
|
105
105
|
if not price:
|
|
106
106
|
return None
|
|
107
|
-
if route
|
|
107
|
+
if route in ("at_service", "pay_at_service"):
|
|
108
108
|
return f"{price}, paid to the business at the appointment (nothing is charged now)"
|
|
109
109
|
if route == "deposit_required":
|
|
110
110
|
return (
|
|
@@ -151,6 +151,18 @@ def payment_context_text(session: Any, total_display: Optional[str]) -> str:
|
|
|
151
151
|
return text
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
def pay_at_service_line(session: Any) -> str:
|
|
155
|
+
"""What is owed and when, from the business's own accepted payment term."""
|
|
156
|
+
term = getattr(session, "accepted_term", None)
|
|
157
|
+
schedules = term.get("schedules") if isinstance(term, dict) else None
|
|
158
|
+
for schedule in schedules or []:
|
|
159
|
+
description = schedule.get("description") if isinstance(schedule, dict) else None
|
|
160
|
+
plain = description.get("plain") if isinstance(description, dict) else None
|
|
161
|
+
if isinstance(plain, str) and plain.strip():
|
|
162
|
+
return f"Nothing was charged. {plain.strip()}"
|
|
163
|
+
return "Nothing was charged. Payment is due at your appointment."
|
|
164
|
+
|
|
165
|
+
|
|
154
166
|
def booked_message(session: Any) -> str:
|
|
155
167
|
"""Plain-language "you're booked" text built from saved state.
|
|
156
168
|
|
|
@@ -171,6 +183,8 @@ def booked_message(session: Any) -> str:
|
|
|
171
183
|
link = getattr(session, "calendar_event_html_link", None)
|
|
172
184
|
if link:
|
|
173
185
|
text += f"\nAdded to your Google Calendar: {link}"
|
|
186
|
+
if getattr(session, "payment_route", None) == "pay_at_service":
|
|
187
|
+
text += "\n" + pay_at_service_line(session)
|
|
174
188
|
booking_url = getattr(getattr(session, "booking", None), "booking_url", None)
|
|
175
189
|
if booking_url:
|
|
176
190
|
text += f"\nTo view, change, or cancel it later (worth saving): {booking_url}"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: 0.
|
|
2
|
+
version: 0.56.0
|
|
3
3
|
name: booking-agent
|
|
4
4
|
description: |
|
|
5
5
|
Books time-based services via USP from a Link wallet. Use when the user wants to book, schedule, reserve, or pay for a service from a business (haircut, lesson, repair, class, appointment, consult). The `booking-agent` CLI owns the booking state machine and refuses to proceed when human gates are skipped. Sign the user in to Link on your own, right after install and at the start of every booking (`booking-agent flow link-auth connect`, then start `booking-agent flow link-auth poll` in the background and post the sign-in URL in the same turn, exactly like `pay poll`); never wait for the user to ask to sign in or to say they approved. After `booking-agent flow business use`, follow `next_command` through `use-ucp-checkout-mode`; businesses without native checkout fail closed. Payment uses `booking-agent flow pay request` (default returns immediately with the approval URL), then start `booking-agent flow pay poll` in the background and post the approval URL to the user in the same turn; the poll finishes checkout once they approve. Confirm through `booking-agent flow book poll --wait`; never start an inbound webhook. Surface the approval URL exactly once. Personal identification (name, phone, email, address) must always come from the user's Link account via `link-cli`, never from chat or placeholders. Before any location-based search, the CLI proposes the Link shipping address and the user confirms it or gives another; online services skip this. Talk to the user in plain everyday language only; never expose CLI, UCP, auth, session, or other implementation details unless they explicitly ask.
|
|
@@ -19,7 +19,7 @@ metadata:
|
|
|
19
19
|
- link-cli
|
|
20
20
|
install:
|
|
21
21
|
- kind: uv
|
|
22
|
-
package: "booking-agent-cli==0.
|
|
22
|
+
package: "booking-agent-cli==0.8.0"
|
|
23
23
|
bins: [booking-agent]
|
|
24
24
|
user-invocable: true
|
|
25
25
|
---
|
|
@@ -220,7 +220,7 @@ To take a fixed, published version instead of the latest, fetch it at the releas
|
|
|
220
220
|
workflow signs it under, and check the signature before you follow it:
|
|
221
221
|
|
|
222
222
|
```bash
|
|
223
|
-
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.
|
|
223
|
+
curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.56.0/booking-agent/skills/booking-agent/SKILL.md
|
|
224
224
|
gh attestation verify SKILL.md \
|
|
225
225
|
--repo wix/booking-agent \
|
|
226
226
|
--signer-workflow wix/booking-agent/.github/workflows/release.yml
|
|
@@ -245,17 +245,17 @@ before installing:
|
|
|
245
245
|
|
|
246
246
|
```bash
|
|
247
247
|
dir="$HOME/.booking-agent/verified-wheels"
|
|
248
|
-
uvx pip download "booking-agent-cli==0.
|
|
248
|
+
uvx pip download "booking-agent-cli==0.8.0" --no-deps --only-binary :all: -d "$dir"
|
|
249
249
|
uvx pypi-attestations==0.0.30 verify pypi --repository https://github.com/wix/booking-agent \
|
|
250
|
-
"$dir/booking_agent_cli-0.
|
|
251
|
-
uv tool install --force "$dir/booking_agent_cli-0.
|
|
250
|
+
"$dir/booking_agent_cli-0.8.0-py3-none-any.whl"
|
|
251
|
+
uv tool install --force "$dir/booking_agent_cli-0.8.0-py3-none-any.whl"
|
|
252
252
|
|
|
253
253
|
booking-agent --help
|
|
254
254
|
```
|
|
255
255
|
|
|
256
256
|
Install only the wheel whose verify step passed, never one that failed it. If `uvx` is not
|
|
257
|
-
available, install the same pinned release with `uv tool install booking-agent-cli==0.
|
|
258
|
-
`pipx install booking-agent-cli==0.
|
|
257
|
+
available, install the same pinned release with `uv tool install booking-agent-cli==0.8.0` or
|
|
258
|
+
`pipx install booking-agent-cli==0.8.0`; the CLI checks its own files against the signed release
|
|
259
259
|
on the first `booking-agent flow init` and refuses to book or pay if they do not match.
|
|
260
260
|
|
|
261
261
|
Harnesses that read the `install` block in this skill's metadata do this for you. Do not go on
|
|
@@ -308,7 +308,7 @@ prints each effective setting and where it came from; `booking-agent config init
|
|
|
308
308
|
|
|
309
309
|
The CLI decides whether a newer version is worth mentioning; you never decide from version
|
|
310
310
|
numbers or release pages. Pass this skill's version on every `flow init` (`--skill-version
|
|
311
|
-
0.
|
|
311
|
+
0.56.0`, from the frontmatter above), so the CLI can tell when these instructions and the
|
|
312
312
|
installed CLI no longer match.
|
|
313
313
|
|
|
314
314
|
- When a command returns `awaiting_user: update_consent`, ask the question in `prompt` in plain
|
|
@@ -470,7 +470,7 @@ confirmation email or on the business's page (`booking_url`). Do not invent a lo
|
|
|
470
470
|
**Step 1b — Create a new session:**
|
|
471
471
|
|
|
472
472
|
```bash
|
|
473
|
-
booking-agent flow init --user-request "<user's exact request>" --skill-version 0.
|
|
473
|
+
booking-agent flow init --user-request "<user's exact request>" --skill-version 0.56.0 \
|
|
474
474
|
--skill-path "<absolute path of the SKILL.md you loaded>"
|
|
475
475
|
```
|
|
476
476
|
|
|
@@ -786,8 +786,7 @@ If `flow book` fails with `mandatory_acceptance_pending`, show the user any poli
|
|
|
786
786
|
`flow book`. **Never** accept policies or tick boxes on their behalf.
|
|
787
787
|
|
|
788
788
|
If `data.checkout_links` or `data.checkout_policies` are present after a successful book, show
|
|
789
|
-
those links before payment. If
|
|
790
|
-
the user policies are "missing."
|
|
789
|
+
those links before payment. If none were sent, say nothing about policies.
|
|
791
790
|
|
|
792
791
|
**Free and pay-at-appointment bookings (`free`, `at_service`).** `flow book` places the booking
|
|
793
792
|
directly and `next_command` is `booking-agent flow book poll --wait`. Before running it, tell the user
|
|
@@ -796,6 +795,9 @@ charged (or the price is paid at the appointment), and you are now waiting for t
|
|
|
796
795
|
confirm. Then run `book poll --wait` and report the result. Never create a payment and skip
|
|
797
796
|
Steps 6b to 6d.
|
|
798
797
|
|
|
798
|
+
**Pay-at-appointment with a checkout (`pay_at_service`).** Nothing is charged; skip `pay request`. Show every `data.payment_disclosures` item (`content_display`) and the amount
|
|
799
|
+
due at the appointment, then run `pay ucp-complete --disclosures-confirmed`. If `merchant_quote.total_changed`, get approval and add `--total-confirmed`.
|
|
800
|
+
|
|
799
801
|
**Deposit bookings (`deposit_required`).** Before any charge, show `data.due_now_display` (the
|
|
800
802
|
deposit charged now), the amount due at the appointment, and every item in
|
|
801
803
|
`data.payment_schedules` and `data.payment_disclosures`, and get the user's agreement. Then run
|
|
@@ -927,8 +929,9 @@ UCP `complete_checkout` (CLI builds this internally): `handler_id: "stripe_payme
|
|
|
927
929
|
|
|
928
930
|
**Payment notes (error recovery only):**
|
|
929
931
|
|
|
930
|
-
- A live catalog with `payment_timing: at_service` is booked directly
|
|
931
|
-
|
|
932
|
+
- A live catalog with `payment_timing: at_service` is booked directly or, when `data.payment_route`
|
|
933
|
+
is `pay_at_service`, through a checkout. Tell the user the quoted price is paid at the
|
|
934
|
+
appointment. Never run `pay request` for it.
|
|
932
935
|
- A live catalog with `payment_timing: deposit_required` goes through checkout. Charge only the
|
|
933
936
|
due-now (immediate) amount from the payment term. Show every disclosure and the amount due at
|
|
934
937
|
the appointment, then pass `--disclosures-confirmed` on `pay request` after the user agrees.
|
|
@@ -963,8 +966,7 @@ data, credentials, or shared payment tokens.
|
|
|
963
966
|
|
|
964
967
|
### Step 8 and after the booking
|
|
965
968
|
|
|
966
|
-
Nothing to run
|
|
967
|
-
calendar event and returns the final answer. See Step 7.
|
|
969
|
+
Nothing to run; Step 7's command already added the calendar event.
|
|
968
970
|
|
|
969
971
|
## Important
|
|
970
972
|
|
|
@@ -670,15 +670,28 @@ def payment_price_for_session(
|
|
|
670
670
|
|
|
671
671
|
|
|
672
672
|
DIRECT_BOOKING_ROUTES = frozenset({"free", "at_service"})
|
|
673
|
-
CHECKOUT_BOOKING_ROUTES = frozenset({"at_booking", "deposit_required"})
|
|
673
|
+
CHECKOUT_BOOKING_ROUTES = frozenset({"at_booking", "deposit_required", "pay_at_service"})
|
|
674
|
+
# Routes where nothing is collected through the booking flow: no Link spend
|
|
675
|
+
# request, no payment instrument. ``pay_at_service`` still goes through a UCP
|
|
676
|
+
# checkout (so an order exists); ``free`` and ``at_service`` do not.
|
|
677
|
+
NO_PAYMENT_ROUTES = frozenset({"free", "at_service", "pay_at_service"})
|
|
674
678
|
|
|
675
679
|
|
|
676
|
-
def classify_payment_route(
|
|
680
|
+
def classify_payment_route(
|
|
681
|
+
live_service: Dict[str, Any],
|
|
682
|
+
*,
|
|
683
|
+
pay_at_service_declared: bool = False,
|
|
684
|
+
) -> Dict[str, Any]:
|
|
677
685
|
"""Classify a live service into a per-service payment route.
|
|
678
686
|
|
|
679
687
|
Timing is a service property, not a business property. Mixed catalogs
|
|
680
688
|
(at_booking, at_service, deposit_required, free on one profile) must route
|
|
681
689
|
each service independently.
|
|
690
|
+
|
|
691
|
+
An ``at_service`` service takes the ``pay_at_service`` checkout route only
|
|
692
|
+
when the business declares the pay-at-service extension; otherwise it keeps
|
|
693
|
+
the direct route (USP 7.6.1: a platform MUST NOT route ``at_service``
|
|
694
|
+
through a checkout the business has not declared the extension for).
|
|
682
695
|
"""
|
|
683
696
|
policies = live_service.get("policies") if isinstance(live_service, dict) else None
|
|
684
697
|
pricing = live_service.get("pricing") if isinstance(live_service, dict) else None
|
|
@@ -734,7 +747,8 @@ def classify_payment_route(live_service: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
734
747
|
if timing == "at_booking":
|
|
735
748
|
return {"route": "at_booking", "timing": timing, "refusal": None}
|
|
736
749
|
if timing == "at_service":
|
|
737
|
-
|
|
750
|
+
route = "pay_at_service" if pay_at_service_declared else "at_service"
|
|
751
|
+
return {"route": route, "timing": timing, "refusal": None}
|
|
738
752
|
if timing == "deposit_required":
|
|
739
753
|
return {
|
|
740
754
|
"route": "deposit_required",
|
|
@@ -758,9 +772,12 @@ def payment_timing_refusal(
|
|
|
758
772
|
live_service: Dict[str, Any],
|
|
759
773
|
*,
|
|
760
774
|
allow_deposits: bool = True,
|
|
775
|
+
pay_at_service_declared: bool = False,
|
|
761
776
|
) -> Optional[Dict[str, str]]:
|
|
762
777
|
"""Return a fail-closed error when this service cannot be booked."""
|
|
763
|
-
classified = classify_payment_route(
|
|
778
|
+
classified = classify_payment_route(
|
|
779
|
+
live_service, pay_at_service_declared=pay_at_service_declared
|
|
780
|
+
)
|
|
764
781
|
if classified["refusal"] is not None:
|
|
765
782
|
return classified["refusal"]
|
|
766
783
|
if classified["route"] == "deposit_required" and not allow_deposits:
|
|
@@ -779,9 +796,13 @@ def payment_timing_changed_refusal(
|
|
|
779
796
|
live_service: Dict[str, Any],
|
|
780
797
|
) -> Optional[Dict[str, str]]:
|
|
781
798
|
"""Fail closed when a later live read disagrees with the discovered timing."""
|
|
782
|
-
classified = classify_payment_route(live_service)
|
|
783
799
|
previous = getattr(session, "payment_timing", None)
|
|
784
800
|
previous_route = getattr(session, "payment_route", None)
|
|
801
|
+
# The business's declaration was read when the route was chosen; the live
|
|
802
|
+
# service read only re-checks the service's own timing.
|
|
803
|
+
classified = classify_payment_route(
|
|
804
|
+
live_service, pay_at_service_declared=previous_route == "pay_at_service"
|
|
805
|
+
)
|
|
785
806
|
new_timing = classified["timing"]
|
|
786
807
|
new_route = classified["route"]
|
|
787
808
|
if previous is None and previous_route is None:
|