diff --git a/.gitignore b/.gitignore index 25ca3ad..85c491c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ .ruff_cache uv.lock .venv -**/node_modules \ No newline at end of file +**/node_modules diff --git a/LICENSE b/LICENSE index 7a4a3ea..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/README.md b/README.md index 2e460f0..e30a896 100644 --- a/README.md +++ b/README.md @@ -25,39 +25,37 @@ Universal Commerce Protocol (UCP). A reference implementation of a UCP Merchant Server using Python and FastAPI. -* **Server**: [Documentation](rest/python/server/README.md) - - * Located in `rest/python/server/`. - * Demonstrates capability discovery, checkout session management, payment - processing, and order lifecycle. - * Includes simulation endpoints for testing. - -* **Client**: - [Happy Path Script](rest/python/client/flower_shop/simple_happy_path_client.py) - - * Located in `rest/python/client/`. - * A script demonstrating a full "happy path" user journey (discovery -> - checkout -> payment). +- **Server**: [Documentation](rest/python/server/README.md) + - Located in `rest/python/server/`. + - Demonstrates capability discovery, checkout session management, payment + processing, and order lifecycle. + - Includes simulation endpoints for testing. + +- **Client**: + [Happy Path Script](rest/python/client/flower_shop/simple_happy_path_client.py) + - Located in `rest/python/client/`. + - A script demonstrating a full "happy path" user journey (discovery -> + checkout -> payment). ### Node.js A reference implementation of a UCP Merchant Server using Node.js, Hono, and Zod. -* **Server**: [Documentation](rest/nodejs/README.md) - * Located in `rest/nodejs/`. - * Demonstrates implementation of UCP specifications for shopping, - checkout, and order management using a Node.js stack. +- **Server**: [Documentation](rest/nodejs/README.md) + - Located in `rest/nodejs/`. + - Demonstrates implementation of UCP specifications for shopping, + checkout, and order management using a Node.js stack. ### A2A (Agent-to-Agent) An AI-powered retail agent implementing UCP via the A2A protocol. -* **Cymbal Retail Agent**: [Documentation](a2a/README.md) - * Located in `a2a/business_agent/`. - * Demonstrates A2A protocol integration with UCP Extension. - * Includes AI-powered shopping assistant with Google ADK and Gemini. - * React-based chat client for user interaction. +- **Cymbal Retail Agent**: [Documentation](a2a/README.md) + - Located in `a2a/business_agent/`. + - Demonstrates A2A protocol integration with UCP Extension. + - Includes AI-powered shopping assistant with Google ADK and Gemini. + - React-based chat client for user interaction. ## Getting Started diff --git a/a2a/DEVELOPER_GUIDE.md b/a2a/DEVELOPER_GUIDE.md index c46b7f0..834f7aa 100644 --- a/a2a/DEVELOPER_GUIDE.md +++ b/a2a/DEVELOPER_GUIDE.md @@ -13,6 +13,7 @@ This guide is for developers who want to understand how the sample works interna **Prerequisites:** Complete the [Quick Start in README.md](README.md#quick-start) first. **What you'll learn:** + - System architecture and component responsibilities - How UCP and A2A protocols integrate with ADK - The checkout state machine and commerce flows @@ -22,16 +23,16 @@ This guide is for developers who want to understand how the sample works interna Choose your path based on your goal: -| Goal | Start Here | Then Read | -|------|------------|-----------| -| **New to AI agents?** | [Glossary](docs/00-glossary.md) | This guide → 01-architecture | -| **Understand the system** | [Architecture](docs/01-architecture.md) | 02-adk-agent → 03-ucp-integration | -| **Add a new tool** | [ADK Agent](docs/02-adk-agent.md) | 06-extending | -| **Modify checkout flow** | [Commerce Flows](docs/04-commerce-flows.md) | 06-extending | -| **Customize the UI** | [Frontend](docs/05-frontend.md) | - | -| **Debug an issue** | [Testing Guide](docs/07-testing-guide.md) | - | -| **Deploy to production** | [Production Notes](docs/08-production-notes.md) | - | -| **Use AI assistant** | [SKILLS.md](SKILLS.md) | Context for Claude Code, Gemini CLI, Cursor, Codex | +| Goal | Start Here | Then Read | +| ------------------------- | ----------------------------------------------- | -------------------------------------------------- | +| **New to AI agents?** | [Glossary](docs/00-glossary.md) | This guide → 01-architecture | +| **Understand the system** | [Architecture](docs/01-architecture.md) | 02-adk-agent → 03-ucp-integration | +| **Add a new tool** | [ADK Agent](docs/02-adk-agent.md) | 06-extending | +| **Modify checkout flow** | [Commerce Flows](docs/04-commerce-flows.md) | 06-extending | +| **Customize the UI** | [Frontend](docs/05-frontend.md) | - | +| **Debug an issue** | [Testing Guide](docs/07-testing-guide.md) | - | +| **Deploy to production** | [Production Notes](docs/08-production-notes.md) | - | +| **Use AI assistant** | [SKILLS.md](SKILLS.md) | Context for Claude Code, Gemini CLI, Cursor, Codex | ## Architecture @@ -44,39 +45,39 @@ Choose your path based on your goal: ### Key Files -| File | Purpose | -|------|---------| -| `business_agent/src/business_agent/agent.py` | ADK agent + 8 tools | -| `business_agent/src/business_agent/store.py` | Checkout state machine | -| `business_agent/src/business_agent/agent_executor.py` | A2A ↔ ADK bridge | -| `chat-client/App.tsx` | React app + A2A messaging | +| File | Purpose | +| ----------------------------------------------------- | ------------------------- | +| `business_agent/src/business_agent/agent.py` | ADK agent + 8 tools | +| `business_agent/src/business_agent/store.py` | Checkout state machine | +| `business_agent/src/business_agent/agent_executor.py` | A2A ↔ ADK bridge | +| `chat-client/App.tsx` | React app + A2A messaging | ### Endpoints -| Endpoint | Purpose | -|----------|---------| -| `GET /.well-known/agent-card.json` | A2A agent discovery | -| `GET /.well-known/ucp` | UCP merchant profile | -| `POST /` | A2A JSON-RPC endpoint | +| Endpoint | Purpose | +| ---------------------------------- | --------------------- | +| `GET /.well-known/agent-card.json` | A2A agent discovery | +| `GET /.well-known/ucp` | UCP merchant profile | +| `POST /` | A2A JSON-RPC endpoint | ### State Keys -| Key | Purpose | -|-----|---------| +| Key | Purpose | +| ------------------ | ------------------------ | | `user:checkout_id` | Current checkout session | -| `__ucp_metadata__` | Negotiated capabilities | -| `__payment_data__` | Payment instrument | +| `__ucp_metadata__` | Negotiated capabilities | +| `__payment_data__` | Payment instrument | ## Deep Dive Guides -| Guide | Topics | -|-------|--------| -| [Glossary](docs/00-glossary.md) | Key terms, acronyms, state keys | -| [Architecture](docs/01-architecture.md) | System components, data flow | -| [ADK Agent](docs/02-adk-agent.md) | Tools, callbacks, session management | -| [UCP Integration](docs/03-ucp-integration.md) | Capabilities, profiles, negotiation | -| [Commerce Flows](docs/04-commerce-flows.md) | Checkout lifecycle, payment | -| [Frontend](docs/05-frontend.md) | React components, A2A client | -| [Extending](docs/06-extending.md) | Add tools, products, capabilities | -| [Testing Guide](docs/07-testing-guide.md) | Testing, debugging, troubleshooting | -| [Production Notes](docs/08-production-notes.md) | Security gaps, deployment checklist | +| Guide | Topics | +| ----------------------------------------------- | ------------------------------------ | +| [Glossary](docs/00-glossary.md) | Key terms, acronyms, state keys | +| [Architecture](docs/01-architecture.md) | System components, data flow | +| [ADK Agent](docs/02-adk-agent.md) | Tools, callbacks, session management | +| [UCP Integration](docs/03-ucp-integration.md) | Capabilities, profiles, negotiation | +| [Commerce Flows](docs/04-commerce-flows.md) | Checkout lifecycle, payment | +| [Frontend](docs/05-frontend.md) | React components, A2A client | +| [Extending](docs/06-extending.md) | Add tools, products, capabilities | +| [Testing Guide](docs/07-testing-guide.md) | Testing, debugging, troubleshooting | +| [Production Notes](docs/08-production-notes.md) | Security gaps, deployment checklist | diff --git a/a2a/README.md b/a2a/README.md index 7e22ad1..b8cd431 100644 --- a/a2a/README.md +++ b/a2a/README.md @@ -54,6 +54,7 @@ The sample uses **[Google ADK](https://google.github.io/adk-docs/)** (Agent Deve

**Key points:** + - **Client** sends requests with `UCP-Agent` header containing its profile URL - **Cymbal Retail Agent** serves both `/.well-known/agent-card.json` (A2A) and `/.well-known/ucp` (UCP Profile) - **Capability Negotiation** happens before processing - agent and client agree on supported features @@ -86,12 +87,14 @@ uv run business_agent ``` **Expected output:** + ``` INFO: Started server process INFO: Uvicorn running on http://0.0.0.0:10999 ``` This starts the Cymbal Retail Agent on port 10999. You can verify by accessing: + - **Agent Card:** http://localhost:10999/.well-known/agent-card.json - **UCP Profile:** http://localhost:10999/.well-known/ucp @@ -127,6 +130,7 @@ npm run dev ``` **Expected output:** + ``` VITE v5.x.x ready ➜ Local: http://localhost:3000/ @@ -164,18 +168,19 @@ The Chat Client UCP Profile can be found at http://localhost:3000/profile/agent- Ready to understand how it works? -| Goal | Resource | -|------|----------| -| **Understand the architecture** | [Developer Guide](DEVELOPER_GUIDE.md) | -| **Deep dive into code** | [Architecture](docs/01-architecture.md) | -| **Extend the sample** | [Extending Guide](docs/06-extending.md) | -| **AI assistant context** | [SKILLS.md](SKILLS.md) - Context for Claude Code, Gemini CLI, Cursor, Codex | +| Goal | Resource | +| ------------------------------- | --------------------------------------------------------------------------- | +| **Understand the architecture** | [Developer Guide](DEVELOPER_GUIDE.md) | +| **Deep dive into code** | [Architecture](docs/01-architecture.md) | +| **Extend the sample** | [Extending Guide](docs/06-extending.md) | +| **AI assistant context** | [SKILLS.md](SKILLS.md) - Context for Claude Code, Gemini CLI, Cursor, Codex | ## What is UCP? **Universal Commerce Protocol (UCP)** is an open standard that enables interoperability between commerce platforms, merchants, and payment providers. It provides standardized data types for commerce transactions. This sample uses the following UCP capabilities: + - `dev.ucp.shopping.checkout` - Checkout session management with status lifecycle: `incomplete` → `ready_for_complete` → `completed` - `dev.ucp.shopping.fulfillment` - Shipping and delivery handling - `dev.ucp.shopping.discount` - Discount and promotional codes @@ -184,11 +189,11 @@ This sample uses the following UCP capabilities: ## Technology Stack -| Technology | Purpose | Used For | -|------------|---------|----------| -| **[Google ADK](https://google.github.io/adk-docs/)** | Agent Framework | AI agent with tools, Gemini LLM integration, session management | -| **[A2A Protocol](https://a2a-protocol.org/latest/)** | Communication | Agent discovery via Agent Card, JSON-RPC messaging, task management | -| **[UCP](https://ucp.dev)** | Commerce Standard | Standardized product, checkout, payment, and order data types | +| Technology | Purpose | Used For | +| ---------------------------------------------------- | ----------------- | ------------------------------------------------------------------- | +| **[Google ADK](https://google.github.io/adk-docs/)** | Agent Framework | AI agent with tools, Gemini LLM integration, session management | +| **[A2A Protocol](https://a2a-protocol.org/latest/)** | Communication | Agent discovery via Agent Card, JSON-RPC messaging, task management | +| **[UCP](https://ucp.dev)** | Commerce Standard | Standardized product, checkout, payment, and order data types | ## Related Resources diff --git a/a2a/SKILLS.md b/a2a/SKILLS.md index 04eba4e..05fbf17 100644 --- a/a2a/SKILLS.md +++ b/a2a/SKILLS.md @@ -21,14 +21,14 @@ AI-powered shopping agent built with Google ADK, demonstrating UCP commerce inte ## Tech Stack -| Layer | Technology | -|-------|------------| -| Agent Framework | [Google ADK](https://google.github.io/adk-docs/) (Agent Development Kit) | -| LLM | Gemini 3.0 Flash | -| Commerce Protocol | [UCP](https://ucp.dev/) (Universal Commerce Protocol) | -| Agent Protocol | [A2A](https://a2a-protocol.org/) (Agent-to-Agent) JSON-RPC 2.0 | -| Backend | Python 3.13, Uvicorn, Starlette, Pydantic | -| Frontend | React 19, TypeScript, Vite, Tailwind | +| Layer | Technology | +| ----------------- | ------------------------------------------------------------------------ | +| Agent Framework | [Google ADK](https://google.github.io/adk-docs/) (Agent Development Kit) | +| LLM | Gemini 3.0 Flash | +| Commerce Protocol | [UCP](https://ucp.dev/) (Universal Commerce Protocol) | +| Agent Protocol | [A2A](https://a2a-protocol.org/) (Agent-to-Agent) JSON-RPC 2.0 | +| Backend | Python 3.13, Uvicorn, Starlette, Pydantic | +| Frontend | React 19, TypeScript, Vite, Tailwind | ## Directory Structure @@ -58,12 +58,12 @@ a2a/ ## Core Concepts -| Term | Definition | -|------|------------| -| **A2A** | Agent-to-Agent Protocol - How agents discover and communicate | -| **UCP** | Universal Commerce Protocol - Standard commerce data types | -| **ADK** | Agent Development Kit - Google's framework for building agents | -| **Tool** | Python function the LLM can invoke (has `ToolContext` parameter) | +| Term | Definition | +| -------------- | ------------------------------------------------------------------ | +| **A2A** | Agent-to-Agent Protocol - How agents discover and communicate | +| **UCP** | Universal Commerce Protocol - Standard commerce data types | +| **ADK** | Agent Development Kit - Google's framework for building agents | +| **Tool** | Python function the LLM can invoke (has `ToolContext` parameter) | | **Capability** | Feature set the agent supports (e.g., `dev.ucp.shopping.checkout`) | ## State Keys (constants.py) @@ -92,16 +92,16 @@ UCP_AGENT_HEADER = "UCP-Agent" # HTTP header for client pr ## Agent Tools (agent.py) -| Tool | Purpose | Returns | -|------|---------|---------| -| `search_shopping_catalog(query)` | Search products by keyword | ProductResults | -| `add_to_checkout(product_id, quantity)` | Add item to checkout | Checkout | -| `remove_from_checkout(product_id)` | Remove item from checkout | Checkout | -| `update_checkout(product_id, quantity)` | Update item quantity | Checkout | -| `get_checkout()` | Get current checkout state | Checkout | -| `update_customer_details(email, address...)` | Set buyer and delivery info | Checkout | -| `start_payment()` | Validate checkout, set ready status | Checkout | -| `complete_checkout()` | Process payment, create order | Checkout + OrderConfirmation | +| Tool | Purpose | Returns | +| -------------------------------------------- | ----------------------------------- | ---------------------------- | +| `search_shopping_catalog(query)` | Search products by keyword | ProductResults | +| `add_to_checkout(product_id, quantity)` | Add item to checkout | Checkout | +| `remove_from_checkout(product_id)` | Remove item from checkout | Checkout | +| `update_checkout(product_id, quantity)` | Update item quantity | Checkout | +| `get_checkout()` | Get current checkout state | Checkout | +| `update_customer_details(email, address...)` | Set buyer and delivery info | Checkout | +| `start_payment()` | Validate checkout, set ready status | Checkout | +| `complete_checkout()` | Process payment, create order | Checkout + OrderConfirmation | ## Checkout State Machine @@ -111,11 +111,11 @@ incomplete → ready_for_complete → completed add item start_payment complete_checkout ``` -| State | Meaning | Transition | -|-------|---------|------------| -| `incomplete` | Missing buyer email or fulfillment address | Add required info | -| `ready_for_complete` | All info collected, awaiting payment | Call `complete_checkout()` | -| `completed` | Order created with OrderConfirmation | Terminal state | +| State | Meaning | Transition | +| -------------------- | ------------------------------------------ | -------------------------- | +| `incomplete` | Missing buyer email or fulfillment address | Add required info | +| `ready_for_complete` | All info collected, awaiting payment | Call `complete_checkout()` | +| `completed` | Order created with OrderConfirmation | Terminal state | ## UCP Capabilities @@ -129,6 +129,7 @@ dev.ucp.shopping.buyer_consent # Consent management (extends checkout) ## Common Tasks ### Add a New Tool + ```python # In agent.py def my_tool(tool_context: ToolContext, param: str) -> dict: @@ -155,19 +156,27 @@ root_agent = Agent(..., tools=[..., my_tool]) ``` ### Add a Product + Edit `data/products.json`: + ```json { "productID": "NEW-001", "name": "New Product", "image": ["http://localhost:10999/images/new.jpg"], - "brand": {"name": "Brand"}, - "offers": {"price": "9.99", "priceCurrency": "USD", "availability": "InStock"} + "brand": { "name": "Brand" }, + "offers": { + "price": "9.99", + "priceCurrency": "USD", + "availability": "InStock" + } } ``` ### Modify Checkout Flow + Key methods in `store.py`: + - `add_to_checkout()` - Creates checkout, adds items - `_recalculate_checkout()` - Updates totals, tax, shipping - `start_payment()` - Validates readiness, transitions state @@ -175,15 +184,15 @@ Key methods in `store.py`: ## Key Files for Changes -| Change | File | -|--------|------| -| Add/modify tools | `agent.py` | -| Checkout logic | `store.py` | -| A2A/ADK bridging | `agent_executor.py` | -| UCP profiles | `data/ucp.json`, `chat-client/profile/agent_profile.json` | -| Products | `data/products.json` | -| Frontend components | `chat-client/components/` | -| Frontend types | `chat-client/types.ts` | +| Change | File | +| ------------------- | --------------------------------------------------------- | +| Add/modify tools | `agent.py` | +| Checkout logic | `store.py` | +| A2A/ADK bridging | `agent_executor.py` | +| UCP profiles | `data/ucp.json`, `chat-client/profile/agent_profile.json` | +| Products | `data/products.json` | +| Frontend components | `chat-client/components/` | +| Frontend types | `chat-client/types.ts` | ## Commands @@ -216,43 +225,45 @@ return {"message": "Error description", "status": "error"} > **WARNING**: This sample is NOT production-ready. See `docs/08-production-notes.md`. -| Component | Current | Production | -|-----------|---------|------------| -| Session Storage | In-memory | Redis | -| Checkout Storage | Python dict | PostgreSQL | -| Authentication | None | JWT/API key | -| Secrets | Plaintext .env | Secret Manager | +| Component | Current | Production | +| ---------------- | -------------- | -------------- | +| Session Storage | In-memory | Redis | +| Checkout Storage | Python dict | PostgreSQL | +| Authentication | None | JWT/API key | +| Secrets | Plaintext .env | Secret Manager | ## Documentation -| Guide | Topics | -|-------|--------| -| [Glossary](docs/00-glossary.md) | Key terms, acronyms, external resources | -| [Architecture](docs/01-architecture.md) | System components, data flow, mock store | -| [ADK Agent](docs/02-adk-agent.md) | Tools, callbacks, prompt engineering | -| [UCP Integration](docs/03-ucp-integration.md) | Capabilities, profiles, negotiation | -| [Commerce Flows](docs/04-commerce-flows.md) | Checkout lifecycle, payment flow | -| [Frontend](docs/05-frontend.md) | React components, A2A client | -| [Extending](docs/06-extending.md) | Add tools, products, capabilities | -| [Testing Guide](docs/07-testing-guide.md) | Testing, debugging, troubleshooting | -| [Production Notes](docs/08-production-notes.md) | Security gaps, deployment checklist | +| Guide | Topics | +| ----------------------------------------------- | ---------------------------------------- | +| [Glossary](docs/00-glossary.md) | Key terms, acronyms, external resources | +| [Architecture](docs/01-architecture.md) | System components, data flow, mock store | +| [ADK Agent](docs/02-adk-agent.md) | Tools, callbacks, prompt engineering | +| [UCP Integration](docs/03-ucp-integration.md) | Capabilities, profiles, negotiation | +| [Commerce Flows](docs/04-commerce-flows.md) | Checkout lifecycle, payment flow | +| [Frontend](docs/05-frontend.md) | React components, A2A client | +| [Extending](docs/06-extending.md) | Add tools, products, capabilities | +| [Testing Guide](docs/07-testing-guide.md) | Testing, debugging, troubleshooting | +| [Production Notes](docs/08-production-notes.md) | Security gaps, deployment checklist | ## External Resources -| Resource | URL | -|----------|-----| -| **ADK Docs** | https://google.github.io/adk-docs/ | -| **A2A Protocol** | https://a2a-protocol.org/latest/ | +| Resource | URL | +| --------------------- | --------------------------------------- | +| **ADK Docs** | https://google.github.io/adk-docs/ | +| **A2A Protocol** | https://a2a-protocol.org/latest/ | | **UCP Specification** | https://ucp.dev/specification/overview/ | -| **Gemini API** | https://ai.google.dev/gemini-api/docs | +| **Gemini API** | https://ai.google.dev/gemini-api/docs | ## Dependencies **Backend** (pyproject.toml): + - `google-adk[a2a]>=1.22.0` - `ucp-sdk==0.1.0` - `pydantic>=2.12.3` **Frontend** (package.json): + - `react ^19.2.0` - `vite ^6.2.0` diff --git a/a2a/business_agent/README.md b/a2a/business_agent/README.md index 7a45684..54c32f5 100644 --- a/a2a/business_agent/README.md +++ b/a2a/business_agent/README.md @@ -30,4 +30,4 @@ Example agent implementing A2A Extension for UCP 2. Copy env.example to .env and update it with relevant Gemini API key. 3. Run `uv run business_agent` 4. This starts the Cymbal Retail Agent on port 10999. You can verify by accessing -the agent card at http://localhost:10999/.well-known/agent-card.json + the agent card at http://localhost:10999/.well-known/agent-card.json diff --git a/a2a/business_agent/env.example b/a2a/business_agent/env.example index fea08a8..8d82236 100644 --- a/a2a/business_agent/env.example +++ b/a2a/business_agent/env.example @@ -1 +1 @@ -GOOGLE_API_KEY= \ No newline at end of file +GOOGLE_API_KEY= diff --git a/a2a/business_agent/src/business_agent/a2a_extensions/base_extension.py b/a2a/business_agent/src/business_agent/a2a_extensions/base_extension.py index 76828c7..5ebc01a 100644 --- a/a2a/business_agent/src/business_agent/a2a_extensions/base_extension.py +++ b/a2a/business_agent/src/business_agent/a2a_extensions/base_extension.py @@ -19,14 +19,13 @@ from a2a.server.agent_execution import RequestContext from a2a.types import AgentCard, AgentExtension + class A2AExtensionBase(ABC): """Base class for A2A extensions.""" URI: str - def __init__( - self, description: str = "", params: dict[str, Any] | None = None - ): + def __init__(self, description: str = "", params: dict[str, Any] | None = None): """Initialize the extension base. Args: diff --git a/a2a/business_agent/src/business_agent/a2a_extensions/ucp_extension.py b/a2a/business_agent/src/business_agent/a2a_extensions/ucp_extension.py index c65f390..43c6a22 100644 --- a/a2a/business_agent/src/business_agent/a2a_extensions/ucp_extension.py +++ b/a2a/business_agent/src/business_agent/a2a_extensions/ucp_extension.py @@ -18,6 +18,7 @@ from ..constants import A2A_UCP_EXTENSION_URL from .base_extension import A2AExtensionBase + class UcpExtension(A2AExtensionBase): """UCP extension implementation.""" diff --git a/a2a/business_agent/src/business_agent/agent.py b/a2a/business_agent/src/business_agent/agent.py index 9f4f3b9..b619ae7 100644 --- a/a2a/business_agent/src/business_agent/agent.py +++ b/a2a/business_agent/src/business_agent/agent.py @@ -45,7 +45,7 @@ def _create_error_response(message: str) -> dict: - return {"message": message, "status": "error"} + return {"message": message, "status": "error"} def search_shopping_catalog(tool_context: ToolContext, query: str) -> dict: @@ -88,9 +88,7 @@ def add_to_checkout( ucp_metadata = tool_context.state.get(ADK_UCP_METADATA_STATE) if not ucp_metadata: - return _create_error_response( - "There was an error creating UCP metadata" - ) + return _create_error_response("There was an error creating UCP metadata") try: checkout = store.add_to_checkout( @@ -139,18 +137,14 @@ def remove_from_checkout(tool_context: ToolContext, product_id: str) -> dict: } except ValueError: logging.exception( - "There was an error removing item from checkout, " - "please retry later." + "There was an error removing item from checkout, please retry later." ) return _create_error_response( - "There was an error removing item from checkout, " - "please retry later." + "There was an error removing item from checkout, please retry later." ) -def update_checkout( - tool_context: ToolContext, product_id: str, quantity: int -) -> dict: +def update_checkout(tool_context: ToolContext, product_id: str, quantity: int) -> dict: """Update the quantity of a product in the checkout session. Args: @@ -169,9 +163,9 @@ def update_checkout( try: return { UCP_CHECKOUT_KEY: ( - store.update_checkout( - checkout_id, product_id, quantity - ).model_dump(mode="json") + store.update_checkout(checkout_id, product_id, quantity).model_dump( + mode="json" + ) ), "status": "success", } @@ -285,9 +279,7 @@ async def complete_checkout(tool_context: ToolContext) -> dict: checkout = store.get_checkout(checkout_id) if checkout is None: - return _create_error_response( - "Checkout not found for the current session." - ) + return _create_error_response("Checkout not found for the current session.") payment_data: dict[str, Any] = tool_context.state.get(ADK_PAYMENT_STATE) @@ -307,15 +299,11 @@ async def complete_checkout(tool_context: ToolContext) -> dict: ) if task is None: - return _create_error_response( - "Failed to receive a valid response from MPP" - ) + return _create_error_response("Failed to receive a valid response from MPP") if task.status is not None and task.status.state == TaskState.completed: payment_instrument = payment_data.get(UCP_PAYMENT_DATA_KEY) - checkout.payment.selected_instrument_id = ( - payment_instrument.root.id - ) + checkout.payment.selected_instrument_id = payment_instrument.root.id checkout.payment.instruments = [payment_instrument] response = store.place_order(checkout_id) @@ -332,8 +320,7 @@ async def complete_checkout(tool_context: ToolContext) -> dict: except Exception: logging.exception("There was an error completing the checkout.") return _create_error_response( - "Sorry, there was an error completing the checkout, " - "please try again." + "Sorry, there was an error completing the checkout, please try again." ) diff --git a/a2a/business_agent/src/business_agent/agent_executor.py b/a2a/business_agent/src/business_agent/agent_executor.py index 96abc16..2d10709 100644 --- a/a2a/business_agent/src/business_agent/agent_executor.py +++ b/a2a/business_agent/src/business_agent/agent_executor.py @@ -135,9 +135,7 @@ async def cancel( "Cancellation is not implemented for ADKAgentExecutor." ) - async def _get_or_create_session( - self, context: RequestContext, user_id: str - ): + async def _get_or_create_session(self, context: RequestContext, user_id: str): """Get an existing session or create a new one. Args: @@ -237,9 +235,7 @@ def _prepare_input( if key in data_part: value = data_part.pop(key) if key == UCP_PAYMENT_DATA_KEY: - payment_payload[key] = ( - PaymentInstrument.model_validate(value) - ) + payment_payload[key] = PaymentInstrument.model_validate(value) else: payment_payload[key] = value @@ -295,9 +291,7 @@ async def _run_agent_and_process_response( list[Part]: The response parts. """ - content = types.Content( - role="user", parts=[types.Part.from_text(text=query)] - ) + content = types.Content(role="user", parts=[types.Part.from_text(text=query)]) state_delta = self._build_initial_state_delta( context, ucp_metadata, payment_data diff --git a/a2a/business_agent/src/business_agent/data/agent_card.json b/a2a/business_agent/src/business_agent/data/agent_card.json index 4732431..08e2f73 100644 --- a/a2a/business_agent/src/business_agent/data/agent_card.json +++ b/a2a/business_agent/src/business_agent/data/agent_card.json @@ -33,16 +33,8 @@ ], "streaming": false }, - "defaultInputModes": [ - "text", - "text/plain", - "application/json" - ], - "defaultOutputModes": [ - "text", - "text/plain", - "application/json" - ], + "defaultInputModes": ["text", "text/plain", "application/json"], + "defaultOutputModes": ["text", "text/plain", "application/json"], "description": "SuperStore Merchant Agent", "name": "SuperStore Merchant Agent", "preferredTransport": "JSONRPC", @@ -54,16 +46,10 @@ "skills": [ { "description": "Helps with product search for given user criteria", - "examples": [ - "Help me find a shirt for my weekend trip" - ], + "examples": ["Help me find a shirt for my weekend trip"], "id": "product_search", "name": "Perform product search", - "tags": [ - "shopping", - "search", - "catalog search" - ] + "tags": ["shopping", "search", "catalog search"] }, { "description": "Adds Checkout functionality for the agent", @@ -74,11 +60,9 @@ ], "id": "checkout", "name": "Checkout", - "tags": [ - "checkout" - ] + "tags": ["checkout"] } ], "url": "http://localhost:10999", "version": "1.0.0" -} \ No newline at end of file +} diff --git a/a2a/business_agent/src/business_agent/data/products.json b/a2a/business_agent/src/business_agent/data/products.json index 880ad33..03e326b 100644 --- a/a2a/business_agent/src/business_agent/data/products.json +++ b/a2a/business_agent/src/business_agent/data/products.json @@ -4,9 +4,7 @@ "productID": "BISC-001", "name": "Chocochip Cookies", "sku": "COOKIES-001", - "image": [ - "http://localhost:10999/images/cookies.jpg" - ], + "image": ["http://localhost:10999/images/cookies.jpg"], "brand": { "@type": "Brand", "name": "CookieCo" @@ -25,15 +23,13 @@ "gtin": "9876543210125", "mpn": "CC-SB-001", "category": "Groceries > Snacks > Cookies & Biscuits" - }, + }, { "@type": "Product", "productID": "STRAW-001", "name": "Fresh Strawberries", "sku": "STRAW-001", - "image": [ - "http://localhost:10999/images/strawberries.jpg" - ], + "image": ["http://localhost:10999/images/strawberries.jpg"], "brand": { "@type": "Brand", "name": "FarmFresh" @@ -58,9 +54,7 @@ "productID": "CHIPS-001", "name": "Classic Potato Chips", "sku": "CHIPS-001", - "image": [ - "http://localhost:10999/images/chips.jpg" - ], + "image": ["http://localhost:10999/images/chips.jpg"], "brand": { "@type": "Brand", "name": "SaltySnacks" @@ -79,14 +73,13 @@ "gtin": "9876543210128", "mpn": "SS-PC-001", "category": "Groceries > Snacks > Chips & Crisps" - },{ + }, + { "@type": "Product", "productID": "SW-CHIPS-001", "name": "Baked Sweet Potato Chips", "sku": "SW-CHIPS-001", - "image": [ - "http://localhost:10999/images/chips.jpg" - ], + "image": ["http://localhost:10999/images/chips.jpg"], "brand": { "@type": "Brand", "name": "SaltySnacks" @@ -111,9 +104,7 @@ "productID": "O-COOKIES-001", "name": "Classic Oat Cookies", "sku": "O-COOKIES-001", - "image": [ - "http://localhost:10999/images/oat_cookies.jpg" - ], + "image": ["http://localhost:10999/images/oat_cookies.jpg"], "brand": { "@type": "Brand", "name": "CookieCo" @@ -138,9 +129,7 @@ "productID": "NUTRIBAR-001", "name": "Nutri-Bar", "sku": "NUTRIBAR-001", - "image": [ - "http://localhost:10999/images/nutribar.jpg" - ], + "image": ["http://localhost:10999/images/nutribar.jpg"], "brand": { "@type": "Brand", "name": "HealthEats" diff --git a/a2a/business_agent/src/business_agent/data/ucp.json b/a2a/business_agent/src/business_agent/data/ucp.json index 034ff6d..75e31d8 100644 --- a/a2a/business_agent/src/business_agent/data/ucp.json +++ b/a2a/business_agent/src/business_agent/data/ucp.json @@ -27,18 +27,20 @@ ] }, "payment": { - "handlers": [{ - "id": "example_payment_provider", - "name": "example.payment.provider", - "version": "2026-01-11", - "spec": "https://pay.provider.example/specs/handlers/payments", - "config_schema": "https://pay.provider.example/specs/handlers/config.json", - "instrument_schemas": [ - "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json" - ], - "config": { - "business_id": "1234567890" + "handlers": [ + { + "id": "example_payment_provider", + "name": "example.payment.provider", + "version": "2026-01-11", + "spec": "https://pay.provider.example/specs/handlers/payments", + "config_schema": "https://pay.provider.example/specs/handlers/config.json", + "instrument_schemas": [ + "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json" + ], + "config": { + "business_id": "1234567890" + } } - }] + ] } -} \ No newline at end of file +} diff --git a/a2a/business_agent/src/business_agent/main.py b/a2a/business_agent/src/business_agent/main.py index d1c95b6..39d0f95 100644 --- a/a2a/business_agent/src/business_agent/main.py +++ b/a2a/business_agent/src/business_agent/main.py @@ -123,4 +123,4 @@ async def run(host, port): if __name__ == "__main__": - run() + run() diff --git a/a2a/business_agent/src/business_agent/models/product_types.py b/a2a/business_agent/src/business_agent/models/product_types.py index 98d67f3..a09f8e0 100644 --- a/a2a/business_agent/src/business_agent/models/product_types.py +++ b/a2a/business_agent/src/business_agent/models/product_types.py @@ -42,18 +42,14 @@ class ImageObject(ProductDiscoveryModel): url: str caption: str | None = None - schema_type: Literal["ImageObject"] = Field( - default="ImageObject", alias="@type" - ) + schema_type: Literal["ImageObject"] = Field(default="ImageObject", alias="@type") class Organization(ProductDiscoveryModel): """Corresponds to schema.org/Organization.""" name: str - schema_type: Literal["Organization"] = Field( - default="Organization", alias="@type" - ) + schema_type: Literal["Organization"] = Field(default="Organization", alias="@type") class Brand(ProductDiscoveryModel): @@ -114,9 +110,9 @@ class BasePriceSpecification(ProductDiscoveryModel): name: str | None = None price: str price_currency: str = Field(alias="priceCurrency") - valid_for_member_tier: ( - MemberProgramTier | list[MemberProgramTier] | None - ) = Field(default=None, alias="validForMemberTier") + valid_for_member_tier: MemberProgramTier | list[MemberProgramTier] | None = Field( + default=None, alias="validForMemberTier" + ) membership_points_earned: str | None = Field( default=None, alias="membershipPointsEarned" ) @@ -217,9 +213,7 @@ class DefinedRegion(ProductDiscoveryModel): default="DefinedRegion", alias="@type" ) address_country: str | None = Field(default=None, alias="addressCountry") - address_region: list[str] | None = Field( - default=None, alias="addressRegion" - ) + address_region: list[str] | None = Field(default=None, alias="addressRegion") class ShippingQuantitativeValue(ProductDiscoveryModel): @@ -239,9 +233,7 @@ class ShippingDeliveryTime(ProductDiscoveryModel): schema_type: Literal["ShippingDeliveryTime"] = Field( default="ShippingDeliveryTime", alias="@type" ) - handling_time: ShippingQuantitativeValue | None = Field( - alias="handlingTime" - ) + handling_time: ShippingQuantitativeValue | None = Field(alias="handlingTime") transit_time: ShippingQuantitativeValue | None = Field(alias="transitTime") @@ -294,12 +286,8 @@ class MerchantReturnPolicy(ProductDiscoveryModel): alias="returnPolicyCategory" ) - merchant_return_days: int | None = Field( - default=None, alias="merchantReturnDays" - ) - return_fees: ReturnFeesEnumeration | None = Field( - default=None, alias="returnFees" - ) + merchant_return_days: int | None = Field(default=None, alias="merchantReturnDays") + return_fees: ReturnFeesEnumeration | None = Field(default=None, alias="returnFees") return_method: ReturnMethodEnumeration | None = Field(alias="returnMethod") return_shipping_fees_amount: MonetaryAmount | None = Field( default=None, alias="returnShippingFeesAmount" @@ -311,9 +299,7 @@ class Rating(ProductDiscoveryModel): schema_type: Literal["Rating"] = Field(default="Rating", alias="@type") rating_value: float = Field(alias="ratingValue") - rating_explanation: str | None = Field( - default=None, alias="ratingExplanation" - ) + rating_explanation: str | None = Field(default=None, alias="ratingExplanation") class Certification(ProductDiscoveryModel): @@ -340,13 +326,11 @@ class Offer(ProductDiscoveryModel): price_specification: ( UnitPriceSpecification | list[UnitPriceSpecification] | None ) = Field(default=None, alias="priceSpecification") - shipping_details: ( - OfferShippingDetails | list[OfferShippingDetails] | None - ) = Field(default=None, alias="shippingDetails") - availability: ItemAvailability | None = None - item_condition: ItemCondition | None = Field( - default=None, alias="itemCondition" + shipping_details: OfferShippingDetails | list[OfferShippingDetails] | None = Field( + default=None, alias="shippingDetails" ) + availability: ItemAvailability | None = None + item_condition: ItemCondition | None = Field(default=None, alias="itemCondition") has_merchant_return_policy: MerchantReturnPolicy | None = Field( default=None, alias="hasMerchantReturnPolicy" ) @@ -356,9 +340,7 @@ class Offer(ProductDiscoveryModel): class MediaObject(ProductDiscoveryModel): """Corresponds to schema.org/MediaObject.""" - schema_type: Literal["MediaObject"] = Field( - default="MediaObject", alias="@type" - ) + schema_type: Literal["MediaObject"] = Field(default="MediaObject", alias="@type") content_url: str | None = Field(default=None, alias="contentUrl") @@ -397,9 +379,7 @@ class Product(ProductDiscoveryModel): has_certification: Certification | list[Certification] | None = Field( default=None, alias="hasCertification" ) - subject_of: Model3D | list[Model3D] | None = Field( - default=None, alias="subjectOf" - ) + subject_of: Model3D | list[Model3D] | None = Field(default=None, alias="subjectOf") width: QuantitativeValue | None = None height: QuantitativeValue | None = None depth: QuantitativeValue | None = None @@ -416,9 +396,7 @@ class ProductGroup(ProductDiscoveryModel): product_group_id: str = Field(alias="productGroupID") image: str | list[str] | list[ImageObject] | None = None has_variant: list[Product] = Field(alias="hasVariant") - schema_type: Literal["ProductGroup"] = Field( - default="ProductGroup", alias="@type" - ) + schema_type: Literal["ProductGroup"] = Field(default="ProductGroup", alias="@type") context: Literal["https://schema.org/"] = Field( default="https://schema.org/", alias="@context" ) diff --git a/a2a/business_agent/src/business_agent/store.py b/a2a/business_agent/src/business_agent/store.py index 26fe800..962d3af 100644 --- a/a2a/business_agent/src/business_agent/store.py +++ b/a2a/business_agent/src/business_agent/store.py @@ -117,10 +117,7 @@ def search_products(self, query: str) -> ProductResults: for product in all_products: if product.product_id not in matching_products and ( keyword in product.name.lower() - or ( - product.category - and keyword in product.category.lower() - ) + or (product.category and keyword in product.category.lower()) ): matching_products[product.product_id] = product @@ -253,9 +250,7 @@ def get_checkout(self, checkout_id: str) -> Checkout | None: """ return self._checkouts.get(checkout_id) - def remove_from_checkout( - self, checkout_id: str, product_id: str - ) -> Checkout: + def remove_from_checkout(self, checkout_id: str, product_id: str) -> Checkout: """Remove a product from the checkout. Args: @@ -398,13 +393,9 @@ def _recalculate_checkout(self, checkout: Checkout) -> None: totals.append(Total(type="tax", display_text="Tax", amount=tax)) final_total += shipping + tax - totals.append( - Total(type="total", display_text="Total", amount=final_total) - ) + totals.append(Total(type="total", display_text="Total", amount=final_total)) checkout.totals = totals - checkout.continue_url = AnyUrl( - f"https://example.com/checkout?id={checkout.id}" - ) + checkout.continue_url = AnyUrl(f"https://example.com/checkout?id={checkout.id}") def add_delivery_address( self, checkout_id: str, address: PostalAddress @@ -426,9 +417,7 @@ def add_delivery_address( if isinstance(checkout, FulfillmentCheckout): dest_id = f"dest_{uuid4().hex[:8]}" destination = FulfillmentDestinationResponse( - root=ShippingDestinationResponse( - id=dest_id, **address.model_dump() - ) + root=ShippingDestinationResponse(id=dest_id, **address.model_dump()) ) fulfillment_options = self._get_fulfillment_options() @@ -481,10 +470,7 @@ def start_payment(self, checkout_id: str) -> Checkout | str: if checkout.buyer is None: messages.append("Provide a buyer email address") - if ( - isinstance(checkout, FulfillmentCheckout) - and checkout.fulfillment is None - ): + if isinstance(checkout, FulfillmentCheckout) and checkout.fulfillment is None: messages.append("Provide a fulfillment address") if messages: diff --git a/a2a/business_agent/src/business_agent/ucp_profile_resolver.py b/a2a/business_agent/src/business_agent/ucp_profile_resolver.py index 3a84c05..0681a28 100644 --- a/a2a/business_agent/src/business_agent/ucp_profile_resolver.py +++ b/a2a/business_agent/src/business_agent/ucp_profile_resolver.py @@ -86,9 +86,7 @@ def resolve_profile(self, client_profile_url: str) -> dict: merchant_version = self.merchant_profile.get("ucp").get("version") client_version = datetime.strptime(client_version, "%Y-%m-%d").date() - merchant_version = datetime.strptime( - merchant_version, "%Y-%m-%d" - ).date() + merchant_version = datetime.strptime(merchant_version, "%Y-%m-%d").date() if client_version > merchant_version: raise ServerError( diff --git a/a2a/chat-client/App.tsx b/a2a/chat-client/App.tsx index 0886e2c..c497344 100644 --- a/a2a/chat-client/App.tsx +++ b/a2a/chat-client/App.tsx @@ -13,18 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {useEffect, useRef, useState} from 'react'; -import ChatInput from './components/ChatInput'; -import ChatMessageComponent from './components/ChatMessage'; -import Header from './components/Header'; -import {appConfig} from './config'; -import {CredentialProviderProxy} from './mocks/credentialProviderProxy'; - -import {type ChatMessage, type PaymentInstrument, type Product, Sender, type Checkout, type PaymentHandler} from './types'; +import { useEffect, useRef, useState } from "react"; +import ChatInput from "./components/ChatInput"; +import ChatMessageComponent from "./components/ChatMessage"; +import Header from "./components/Header"; +import { appConfig } from "./config"; +import { CredentialProviderProxy } from "./mocks/credentialProviderProxy"; + +import { + type ChatMessage, + type PaymentInstrument, + type Product, + Sender, + type Checkout, + type PaymentHandler, +} from "./types"; type RequestPart = - | {type: 'text'; text: string} - | {type: 'data'; data: Record}; + | { type: "text"; text: string } + | { type: "data"; data: Record }; function createChatMessage( sender: Sender, @@ -42,7 +49,7 @@ function createChatMessage( const initialMessage: ChatMessage = createChatMessage( Sender.MODEL, appConfig.defaultMessage, - {id: 'initial'}, + { id: "initial" }, ); /** @@ -50,7 +57,9 @@ const initialMessage: ChatMessage = createChatMessage( * Only for demo purposes, not intended for production use. */ function App() { - const [user_email, _setUserEmail] = useState('foo@example.com'); + const [user_email, _setUserEmail] = useState( + "foo@example.com", + ); const [messages, setMessages] = useState([initialMessage]); const [isLoading, setIsLoading] = useState(false); const [contextId, setContextId] = useState(null); @@ -69,15 +78,15 @@ function App() { const handleAddToCheckout = (productToAdd: Product) => { const actionPayload = JSON.stringify({ - action: 'add_to_checkout', + action: "add_to_checkout", product_id: productToAdd.productID, quantity: 1, }); - handleSendMessage(actionPayload, {isUserAction: true}); + handleSendMessage(actionPayload, { isUserAction: true }); }; const handleStartPayment = () => { - const actionPayload = JSON.stringify({action: 'start_payment'}); + const actionPayload = JSON.stringify({ action: "start_payment" }); handleSendMessage(actionPayload, { isUserAction: true, }); @@ -95,7 +104,7 @@ function App() { //find the handler with id "example_payment_provider" const handler = checkout.payment.handlers.find( - (handler: PaymentHandler) => handler.id === 'example_payment_provider', + (handler: PaymentHandler) => handler.id === "example_payment_provider", ); if (!handler) { const errorMessage = createChatMessage( @@ -114,12 +123,12 @@ function App() { ); const paymentMethods = paymentResponse.payment_method_aliases; - const paymentSelectorMessage = createChatMessage(Sender.MODEL, '', { + const paymentSelectorMessage = createChatMessage(Sender.MODEL, "", { paymentMethods, }); setMessages((prev) => [...prev, paymentSelectorMessage]); } catch (error) { - console.error('Failed to resolve mandate:', error); + console.error("Failed to resolve mandate:", error); const errorMessage = createChatMessage( Sender.MODEL, "Sorry, I couldn't retrieve payment methods.", @@ -136,13 +145,13 @@ function App() { const userActionMessage = createChatMessage( Sender.USER, `User selected payment method: ${selectedMethod}`, - {isUserAction: true}, + { isUserAction: true }, ); setMessages((prev) => [...prev, userActionMessage]); try { if (!user_email) { - throw new Error('User email is not set.'); + throw new Error("User email is not set."); } const paymentInstrument = @@ -152,15 +161,15 @@ function App() { ); if (!paymentInstrument || !paymentInstrument.credential) { - throw new Error('Failed to retrieve payment credential'); + throw new Error("Failed to retrieve payment credential"); } - const paymentInstrumentMessage = createChatMessage(Sender.MODEL, '', { + const paymentInstrumentMessage = createChatMessage(Sender.MODEL, "", { paymentInstrument, }); setMessages((prev) => [...prev, paymentInstrumentMessage]); } catch (error) { - console.error('Failed to process payment mandate:', error); + console.error("Failed to process payment mandate:", error); const errorMessage = createChatMessage( Sender.MODEL, "Sorry, I couldn't process the payment. Please try again.", @@ -174,7 +183,7 @@ function App() { const userActionMessage = createChatMessage( Sender.USER, `User confirmed payment.`, - {isUserAction: true}, + { isUserAction: true }, ); // Let handleSendMessage manage the loading indicator setMessages((prev) => [ @@ -184,12 +193,12 @@ function App() { try { const parts: RequestPart[] = [ - {type: 'data', data: {'action': 'complete_checkout'}}, + { type: "data", data: { action: "complete_checkout" } }, { - type: 'data', + type: "data", data: { - 'a2a.ucp.checkout.payment_data': paymentInstrument, - 'a2a.ucp.checkout.risk_signals': {'data': 'some risk data'}, + "a2a.ucp.checkout.payment_data": paymentInstrument, + "a2a.ucp.checkout.risk_signals": { data: "some risk data" }, }, }, ]; @@ -198,10 +207,10 @@ function App() { isUserAction: true, }); } catch (error) { - console.error('Error confirming payment:', error); + console.error("Error confirming payment:", error); const errorMessage = createChatMessage( Sender.MODEL, - 'Sorry, there was an issue confirming your payment.', + "Sorry, there was an issue confirming your payment.", ); // If handleSendMessage wasn't called, we might need to manually update state // In this case, we remove the loading indicator that handleSendMessage would have added @@ -212,17 +221,17 @@ function App() { const handleSendMessage = async ( messageContent: string | RequestPart[], - options?: {isUserAction?: boolean; headers?: Record}, + options?: { isUserAction?: boolean; headers?: Record }, ) => { if (isLoading) return; const userMessage = createChatMessage( Sender.USER, options?.isUserAction - ? '' - : typeof messageContent === 'string' + ? "" + : typeof messageContent === "string" ? messageContent - : 'Sent complex data', + : "Sent complex data", ); if (userMessage.text) { // Only add if there's text @@ -230,14 +239,14 @@ function App() { } setMessages((prev) => [ ...prev, - createChatMessage(Sender.MODEL, '', {isLoading: true}), + createChatMessage(Sender.MODEL, "", { isLoading: true }), ]); setIsLoading(true); try { const requestParts = - typeof messageContent === 'string' - ? [{type: 'text', text: messageContent}] + typeof messageContent === "string" + ? [{ type: "text", text: messageContent }] : messageContent; const requestParams: { @@ -254,10 +263,10 @@ function App() { }; } = { message: { - role: 'user', + role: "user", parts: requestParts, messageId: crypto.randomUUID(), - kind: 'message', + kind: "message", }, configuration: { historyLength: 0, @@ -272,20 +281,20 @@ function App() { } const defaultHeaders = { - 'Content-Type': 'application/json', - 'X-A2A-Extensions': - 'https://ucp.dev/specification/reference?v=2026-01-11', - 'UCP-Agent': + "Content-Type": "application/json", + "X-A2A-Extensions": + "https://ucp.dev/specification/reference?v=2026-01-11", + "UCP-Agent": 'profile="http://localhost:3000/profile/agent_profile.json"', }; - const response = await fetch('/api', { - method: 'POST', - headers: {...defaultHeaders, ...options?.headers}, + const response = await fetch("/api", { + method: "POST", + headers: { ...defaultHeaders, ...options?.headers }, body: JSON.stringify({ - jsonrpc: '2.0', + jsonrpc: "2.0", id: crypto.randomUUID(), - method: 'message/send', + method: "message/send", params: requestParams, }), }); @@ -303,7 +312,7 @@ function App() { //if there is a task and it's in one of the active states if ( data.result?.id && - data.result?.status?.state in ['working', 'submitted', 'input-required'] + data.result?.status?.state in ["working", "submitted", "input-required"] ) { setTaskId(data.result.id); } else { @@ -311,7 +320,7 @@ function App() { setTaskId(undefined); } - const combinedBotMessage = createChatMessage(Sender.MODEL, ''); + const combinedBotMessage = createChatMessage(Sender.MODEL, ""); const responseParts = data.result?.parts || data.result?.status?.message?.parts || []; @@ -320,17 +329,17 @@ function App() { if (part.text) { // Simple text combinedBotMessage.text += - (combinedBotMessage.text ? '\n' : '') + part.text; - } else if (part.data?.['a2a.product_results']) { + (combinedBotMessage.text ? "\n" : "") + part.text; + } else if (part.data?.["a2a.product_results"]) { // Product results combinedBotMessage.text += - (combinedBotMessage.text ? '\n' : '') + - (part.data['a2a.product_results'].content || ''); + (combinedBotMessage.text ? "\n" : "") + + (part.data["a2a.product_results"].content || ""); combinedBotMessage.products = - part.data['a2a.product_results'].results; - } else if (part.data?.['a2a.ucp.checkout']) { + part.data["a2a.product_results"].results; + } else if (part.data?.["a2a.ucp.checkout"]) { // Checkout - combinedBotMessage.checkout = part.data['a2a.ucp.checkout']; + combinedBotMessage.checkout = part.data["a2a.ucp.checkout"]; } } @@ -354,10 +363,10 @@ function App() { ]); } } catch (error) { - console.error('Error sending message:', error); + console.error("Error sending message:", error); const errorMessage = createChatMessage( Sender.MODEL, - 'Sorry, something went wrong. Please try again.', + "Sorry, something went wrong. Please try again.", ); // Replace the placeholder with the error message setMessages((prev) => [...prev.slice(0, -1), errorMessage]); @@ -373,25 +382,27 @@ function App() {
+ className="flex-grow overflow-y-auto p-4 md:p-6 space-y-2" + > {messages.map((msg, index) => ( + isLastCheckout={index === lastCheckoutIndex} + > ))}
diff --git a/a2a/chat-client/README.md b/a2a/chat-client/README.md index a11c94f..af21d9a 100644 --- a/a2a/chat-client/README.md +++ b/a2a/chat-client/README.md @@ -16,7 +16,7 @@ ## Run Locally -**Prerequisites:** Node.js +**Prerequisites:** Node.js 1. Install dependencies: `npm install` diff --git a/a2a/chat-client/components/BotLogo.tsx b/a2a/chat-client/components/BotLogo.tsx index 3f018e0..4d3638c 100644 --- a/a2a/chat-client/components/BotLogo.tsx +++ b/a2a/chat-client/components/BotLogo.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type React from 'react'; +import type React from "react"; const BotLogo = (props: React.SVGProps) => ( ) => ( xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" - {...props}> + {...props} + > void; @@ -30,20 +30,21 @@ function SendIcon(props: React.SVGProps) { xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" - className="w-6 h-6"> + className="w-6 h-6" + > ); } -function ChatInput({onSendMessage, isLoading}: ChatInputProps) { - const [inputValue, setInputValue] = useState(''); +function ChatInput({ onSendMessage, isLoading }: ChatInputProps) { + const [inputValue, setInputValue] = useState(""); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); if (inputValue.trim() && !isLoading) { onSendMessage(inputValue.trim()); - setInputValue(''); + setInputValue(""); } }; @@ -51,7 +52,8 @@ function ChatInput({onSendMessage, isLoading}: ChatInputProps) {
+ className="flex items-center space-x-3 max-w-4xl mx-auto" + > + aria-label="Send message" + > diff --git a/a2a/chat-client/components/ChatMessage.tsx b/a2a/chat-client/components/ChatMessage.tsx index 8fdec6f..7a8500e 100644 --- a/a2a/chat-client/components/ChatMessage.tsx +++ b/a2a/chat-client/components/ChatMessage.tsx @@ -13,19 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {appConfig} from '@/config'; +import { appConfig } from "@/config"; import { type ChatMessage, type Checkout, type PaymentInstrument, type Product, Sender, -} from '../types'; -import CheckoutComponent from './Checkout'; -import PaymentConfirmationComponent from './PaymentConfirmation'; -import PaymentMethodSelector from './PaymentMethodSelector'; -import ProductCard from './ProductCard'; -import UserLogo from './UserLogo'; +} from "../types"; +import CheckoutComponent from "./Checkout"; +import PaymentConfirmationComponent from "./PaymentConfirmation"; +import PaymentMethodSelector from "./PaymentMethodSelector"; +import ProductCard from "./ProductCard"; +import UserLogo from "./UserLogo"; interface ChatMessageProps { message: ChatMessage; diff --git a/a2a/chat-client/components/Checkout.tsx b/a2a/chat-client/components/Checkout.tsx index 607c2ba..d9e760e 100644 --- a/a2a/chat-client/components/Checkout.tsx +++ b/a2a/chat-client/components/Checkout.tsx @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type React from 'react'; -import {useState} from 'react'; +import type React from "react"; +import { useState } from "react"; -import type {Checkout, CheckoutItem} from '../types'; +import type { Checkout, CheckoutItem } from "../types"; interface CheckoutProps { checkout: Checkout; @@ -35,7 +35,7 @@ const CheckoutComponent: React.FC = ({ : checkout.line_items.slice(0, 5); const formatCurrency = (amount: number, currency: string) => { - const currencySymbol = currency === 'EUR' ? '€' : '$'; + const currencySymbol = currency === "EUR" ? "€" : "$"; return `${currencySymbol}${(amount / 100).toFixed(2)}`; }; @@ -44,10 +44,10 @@ const CheckoutComponent: React.FC = ({ }; const getItemTotal = (lineItem: CheckoutItem) => { - return lineItem.totals.find((t) => t.type === 'total'); + return lineItem.totals.find((t) => t.type === "total"); }; - const grandTotal = getTotal('total'); + const grandTotal = getTotal("total"); return (
@@ -61,16 +61,17 @@ const CheckoutComponent: React.FC = ({ fill="none" viewBox="0 0 24 24" stroke="currentColor" - strokeWidth={2}> + strokeWidth={2} + > - {checkout.status === 'completed' - ? 'Order Confirmed' - : 'Checkout Summary'} + {checkout.status === "completed" + ? "Order Confirmed" + : "Checkout Summary"} {checkout.order?.id && (

@@ -105,20 +106,22 @@ const CheckoutComponent: React.FC = ({

)}
{checkout.totals - .filter((t) => t.type !== 'total' && t.amount > 0) + .filter((t) => t.type !== "total" && t.amount > 0) .map((total) => (
+ className="flex justify-between items-center" + > {total.display_text} {formatCurrency(total.amount, checkout.currency)} @@ -139,14 +142,15 @@ const CheckoutComponent: React.FC = ({

Checkout ID: {checkout.id}

- {checkout.status !== 'completed' && ( + {checkout.status !== "completed" && (
{checkout.continue_url && ( + className="bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded text-sm" + > Go to Checkout )} @@ -154,7 +158,8 @@ const CheckoutComponent: React.FC = ({ )} @@ -162,7 +167,8 @@ const CheckoutComponent: React.FC = ({ )} @@ -171,7 +177,8 @@ const CheckoutComponent: React.FC = ({ {checkout.order?.permalink_url && ( + className="block mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-center" + > View Order )} diff --git a/a2a/chat-client/components/Header.tsx b/a2a/chat-client/components/Header.tsx index 8d5c8a9..820b864 100644 --- a/a2a/chat-client/components/Header.tsx +++ b/a2a/chat-client/components/Header.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {appConfig} from '@/config'; +import { appConfig } from "@/config"; function Header() { return ( diff --git a/a2a/chat-client/components/PaymentConfirmation.tsx b/a2a/chat-client/components/PaymentConfirmation.tsx index 592c6a9..6716dc1 100644 --- a/a2a/chat-client/components/PaymentConfirmation.tsx +++ b/a2a/chat-client/components/PaymentConfirmation.tsx @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type React from 'react'; -import {useState} from 'react'; -import type {PaymentInstrument} from '../types'; +import type React from "react"; +import { useState } from "react"; +import type { PaymentInstrument } from "../types"; interface PaymentConfirmationProps { paymentInstrument: PaymentInstrument; @@ -42,7 +42,7 @@ const PaymentConfirmationComponent: React.FC = ({
Selected Payment Method - {paymentInstrument.brand.toUpperCase()} ending in{' '} + {paymentInstrument.brand.toUpperCase()} ending in{" "} {paymentInstrument.last_digits}
@@ -53,7 +53,8 @@ const PaymentConfirmationComponent: React.FC = ({ type="button" onClick={handleConfirmClick} disabled={isConfirming} - className="flex justify-center items-center w-full text-center bg-green-500 text-white py-2 rounded-md hover:bg-green-600 transition-colors disabled:bg-green-400 disabled:cursor-wait"> + className="flex justify-center items-center w-full text-center bg-green-500 text-white py-2 rounded-md hover:bg-green-600 transition-colors disabled:bg-green-400 disabled:cursor-wait" + > {isConfirming ? ( <> = ({ className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" - viewBox="0 0 24 24"> + viewBox="0 0 24 24" + > + strokeWidth="4" + > + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" + > Processing... ) : ( - 'Confirm Purchase' + "Confirm Purchase" )}
diff --git a/a2a/chat-client/components/PaymentMethodSelector.tsx b/a2a/chat-client/components/PaymentMethodSelector.tsx index 4b2fb56..42b45e0 100644 --- a/a2a/chat-client/components/PaymentMethodSelector.tsx +++ b/a2a/chat-client/components/PaymentMethodSelector.tsx @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type React from 'react'; -import {useState} from 'react'; -import type {PaymentMethod} from '../types'; +import type React from "react"; +import { useState } from "react"; +import type { PaymentMethod } from "../types"; interface PaymentMethodSelectorProps { paymentMethods: PaymentMethod[]; @@ -43,7 +43,8 @@ const PaymentMethodSelector: React.FC = ({ {paymentMethods.map((method) => (
diff --git a/a2a/chat-client/components/ProductCard.tsx b/a2a/chat-client/components/ProductCard.tsx index ae8c23f..ffd12e0 100644 --- a/a2a/chat-client/components/ProductCard.tsx +++ b/a2a/chat-client/components/ProductCard.tsx @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type React from 'react'; -import type {Product} from '../types'; +import type React from "react"; +import type { Product } from "../types"; interface ProductCardProps { product: Product; onAddToCart: (product: Product) => void; } -const ProductCard: React.FC = ({product, onAddToCart}) => { - const isAvailable = product.offers.availability.includes('InStock'); +const ProductCard: React.FC = ({ product, onAddToCart }) => { + const isAvailable = product.offers.availability.includes("InStock"); const handleAddToCartClick = () => onAddToCart?.(product); return ( @@ -35,25 +35,28 @@ const ProductCard: React.FC = ({product, onAddToCart}) => {

+ title={product.name} + > {product.name}

{product.brand.name}

- {product.offers.priceCurrency === 'EUR' ? '€' : '$'} + {product.offers.priceCurrency === "EUR" ? "€" : "$"} {product.offers.price}

- {isAvailable ? 'In Stock' : 'Out of Stock'} + className={`px-2 py-1 text-xs font-semibold rounded-full ${isAvailable ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}`} + > + {isAvailable ? "In Stock" : "Out of Stock"}
diff --git a/a2a/chat-client/components/UserLogo.tsx b/a2a/chat-client/components/UserLogo.tsx index eb5a4a5..4812c82 100644 --- a/a2a/chat-client/components/UserLogo.tsx +++ b/a2a/chat-client/components/UserLogo.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type React from 'react'; +import type React from "react"; const UserLogo = (props: React.SVGProps) => ( ) => ( xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" - {...props}> + {...props} + > - + Agent - - - + + +
- - + + diff --git a/a2a/chat-client/index.tsx b/a2a/chat-client/index.tsx index 27455e1..2faacbd 100644 --- a/a2a/chat-client/index.tsx +++ b/a2a/chat-client/index.tsx @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; -const rootElement = document.getElementById('root'); +const rootElement = document.getElementById("root"); if (!rootElement) { - throw new Error('Could not find root element to mount to'); + throw new Error("Could not find root element to mount to"); } const root = ReactDOM.createRoot(rootElement); diff --git a/a2a/chat-client/metadata.json b/a2a/chat-client/metadata.json index 6c89521..8d7044a 100644 --- a/a2a/chat-client/metadata.json +++ b/a2a/chat-client/metadata.json @@ -2,4 +2,4 @@ "name": "Simple Chat", "description": "A simple chat application", "requestFramePermissions": [] -} \ No newline at end of file +} diff --git a/a2a/chat-client/mocks/credentialProviderProxy.ts b/a2a/chat-client/mocks/credentialProviderProxy.ts index 58cbc7e..1e85a44 100644 --- a/a2a/chat-client/mocks/credentialProviderProxy.ts +++ b/a2a/chat-client/mocks/credentialProviderProxy.ts @@ -13,42 +13,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type {PaymentInstrument, PaymentMethod} from '../types'; +import type { PaymentInstrument, PaymentMethod } from "../types"; /** * A mock CredentialProvider to simulate calls to a remote service for credentials. * In a real application, this would make a network request to a provider's service. */ export class CredentialProviderProxy { - handler_id = 'example_payment_provider'; - handler_name = 'example.payment.provider'; + handler_id = "example_payment_provider"; + handler_name = "example.payment.provider"; - _getMockPaymentMethods(): {payment_method_aliases: PaymentMethod[]} { + _getMockPaymentMethods(): { payment_method_aliases: PaymentMethod[] } { return { - 'payment_method_aliases': [ + payment_method_aliases: [ { - 'id': 'instr_1', - 'type': 'card', - 'brand': 'amex', - 'last_digits': '1111', - 'expiry_month': 12, - 'expiry_year': 2026, + id: "instr_1", + type: "card", + brand: "amex", + last_digits: "1111", + expiry_month: 12, + expiry_year: 2026, }, { - 'id': 'instr_2', - 'type': 'card', - 'brand': 'visa', - 'last_digits': '8888', - 'expiry_month': 12, - 'expiry_year': 2026, + id: "instr_2", + type: "card", + brand: "visa", + last_digits: "8888", + expiry_month: 12, + expiry_year: 2026, }, { - 'id': 'instr_3', - 'type': 'card', - 'brand': 'mastercard', - 'last_digits': '5555', - 'expiry_month': 12, - 'expiry_year': 2026, + id: "instr_3", + type: "card", + brand: "mastercard", + last_digits: "5555", + expiry_month: 12, + expiry_year: 2026, }, ], }; @@ -57,12 +57,12 @@ export class CredentialProviderProxy { * Simulates fetching supported payment methods based on the cart mandate. * @param config The payment handler config defined by the merchant. * @returns A promise that resolves to a mock payment methods response. - */ + */ async getSupportedPaymentMethods( user_email: string, // biome-ignore lint/suspicious/noExplicitAny: no specific type for config config: any, - ): Promise<{payment_method_aliases: PaymentMethod[]}> { + ): Promise<{ payment_method_aliases: PaymentMethod[] }> { console.log( `CredentialProviderProxy: Simulating fetch for ${user_email} supported payment methods with config:`, config, @@ -102,8 +102,8 @@ export class CredentialProviderProxy { handler_id: this.handler_id, handler_name: this.handler_name, credential: { - type: 'token', - token: `mock_token_${randomId}` + type: "token", + token: `mock_token_${randomId}`, }, }; } diff --git a/a2a/chat-client/profile/agent_profile.json b/a2a/chat-client/profile/agent_profile.json index afd81b6..18cef0f 100644 --- a/a2a/chat-client/profile/agent_profile.json +++ b/a2a/chat-client/profile/agent_profile.json @@ -1,7 +1,7 @@ { "ucp": { "version": "2026-01-11", - "capabilities": [ + "capabilities": [ { "name": "dev.ucp.shopping.checkout", "version": "2026-01-11", @@ -30,15 +30,17 @@ ] }, "payment": { - "handlers": [{ - "id": "example_payment_provider", - "name": "example.payment.provider", - "version": "2026-01-11", - "spec": "https://pay.provider.example/specs/handlers/payments", - "config_schema": "https://pay.provider.example/specs/handlers/config.json", - "instrument_schemas": [ - "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json" - ] - }] + "handlers": [ + { + "id": "example_payment_provider", + "name": "example.payment.provider", + "version": "2026-01-11", + "spec": "https://pay.provider.example/specs/handlers/payments", + "config_schema": "https://pay.provider.example/specs/handlers/config.json", + "instrument_schemas": [ + "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json" + ] + } + ] } -} \ No newline at end of file +} diff --git a/a2a/chat-client/tsconfig.json b/a2a/chat-client/tsconfig.json index 2c6eed5..f9d60ed 100644 --- a/a2a/chat-client/tsconfig.json +++ b/a2a/chat-client/tsconfig.json @@ -4,26 +4,18 @@ "experimentalDecorators": true, "useDefineForClassFields": false, "module": "ESNext", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "skipLibCheck": true, - "types": [ - "node" - ], + "types": ["node"], "moduleResolution": "bundler", "isolatedModules": true, "moduleDetection": "force", "allowJs": true, "jsx": "react-jsx", "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] }, "allowImportingTsExtensions": true, "noEmit": true } -} \ No newline at end of file +} diff --git a/a2a/chat-client/types.ts b/a2a/chat-client/types.ts index 4170391..a87bac3 100644 --- a/a2a/chat-client/types.ts +++ b/a2a/chat-client/types.ts @@ -14,15 +14,15 @@ * limitations under the License. */ export enum Sender { - USER = 'user', - MODEL = 'model', + USER = "user", + MODEL = "model", } export interface Product { productID: string; name: string; image: string[]; - brand: {name: string}; + brand: { name: string }; offers: { price: string; priceCurrency: string; @@ -67,7 +67,6 @@ export interface ChatMessage { paymentInstrument?: PaymentInstrument; } - export interface CheckoutTotal { type: string; display_text: string; diff --git a/a2a/chat-client/vite.config.ts b/a2a/chat-client/vite.config.ts index 6cdf726..d6b18ee 100644 --- a/a2a/chat-client/vite.config.ts +++ b/a2a/chat-client/vite.config.ts @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import react from '@vitejs/plugin-react'; -import path from 'node:path'; -import {defineConfig} from 'vite'; +import react from "@vitejs/plugin-react"; +import path from "node:path"; +import { defineConfig } from "vite"; -export default defineConfig(() => { +export default defineConfig(() => { return { server: { port: 3000, - host: '0.0.0.0', + host: "0.0.0.0", proxy: { - '/api': { - target: 'http://localhost:10999', + "/api": { + target: "http://localhost:10999", changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), + rewrite: (path) => path.replace(/^\/api/, ""), secure: false, }, }, @@ -35,7 +35,7 @@ export default defineConfig(() => { define: {}, resolve: { alias: { - '@': path.resolve(__dirname, '.'), + "@": path.resolve(__dirname, "."), }, }, }; diff --git a/a2a/docs/00-glossary.md b/a2a/docs/00-glossary.md index 4d8806d..7563930 100644 --- a/a2a/docs/00-glossary.md +++ b/a2a/docs/00-glossary.md @@ -12,26 +12,26 @@ Quick reference for key terms used throughout this documentation. ## Core Concepts -| Term | Definition | Example in This Sample | -|------|------------|------------------------| -| **A2A** | Agent-to-Agent Protocol - How AI agents discover and communicate with each other | `/.well-known/agent-card.json` endpoint | -| **UCP** | Universal Commerce Protocol - Standard data types for commerce transactions | `Checkout`, `LineItem`, `PaymentInstrument` | -| **ADK** | Agent Development Kit - Google's framework for building agents with tools | `Agent()`, `ToolContext`, `Runner` | -| **Agent** | In this sample: the Cymbal Retail Agent service (includes LLM + tools + state) | The backend running on port 10999 | -| **Tool** | A Python function the LLM can invoke to perform actions | `search_shopping_catalog()`, `add_to_checkout()` | -| **Capability** | A feature set the agent supports, declared in UCP profile | `dev.ucp.shopping.checkout` | -| **Negotiation** | Client and merchant agreeing on shared capabilities before transacting | Happens when first message is sent | +| Term | Definition | Example in This Sample | +| --------------- | -------------------------------------------------------------------------------- | ------------------------------------------------ | +| **A2A** | Agent-to-Agent Protocol - How AI agents discover and communicate with each other | `/.well-known/agent-card.json` endpoint | +| **UCP** | Universal Commerce Protocol - Standard data types for commerce transactions | `Checkout`, `LineItem`, `PaymentInstrument` | +| **ADK** | Agent Development Kit - Google's framework for building agents with tools | `Agent()`, `ToolContext`, `Runner` | +| **Agent** | In this sample: the Cymbal Retail Agent service (includes LLM + tools + state) | The backend running on port 10999 | +| **Tool** | A Python function the LLM can invoke to perform actions | `search_shopping_catalog()`, `add_to_checkout()` | +| **Capability** | A feature set the agent supports, declared in UCP profile | `dev.ucp.shopping.checkout` | +| **Negotiation** | Client and merchant agreeing on shared capabilities before transacting | Happens when first message is sent | --- ## Protocol Terms -| Term | What It Does | Where to Find It | -|------|--------------|------------------| -| **Agent Card** | JSON file declaring agent identity and capabilities | `/.well-known/agent-card.json` | -| **UCP Profile** | JSON file declaring commerce capabilities and payment handlers | `/.well-known/ucp` | -| **JSON-RPC 2.0** | Message format used for A2A communication | Request/response structure in A2A calls | -| **UCP-Agent Header** | HTTP header containing client's profile URL | Sent with every A2A request | +| Term | What It Does | Where to Find It | +| -------------------- | -------------------------------------------------------------- | --------------------------------------- | +| **Agent Card** | JSON file declaring agent identity and capabilities | `/.well-known/agent-card.json` | +| **UCP Profile** | JSON file declaring commerce capabilities and payment handlers | `/.well-known/ucp` | +| **JSON-RPC 2.0** | Message format used for A2A communication | Request/response structure in A2A calls | +| **UCP-Agent Header** | HTTP header containing client's profile URL | Sent with every A2A request | --- @@ -39,15 +39,16 @@ Quick reference for key terms used throughout this documentation. State is stored in ADK's session service (in-memory by default). -| Key | Purpose | Lifetime | -|-----|---------|----------| -| `user:checkout_id` | Current checkout session ID | Until checkout completed or session expires | -| `__ucp_metadata__` | Negotiated capabilities from client/merchant profiles | Set once per session | -| `__payment_data__` | Payment instrument for current checkout | Set during payment flow | -| `__session_extensions__` | Active A2A extensions for this session | Set once per session | -| `temp:LATEST_TOOL_RESULT` | Temporary storage for last UCP tool response | Cleared after each agent response | +| Key | Purpose | Lifetime | +| ------------------------- | ----------------------------------------------------- | ------------------------------------------- | +| `user:checkout_id` | Current checkout session ID | Until checkout completed or session expires | +| `__ucp_metadata__` | Negotiated capabilities from client/merchant profiles | Set once per session | +| `__payment_data__` | Payment instrument for current checkout | Set during payment flow | +| `__session_extensions__` | Active A2A extensions for this session | Set once per session | +| `temp:LATEST_TOOL_RESULT` | Temporary storage for last UCP tool response | Cleared after each agent response | **Naming conventions**: + - `user:` prefix — User-scoped data (persists across turns) - `__` prefix — System/internal data (managed by framework) - `temp:` prefix — Temporary data (cleared after use) @@ -63,24 +64,24 @@ The checkout follows a 3-state lifecycle:

Figure 1: Checkout state transitions from incomplete → ready_for_complete → completed

-| State | Meaning | What's Needed to Progress | -|-------|---------|---------------------------| -| `incomplete` | Missing required info | Add email, address, or items | -| `ready_for_complete` | Ready for payment | User confirms payment | -| `completed` | Order placed successfully | Terminal state - checkout finalized | +| State | Meaning | What's Needed to Progress | +| -------------------- | ------------------------- | ----------------------------------- | +| `incomplete` | Missing required info | Add email, address, or items | +| `ready_for_complete` | Ready for payment | User confirms payment | +| `completed` | Order placed successfully | Terminal state - checkout finalized | --- ## ADK Components -| Component | Role | File | -|-----------|------|------| -| **Agent** | Orchestrates LLM and tools | `agent.py` | -| **Tool** | Individual function the LLM can call | Defined in `agent.py` | -| **ToolContext** | Provides state access to tools | Passed to each tool function | -| **Runner** | Executes agent with session management | `InMemoryRunner` | -| **Session** | Stores conversation history and state | `InMemorySessionService` | -| **Callback** | Hook to modify tool/agent output | `after_tool_callback`, `after_agent_callback` | +| Component | Role | File | +| --------------- | -------------------------------------- | --------------------------------------------- | +| **Agent** | Orchestrates LLM and tools | `agent.py` | +| **Tool** | Individual function the LLM can call | Defined in `agent.py` | +| **ToolContext** | Provides state access to tools | Passed to each tool function | +| **Runner** | Executes agent with session management | `InMemoryRunner` | +| **Session** | Stores conversation history and state | `InMemorySessionService` | +| **Callback** | Hook to modify tool/agent output | `after_tool_callback`, `after_agent_callback` | --- @@ -95,13 +96,13 @@ The checkout follows a 3-state lifecycle: ## Common Acronyms -| Acronym | Full Name | Context | -|---------|-----------|---------| -| A2A | Agent-to-Agent | Protocol for agent communication | -| UCP | Universal Commerce Protocol | Commerce data standard | -| ADK | Agent Development Kit | Google's agent framework | -| LLM | Large Language Model | Gemini 3.0 Flash in this sample | -| SDK | Software Development Kit | UCP Python SDK | +| Acronym | Full Name | Context | +| ------- | --------------------------- | -------------------------------- | +| A2A | Agent-to-Agent | Protocol for agent communication | +| UCP | Universal Commerce Protocol | Commerce data standard | +| ADK | Agent Development Kit | Google's agent framework | +| LLM | Large Language Model | Gemini 3.0 Flash in this sample | +| SDK | Software Development Kit | UCP Python SDK | --- @@ -111,41 +112,41 @@ Official documentation for the core technologies used in this sample. ### ADK (Agent Development Kit) -| Resource | URL | -|----------|-----| -| **Official Docs** | [google.github.io/adk-docs](https://google.github.io/adk-docs/) | -| **Getting Started** | [ADK Get Started Guide](https://google.github.io/adk-docs/get-started/) | -| **Agents Guide** | [Building Agents](https://google.github.io/adk-docs/agents/) | -| **GitHub (Python SDK)** | [github.com/google/adk-python](https://github.com/google/adk-python) | -| **Google Cloud Docs** | [Vertex AI Agent Builder](https://docs.cloud.google.com/agent-builder/agent-development-kit/overview) | +| Resource | URL | +| ----------------------- | ----------------------------------------------------------------------------------------------------- | +| **Official Docs** | [google.github.io/adk-docs](https://google.github.io/adk-docs/) | +| **Getting Started** | [ADK Get Started Guide](https://google.github.io/adk-docs/get-started/) | +| **Agents Guide** | [Building Agents](https://google.github.io/adk-docs/agents/) | +| **GitHub (Python SDK)** | [github.com/google/adk-python](https://github.com/google/adk-python) | +| **Google Cloud Docs** | [Vertex AI Agent Builder](https://docs.cloud.google.com/agent-builder/agent-development-kit/overview) | ### A2A (Agent-to-Agent Protocol) -| Resource | URL | -|----------|-----| -| **Official Protocol Site** | [a2a-protocol.org](https://a2a-protocol.org/latest/) | -| **Specification** | [A2A Specification](https://a2a-protocol.org/latest/specification/) | -| **ADK Integration** | [ADK with A2A](https://google.github.io/adk-docs/a2a/) | -| **GitHub Repository** | [github.com/a2aproject/A2A](https://github.com/a2aproject/A2A) | -| **Google Cloud Docs** | [A2A Agents on Cloud Run](https://docs.cloud.google.com/run/docs/ai/a2a-agents) | +| Resource | URL | +| -------------------------- | ------------------------------------------------------------------------------- | +| **Official Protocol Site** | [a2a-protocol.org](https://a2a-protocol.org/latest/) | +| **Specification** | [A2A Specification](https://a2a-protocol.org/latest/specification/) | +| **ADK Integration** | [ADK with A2A](https://google.github.io/adk-docs/a2a/) | +| **GitHub Repository** | [github.com/a2aproject/A2A](https://github.com/a2aproject/A2A) | +| **Google Cloud Docs** | [A2A Agents on Cloud Run](https://docs.cloud.google.com/run/docs/ai/a2a-agents) | ### UCP (Universal Commerce Protocol) -| Resource | URL | -|----------|-----| -| **Official Site** | [ucp.dev](https://ucp.dev/) | -| **Specification Overview** | [UCP Specification](https://ucp.dev/specification/overview/) | -| **Developer Guide** | [Google Merchant UCP Guide](https://developers.google.com/merchant/ucp) | -| **GitHub Repository** | [github.com/Universal-Commerce-Protocol/ucp](https://github.com/Universal-Commerce-Protocol/ucp) | -| **Python SDK** | [github.com/Universal-Commerce-Protocol/python-sdk](https://github.com/Universal-Commerce-Protocol/python-sdk) | +| Resource | URL | +| -------------------------- | -------------------------------------------------------------------------------------------------------------- | +| **Official Site** | [ucp.dev](https://ucp.dev/) | +| **Specification Overview** | [UCP Specification](https://ucp.dev/specification/overview/) | +| **Developer Guide** | [Google Merchant UCP Guide](https://developers.google.com/merchant/ucp) | +| **GitHub Repository** | [github.com/Universal-Commerce-Protocol/ucp](https://github.com/Universal-Commerce-Protocol/ucp) | +| **Python SDK** | [github.com/Universal-Commerce-Protocol/python-sdk](https://github.com/Universal-Commerce-Protocol/python-sdk) | ### Related Technologies -| Technology | Documentation | -|------------|---------------| -| **Gemini API** | [ai.google.dev/gemini-api/docs](https://ai.google.dev/gemini-api/docs) | -| **MCP (Model Context Protocol)** | [modelcontextprotocol.io](https://modelcontextprotocol.io/) | -| **JSON-RPC 2.0** | [jsonrpc.org/specification](https://www.jsonrpc.org/specification) | +| Technology | Documentation | +| -------------------------------- | ---------------------------------------------------------------------- | +| **Gemini API** | [ai.google.dev/gemini-api/docs](https://ai.google.dev/gemini-api/docs) | +| **MCP (Model Context Protocol)** | [modelcontextprotocol.io](https://modelcontextprotocol.io/) | +| **JSON-RPC 2.0** | [jsonrpc.org/specification](https://www.jsonrpc.org/specification) | --- diff --git a/a2a/docs/01-architecture.md b/a2a/docs/01-architecture.md index 29c9757..51ec604 100644 --- a/a2a/docs/01-architecture.md +++ b/a2a/docs/01-architecture.md @@ -24,24 +24,24 @@ The architecture follows a clean separation of concerns: ### Backend -| Component | File | Responsibility | -|-----------|------|----------------| -| A2A Server | `main.py` | HTTP server, routing, static files | -| Agent Executor | `agent_executor.py` | Bridge A2A ↔ ADK, session management | -| Profile Resolver | `ucp_profile_resolver.py` | UCP capability negotiation | -| ADK Agent | `agent.py` | LLM reasoning, tool execution | -| Retail Store | `store.py` | Products, checkouts, orders | -| Payment Processor | `payment_processor.py` | Mock payment handling | +| Component | File | Responsibility | +| ----------------- | ------------------------- | ------------------------------------ | +| A2A Server | `main.py` | HTTP server, routing, static files | +| Agent Executor | `agent_executor.py` | Bridge A2A ↔ ADK, session management | +| Profile Resolver | `ucp_profile_resolver.py` | UCP capability negotiation | +| ADK Agent | `agent.py` | LLM reasoning, tool execution | +| Retail Store | `store.py` | Products, checkouts, orders | +| Payment Processor | `payment_processor.py` | Mock payment handling | ### Frontend -| Component | File | Responsibility | -|-----------|------|----------------| -| App | `App.tsx` | State management, A2A messaging | -| ChatMessage | `components/ChatMessage.tsx` | Message rendering | -| Checkout | `components/Checkout.tsx` | Checkout display | -| ProductCard | `components/ProductCard.tsx` | Product cards | -| PaymentMethodSelector | `components/PaymentMethodSelector.tsx` | Payment selection | +| Component | File | Responsibility | +| --------------------- | -------------------------------------- | ------------------------------- | +| App | `App.tsx` | State management, A2A messaging | +| ChatMessage | `components/ChatMessage.tsx` | Message rendering | +| Checkout | `components/Checkout.tsx` | Checkout display | +| ProductCard | `components/ProductCard.tsx` | Product cards | +| PaymentMethodSelector | `components/PaymentMethodSelector.tsx` | Payment selection | ## Request Flow @@ -61,12 +61,12 @@ The architecture follows a clean separation of concerns: ## Layer Responsibilities -| Layer | Input | Output | Key Class | -|-------|-------|--------|-----------| -| **A2A Server** | HTTP request | HTTP response | `A2AStarletteApplication` | -| **Agent Executor** | A2A context | Event queue | `ADKAgentExecutor` | -| **ADK Agent** | User query + state | Tool results | `Agent` (google.adk) | -| **Retail Store** | Method calls | Domain objects | `RetailStore` | +| Layer | Input | Output | Key Class | +| ------------------ | ------------------ | -------------- | ------------------------- | +| **A2A Server** | HTTP request | HTTP response | `A2AStarletteApplication` | +| **Agent Executor** | A2A context | Event queue | `ADKAgentExecutor` | +| **ADK Agent** | User query + state | Tool results | `Agent` (google.adk) | +| **Retail Store** | Method calls | Domain objects | `RetailStore` | ## Mock Store Architecture @@ -91,21 +91,21 @@ The diagram illustrates the separation between: - **Replace These (Mock Layer)** — products.json, In-Memory Dict, MockPaymentProcessor — swap these with real implementations - **Your Backend** — Commerce API (Shopify, Magento), Database, Payment Provider (Stripe, Adyen) -| Storage | Type | Purpose | -|---------|------|---------| -| `_products` | `dict[str, Product]` | Product catalog (loaded from `products.json`) | -| `_checkouts` | `dict[str, Checkout]` | Active shopping sessions | -| `_orders` | `dict[str, Checkout]` | Completed orders | +| Storage | Type | Purpose | +| ------------ | --------------------- | --------------------------------------------- | +| `_products` | `dict[str, Product]` | Product catalog (loaded from `products.json`) | +| `_checkouts` | `dict[str, Checkout]` | Active shopping sessions | +| `_orders` | `dict[str, Checkout]` | Completed orders | ### Key Methods -| Method | Line | Called By | Purpose | -|--------|------|-----------|---------| -| `search_products()` | 100 | `search_shopping_catalog` tool | Keyword search in catalog | -| `add_to_checkout()` | 186 | `add_to_checkout` tool | Create/update checkout session | -| `get_checkout()` | 244 | `get_checkout` tool | Retrieve current checkout state | -| `start_payment()` | 463 | `start_payment` tool | Validate checkout for payment | -| `place_order()` | 498 | `complete_checkout` tool | Finalize order, generate confirmation | +| Method | Line | Called By | Purpose | +| ------------------- | ---- | ------------------------------ | ------------------------------------- | +| `search_products()` | 100 | `search_shopping_catalog` tool | Keyword search in catalog | +| `add_to_checkout()` | 186 | `add_to_checkout` tool | Create/update checkout session | +| `get_checkout()` | 244 | `get_checkout` tool | Retrieve current checkout state | +| `start_payment()` | 463 | `start_payment` tool | Validate checkout for payment | +| `place_order()` | 498 | `complete_checkout` tool | Finalize order, generate confirmation | ### Replacing with Real Backend @@ -158,18 +158,18 @@ store = ShopifyStore( ### What to Keep vs Replace -| Keep (UCP Patterns) | Replace (Mock Specifics) | -|---------------------|--------------------------| -| Tool function signatures | Data storage layer | -| State management via ToolContext | Product catalog source | -| Checkout type generation | Tax/shipping calculation | -| Response formatting with UCP keys | Payment processing | -| A2A/ADK bridging | Order persistence | +| Keep (UCP Patterns) | Replace (Mock Specifics) | +| --------------------------------- | ------------------------ | +| Tool function signatures | Data storage layer | +| State management via ToolContext | Product catalog source | +| Checkout type generation | Tax/shipping calculation | +| Response formatting with UCP keys | Payment processing | +| A2A/ADK bridging | Order persistence | ## Discovery Endpoints -| Endpoint | Purpose | Source | -|----------|---------|--------| +| Endpoint | Purpose | Source | +| ------------------------------ | ---------------------- | ---------------------- | | `/.well-known/agent-card.json` | A2A agent capabilities | `data/agent_card.json` | -| `/.well-known/ucp` | UCP merchant profile | `data/ucp.json` | -| `/images/*` | Product images | `data/images/` | +| `/.well-known/ucp` | UCP merchant profile | `data/ucp.json` | +| `/images/*` | Product images | `data/images/` | diff --git a/a2a/docs/02-adk-agent.md b/a2a/docs/02-adk-agent.md index b8bf9f3..b618afb 100644 --- a/a2a/docs/02-adk-agent.md +++ b/a2a/docs/02-adk-agent.md @@ -61,16 +61,16 @@ def tool_function(tool_context: ToolContext, param: str) -> dict: ## All 8 Tools -| Tool | Line | Purpose | State Access | -|------|------|---------|--------------| -| `search_shopping_catalog` | 51 | Search products | Read metadata | -| `add_to_checkout` | 73 | Add item to cart | Read/write checkout_id | -| `remove_from_checkout` | 115 | Remove item | Read checkout_id | -| `update_checkout` | 151 | Update quantity | Read checkout_id | -| `get_checkout` | 187 | Get current state | Read checkout_id | -| `update_customer_details` | 212 | Set buyer/address | Read checkout_id | -| `start_payment` | 340 | Begin payment flow | Read checkout_id | -| `complete_checkout` | 270 | Finalize order | Read checkout_id, payment | +| Tool | Line | Purpose | State Access | +| ------------------------- | ---- | ------------------ | ------------------------- | +| `search_shopping_catalog` | 51 | Search products | Read metadata | +| `add_to_checkout` | 73 | Add item to cart | Read/write checkout_id | +| `remove_from_checkout` | 115 | Remove item | Read checkout_id | +| `update_checkout` | 151 | Update quantity | Read checkout_id | +| `get_checkout` | 187 | Get current state | Read checkout_id | +| `update_customer_details` | 212 | Set buyer/address | Read checkout_id | +| `start_payment` | 340 | Begin payment flow | Read checkout_id | +| `complete_checkout` | 270 | Finalize order | Read checkout_id, payment | ## Tool Execution Flow @@ -109,11 +109,13 @@ In a typical shopping session, multiple tools are called across turns: ADK callbacks solve a key problem: **the LLM sees tool results as text, but the frontend needs structured data**. Without callbacks: + - Tool returns `{UCP_CHECKOUT_KEY: {...checkout data...}}` - LLM summarizes: "Added cookies to your cart for $4.99" - Frontend only sees text, can't render checkout UI With callbacks: + - `after_tool_callback` captures the structured data in state - `after_agent_callback` attaches it to the response as a `data` part - Frontend receives both text AND structured data for rich UI @@ -288,18 +290,18 @@ ERROR HANDLING: ### Model Configuration -| Setting | Current Value | Purpose | -|---------|---------------|---------| -| `model` | `gemini-3-flash-preview` | Fast, accurate tool calling | -| `temperature` | Default (not set) | Balanced creativity vs determinism | -| `max_tokens` | Default (not set) | Response length limit | +| Setting | Current Value | Purpose | +| ------------- | ------------------------ | ---------------------------------- | +| `model` | `gemini-3-flash-preview` | Fast, accurate tool calling | +| `temperature` | Default (not set) | Balanced creativity vs determinism | +| `max_tokens` | Default (not set) | Response length limit | **Model Selection Guide:** -| Model | Best For | Tradeoff | -|-------|----------|----------| -| Gemini 3.0 Flash | Tool-heavy agents (this sample) | Fastest, 99% tool accuracy | -| Gemini 2.0 Pro | Complex reasoning, ambiguous queries | Slower, better nuanced understanding | +| Model | Best For | Tradeoff | +| ---------------- | ------------------------------------ | ------------------------------------ | +| Gemini 3.0 Flash | Tool-heavy agents (this sample) | Fastest, 99% tool accuracy | +| Gemini 2.0 Pro | Complex reasoning, ambiguous queries | Slower, better nuanced understanding | To change the model, edit `agent.py:437`: diff --git a/a2a/docs/03-ucp-integration.md b/a2a/docs/03-ucp-integration.md index 38d002a..31218dd 100644 --- a/a2a/docs/03-ucp-integration.md +++ b/a2a/docs/03-ucp-integration.md @@ -11,11 +11,13 @@ Different commerce platforms support different features. A basic merchant might only support checkout, while an advanced one offers loyalty points, subscriptions, and gift cards. **Without negotiation**: + - Client assumes all features available → breaks when merchant lacks support - Merchant sends all data → client can't render unknown fields - Tight coupling between specific client and merchant versions **With negotiation**: + - Client declares what it supports: "I can handle checkout, fulfillment, discounts" - Merchant declares what it offers: "I support checkout, fulfillment" - Intersection becomes the contract: "We'll use checkout + fulfillment" @@ -25,12 +27,12 @@ This enables any UCP-compliant client to work with any UCP-compliant merchant. ## UCP Capabilities -| Capability | Purpose | Extends | -|------------|---------|---------| -| `dev.ucp.shopping.checkout` | Base checkout session | - | -| `dev.ucp.shopping.fulfillment` | Shipping address, delivery options | checkout | -| `dev.ucp.shopping.discount` | Promotional codes | checkout | -| `dev.ucp.shopping.buyer_consent` | Consent management | checkout | +| Capability | Purpose | Extends | +| -------------------------------- | ---------------------------------- | -------- | +| `dev.ucp.shopping.checkout` | Base checkout session | - | +| `dev.ucp.shopping.fulfillment` | Shipping address, delivery options | checkout | +| `dev.ucp.shopping.discount` | Promotional codes | checkout | +| `dev.ucp.shopping.buyer_consent` | Consent management | checkout | ## Profile Structure @@ -64,11 +66,13 @@ This enables any UCP-compliant client to work with any UCP-compliant merchant. ] }, "payment": { - "handlers": [{ - "id": "example_payment_provider", - "name": "example.payment.provider", - "version": "2026-01-11" - }] + "handlers": [ + { + "id": "example_payment_provider", + "name": "example.payment.provider", + "version": "2026-01-11" + } + ] } } ``` @@ -82,10 +86,10 @@ This enables any UCP-compliant client to work with any UCP-compliant merchant. "ucp": { "version": "2026-01-11", "capabilities": [ - {"name": "dev.ucp.shopping.checkout"}, - {"name": "dev.ucp.shopping.fulfillment"}, - {"name": "dev.ucp.shopping.discount"}, - {"name": "dev.ucp.shopping.buyer_consent"} + { "name": "dev.ucp.shopping.checkout" }, + { "name": "dev.ucp.shopping.fulfillment" }, + { "name": "dev.ucp.shopping.discount" }, + { "name": "dev.ucp.shopping.buyer_consent" } ] } } @@ -177,6 +181,7 @@ UCP_RISK_SIGNALS_KEY = "a2a.ucp.checkout.risk_signals" # Risk data ## Adding a New Capability 1. **Update merchant profile** (`data/ucp.json`): + ```json { "capabilities": [ @@ -187,6 +192,7 @@ UCP_RISK_SIGNALS_KEY = "a2a.ucp.checkout.risk_signals" # Risk data ``` 2. **Update type generator** (`helpers/type_generator.py`): + ```python if "dev.ucp.shopping.new_capability" in active: bases.append(NewCapabilityCheckout) diff --git a/a2a/docs/04-commerce-flows.md b/a2a/docs/04-commerce-flows.md index f8c18c0..5d7bd46 100644 --- a/a2a/docs/04-commerce-flows.md +++ b/a2a/docs/04-commerce-flows.md @@ -10,15 +10,16 @@ The checkout state machine prevents common e-commerce errors: -| State | Purpose | -|-------|---------| -| `incomplete` | Cart mode - freely add/remove items, no commitment yet | +| State | Purpose | +| -------------------- | ----------------------------------------------------------- | +| `incomplete` | Cart mode - freely add/remove items, no commitment yet | | `ready_for_complete` | Validation gate - all required info collected, price locked | -| `completed` | Finalized - order placed, no modifications possible | +| `completed` | Finalized - order placed, no modifications possible | **Why not just "in cart" and "ordered"?** The `ready_for_complete` state serves as a critical checkpoint: + - Validates buyer email exists (for order confirmation) - Validates shipping address (for fulfillment) - Locks in pricing (prevents race conditions during payment) @@ -35,19 +36,19 @@ Without this intermediate state, you'd risk creating orders with missing shippin ### State Definitions -| State | Meaning | Missing | -|-------|---------|---------| -| `incomplete` | Cart has items but missing info | Buyer email or fulfillment address | -| `ready_for_complete` | All info collected | Awaiting payment confirmation | -| `completed` | Order placed | - | +| State | Meaning | Missing | +| -------------------- | ------------------------------- | ---------------------------------- | +| `incomplete` | Cart has items but missing info | Buyer email or fulfillment address | +| `ready_for_complete` | All info collected | Awaiting payment confirmation | +| `completed` | Order placed | - | ### Transition Triggers -| From | To | Tool | Condition | -|------|----|----- |-----------| -| - | incomplete | `add_to_checkout` | First item added | -| incomplete | ready_for_complete | `start_payment` | Buyer + address present | -| ready_for_complete | completed | `complete_checkout` | Payment validated | +| From | To | Tool | Condition | +| ------------------ | ------------------ | ------------------- | ----------------------- | +| - | incomplete | `add_to_checkout` | First item added | +| incomplete | ready_for_complete | `start_payment` | Buyer + address present | +| ready_for_complete | completed | `complete_checkout` | Payment validated | ## Checkout Object Structure @@ -103,11 +104,11 @@ Without this intermediate state, you'd risk creating orders with missing shippin ### Payment Components -| Component | Location | Role | -|-----------|----------|------| -| CredentialProviderProxy | `chat-client/mocks/` | Mock payment method provider | -| PaymentMethodSelector | `chat-client/components/` | UI for method selection | -| MockPaymentProcessor | `business_agent/payment_processor.py` | Simulates payment validation | +| Component | Location | Role | +| ----------------------- | ------------------------------------- | ---------------------------- | +| CredentialProviderProxy | `chat-client/mocks/` | Mock payment method provider | +| PaymentMethodSelector | `chat-client/components/` | UI for method selection | +| MockPaymentProcessor | `business_agent/payment_processor.py` | Simulates payment validation | ### PaymentInstrument Structure diff --git a/a2a/docs/05-frontend.md b/a2a/docs/05-frontend.md index afd4c58..763f404 100644 --- a/a2a/docs/05-frontend.md +++ b/a2a/docs/05-frontend.md @@ -43,14 +43,14 @@ const [taskId, setTaskId] = useState(null); ### Handler Functions -| Handler | Purpose | -|---------|---------| -| `handleSendMessage(content, options)` | Send A2A message, parse response | -| `handleAddToCheckout(product)` | Add product to cart | -| `handleStartPayment()` | Initiate payment flow | -| `handlePaymentMethodSelection(checkout)` | Fetch available methods | -| `handlePaymentMethodSelected(method)` | Get payment token | -| `handleConfirmPayment(instrument)` | Complete checkout | +| Handler | Purpose | +| ---------------------------------------- | -------------------------------- | +| `handleSendMessage(content, options)` | Send A2A message, parse response | +| `handleAddToCheckout(product)` | Add product to cart | +| `handleStartPayment()` | Initiate payment flow | +| `handlePaymentMethodSelection(checkout)` | Fetch available methods | +| `handlePaymentMethodSelected(method)` | Get payment token | +| `handleConfirmPayment(instrument)` | Complete checkout | ## A2A Communication @@ -64,12 +64,12 @@ const request = { params: { message: { role: "user", - parts: [{type: "text", text: "show me cookies"}], - contextId: contextId, // From previous response - taskId: taskId, // For multi-turn tasks + parts: [{ type: "text", text: "show me cookies" }], + contextId: contextId, // From previous response + taskId: taskId, // For multi-turn tasks }, - configuration: { historyLength: 0 } - } + configuration: { historyLength: 0 }, + }, }; fetch("/api", { @@ -77,9 +77,9 @@ fetch("/api", { headers: { "Content-Type": "application/json", "X-A2A-Extensions": "https://ucp.dev/specification/reference?v=2026-01-11", - "UCP-Agent": `profile="http://localhost:3000/profile/agent_profile.json"` + "UCP-Agent": `profile="http://localhost:3000/profile/agent_profile.json"`, }, - body: JSON.stringify(request) + body: JSON.stringify(request), }); ``` @@ -107,20 +107,20 @@ for (const part of data.result?.status?.message?.parts || []) { ## Key Components -| Component | Props | Renders | -|-----------|-------|---------| -| `ProductCard` | `product`, `onAddToCart` | Product image, name, price, stock | -| `Checkout` | `checkout`, `onCheckout`, `onCompletePayment` | Line items, totals, action buttons | -| `PaymentMethodSelector` | `paymentMethods`, `onSelect` | Radio list of methods | -| `PaymentConfirmation` | `paymentInstrument`, `onConfirm` | Confirm button | -| `ChatMessage` | `message`, handlers | Combines all above based on data | +| Component | Props | Renders | +| ----------------------- | --------------------------------------------- | ---------------------------------- | +| `ProductCard` | `product`, `onAddToCart` | Product image, name, price, stock | +| `Checkout` | `checkout`, `onCheckout`, `onCompletePayment` | Line items, totals, action buttons | +| `PaymentMethodSelector` | `paymentMethods`, `onSelect` | Radio list of methods | +| `PaymentConfirmation` | `paymentInstrument`, `onConfirm` | Confirm button | +| `ChatMessage` | `message`, handlers | Combines all above based on data | ## Types (types.ts) ```typescript interface ChatMessage { id: string; - sender: Sender; // USER | MODEL + sender: Sender; // USER | MODEL text: string; products?: Product[]; isLoading?: boolean; @@ -135,7 +135,7 @@ interface Checkout { line_items: CheckoutItem[]; currency: string; continue_url?: string | null; - status: string; // incomplete | ready_for_complete | completed + status: string; // incomplete | ready_for_complete | completed totals: CheckoutTotal[]; order_id?: string; order_permalink_url?: string; @@ -173,34 +173,46 @@ Mock payment provider in `mocks/credentialProviderProxy.ts`: ```typescript class CredentialProviderProxy { - handler_id = 'example_payment_provider'; - handler_name = 'example.payment.provider'; + handler_id = "example_payment_provider"; + handler_name = "example.payment.provider"; // Returns mock payment methods (wrapped in object) async getSupportedPaymentMethods( user_email: string, - config: any - ): Promise<{payment_method_aliases: PaymentMethod[]}> { + config: any, + ): Promise<{ payment_method_aliases: PaymentMethod[] }> { return { payment_method_aliases: [ - { id: "instr_1", type: "card", brand: "amex", - last_digits: "1111", expiry_month: 12, expiry_year: 2026 }, - { id: "instr_2", type: "card", brand: "visa", - last_digits: "8888", expiry_month: 12, expiry_year: 2026 }, - ] + { + id: "instr_1", + type: "card", + brand: "amex", + last_digits: "1111", + expiry_month: 12, + expiry_year: 2026, + }, + { + id: "instr_2", + type: "card", + brand: "visa", + last_digits: "8888", + expiry_month: 12, + expiry_year: 2026, + }, + ], }; } // Converts method to PaymentInstrument with token async getPaymentToken( user_email: string, - payment_method_id: string + payment_method_id: string, ): Promise { return { ...payment_method, handler_id: this.handler_id, handler_name: this.handler_name, - credential: { type: "token", token: `mock_token_${uuid}` } + credential: { type: "token", token: `mock_token_${uuid}` }, }; } } @@ -231,6 +243,6 @@ export const appConfig = new AppProperties( "Your personal shopping assistant.", "/images/logo.jpg", "Hello, I am your Business Agent...", - "Shop with Business Agent" + "Shop with Business Agent", ); ``` diff --git a/a2a/docs/06-extending.md b/a2a/docs/06-extending.md index 33dfcdf..cae5b82 100644 --- a/a2a/docs/06-extending.md +++ b/a2a/docs/06-extending.md @@ -28,7 +28,7 @@ "name": "Organic Trail Mix", "@type": "Product", "image": ["http://localhost:10999/images/trail_mix.jpg"], - "brand": {"name": "Nature's Best", "@type": "Brand"}, + "brand": { "name": "Nature's Best", "@type": "Brand" }, "offers": { "price": "6.99", "priceCurrency": "USD", @@ -327,27 +327,33 @@ def move_to_checkout(tool_context: ToolContext, product_id: str) -> dict: ### Step 1: Update Profiles **Merchant** (`data/ucp.json`): + ```json { "payment": { - "handlers": [{ - "id": "stripe_handler", - "name": "stripe.payment.provider", - "version": "2026-01-11", - "config": {"business_id": "acct_123456"} - }] + "handlers": [ + { + "id": "stripe_handler", + "name": "stripe.payment.provider", + "version": "2026-01-11", + "config": { "business_id": "acct_123456" } + } + ] } } ``` **Client** (`chat-client/profile/agent_profile.json`): + ```json { "payment": { - "handlers": [{ - "id": "stripe_handler", - "name": "stripe.payment.provider" - }] + "handlers": [ + { + "id": "stripe_handler", + "name": "stripe.payment.provider" + } + ] } } ``` @@ -392,8 +398,8 @@ Replace `CredentialProviderProxy` in `chat-client/mocks/`: ```typescript class StripeCredentialProvider { - handler_id = 'stripe_handler'; - handler_name = 'stripe.payment.provider'; + handler_id = "stripe_handler"; + handler_name = "stripe.payment.provider"; async getSupportedPaymentMethods(email: string) { // Call your payment service to get saved methods @@ -406,7 +412,7 @@ class StripeCredentialProvider { const { token } = await stripe.createToken(card); return { ...method, - credential: { type: "token", token: token.id } + credential: { type: "token", token: token.id }, }; } } @@ -470,6 +476,7 @@ User: "Show me similar cookies" ### Journey: Guest vs Returning Customer **Guest User**: + ``` User: Adds items, enters email: new@example.com → Agent: No saved addresses, asks for full address @@ -477,6 +484,7 @@ User: Adds items, enters email: new@example.com ``` **Returning Customer**: + ``` User: Adds items, enters email: returning@example.com → Agent: get_saved_addresses(email) @@ -493,6 +501,7 @@ User: "Yes" ## Part 6: Replacing the Mock Store See [Architecture: Mock Store](./01-architecture.md#mock-store-architecture) for: + - Store structure diagram - Key methods to implement - Interface definition @@ -502,8 +511,8 @@ See [Architecture: Mock Store](./01-architecture.md#mock-store-architecture) for Quick summary: | Keep (UCP/ADK patterns) | Replace (Mock specifics) | -|-------------------------|--------------------------| -| Tool signatures | Data storage | -| State management | Product catalog | -| Type generation | Tax/shipping logic | -| Response formatting | Payment processing | +| ----------------------- | ------------------------ | +| Tool signatures | Data storage | +| State management | Product catalog | +| Type generation | Tax/shipping logic | +| Response formatting | Payment processing | diff --git a/a2a/docs/07-testing-guide.md b/a2a/docs/07-testing-guide.md index 0516b35..e38702b 100644 --- a/a2a/docs/07-testing-guide.md +++ b/a2a/docs/07-testing-guide.md @@ -60,6 +60,7 @@ curl -s http://localhost:3000/profile/agent_profile.json | jq . 9. Verify order confirmation appears with order ID and permalink **Expected State Transitions**: + - After step 3: `status: "incomplete"` - After step 5: `status: "incomplete"` (ready for payment start) - After step 6: `status: "ready_for_complete"` @@ -67,13 +68,13 @@ curl -s http://localhost:3000/profile/agent_profile.json | jq . ### Error Scenarios -| Scenario | How to Test | Expected Behavior | -|----------|-------------|-------------------| -| No checkout exists | Call `get_checkout` without adding items | "Checkout not created" error | -| Missing address | Skip address, call `start_payment` | Agent prompts for address | -| Missing email | Skip email, call `start_payment` | Agent prompts for email | -| Invalid product | `add_to_checkout("INVALID-ID", 1)` | "Product not found" error | -| Quantity update | Add item, then `update_checkout` with qty=0 | Item removed from checkout | +| Scenario | How to Test | Expected Behavior | +| ------------------ | ------------------------------------------- | ---------------------------- | +| No checkout exists | Call `get_checkout` without adding items | "Checkout not created" error | +| Missing address | Skip address, call `start_payment` | Agent prompts for address | +| Missing email | Skip email, call `start_payment` | Agent prompts for email | +| Invalid product | `add_to_checkout("INVALID-ID", 1)` | "Product not found" error | +| Quantity update | Add item, then `update_checkout` with qty=0 | Item removed from checkout | ## Debugging Guide @@ -137,36 +138,36 @@ curl -X POST http://localhost:10999/ \ ## Common Issues -| Issue | Likely Cause | Fix | -|-------|--------------|-----| -| Server won't start | Missing `GOOGLE_API_KEY` | Add key to `.env` file | -| "Profile fetch failed" | Frontend not running | Start chat-client on :3000 | -| "Version unsupported" | Profile version mismatch | Align `version` in both `ucp.json` and `agent_profile.json` | -| "Checkout not found" | Session expired or no items | Call `add_to_checkout` first | -| UI not updating | Missing contextId | Check `contextId` in response, ensure it's passed to next request | -| "Missing UCP metadata" | Header not sent | Verify `UCP-Agent` header in request | -| Payment methods empty | CredentialProviderProxy issue | Check browser console for mock provider errors | +| Issue | Likely Cause | Fix | +| ---------------------- | ----------------------------- | ----------------------------------------------------------------- | +| Server won't start | Missing `GOOGLE_API_KEY` | Add key to `.env` file | +| "Profile fetch failed" | Frontend not running | Start chat-client on :3000 | +| "Version unsupported" | Profile version mismatch | Align `version` in both `ucp.json` and `agent_profile.json` | +| "Checkout not found" | Session expired or no items | Call `add_to_checkout` first | +| UI not updating | Missing contextId | Check `contextId` in response, ensure it's passed to next request | +| "Missing UCP metadata" | Header not sent | Verify `UCP-Agent` header in request | +| Payment methods empty | CredentialProviderProxy issue | Check browser console for mock provider errors | ## Troubleshooting Guide ### Setup Failures -| Error Message | Cause | Solution | -|---------------|-------|----------| -| `Address already in use :10999` | Agent already running or port in use | `kill $(lsof -t -i:10999)` or use different port | -| `GOOGLE_API_KEY not found` | Missing or empty .env file | Create `.env` from `env.example`, add your key | -| `No module named 'business_agent'` | Not in virtualenv or deps not installed | Run `uv sync` in `business_agent/` directory | -| `npm ERR! ENOENT package.json` | Wrong directory | `cd chat-client` before running `npm install` | -| `Connection refused :10999` | Backend not running | Start backend first with `uv run business_agent` | +| Error Message | Cause | Solution | +| ---------------------------------- | --------------------------------------- | ------------------------------------------------ | +| `Address already in use :10999` | Agent already running or port in use | `kill $(lsof -t -i:10999)` or use different port | +| `GOOGLE_API_KEY not found` | Missing or empty .env file | Create `.env` from `env.example`, add your key | +| `No module named 'business_agent'` | Not in virtualenv or deps not installed | Run `uv sync` in `business_agent/` directory | +| `npm ERR! ENOENT package.json` | Wrong directory | `cd chat-client` before running `npm install` | +| `Connection refused :10999` | Backend not running | Start backend first with `uv run business_agent` | ### Runtime Errors -| Symptom | Debug Steps | -|---------|-------------| -| **"Checkout not found"** | 1. Check `contextId` is passed from previous response
2. Verify session hasn't expired
3. Add an item first with `add_to_checkout` | -| **Products not returning** | 1. Enable DEBUG logging
2. Check if `search_shopping_catalog` tool is being called
3. Verify products.json exists and is valid JSON | -| **Payment flow hangs** | 1. Check browser console for CredentialProviderProxy errors
2. Verify mock payment methods are returned
3. Check `start_payment` was called successfully | -| **UI not updating after action** | 1. Verify `contextId` threading in App.tsx
2. Check response structure in browser DevTools
3. Look for React state update issues | +| Symptom | Debug Steps | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **"Checkout not found"** | 1. Check `contextId` is passed from previous response
2. Verify session hasn't expired
3. Add an item first with `add_to_checkout` | +| **Products not returning** | 1. Enable DEBUG logging
2. Check if `search_shopping_catalog` tool is being called
3. Verify products.json exists and is valid JSON | +| **Payment flow hangs** | 1. Check browser console for CredentialProviderProxy errors
2. Verify mock payment methods are returned
3. Check `start_payment` was called successfully | +| **UI not updating after action** | 1. Verify `contextId` threading in App.tsx
2. Check response structure in browser DevTools
3. Look for React state update issues | ### Step-by-Step Diagnosis @@ -225,6 +226,7 @@ def my_tool(tool_context: ToolContext, query: str) -> dict: ### Browser Debugging **Check Network Requests:** + 1. Open DevTools (F12) → Network tab 2. Filter by "localhost:10999" 3. Click on request → Headers tab @@ -232,6 +234,7 @@ def my_tool(tool_context: ToolContext, query: str) -> dict: 5. Click Response tab to see A2A response **Check Console Errors:** + 1. Open DevTools → Console tab 2. Look for red error messages 3. Common issues: @@ -243,23 +246,23 @@ def my_tool(tool_context: ToolContext, query: str) -> dict: ### Ports & URLs -| Service | Port | Endpoints | -|---------|------|-----------| -| Backend | 10999 | `/` (A2A), `/.well-known/agent-card.json`, `/.well-known/ucp` | -| Frontend | 3000 | `/`, `/profile/agent_profile.json` | +| Service | Port | Endpoints | +| -------- | ----- | ------------------------------------------------------------- | +| Backend | 10999 | `/` (A2A), `/.well-known/agent-card.json`, `/.well-known/ucp` | +| Frontend | 3000 | `/`, `/profile/agent_profile.json` | ### Environment Variables -| Variable | Required | Purpose | -|----------|----------|---------| -| `GOOGLE_API_KEY` | Yes | Gemini API access for LLM | +| Variable | Required | Purpose | +| ---------------- | -------- | ------------------------- | +| `GOOGLE_API_KEY` | Yes | Gemini API access for LLM | ### Key Files for Debugging -| Symptom | Check This File | What to Look For | -|---------|-----------------|------------------| -| Tool not called | `agent.py` | Tool in `tools=[]` list | -| State issues | `constants.py` | State key names | -| Checkout errors | `store.py` | State machine logic | +| Symptom | Check This File | What to Look For | +| --------------- | ------------------------- | --------------------------- | +| Tool not called | `agent.py` | Tool in `tools=[]` list | +| State issues | `constants.py` | State key names | +| Checkout errors | `store.py` | State machine logic | | UCP negotiation | `ucp_profile_resolver.py` | Version/capability matching | -| Frontend errors | `App.tsx` | Request/response handling | +| Frontend errors | `App.tsx` | Request/response handling | diff --git a/a2a/docs/08-production-notes.md b/a2a/docs/08-production-notes.md index e7da6f2..d00e21b 100644 --- a/a2a/docs/08-production-notes.md +++ b/a2a/docs/08-production-notes.md @@ -18,17 +18,18 @@

Figure 1: In-memory sample components vs production-ready replacements

-| Component | Current State | Production Requirement | -|-----------|---------------|------------------------| -| Session Storage | `InMemorySessionService` (lost on restart) | Redis or database | -| Task Storage | `InMemoryTaskStore` (lost on restart) | Persistent task store | -| Checkout Storage | `RetailStore._checkouts` dict (in-memory) | PostgreSQL or similar | -| Order Storage | `RetailStore._orders` dict (in-memory) | PostgreSQL or similar | -| Concurrency | No locking on `_checkouts` | Checkout-level locks | -| Authentication | None (trusts `context_id`) | JWT or API key validation | -| Secrets | Plaintext .env | Secret Manager (GCP/AWS) | +| Component | Current State | Production Requirement | +| ---------------- | ------------------------------------------ | ------------------------- | +| Session Storage | `InMemorySessionService` (lost on restart) | Redis or database | +| Task Storage | `InMemoryTaskStore` (lost on restart) | Persistent task store | +| Checkout Storage | `RetailStore._checkouts` dict (in-memory) | PostgreSQL or similar | +| Order Storage | `RetailStore._orders` dict (in-memory) | PostgreSQL or similar | +| Concurrency | No locking on `_checkouts` | Checkout-level locks | +| Authentication | None (trusts `context_id`) | JWT or API key validation | +| Secrets | Plaintext .env | Secret Manager (GCP/AWS) | **Key insight**: The `RetailStore` class (in `store.py`) stores all business data in plain Python dictionaries: + - `self._checkouts = {}` — All active checkout sessions - `self._orders = {}` — All completed orders - `self._products` — Product catalog (loaded from JSON) @@ -59,6 +60,7 @@ user_id = validate_jwt(context.headers.get("Authorization")) ### 2. Profile URL Not Validated The `UCP-Agent` header can point to any URL. A malicious client could point to: + - Internal services (SSRF attack) - Slow servers (DoS the agent startup) @@ -91,6 +93,7 @@ logger.info(f"Processing payment for checkout {checkout_id}") ### Race Condition in Checkout Between `start_payment()` and `complete_checkout()`, another request could: + - Add items (changing the total) - Change the address (affecting tax) - Call `start_payment()` again @@ -131,12 +134,14 @@ def add_to_checkout( ### Required Before Production **Infrastructure:** + - [ ] Create Dockerfile with non-root user - [ ] Add `/health` endpoint (liveness probe) - [ ] Add `/ready` endpoint (readiness probe) - [ ] Configure Kubernetes manifests or Cloud Run **State Management:** + - [ ] Replace `InMemorySessionService` with Redis-backed store - [ ] Replace `InMemoryTaskStore` with persistent store - [ ] Replace `RetailStore._checkouts` dict with database (PostgreSQL) @@ -144,17 +149,20 @@ def add_to_checkout( - [ ] Add session TTL and cleanup **Security:** + - [ ] Add request authentication (JWT/API key) - [ ] Validate profile URLs against whitelist - [ ] Move `GOOGLE_API_KEY` to Secret Manager - [ ] Add rate limiting per user **Observability:** + - [ ] Add structured JSON logging - [ ] Add Prometheus metrics endpoint - [ ] Add request tracing (OpenTelemetry) **Reliability:** + - [ ] Add checkout-level locking - [ ] Implement idempotency for state-changing operations - [ ] Add circuit breaker for Gemini API calls @@ -243,14 +251,14 @@ async def ready(request: Request) -> JSONResponse: Externalize these hardcoded values: -| Variable | Current | Purpose | -|----------|---------|---------| -| `AGENT_HOST` | `localhost` | Bind address | -| `AGENT_PORT` | `10999` | Listen port | -| `LOG_LEVEL` | `INFO` | Logging verbosity | -| `GOOGLE_API_KEY` | `.env` file | Gemini API access | -| `SESSION_TTL` | Unlimited | Session expiration (seconds) | -| `REDIS_URL` | N/A | Distributed session storage | +| Variable | Current | Purpose | +| ---------------- | ----------- | ---------------------------- | +| `AGENT_HOST` | `localhost` | Bind address | +| `AGENT_PORT` | `10999` | Listen port | +| `LOG_LEVEL` | `INFO` | Logging verbosity | +| `GOOGLE_API_KEY` | `.env` file | Gemini API access | +| `SESSION_TTL` | Unlimited | Session expiration (seconds) | +| `REDIS_URL` | N/A | Distributed session storage | --- diff --git a/rest/nodejs/README.md b/rest/nodejs/README.md index 7209b58..a99fd56 100644 --- a/rest/nodejs/README.md +++ b/rest/nodejs/README.md @@ -22,8 +22,8 @@ implement the UCP specifications for shopping, checkout, and order management. ## Prerequisites -* Node.js (v20 or higher recommended) -* npm (Node Package Manager) +- Node.js (v20 or higher recommended) +- npm (Node Package Manager) ## Setup @@ -109,9 +109,9 @@ use the official UCP Conformance Test Suite. ## Project Structure -* `src/api`: Contains the implementation of UCP services (Discovery, Checkout, - Order). -* `src/data`: Database access layer (SQLite). -* `src/models`: TypeScript types and Zod schemas (some generated from specs). -* `src/utils`: Helper utilities for validation and logging. -* `databases`: Directory where SQLite database files are stored. +- `src/api`: Contains the implementation of UCP services (Discovery, Checkout, + Order). +- `src/data`: Database access layer (SQLite). +- `src/models`: TypeScript types and Zod schemas (some generated from specs). +- `src/utils`: Helper utilities for validation and logging. +- `databases`: Directory where SQLite database files are stored. diff --git a/rest/nodejs/scripts/generate_models.sh b/rest/nodejs/scripts/generate_models.sh old mode 100644 new mode 100755 diff --git a/rest/nodejs/src/api/checkout.ts b/rest/nodejs/src/api/checkout.ts index 0a588a6..840b3d0 100644 --- a/rest/nodejs/src/api/checkout.ts +++ b/rest/nodejs/src/api/checkout.ts @@ -1,18 +1,51 @@ -import {createHash} from 'crypto'; -import {type Context} from 'hono'; -import {v4 as uuidv4} from 'uuid'; -import {z} from 'zod'; - -import {getCheckoutSession, getIdempotencyRecord, getInventory, getOrder, getProduct, logRequest, releaseStock, reserveStock, saveCheckout, saveIdempotencyRecord, saveOrder} from '../data'; -import {CheckoutResponseStatusSchema, type Expectation, type ExpectationLineItem, type ExtendedCheckoutCreateRequest, type ExtendedCheckoutResponse, type ExtendedCheckoutUpdateRequest, ExtendedPaymentCredentialSchema, type FulfillmentDestinationRequest, type FulfillmentDestinationResponse, type FulfillmentOptionResponse, type FulfillmentRequest, type FulfillmentResponse, type LineItemCreateRequest, type LineItemResponse, type Order, type OrderLineItem, type PaymentCreateRequest, PaymentDataSchema, type PostalAddress} from '../models'; +import { createHash } from "crypto"; +import { type Context } from "hono"; +import { v4 as uuidv4 } from "uuid"; +import { z } from "zod"; + +import { + getCheckoutSession, + getIdempotencyRecord, + getInventory, + getOrder, + getProduct, + logRequest, + releaseStock, + reserveStock, + saveCheckout, + saveIdempotencyRecord, + saveOrder, +} from "../data"; +import { + CheckoutResponseStatusSchema, + type Expectation, + type ExpectationLineItem, + type ExtendedCheckoutCreateRequest, + type ExtendedCheckoutResponse, + type ExtendedCheckoutUpdateRequest, + ExtendedPaymentCredentialSchema, + type FulfillmentDestinationRequest, + type FulfillmentDestinationResponse, + type FulfillmentOptionResponse, + type FulfillmentRequest, + type FulfillmentResponse, + type LineItemCreateRequest, + type LineItemResponse, + type Order, + type OrderLineItem, + type PaymentCreateRequest, + PaymentDataSchema, + type PostalAddress, +} from "../models"; /** * Schema for the request body when completing a checkout session. */ -export const zCompleteCheckoutRequest = - z.object({ - risk_signals: z.record(z.string(), z.unknown()).optional(), - }).extend(PaymentDataSchema.shape); +export const zCompleteCheckoutRequest = z + .object({ + risk_signals: z.record(z.string(), z.unknown()).optional(), + }) + .extend(PaymentDataSchema.shape); /** * Type definition for the complete checkout request body. @@ -25,24 +58,22 @@ export type CompleteCheckoutRequest = z.infer; export class CheckoutService { private computeHash(data: unknown): string { const replacer = (_key: string, value: unknown) => - typeof value === 'object' && value !== null && !Array.isArray(value) ? - Object.keys(value as Record) + typeof value === "object" && value !== null && !Array.isArray(value) + ? Object.keys(value as Record) .sort() - .reduce>( - (sorted, k) => { - sorted[k] = (value as Record)[k]; - return sorted; - }, - {}) : - value; - return createHash('sha256') - .update(JSON.stringify(data, replacer)) - .digest('hex'); + .reduce>((sorted, k) => { + sorted[k] = (value as Record)[k]; + return sorted; + }, {}) + : value; + return createHash("sha256") + .update(JSON.stringify(data, replacer)) + .digest("hex"); } private async parseAgentProfile( - ucpAgentHeader: string|undefined, - ): Promise<{webhook_url?: string}|undefined> { + ucpAgentHeader: string | undefined, + ): Promise<{ webhook_url?: string } | undefined> { if (!ucpAgentHeader) return undefined; const match = ucpAgentHeader.match(/profile="([^"]+)"/); @@ -51,20 +82,24 @@ export class CheckoutService { const profileUri = match[1]; try { - let profileData: { - ucp?: { - capabilities?: - Array<{name: string; config?: {webhook_url?: string};}>; - }; - }|undefined; + let profileData: + | { + ucp?: { + capabilities?: Array<{ + name: string; + config?: { webhook_url?: string }; + }>; + }; + } + | undefined; - if (profileUri.startsWith('data:')) { - const base64Data = profileUri.split(',')[1]; + if (profileUri.startsWith("data:")) { + const base64Data = profileUri.split(",")[1]; if (base64Data) { - const jsonStr = Buffer.from(base64Data, 'base64').toString('utf-8'); + const jsonStr = Buffer.from(base64Data, "base64").toString("utf-8"); profileData = JSON.parse(jsonStr); } - } else if (profileUri.startsWith('http')) { + } else if (profileUri.startsWith("http")) { const response = await fetch(profileUri); if (response.ok) { profileData = (await response.json()) as typeof profileData; @@ -73,27 +108,27 @@ export class CheckoutService { if (profileData && profileData.ucp && profileData.ucp.capabilities) { const orderCap = profileData.ucp.capabilities.find( - (c) => c.name === 'dev.ucp.shopping.order', + (c) => c.name === "dev.ucp.shopping.order", ); if (orderCap && orderCap.config && orderCap.config.webhook_url) { - return {webhook_url: orderCap.config.webhook_url}; + return { webhook_url: orderCap.config.webhook_url }; } } } catch (e) { - console.warn('Failed to fetch or parse agent profile', e); + console.warn("Failed to fetch or parse agent profile", e); } return undefined; } private async notifyWebhook( - checkout: ExtendedCheckoutResponse, - eventType: string, - ): Promise { + checkout: ExtendedCheckoutResponse, + eventType: string, + ): Promise { if (!checkout.platform?.webhook_url) { return; } const webhookUrl = checkout.platform.webhook_url; - let orderData: Order|undefined = undefined; + let orderData: Order | undefined = undefined; if (checkout.order_id) { orderData = getOrder(checkout.order_id); } @@ -106,8 +141,8 @@ export class CheckoutService { try { await fetch(webhookUrl, { - method: 'POST', - headers: {'Content-Type': 'application/json'}, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } catch (e) { @@ -116,38 +151,38 @@ export class CheckoutService { } private constructFulfillmentResponse( - reqFulfillment: FulfillmentRequest|undefined, - lineItems: LineItemResponse[], - existingFulfillment?: FulfillmentResponse, - ): FulfillmentResponse|undefined { + reqFulfillment: FulfillmentRequest | undefined, + lineItems: LineItemResponse[], + existingFulfillment?: FulfillmentResponse, + ): FulfillmentResponse | undefined { if (!reqFulfillment) { return undefined; } const mockDestinations: FulfillmentDestinationResponse[] = [ { - id: 'dest_1', - name: 'Home (US)', - address_country: 'US', + id: "dest_1", + name: "Home (US)", + address_country: "US", address: { - address_country: 'US', - street_address: '1600 Amphitheatre Pkwy', - address_locality: 'Mountain View', - address_region: 'CA', - postal_code: '94043', - full_name: 'John Doe', + address_country: "US", + street_address: "1600 Amphitheatre Pkwy", + address_locality: "Mountain View", + address_region: "CA", + postal_code: "94043", + full_name: "John Doe", }, }, { - id: 'dest_2', - name: 'Office (DE)', - address_country: 'DE', + id: "dest_2", + name: "Office (DE)", + address_country: "DE", address: { - address_country: 'DE', - street_address: 'ABC Str. 1', - address_locality: 'Berlin', - postal_code: '10115', - full_name: 'Max Mustermann', + address_country: "DE", + street_address: "ABC Str. 1", + address_locality: "Berlin", + postal_code: "10115", + full_name: "Max Mustermann", }, }, ]; @@ -158,16 +193,16 @@ export class CheckoutService { let destinations: FulfillmentDestinationResponse[] = mockDestinations; if (m.destinations && Array.isArray(m.destinations)) { destinations = m.destinations.map( - (d): FulfillmentDestinationResponse => ({ - ...d, - id: d.id || `dest_${uuidv4()}`, - }), + (d): FulfillmentDestinationResponse => ({ + ...d, + id: d.id || `dest_${uuidv4()}`, + }), ); } else if (existingFulfillment && existingFulfillment.methods) { // Default to shipping if type is not provided in request - const targetType = m.type || 'shipping'; + const targetType = m.type || "shipping"; const existingMethod = existingFulfillment.methods.find( - (em) => em.type === targetType, + (em) => em.type === targetType, ); if (existingMethod && existingMethod.destinations) { destinations = existingMethod.destinations; @@ -175,18 +210,16 @@ export class CheckoutService { } // Initialize groups, preserving selection if provided - const groups = - (m.groups || - []).map((g) => ({ - id: `group_${uuidv4()}`, - line_item_ids: lineItems.map((li) => li.id), - selected_option_id: g.selected_option_id, - options: [], // Will be populated in recalculateTotals - })); + const groups = (m.groups || []).map((g) => ({ + id: `group_${uuidv4()}`, + line_item_ids: lineItems.map((li) => li.id), + selected_option_id: g.selected_option_id, + options: [], // Will be populated in recalculateTotals + })); return { id: `method_${uuidv4()}`, - type: m.type || 'shipping', + type: m.type || "shipping", line_item_ids: lineItems.map((li) => li.id), destinations, selected_destination_id: m.selected_destination_id, @@ -211,23 +244,26 @@ export class CheckoutService { const lineTotal = product.price * line.quantity; line.totals = [ - {type: 'subtotal', amount: lineTotal}, - {type: 'total', amount: lineTotal}, + { type: "subtotal", amount: lineTotal }, + { type: "total", amount: lineTotal }, ]; grandTotal += lineTotal; } checkout.totals = []; - checkout.totals.push({type: 'subtotal', amount: grandTotal}); + checkout.totals.push({ type: "subtotal", amount: grandTotal }); // Fulfillment Logic (Mock) if (checkout.fulfillment?.methods) { for (const method of checkout.fulfillment.methods) { - if (method.type === 'shipping' && method.selected_destination_id && - method.destinations) { + if ( + method.type === "shipping" && + method.selected_destination_id && + method.destinations + ) { const dest = method.destinations.find( - (d: FulfillmentDestinationResponse) => - d.id === method.selected_destination_id, + (d: FulfillmentDestinationResponse) => + d.id === method.selected_destination_id, ); // Extract country from flat field or nested address @@ -236,34 +272,33 @@ export class CheckoutService { country = dest.address.address_country; } - if (dest && country) { const options: FulfillmentOptionResponse[] = []; - if (country === 'US') { + if (country === "US") { options.push( - { - id: 'std-ship', - title: 'Standard Shipping', - description: 'Arrives in 5-7 days', - total: 500, - subtotal: 500, - tax: 0, - }, - { - id: 'exp-ship-us', - title: 'Express Shipping (US)', - description: 'Arrives in 2 days', - total: 1500, - subtotal: 1500, - tax: 0, - }, + { + id: "std-ship", + title: "Standard Shipping", + description: "Arrives in 5-7 days", + total: 500, + subtotal: 500, + tax: 0, + }, + { + id: "exp-ship-us", + title: "Express Shipping (US)", + description: "Arrives in 2 days", + total: 1500, + subtotal: 1500, + tax: 0, + }, ); } else { options.push({ - id: 'exp-ship-intl', - title: 'International Express', - description: 'Arrives in 5-10 days', + id: "exp-ship-intl", + title: "International Express", + description: "Arrives in 5-10 days", total: 3000, subtotal: 3000, tax: 0, @@ -290,13 +325,13 @@ export class CheckoutService { for (const group of method.groups) { if (group.selected_option_id && group.options) { const selected = group.options.find( - (o: FulfillmentOptionResponse) => - o.id === group.selected_option_id, + (o: FulfillmentOptionResponse) => + o.id === group.selected_option_id, ); if (selected) { grandTotal += selected.total; checkout.totals.push({ - type: 'fulfillment', + type: "fulfillment", amount: selected.total, display_text: selected.title, }); @@ -315,21 +350,21 @@ export class CheckoutService { checkout.discounts.applied = []; if (checkout.discounts.codes) { for (const code of checkout.discounts.codes) { - if (typeof code === 'string' && code.toUpperCase() === '10OFF') { + if (typeof code === "string" && code.toUpperCase() === "10OFF") { const discountAmount = Math.floor(grandTotal * 0.1); grandTotal -= discountAmount; checkout.discounts.applied.push({ code, - title: '10% Off', + title: "10% Off", amount: discountAmount, - allocations: [{path: 'subtotal', amount: discountAmount}], + allocations: [{ path: "subtotal", amount: discountAmount }], }); - checkout.totals.push({type: 'discount', amount: discountAmount}); + checkout.totals.push({ type: "discount", amount: discountAmount }); } } } - checkout.totals.push({type: 'total', amount: grandTotal}); + checkout.totals.push({ type: "total", amount: grandTotal }); } private validateInventory(checkout: ExtendedCheckoutResponse): void { @@ -342,10 +377,10 @@ export class CheckoutService { } createCheckout = async (c: Context) => { - const idempotencyKey = c.req.header('Idempotency-Key'); - const ucpAgent = c.req.header('UCP-Agent'); + const idempotencyKey = c.req.header("Idempotency-Key"); + const ucpAgent = c.req.header("UCP-Agent"); const request = await c.req.json(); - let requestHash = ''; + let requestHash = ""; if (idempotencyKey) { requestHash = this.computeHash(request); @@ -353,8 +388,8 @@ export class CheckoutService { if (record) { if (record.request_hash !== requestHash) { return c.json( - {detail: 'Idempotency key reused with different parameters'}, - 409, + { detail: "Idempotency key reused with different parameters" }, + 409, ); } return c.json(JSON.parse(record.response_body), 201); @@ -364,7 +399,7 @@ export class CheckoutService { const checkoutId = uuidv4(); // Log Request - logRequest('POST', '/checkout-sessions', checkoutId, request); + logRequest("POST", "/checkout-sessions", checkoutId, request); try { // Validate items exists and build initial line items from request @@ -377,12 +412,12 @@ export class CheckoutService { const quantity = reqLine.quantity; if (!productId) { - return c.json({detail: `Line item ${i} missing product ID`}, 400); + return c.json({ detail: `Line item ${i} missing product ID` }, 400); } const product = getProduct(productId); if (!product) { - return c.json({detail: `Product ${productId} not found`}, 400); + return c.json({ detail: `Product ${productId} not found` }, 400); } lineItems.push({ @@ -398,23 +433,23 @@ export class CheckoutService { }); } - const {fulfillment: _reqFulfillment, ...requestBody} = request; + const { fulfillment: _reqFulfillment, ...requestBody } = request; const fulfillment = this.constructFulfillmentResponse( - _reqFulfillment, - lineItems, + _reqFulfillment, + lineItems, ); // Construct authoritative checkout const platformConfig = await this.parseAgentProfile(ucpAgent); const checkout: ExtendedCheckoutResponse = { - ...requestBody, // Copy other fields like ucp, etc. + ...requestBody, // Copy other fields like ucp, etc. id: checkoutId, fulfillment, - ucp: {version: '2022-01-01', capabilities: []}, + ucp: { version: "2022-01-01", capabilities: [] }, status: CheckoutResponseStatusSchema.enum.incomplete, - currency: request.currency || 'USD', + currency: request.currency || "USD", line_items: lineItems, totals: [], links: [], @@ -423,12 +458,12 @@ export class CheckoutService { ...request.payment, handlers: [ { - id: 'google_pay', - name: 'google.pay', - version: '2025-03-25', - spec: 'https://example.com/spec', - config_schema: 'https://example.com/schema', - instrument_schemas: ['https://example.com/instrument_schema'], + id: "google_pay", + name: "google.pay", + version: "2025-03-25", + spec: "https://example.com/spec", + config_schema: "https://example.com/schema", + instrument_schemas: ["https://example.com/instrument_schema"], config: {}, }, ], @@ -443,41 +478,41 @@ export class CheckoutService { if (idempotencyKey) { saveIdempotencyRecord( - idempotencyKey, - requestHash, - 201, - JSON.stringify(checkout), + idempotencyKey, + requestHash, + 201, + JSON.stringify(checkout), ); } return c.json(checkout, 201); } catch (e: unknown) { return c.json( - {detail: e instanceof Error ? e.message : String(e)}, - 400, + { detail: e instanceof Error ? e.message : String(e) }, + 400, ); } }; getCheckout = async (c: Context) => { - const id = c.req.param('id'); + const id = c.req.param("id"); // Log Request - logRequest('GET', `/checkout-sessions/${id}`, id, {}); + logRequest("GET", `/checkout-sessions/${id}`, id, {}); const checkout = getCheckoutSession(id); if (!checkout) { - return c.json({detail: 'Checkout session not found'}, 404); + return c.json({ detail: "Checkout session not found" }, 404); } return c.json(checkout, 200); }; updateCheckout = async (c: Context) => { - const id = c.req.param('id'); - const idempotencyKey = c.req.header('Idempotency-Key'); - const ucpAgent = c.req.header('UCP-Agent'); + const id = c.req.param("id"); + const idempotencyKey = c.req.header("Idempotency-Key"); + const ucpAgent = c.req.header("UCP-Agent"); const updateRequest = await c.req.json(); - let requestHash = ''; + let requestHash = ""; if (idempotencyKey) { requestHash = this.computeHash(updateRequest); @@ -485,8 +520,8 @@ export class CheckoutService { if (record) { if (record.request_hash !== requestHash) { return c.json( - {detail: 'Idempotency key reused with different parameters'}, - 409, + { detail: "Idempotency key reused with different parameters" }, + 409, ); } return c.json(JSON.parse(record.response_body), 200); @@ -494,18 +529,20 @@ export class CheckoutService { } // Log Request - logRequest('PUT', `/checkout-sessions/${id}`, id, updateRequest); + logRequest("PUT", `/checkout-sessions/${id}`, id, updateRequest); const existing = getCheckoutSession(id); if (!existing) { - return c.json({detail: 'Checkout session not found'}, 404); + return c.json({ detail: "Checkout session not found" }, 404); } - if (existing.status === CheckoutResponseStatusSchema.enum.completed || - existing.status === CheckoutResponseStatusSchema.enum.canceled) { + if ( + existing.status === CheckoutResponseStatusSchema.enum.completed || + existing.status === CheckoutResponseStatusSchema.enum.canceled + ) { return c.json( - {detail: `Cannot update a ${existing.status} checkout session`}, - 409, + { detail: `Cannot update a ${existing.status} checkout session` }, + 409, ); } @@ -536,11 +573,11 @@ export class CheckoutService { const quantity = reqLine.quantity; if (!productId) { - return c.json({detail: `Line item missing product ID`}, 400); + return c.json({ detail: `Line item missing product ID` }, 400); } const product = getProduct(productId); if (!product) { - return c.json({detail: `Product ${productId} not found`}, 400); + return c.json({ detail: `Product ${productId} not found` }, 400); } newLineItems.push({ @@ -559,9 +596,9 @@ export class CheckoutService { if (updateRequest.fulfillment) { existing.fulfillment = this.constructFulfillmentResponse( - updateRequest.fulfillment, - existing.line_items, - existing.fulfillment, + updateRequest.fulfillment, + existing.line_items, + existing.fulfillment, ); } @@ -574,27 +611,27 @@ export class CheckoutService { if (idempotencyKey) { saveIdempotencyRecord( - idempotencyKey, - requestHash, - 200, - JSON.stringify(existing), + idempotencyKey, + requestHash, + 200, + JSON.stringify(existing), ); } return c.json(existing, 200); } catch (e: unknown) { return c.json( - {detail: e instanceof Error ? e.message : String(e)}, - 400, + { detail: e instanceof Error ? e.message : String(e) }, + 400, ); } }; completeCheckout = async (c: Context) => { - const id = c.req.param('id'); - const idempotencyKey = c.req.header('Idempotency-Key'); + const id = c.req.param("id"); + const idempotencyKey = c.req.header("Idempotency-Key"); const rawBody = await c.req.json(); - let requestHash = ''; + let requestHash = ""; // Idempotency check for payment data if (idempotencyKey) { @@ -603,8 +640,8 @@ export class CheckoutService { if (record) { if (record.request_hash !== requestHash) { return c.json( - {detail: 'Idempotency key reused with different parameters'}, - 409, + { detail: "Idempotency key reused with different parameters" }, + 409, ); } return c.json(JSON.parse(record.response_body), 200); @@ -612,65 +649,70 @@ export class CheckoutService { } // Log Request - logRequest('POST', `/checkout-sessions/${id}/complete`, id, rawBody); + logRequest("POST", `/checkout-sessions/${id}/complete`, id, rawBody); const checkout = getCheckoutSession(id); if (!checkout) { - return c.json({detail: 'Checkout session not found'}, 404); + return c.json({ detail: "Checkout session not found" }, 404); } - if (checkout.status === CheckoutResponseStatusSchema.enum.completed || - checkout.status === CheckoutResponseStatusSchema.enum.canceled) { + if ( + checkout.status === CheckoutResponseStatusSchema.enum.completed || + checkout.status === CheckoutResponseStatusSchema.enum.canceled + ) { // If already completed and not caught by idempotency, it's a conflict - return c.json({detail: `Checkout already completed or canceled`}, 409); + return c.json({ detail: `Checkout already completed or canceled` }, 409); } // Process Payment const selectedInstrument = rawBody.payment_data; if (!selectedInstrument) { - return c.json({detail: 'Missing payment data'}, 400); + return c.json({ detail: "Missing payment data" }, 400); } if (selectedInstrument) { const handlerId = selectedInstrument.handler_id; const credential = selectedInstrument.credential; if (!credential) { - return c.json({detail: 'Missing credentials in instrument'}, 400); + return c.json({ detail: "Missing credentials in instrument" }, 400); } - if (selectedInstrument.type === 'card' && credential.type === 'card') { + if (selectedInstrument.type === "card" && credential.type === "card") { // success } else { const parsedCredential = - ExtendedPaymentCredentialSchema.safeParse(credential); - const token = - parsedCredential.success ? parsedCredential.data.token : undefined; + ExtendedPaymentCredentialSchema.safeParse(credential); + const token = parsedCredential.success + ? parsedCredential.data.token + : undefined; - if (handlerId === 'mock_payment_handler') { - if (token === 'success_token') { + if (handlerId === "mock_payment_handler") { + if (token === "success_token") { // Success - } else if (token === 'fail_token') { + } else if (token === "fail_token") { return c.json( - {detail: 'Payment Failed: Insufficient Funds (Mock)'}, - 402, + { detail: "Payment Failed: Insufficient Funds (Mock)" }, + 402, ); - } else if (token === 'fraud_token') { + } else if (token === "fraud_token") { return c.json( - {detail: 'Payment Failed: Fraud Detected (Mock)'}, - 403, + { detail: "Payment Failed: Fraud Detected (Mock)" }, + 403, ); } else { - return c.json({detail: `Unknown mock token: ${token}`}, 400); + return c.json({ detail: `Unknown mock token: ${token}` }, 400); } } else if ( - handlerId === 'google_pay' || handlerId === 'gpay' || - handlerId === 'shop_pay') { + handlerId === "google_pay" || + handlerId === "gpay" || + handlerId === "shop_pay" + ) { // Mock success } else { return c.json( - {detail: `Unsupported payment handler: ${handlerId}`}, - 400, + { detail: `Unsupported payment handler: ${handlerId}` }, + 400, ); } } @@ -678,7 +720,7 @@ export class CheckoutService { // Atomic Inventory Reservation and Completion try { - const reservedItems: Array<{id: string; qty: number}> = []; + const reservedItems: Array<{ id: string; qty: number }> = []; for (const line of checkout.line_items) { const product = getProduct(line.item.id); @@ -690,11 +732,11 @@ export class CheckoutService { releaseStock(reserved.id, reserved.qty); } return c.json( - {detail: `Item ${line.item.id} is out of stock`}, - 409, + { detail: `Item ${line.item.id} is out of stock` }, + 409, ); } - reservedItems.push({id: line.item.id, qty: line.quantity}); + reservedItems.push({ id: line.item.id, qty: line.quantity }); } } @@ -713,8 +755,8 @@ export class CheckoutService { let fulfillmentAddress: PostalAddress = {}; if (method.selected_destination_id && method.destinations) { const dest = method.destinations.find( - (d: FulfillmentDestinationResponse) => - d.id === method.selected_destination_id, + (d: FulfillmentDestinationResponse) => + d.id === method.selected_destination_id, ); if (dest) { if (dest.address) { @@ -731,8 +773,8 @@ export class CheckoutService { for (const group of method.groups) { if (group.selected_option_id && group.options) { const selected = group.options.find( - (opt: FulfillmentOptionResponse) => - opt.id === group.selected_option_id, + (opt: FulfillmentOptionResponse) => + opt.id === group.selected_option_id, ); if (selected) { const expectationId = `exp_${uuidv4()}`; @@ -741,7 +783,7 @@ export class CheckoutService { if (group.line_item_ids) { for (const liId of group.line_item_ids) { const checkoutLineItem = checkout.line_items.find( - (li) => li.id === liId, + (li) => li.id === liId, ); if (checkoutLineItem) { expLineItems.push({ @@ -767,19 +809,19 @@ export class CheckoutService { } const orderLineItems: OrderLineItem[] = checkout.line_items.map( - (li: LineItemResponse) => { - return { - id: li.id, - item: li.item, - quantity: { - total: li.quantity, - fulfilled: 0, - }, - totals: li.totals, - status: 'processing', - parent_id: li.parent_id, - }; - }, + (li: LineItemResponse) => { + return { + id: li.id, + item: li.item, + quantity: { + total: li.quantity, + fulfilled: 0, + }, + totals: li.totals, + status: "processing", + parent_id: li.parent_id, + }; + }, ); const order: Order = { @@ -803,29 +845,29 @@ export class CheckoutService { saveCheckout(id, checkout.status, checkout); // Notify webhook - await this.notifyWebhook(checkout, 'order_placed'); + await this.notifyWebhook(checkout, "order_placed"); if (idempotencyKey) { saveIdempotencyRecord( - idempotencyKey, - requestHash, - 200, - JSON.stringify(checkout), + idempotencyKey, + requestHash, + 200, + JSON.stringify(checkout), ); } return c.json(checkout, 200); } catch (e) { - console.error('Error completing checkout', e); - return c.json({detail: 'Internal server error'}, 500); + console.error("Error completing checkout", e); + return c.json({ detail: "Internal server error" }, 500); } }; cancelCheckout = async (c: Context) => { - const id = c.req.param('id'); - const idempotencyKey = c.req.header('Idempotency-Key'); - const rawBody = {}; // Empty body for cancel usually - let requestHash = ''; + const id = c.req.param("id"); + const idempotencyKey = c.req.header("Idempotency-Key"); + const rawBody = {}; // Empty body for cancel usually + let requestHash = ""; if (idempotencyKey) { requestHash = this.computeHash(rawBody); @@ -833,26 +875,28 @@ export class CheckoutService { if (record) { if (record.request_hash !== requestHash) { return c.json( - {detail: 'Idempotency key reused with different parameters'}, - 409, + { detail: "Idempotency key reused with different parameters" }, + 409, ); } return c.json(JSON.parse(record.response_body), 200); } } - logRequest('POST', `/checkout-sessions/${id}/cancel`, id, rawBody); + logRequest("POST", `/checkout-sessions/${id}/cancel`, id, rawBody); const checkout = getCheckoutSession(id); if (!checkout) { - return c.json({detail: 'Checkout session not found'}, 404); + return c.json({ detail: "Checkout session not found" }, 404); } - if (checkout.status === CheckoutResponseStatusSchema.enum.completed || - checkout.status === CheckoutResponseStatusSchema.enum.canceled) { + if ( + checkout.status === CheckoutResponseStatusSchema.enum.completed || + checkout.status === CheckoutResponseStatusSchema.enum.canceled + ) { return c.json( - {detail: `Cannot cancel a ${checkout.status} checkout session`}, - 409, + { detail: `Cannot cancel a ${checkout.status} checkout session` }, + 409, ); } @@ -861,20 +905,20 @@ export class CheckoutService { if (idempotencyKey) { saveIdempotencyRecord( - idempotencyKey, - requestHash, - 200, - JSON.stringify(checkout), + idempotencyKey, + requestHash, + 200, + JSON.stringify(checkout), ); } return c.json(checkout, 200); }; - shipOrder = async(orderId: string): Promise => { + shipOrder = async (orderId: string): Promise => { const order = getOrder(orderId); if (!order) { - throw new Error('Order not found'); + throw new Error("Order not found"); } if (!order.fulfillment.events) { @@ -883,7 +927,7 @@ export class CheckoutService { order.fulfillment.events.push({ id: `evt_${uuidv4()}`, - type: 'shipped', + type: "shipped", occurred_at: new Date(), line_items: [], }); @@ -892,7 +936,7 @@ export class CheckoutService { const checkout = getCheckoutSession(order.checkout_id); if (checkout) { - await this.notifyWebhook(checkout, 'order_shipped'); + await this.notifyWebhook(checkout, "order_shipped"); } }; } diff --git a/rest/nodejs/src/api/discovery.ts b/rest/nodejs/src/api/discovery.ts index 2f70284..14b992f 100644 --- a/rest/nodejs/src/api/discovery.ts +++ b/rest/nodejs/src/api/discovery.ts @@ -1,5 +1,5 @@ -import {type Context} from 'hono'; -import {type UcpDiscoveryProfile} from '../models'; +import { type Context } from "hono"; +import { type UcpDiscoveryProfile } from "../models"; /** * Service for handling UCP discovery requests. @@ -10,7 +10,7 @@ import {type UcpDiscoveryProfile} from '../models'; * capabilities (checkout, order, etc.), and supported payment handlers. */ export class DiscoveryService { - readonly ucpVersion = '2026-01-11'; + readonly ucpVersion = "2026-01-11"; /** * Returns the merchant profile, detailing the server's UCP configuration. @@ -28,108 +28,108 @@ export class DiscoveryService { ucp: { version: this.ucpVersion, services: { - 'dev.ucp.shopping': { + "dev.ucp.shopping": { version: this.ucpVersion, - spec: 'https://ucp.dev/specs/shopping', + spec: "https://ucp.dev/specs/shopping", rest: { - schema: 'https://ucp.dev/services/shopping/openapi.json', - endpoint: 'http://localhost:3000', + schema: "https://ucp.dev/services/shopping/openapi.json", + endpoint: "http://localhost:3000", }, }, }, capabilities: [ { - name: 'dev.ucp.shopping.checkout', + name: "dev.ucp.shopping.checkout", version: this.ucpVersion, - spec: 'https://ucp.dev/specs/shopping/checkout', - schema: 'https://ucp.dev/schemas/shopping/checkout.json', + spec: "https://ucp.dev/specs/shopping/checkout", + schema: "https://ucp.dev/schemas/shopping/checkout.json", }, { - name: 'dev.ucp.shopping.order', + name: "dev.ucp.shopping.order", version: this.ucpVersion, - spec: 'https://ucp.dev/specs/shopping/order', - schema: 'https://ucp.dev/schemas/shopping/order.json', + spec: "https://ucp.dev/specs/shopping/order", + schema: "https://ucp.dev/schemas/shopping/order.json", }, { - name: 'dev.ucp.shopping.refund', + name: "dev.ucp.shopping.refund", version: this.ucpVersion, - spec: 'https://ucp.dev/specs/shopping/refund', - schema: 'https://ucp.dev/schemas/shopping/refund.json', - extends: 'dev.ucp.shopping.order', + spec: "https://ucp.dev/specs/shopping/refund", + schema: "https://ucp.dev/schemas/shopping/refund.json", + extends: "dev.ucp.shopping.order", }, { - name: 'dev.ucp.shopping.return', + name: "dev.ucp.shopping.return", version: this.ucpVersion, - spec: 'https://ucp.dev/specs/shopping/return', - schema: 'https://ucp.dev/schemas/shopping/return.json', - extends: 'dev.ucp.shopping.order', + spec: "https://ucp.dev/specs/shopping/return", + schema: "https://ucp.dev/schemas/shopping/return.json", + extends: "dev.ucp.shopping.order", }, { - name: 'dev.ucp.shopping.dispute', + name: "dev.ucp.shopping.dispute", version: this.ucpVersion, - spec: 'https://ucp.dev/specs/shopping/dispute', - schema: 'https://ucp.dev/schemas/shopping/dispute.json', - extends: 'dev.ucp.shopping.order', + spec: "https://ucp.dev/specs/shopping/dispute", + schema: "https://ucp.dev/schemas/shopping/dispute.json", + extends: "dev.ucp.shopping.order", }, { - name: 'dev.ucp.shopping.discount', + name: "dev.ucp.shopping.discount", version: this.ucpVersion, - spec: 'https://ucp.dev/specs/shopping/discount', - schema: 'https://ucp.dev/schemas/shopping/discount.json', - extends: 'dev.ucp.shopping.checkout', + spec: "https://ucp.dev/specs/shopping/discount", + schema: "https://ucp.dev/schemas/shopping/discount.json", + extends: "dev.ucp.shopping.checkout", }, { - name: 'dev.ucp.shopping.fulfillment', + name: "dev.ucp.shopping.fulfillment", version: this.ucpVersion, - spec: 'https://ucp.dev/specs/shopping/fulfillment', - schema: 'https://ucp.dev/schemas/shopping/fulfillment.json', - extends: 'dev.ucp.shopping.checkout', + spec: "https://ucp.dev/specs/shopping/fulfillment", + schema: "https://ucp.dev/schemas/shopping/fulfillment.json", + extends: "dev.ucp.shopping.checkout", }, { - name: 'dev.ucp.shopping.buyer_consent', + name: "dev.ucp.shopping.buyer_consent", version: this.ucpVersion, - spec: 'https://ucp.dev/specs/shopping/buyer_consent', - schema: 'https://ucp.dev/schemas/shopping/buyer_consent.json', - extends: 'dev.ucp.shopping.checkout', + spec: "https://ucp.dev/specs/shopping/buyer_consent", + schema: "https://ucp.dev/schemas/shopping/buyer_consent.json", + extends: "dev.ucp.shopping.checkout", }, ], }, payment: { handlers: [ { - id: 'shop_pay', - name: 'com.shopify.shop_pay', - version: '2026-01-11', - spec: 'https://shopify.dev/ucp/handlers/shop_pay', + id: "shop_pay", + name: "com.shopify.shop_pay", + version: "2026-01-11", + spec: "https://shopify.dev/ucp/handlers/shop_pay", config_schema: - 'https://shopify.dev/ucp/handlers/shop_pay/config.json', + "https://shopify.dev/ucp/handlers/shop_pay/config.json", instrument_schemas: [ - 'https://shopify.dev/ucp/handlers/shop_pay/instrument.json', + "https://shopify.dev/ucp/handlers/shop_pay/instrument.json", ], config: { - shop_id: 'test-shop-id', + shop_id: "test-shop-id", }, }, { - id: 'google_pay', - name: 'google.pay', - version: '1.0', - spec: 'https://example.com/spec', - config_schema: 'https://example.com/schema', + id: "google_pay", + name: "google.pay", + version: "1.0", + spec: "https://example.com/spec", + config_schema: "https://example.com/schema", instrument_schemas: [], config: {}, }, { - id: 'mock_payment_handler', - name: 'dev.ucp.mock_payment', - version: '1.0', - spec: 'https://ucp.dev/specs/mock', - config_schema: 'https://ucp.dev/schemas/mock.json', + id: "mock_payment_handler", + name: "dev.ucp.mock_payment", + version: "1.0", + spec: "https://ucp.dev/specs/mock", + config_schema: "https://ucp.dev/schemas/mock.json", instrument_schemas: [ - 'https://ucp.dev/schemas/shopping/types/card_payment_instrument.json', + "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json", ], config: { - supported_tokens: ['success_token', 'fail_token'], + supported_tokens: ["success_token", "fail_token"], }, }, ], diff --git a/rest/nodejs/src/api/order.ts b/rest/nodejs/src/api/order.ts index 463172b..a65f25c 100644 --- a/rest/nodejs/src/api/order.ts +++ b/rest/nodejs/src/api/order.ts @@ -1,31 +1,31 @@ -import {type Context} from 'hono'; -import {getOrder, logRequest, saveOrder} from '../data'; -import {type Order} from '../models'; +import { type Context } from "hono"; +import { getOrder, logRequest, saveOrder } from "../data"; +import { type Order } from "../models"; /** * Service for managing orders. */ export class OrderService { getOrder = async (c: Context) => { - const id = c.req.param('id'); + const id = c.req.param("id"); // Log Request - logRequest('GET', `/orders/${id}`, undefined, {}); + logRequest("GET", `/orders/${id}`, undefined, {}); const order = getOrder(id); if (!order) { - return c.json({error: 'Order not found'}, 404); + return c.json({ error: "Order not found" }, 404); } return c.json(order, 200); }; updateOrder = async (c: Context) => { - const id = c.req.param('id'); + const id = c.req.param("id"); const updateRequest = await c.req.json(); // Log Request logRequest( - 'PUT', + "PUT", `/orders/${id}`, updateRequest.checkout_id, updateRequest, @@ -33,7 +33,7 @@ export class OrderService { const existing = getOrder(id); if (!existing) { - return c.json({error: 'Order not found'}, 404); + return c.json({ error: "Order not found" }, 404); } // Ensure ID matches diff --git a/rest/nodejs/src/api/testing.ts b/rest/nodejs/src/api/testing.ts index 6425aac..00b2144 100644 --- a/rest/nodejs/src/api/testing.ts +++ b/rest/nodejs/src/api/testing.ts @@ -1,28 +1,28 @@ -import {type Context} from 'hono'; +import { type Context } from "hono"; -import {CheckoutService} from './checkout'; +import { CheckoutService } from "./checkout"; export class TestingService { constructor(private readonly checkoutService: CheckoutService) {} shipOrder = async (c: Context) => { - const secret = c.req.header('Simulation-Secret'); + const secret = c.req.header("Simulation-Secret"); const expectedSecret = - process.env.SIMULATION_SECRET || 'super-secret-sim-key'; + process.env.SIMULATION_SECRET || "super-secret-sim-key"; if (secret !== expectedSecret) { - return c.json({detail: 'Invalid Simulation Secret'}, 403); + return c.json({ detail: "Invalid Simulation Secret" }, 403); } - const id = c.req.param('id'); + const id = c.req.param("id"); try { await this.checkoutService.shipOrder(id); - return c.json({status: 'shipped'}, 200); + return c.json({ status: "shipped" }, 200); } catch (e: any) { - if (e.message === 'Order not found') { - return c.json({detail: 'Order not found'}, 404); + if (e.message === "Order not found") { + return c.json({ detail: "Order not found" }, 404); } - return c.json({detail: e.message}, 500); + return c.json({ detail: e.message }, 500); } }; } diff --git a/rest/nodejs/src/data/db.ts b/rest/nodejs/src/data/db.ts index 4959b28..e4afb70 100644 --- a/rest/nodejs/src/data/db.ts +++ b/rest/nodejs/src/data/db.ts @@ -1,4 +1,4 @@ -import Database from 'better-sqlite3'; +import Database from "better-sqlite3"; let productsDb: Database.Database | null = null; let transactionsDb: Database.Database | null = null; @@ -76,7 +76,7 @@ export function initDbs(productsPath: string, transactionsPath: string) { */ export function getProductsDb(): Database.Database { if (!productsDb) { - throw new Error('Products DB not initialized. Call initDbs first.'); + throw new Error("Products DB not initialized. Call initDbs first."); } return productsDb; } @@ -89,7 +89,7 @@ export function getProductsDb(): Database.Database { */ export function getTransactionsDb(): Database.Database { if (!transactionsDb) { - throw new Error('Transactions DB not initialized. Call initDbs first.'); + throw new Error("Transactions DB not initialized. Call initDbs first."); } return transactionsDb; } diff --git a/rest/nodejs/src/data/index.ts b/rest/nodejs/src/data/index.ts index 715ffd4..4d5e042 100644 --- a/rest/nodejs/src/data/index.ts +++ b/rest/nodejs/src/data/index.ts @@ -4,7 +4,7 @@ * transactions databases (SQLite). */ -export * from './db'; -export * from './inventory'; -export * from './products'; -export * from './transactions'; +export * from "./db"; +export * from "./inventory"; +export * from "./products"; +export * from "./transactions"; diff --git a/rest/nodejs/src/data/inventory.ts b/rest/nodejs/src/data/inventory.ts index bb2a966..d23d373 100644 --- a/rest/nodejs/src/data/inventory.ts +++ b/rest/nodejs/src/data/inventory.ts @@ -1,4 +1,4 @@ -import {getTransactionsDb} from './db'; +import { getTransactionsDb } from "./db"; /** * Retrieves the available inventory quantity for a given product. @@ -9,9 +9,9 @@ import {getTransactionsDb} from './db'; export function getInventory(productId: string): number | undefined { const db = getTransactionsDb(); const stmt = db.prepare( - 'SELECT quantity FROM inventory WHERE product_id = ?', + "SELECT quantity FROM inventory WHERE product_id = ?", ); - const result = stmt.get(productId) as {quantity: number} | undefined; + const result = stmt.get(productId) as { quantity: number } | undefined; return result?.quantity; } diff --git a/rest/nodejs/src/data/products.ts b/rest/nodejs/src/data/products.ts index 203ffb4..f6c3623 100644 --- a/rest/nodejs/src/data/products.ts +++ b/rest/nodejs/src/data/products.ts @@ -1,4 +1,4 @@ -import {getProductsDb} from './db'; +import { getProductsDb } from "./db"; /** * Represents a product in the catalog. @@ -19,7 +19,7 @@ export interface Product { export function getProduct(productId: string): Product | undefined { const db = getProductsDb(); const stmt = db.prepare( - 'SELECT id, title, price, image_url FROM products WHERE id = ?', + "SELECT id, title, price, image_url FROM products WHERE id = ?", ); const result = stmt.get(productId) as Product | undefined; return result; diff --git a/rest/nodejs/src/data/transactions.ts b/rest/nodejs/src/data/transactions.ts index 0198755..c2369a8 100644 --- a/rest/nodejs/src/data/transactions.ts +++ b/rest/nodejs/src/data/transactions.ts @@ -1,6 +1,6 @@ -import {type ExtendedCheckoutResponse, type Order} from '../models'; +import { type ExtendedCheckoutResponse, type Order } from "../models"; -import {getTransactionsDb} from './db'; +import { getTransactionsDb } from "./db"; /** * Represents the structure of a checkout session stored in the database. @@ -8,7 +8,7 @@ import {getTransactionsDb} from './db'; export interface CheckoutSession { id: string; status: string; - data: string; // JSON string + data: string; // JSON string } export interface IdempotencyRecord { @@ -29,26 +29,26 @@ export interface IdempotencyRecord { * @param checkoutObj The full checkout object to be serialized and stored. */ export function saveCheckout( - checkoutId: string, - status: string, - checkoutObj: ExtendedCheckoutResponse, - ): void { + checkoutId: string, + status: string, + checkoutObj: ExtendedCheckoutResponse, +): void { const db = getTransactionsDb(); // Check if exists - const existingStmt = db.prepare('SELECT id FROM checkouts WHERE id = ?'); + const existingStmt = db.prepare("SELECT id FROM checkouts WHERE id = ?"); const existing = existingStmt.get(checkoutId); const dataStr = JSON.stringify(checkoutObj); if (existing) { const updateStmt = db.prepare( - 'UPDATE checkouts SET status = ?, data = ? WHERE id = ?', + "UPDATE checkouts SET status = ?, data = ? WHERE id = ?", ); updateStmt.run(status, dataStr, checkoutId); } else { const insertStmt = db.prepare( - 'INSERT INTO checkouts (id, status, data) VALUES (?, ?, ?)', + "INSERT INTO checkouts (id, status, data) VALUES (?, ?, ?)", ); insertStmt.run(checkoutId, status, dataStr); } @@ -63,17 +63,17 @@ export function saveCheckout( * undefined. */ export function getCheckoutSession( - checkoutId: string, - ): ExtendedCheckoutResponse|undefined { + checkoutId: string, +): ExtendedCheckoutResponse | undefined { const db = getTransactionsDb(); - const stmt = db.prepare('SELECT data FROM checkouts WHERE id = ?'); - const result = stmt.get(checkoutId) as {data: string} | undefined; + const stmt = db.prepare("SELECT data FROM checkouts WHERE id = ?"); + const result = stmt.get(checkoutId) as { data: string } | undefined; if (result) { try { return JSON.parse(result.data) as ExtendedCheckoutResponse; } catch (e) { - console.error('Failed to parse checkout data', e); + console.error("Failed to parse checkout data", e); return undefined; } } @@ -84,30 +84,30 @@ export function saveOrder(orderId: string, orderObj: Order): void { const db = getTransactionsDb(); const dataStr = JSON.stringify(orderObj); - const existingStmt = db.prepare('SELECT id FROM orders WHERE id = ?'); + const existingStmt = db.prepare("SELECT id FROM orders WHERE id = ?"); const existing = existingStmt.get(orderId); if (existing) { - const updateStmt = db.prepare('UPDATE orders SET data = ? WHERE id = ?'); + const updateStmt = db.prepare("UPDATE orders SET data = ? WHERE id = ?"); updateStmt.run(dataStr, orderId); } else { const insertStmt = db.prepare( - 'INSERT INTO orders (id, data) VALUES (?, ?)', + "INSERT INTO orders (id, data) VALUES (?, ?)", ); insertStmt.run(orderId, dataStr); } } -export function getOrder(orderId: string): Order|undefined { +export function getOrder(orderId: string): Order | undefined { const db = getTransactionsDb(); - const stmt = db.prepare('SELECT data FROM orders WHERE id = ?'); - const result = stmt.get(orderId) as {data: string} | undefined; + const stmt = db.prepare("SELECT data FROM orders WHERE id = ?"); + const result = stmt.get(orderId) as { data: string } | undefined; if (result) { try { return JSON.parse(result.data) as Order; } catch (e) { - console.error('Failed to parse order data', e); + console.error("Failed to parse order data", e); return undefined; } } @@ -115,37 +115,37 @@ export function getOrder(orderId: string): Order|undefined { } export function logRequest( - method: string, - url: string, - checkoutId: string|undefined, - payload: unknown, - ): void { + method: string, + url: string, + checkoutId: string | undefined, + payload: unknown, +): void { const db = getTransactionsDb(); const stmt = db.prepare( - 'INSERT INTO request_logs (method, url, checkout_id, payload) VALUES (?, ?, ?, ?)', + "INSERT INTO request_logs (method, url, checkout_id, payload) VALUES (?, ?, ?, ?)", ); stmt.run(method, url, checkoutId || null, JSON.stringify(payload)); } export function getIdempotencyRecord( - key: string, - ): IdempotencyRecord|undefined { + key: string, +): IdempotencyRecord | undefined { const db = getTransactionsDb(); const stmt = db.prepare( - 'SELECT key, request_hash, response_status, response_body FROM idempotency_keys WHERE key = ?', + "SELECT key, request_hash, response_status, response_body FROM idempotency_keys WHERE key = ?", ); return stmt.get(key) as IdempotencyRecord | undefined; } export function saveIdempotencyRecord( - key: string, - requestHash: string, - status: number, - responseBody: string, - ): void { + key: string, + requestHash: string, + status: number, + responseBody: string, +): void { const db = getTransactionsDb(); const stmt = db.prepare( - 'INSERT OR REPLACE INTO idempotency_keys (key, request_hash, response_status, response_body) VALUES (?, ?, ?, ?)', + "INSERT OR REPLACE INTO idempotency_keys (key, request_hash, response_status, response_body) VALUES (?, ?, ?, ?)", ); stmt.run(key, requestHash, status, responseBody); } diff --git a/rest/nodejs/src/index.ts b/rest/nodejs/src/index.ts index f03ce15..7ffdf3d 100644 --- a/rest/nodejs/src/index.ts +++ b/rest/nodejs/src/index.ts @@ -1,20 +1,24 @@ -import {serve} from '@hono/node-server'; -import {zValidator} from '@hono/zod-validator'; -import {type Context, Hono} from 'hono'; -import {requestId} from 'hono/request-id'; -import {pinoHttp} from 'pino-http'; +import { serve } from "@hono/node-server"; +import { zValidator } from "@hono/zod-validator"; +import { type Context, Hono } from "hono"; +import { requestId } from "hono/request-id"; +import { pinoHttp } from "pino-http"; -import {CheckoutService, zCompleteCheckoutRequest} from './api/checkout'; -import {DiscoveryService} from './api/discovery'; -import {OrderService} from './api/order'; -import {TestingService} from './api/testing'; -import {initDbs} from './data/db'; -import {ExtendedCheckoutCreateRequestSchema, ExtendedCheckoutUpdateRequestSchema, OrderSchema,} from './models'; -import {IdParamSchema, prettyValidation} from './utils/validation'; +import { CheckoutService, zCompleteCheckoutRequest } from "./api/checkout"; +import { DiscoveryService } from "./api/discovery"; +import { OrderService } from "./api/order"; +import { TestingService } from "./api/testing"; +import { initDbs } from "./data/db"; +import { + ExtendedCheckoutCreateRequestSchema, + ExtendedCheckoutUpdateRequestSchema, + OrderSchema, +} from "./models"; +import { IdParamSchema, prettyValidation } from "./utils/validation"; const app = new Hono(); -initDbs('databases/products.db', 'databases/transactions.db'); +initDbs("databases/products.db", "databases/transactions.db"); const checkoutService = new CheckoutService(); const orderService = new OrderService(); @@ -30,7 +34,7 @@ app.use(async (c: Context, next: () => Promise) => { pinoHttp({ quietReqLogger: true, transport: { - target: 'pino-http-print', + target: "pino-http-print", options: { destination: 1, all: true, @@ -40,14 +44,14 @@ app.use(async (c: Context, next: () => Promise) => { })(c.env.incoming, c.env.outgoing, () => resolve()), ); - c.set('logger', c.env.incoming.log); + c.set("logger", c.env.incoming.log); await next(); }); // Middleware for Version Negotiation app.use(async (c: Context, next: () => Promise) => { - const ucpAgent = c.req.header('UCP-Agent'); + const ucpAgent = c.req.header("UCP-Agent"); if (ucpAgent) { // Simple regex to find version="YYYY-MM-DD" const match = ucpAgent.match(/version="([^"]+)"/); @@ -58,7 +62,7 @@ app.use(async (c: Context, next: () => Promise) => { // Ideally we'd parse and check compatibility. if (clientVersion > serverVersion) { return c.json( - {error: `Unsupported UCP version: ${clientVersion}`}, + { error: `Unsupported UCP version: ${clientVersion}` }, 400, ); } @@ -68,55 +72,55 @@ app.use(async (c: Context, next: () => Promise) => { }); /* Discovery endpoints */ -app.get('/.well-known/ucp', discoveryService.getMerchantProfile); +app.get("/.well-known/ucp", discoveryService.getMerchantProfile); /* Checkout Capability endpoints */ app.post( - '/checkout-sessions', - zValidator('json', ExtendedCheckoutCreateRequestSchema, prettyValidation), + "/checkout-sessions", + zValidator("json", ExtendedCheckoutCreateRequestSchema, prettyValidation), checkoutService.createCheckout, ); app.get( - '/checkout-sessions/:id', - zValidator('param', IdParamSchema, prettyValidation), + "/checkout-sessions/:id", + zValidator("param", IdParamSchema, prettyValidation), checkoutService.getCheckout, ); app.put( - '/checkout-sessions/:id', - zValidator('param', IdParamSchema, prettyValidation), - zValidator('json', ExtendedCheckoutUpdateRequestSchema, prettyValidation), + "/checkout-sessions/:id", + zValidator("param", IdParamSchema, prettyValidation), + zValidator("json", ExtendedCheckoutUpdateRequestSchema, prettyValidation), checkoutService.updateCheckout, ); app.post( - '/checkout-sessions/:id/complete', - zValidator('param', IdParamSchema, prettyValidation), - zValidator('json', zCompleteCheckoutRequest, prettyValidation), + "/checkout-sessions/:id/complete", + zValidator("param", IdParamSchema, prettyValidation), + zValidator("json", zCompleteCheckoutRequest, prettyValidation), checkoutService.completeCheckout, ); app.post( - '/checkout-sessions/:id/cancel', - zValidator('param', IdParamSchema, prettyValidation), + "/checkout-sessions/:id/cancel", + zValidator("param", IdParamSchema, prettyValidation), checkoutService.cancelCheckout, ); /* Order Capability endpoints */ app.get( - '/orders/:id', - zValidator('param', IdParamSchema, prettyValidation), + "/orders/:id", + zValidator("param", IdParamSchema, prettyValidation), orderService.getOrder, ); app.put( - '/orders/:id', - zValidator('param', IdParamSchema, prettyValidation), - zValidator('json', OrderSchema, prettyValidation), + "/orders/:id", + zValidator("param", IdParamSchema, prettyValidation), + zValidator("json", OrderSchema, prettyValidation), orderService.updateOrder, ); /* Testing endpoints */ app.post( - '/testing/simulate-shipping/:id', - zValidator('param', IdParamSchema, prettyValidation), - testingService.shipOrder, + "/testing/simulate-shipping/:id", + zValidator("param", IdParamSchema, prettyValidation), + testingService.shipOrder, ); serve( diff --git a/rest/nodejs/src/models/extensions.ts b/rest/nodejs/src/models/extensions.ts index 14804cf..7c2b3f5 100644 --- a/rest/nodejs/src/models/extensions.ts +++ b/rest/nodejs/src/models/extensions.ts @@ -1,12 +1,24 @@ -import {z} from 'zod'; +import { z } from "zod"; -import {CheckoutCreateRequestSchema, CheckoutUpdateRequestSchema, CheckoutWithBuyerConsentSchema, CheckoutWithDiscountSchema, CheckoutWithFulfillmentCreateRequestSchema, CheckoutWithFulfillmentResponseSchema, CheckoutWithFulfillmentUpdateRequestSchema, FulfillmentDestinationResponseSchema, OrderSchema, PaymentCredentialSchema} from './spec_generated'; +import { + CheckoutCreateRequestSchema, + CheckoutUpdateRequestSchema, + CheckoutWithBuyerConsentSchema, + CheckoutWithDiscountSchema, + CheckoutWithFulfillmentCreateRequestSchema, + CheckoutWithFulfillmentResponseSchema, + CheckoutWithFulfillmentUpdateRequestSchema, + FulfillmentDestinationResponseSchema, + OrderSchema, + PaymentCredentialSchema, +} from "./spec_generated"; export const ExtendedPaymentCredentialSchema = PaymentCredentialSchema.extend({ token: z.string().optional(), }); -export type ExtendedPaymentCredential = - z.infer; +export type ExtendedPaymentCredential = z.infer< + typeof ExtendedPaymentCredentialSchema +>; export const PlatformConfigSchema = z.object({ webhook_url: z.string().url().optional(), @@ -14,38 +26,38 @@ export const PlatformConfigSchema = z.object({ export type PlatformConfig = z.infer; export const ExtendedCheckoutResponseSchema = - CheckoutWithFulfillmentResponseSchema - .extend(CheckoutWithDiscountSchema.shape) - .extend(CheckoutWithBuyerConsentSchema.shape) - .extend({ - 'order_id': z.string().optional(), - 'order_permalink_url': z.string().optional(), - 'platform': PlatformConfigSchema.optional(), - }); -export type ExtendedCheckoutResponse = - z.infer; + CheckoutWithFulfillmentResponseSchema.extend(CheckoutWithDiscountSchema.shape) + .extend(CheckoutWithBuyerConsentSchema.shape) + .extend({ + order_id: z.string().optional(), + order_permalink_url: z.string().optional(), + platform: PlatformConfigSchema.optional(), + }); +export type ExtendedCheckoutResponse = z.infer< + typeof ExtendedCheckoutResponseSchema +>; export const ExtendedCheckoutCreateRequestSchema = - CheckoutCreateRequestSchema - .extend( - CheckoutWithFulfillmentCreateRequestSchema.pick({fulfillment: true}) - .shape, - ) - .extend(CheckoutWithDiscountSchema.pick({discounts: true}).shape) - .extend(CheckoutWithBuyerConsentSchema.pick({buyer: true}).shape) -export type ExtendedCheckoutCreateRequest = - z.infer; + CheckoutCreateRequestSchema.extend( + CheckoutWithFulfillmentCreateRequestSchema.pick({ fulfillment: true }) + .shape, + ) + .extend(CheckoutWithDiscountSchema.pick({ discounts: true }).shape) + .extend(CheckoutWithBuyerConsentSchema.pick({ buyer: true }).shape); +export type ExtendedCheckoutCreateRequest = z.infer< + typeof ExtendedCheckoutCreateRequestSchema +>; export const ExtendedCheckoutUpdateRequestSchema = - CheckoutUpdateRequestSchema - .extend( - CheckoutWithFulfillmentUpdateRequestSchema.pick({fulfillment: true}) - .shape, - ) - .extend(CheckoutWithDiscountSchema.pick({discounts: true}).shape) - .extend(CheckoutWithBuyerConsentSchema.pick({buyer: true}).shape) -export type ExtendedCheckoutUpdateRequest = - z.infer; + CheckoutUpdateRequestSchema.extend( + CheckoutWithFulfillmentUpdateRequestSchema.pick({ fulfillment: true }) + .shape, + ) + .extend(CheckoutWithDiscountSchema.pick({ discounts: true }).shape) + .extend(CheckoutWithBuyerConsentSchema.pick({ buyer: true }).shape); +export type ExtendedCheckoutUpdateRequest = z.infer< + typeof ExtendedCheckoutUpdateRequestSchema +>; export const OrderUpdateSchema = OrderSchema; export type OrderUpdate = z.infer; diff --git a/rest/nodejs/src/models/index.ts b/rest/nodejs/src/models/index.ts index 4c43fd0..28e9880 100644 --- a/rest/nodejs/src/models/index.ts +++ b/rest/nodejs/src/models/index.ts @@ -1,2 +1,2 @@ -export * from './extensions'; -export * from './spec_generated'; +export * from "./extensions"; +export * from "./spec_generated"; diff --git a/rest/nodejs/src/models/spec_generated.ts b/rest/nodejs/src/models/spec_generated.ts index 369dc41..50920ce 100644 --- a/rest/nodejs/src/models/spec_generated.ts +++ b/rest/nodejs/src/models/spec_generated.ts @@ -1,51 +1,40 @@ -import * as z from 'zod'; +import * as z from "zod"; // Key usage. Should be 'sig' for signing keys. -export const UseSchema = z.enum([ - 'enc', - 'sig', -]); +export const UseSchema = z.enum(["enc", "sig"]); export type Use = z.infer; // The type of card number. Network tokens are preferred with fallback to FPAN. // See PCI Scope for more details. -export const CardNumberTypeSchema = z.enum([ - 'dpan', - 'fpan', - 'network_token', -]); +export const CardNumberTypeSchema = z.enum(["dpan", "fpan", "network_token"]); export type CardNumberType = z.infer; // A URI pointing to a schema definition (e.g., JSON Schema) used to validate // the structure of the instrument object. -export const CardPaymentInstrumentTypeSchema = z.enum([ - 'card', -]); -export type CardPaymentInstrumentType = - z.infer; +export const CardPaymentInstrumentTypeSchema = z.enum(["card"]); +export type CardPaymentInstrumentType = z.infer< + typeof CardPaymentInstrumentTypeSchema +>; // Type of total categorization. export const TotalResponseTypeSchema = z.enum([ - 'discount', - 'fee', - 'fulfillment', - 'items_discount', - 'subtotal', - 'tax', - 'total', + "discount", + "fee", + "fulfillment", + "items_discount", + "subtotal", + "tax", + "total", ]); export type TotalResponseType = z.infer; // Content format, default = plain. -export const ContentTypeSchema = z.enum([ - 'markdown', - 'plain', -]); +export const ContentTypeSchema = z.enum(["markdown", "plain"]); export type ContentType = z.infer; // Declares who resolves this error. 'recoverable': agent can fix via API. @@ -56,59 +45,51 @@ export type ContentType = z.infer; // 'requires_*' severity contribute to 'status: requires_escalation'. export const SeveritySchema = z.enum([ - 'recoverable', - 'requires_buyer_input', - 'requires_buyer_review', + "recoverable", + "requires_buyer_input", + "requires_buyer_review", ]); export type Severity = z.infer; - -export const MessageTypeSchema = z.enum([ - 'error', - 'info', - 'warning', -]); +export const MessageTypeSchema = z.enum(["error", "info", "warning"]); export type MessageType = z.infer; // Checkout state indicating the current phase and required action. See Checkout // Status lifecycle documentation for state transition details. export const CheckoutResponseStatusSchema = z.enum([ - 'canceled', - 'complete_in_progress', - 'completed', - 'incomplete', - 'ready_for_complete', - 'requires_escalation', + "canceled", + "complete_in_progress", + "completed", + "incomplete", + "ready_for_complete", + "requires_escalation", ]); -export type CheckoutResponseStatus = - z.infer; +export type CheckoutResponseStatus = z.infer< + typeof CheckoutResponseStatusSchema +>; // Adjustment status. export const AdjustmentStatusSchema = z.enum([ - 'completed', - 'failed', - 'pending', + "completed", + "failed", + "pending", ]); export type AdjustmentStatus = z.infer; // Delivery method type (shipping, pickup, digital). -export const MethodTypeSchema = z.enum([ - 'digital', - 'pickup', - 'shipping', -]); +export const MethodTypeSchema = z.enum(["digital", "pickup", "shipping"]); export type MethodType = z.infer; // Derived status: fulfilled if quantity.fulfilled == quantity.total, partial if // quantity.fulfilled > 0, otherwise processing. export const OrderLineItemStatusSchema = z.enum([ - 'fulfilled', - 'partial', - 'processing', + "fulfilled", + "partial", + "processing", ]); export type OrderLineItemStatus = z.infer; @@ -116,906 +97,920 @@ export type OrderLineItemStatus = z.infer; // // Fulfillment method type. -export const TypeElementSchema = z.enum([ - 'pickup', - 'shipping', -]); +export const TypeElementSchema = z.enum(["pickup", "shipping"]); export type TypeElement = z.infer; - -export const MessageErrorTypeSchema = z.enum([ - 'error', -]); +export const MessageErrorTypeSchema = z.enum(["error"]); export type MessageErrorType = z.infer; - -export const MessageInfoTypeSchema = z.enum([ - 'info', -]); +export const MessageInfoTypeSchema = z.enum(["info"]); export type MessageInfoType = z.infer; - -export const MessageWarningTypeSchema = z.enum([ - 'warning', -]); +export const MessageWarningTypeSchema = z.enum(["warning"]); export type MessageWarningType = z.infer; // Current fulfillment status. export const OrderFulfillmentDetailStatusSchema = z.enum([ - 'canceled', - 'delivered', - 'failed_attempt', - 'in_transit', - 'out_for_delivery', - 'processing', - 'shipped', + "canceled", + "delivered", + "failed_attempt", + "in_transit", + "out_for_delivery", + "processing", + "shipped", ]); -export type OrderFulfillmentDetailStatus = - z.infer; +export type OrderFulfillmentDetailStatus = z.infer< + typeof OrderFulfillmentDetailStatusSchema +>; // Allocation method. 'each' = applied independently per item. 'across' = split // proportionally by value. -export const MethodSchema = z.enum([ - 'across', - 'each', -]); +export const MethodSchema = z.enum(["across", "each"]); export type Method = z.infer; export const PaymentHandlerResponseSchema = z.object({ - 'config': z.record(z.string(), z.any()), - 'config_schema': z.string(), - 'id': z.string(), - 'instrument_schemas': z.array(z.string()), - 'name': z.string(), - 'spec': z.string(), - 'version': z.string(), -}); -export type PaymentHandlerResponse = - z.infer; + config: z.record(z.string(), z.any()), + config_schema: z.string(), + id: z.string(), + instrument_schemas: z.array(z.string()), + name: z.string(), + spec: z.string(), + version: z.string(), +}); +export type PaymentHandlerResponse = z.infer< + typeof PaymentHandlerResponseSchema +>; export const SigningKeySchema = z.object({ - 'alg': z.string().optional(), - 'crv': z.string().optional(), - 'e': z.string().optional(), - 'kid': z.string(), - 'kty': z.string(), - 'n': z.string().optional(), - 'use': UseSchema.optional(), - 'x': z.string().optional(), - 'y': z.string().optional(), + alg: z.string().optional(), + crv: z.string().optional(), + e: z.string().optional(), + kid: z.string(), + kty: z.string(), + n: z.string().optional(), + use: UseSchema.optional(), + x: z.string().optional(), + y: z.string().optional(), }); export type SigningKey = z.infer; export const CapabilityDiscoverySchema = z.object({ - 'config': z.record(z.string(), z.any()).optional(), - 'extends': z.string().optional(), - 'name': z.string(), - 'schema': z.string(), - 'spec': z.string(), - 'version': z.string(), + config: z.record(z.string(), z.any()).optional(), + extends: z.string().optional(), + name: z.string(), + schema: z.string(), + spec: z.string(), + version: z.string(), }); export type CapabilityDiscovery = z.infer; export const A2ASchema = z.object({ - 'endpoint': z.string(), + endpoint: z.string(), }); export type A2A = z.infer; export const EmbeddedSchema = z.object({ - 'schema': z.string(), + schema: z.string(), }); export type Embedded = z.infer; export const McpSchema = z.object({ - 'endpoint': z.string(), - 'schema': z.string(), + endpoint: z.string(), + schema: z.string(), }); export type Mcp = z.infer; export const RestSchema = z.object({ - 'endpoint': z.string(), - 'schema': z.string(), + endpoint: z.string(), + schema: z.string(), }); export type Rest = z.infer; export const BuyerSchema = z.object({ - 'email': z.string().optional(), - 'first_name': z.string().optional(), - 'full_name': z.string().optional(), - 'last_name': z.string().optional(), - 'phone_number': z.string().optional(), + email: z.string().optional(), + first_name: z.string().optional(), + full_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), }); export type Buyer = z.infer; export const ItemCreateRequestSchema = z.object({ - 'id': z.string(), + id: z.string(), }); export type ItemCreateRequest = z.infer; export const PostalAddressSchema = z.object({ - 'address_country': z.string().optional(), - 'address_locality': z.string().optional(), - 'address_region': z.string().optional(), - 'extended_address': z.string().optional(), - 'first_name': z.string().optional(), - 'full_name': z.string().optional(), - 'last_name': z.string().optional(), - 'phone_number': z.string().optional(), - 'postal_code': z.string().optional(), - 'street_address': z.string().optional(), + address_country: z.string().optional(), + address_locality: z.string().optional(), + address_region: z.string().optional(), + extended_address: z.string().optional(), + first_name: z.string().optional(), + full_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), + postal_code: z.string().optional(), + street_address: z.string().optional(), }); export type PostalAddress = z.infer; export const PaymentCredentialSchema = z.object({ - 'type': z.string(), - 'card_number_type': CardNumberTypeSchema.optional(), - 'cryptogram': z.string().optional(), - 'cvc': z.string().optional(), - 'eci_value': z.string().optional(), - 'expiry_month': z.number().optional(), - 'expiry_year': z.number().optional(), - 'name': z.string().optional(), - 'number': z.string().optional(), + type: z.string(), + card_number_type: CardNumberTypeSchema.optional(), + cryptogram: z.string().optional(), + cvc: z.string().optional(), + eci_value: z.string().optional(), + expiry_month: z.number().optional(), + expiry_year: z.number().optional(), + name: z.string().optional(), + number: z.string().optional(), }); export type PaymentCredential = z.infer; export const ItemResponseSchema = z.object({ - 'id': z.string(), - 'image_url': z.string().optional(), - 'price': z.number(), - 'title': z.string(), + id: z.string(), + image_url: z.string().optional(), + price: z.number(), + title: z.string(), }); export type ItemResponse = z.infer; export const TotalResponseSchema = z.object({ - 'amount': z.number(), - 'display_text': z.string().optional(), - 'type': TotalResponseTypeSchema, + amount: z.number(), + display_text: z.string().optional(), + type: TotalResponseTypeSchema, }); export type TotalResponse = z.infer; export const LinkSchema = z.object({ - 'title': z.string().optional(), - 'type': z.string(), - 'url': z.string(), + title: z.string().optional(), + type: z.string(), + url: z.string(), }); export type Link = z.infer; export const MessageSchema = z.object({ - 'code': z.string().optional(), - 'content': z.string(), - 'content_type': ContentTypeSchema.optional(), - 'path': z.string().optional(), - 'severity': SeveritySchema.optional(), - 'type': MessageTypeSchema, + code: z.string().optional(), + content: z.string(), + content_type: ContentTypeSchema.optional(), + path: z.string().optional(), + severity: SeveritySchema.optional(), + type: MessageTypeSchema, }); export type Message = z.infer; export const CapabilityResponseSchema = z.object({ - 'config': z.record(z.string(), z.any()).optional(), - 'extends': z.string().optional(), - 'name': z.string(), - 'schema': z.string().optional(), - 'spec': z.string().optional(), - 'version': z.string(), + config: z.record(z.string(), z.any()).optional(), + extends: z.string().optional(), + name: z.string(), + schema: z.string().optional(), + spec: z.string().optional(), + version: z.string(), }); export type CapabilityResponse = z.infer; export const ItemUpdateRequestSchema = z.object({ - 'id': z.string(), + id: z.string(), }); export type ItemUpdateRequest = z.infer; export const AdjustmentLineItemSchema = z.object({ - 'id': z.string(), - 'quantity': z.number(), + id: z.string(), + quantity: z.number(), }); export type AdjustmentLineItem = z.infer; export const FulfillmentEventLineItemSchema = z.object({ - 'id': z.string(), - 'quantity': z.number(), + id: z.string(), + quantity: z.number(), }); -export type FulfillmentEventLineItem = - z.infer; +export type FulfillmentEventLineItem = z.infer< + typeof FulfillmentEventLineItemSchema +>; export const ExpectationLineItemSchema = z.object({ - 'id': z.string(), - 'quantity': z.number(), + id: z.string(), + quantity: z.number(), }); export type ExpectationLineItem = z.infer; export const QuantitySchema = z.object({ - 'fulfilled': z.number(), - 'total': z.number(), + fulfilled: z.number(), + total: z.number(), }); export type Quantity = z.infer; export const UcpOrderResponseSchema = z.object({ - 'capabilities': z.array(CapabilityResponseSchema), - 'version': z.string(), + capabilities: z.array(CapabilityResponseSchema), + version: z.string(), }); export type UcpOrderResponse = z.infer; export const PaymentAccountInfoSchema = z.object({ - 'payment_account_reference': z.string().optional(), + payment_account_reference: z.string().optional(), }); export type PaymentAccountInfo = z.infer; export const IdentityClassSchema = z.object({ - 'access_token': z.string(), + access_token: z.string(), }); export type IdentityClass = z.infer; export const CardCredentialSchema = z.object({ - 'card_number_type': CardNumberTypeSchema, - 'cryptogram': z.string().optional(), - 'cvc': z.string().optional(), - 'eci_value': z.string().optional(), - 'expiry_month': z.number().optional(), - 'expiry_year': z.number().optional(), - 'name': z.string().optional(), - 'number': z.string().optional(), - 'type': CardPaymentInstrumentTypeSchema, + card_number_type: CardNumberTypeSchema, + cryptogram: z.string().optional(), + cvc: z.string().optional(), + eci_value: z.string().optional(), + expiry_month: z.number().optional(), + expiry_year: z.number().optional(), + name: z.string().optional(), + number: z.string().optional(), + type: CardPaymentInstrumentTypeSchema, }); export type CardCredential = z.infer; export const CardPaymentInstrumentSchema = z.object({ - 'billing_address': PostalAddressSchema.optional(), - 'credential': PaymentCredentialSchema.optional(), - 'handler_id': z.string(), - 'id': z.string(), - 'type': CardPaymentInstrumentTypeSchema, - 'brand': z.string(), - 'expiry_month': z.number().optional(), - 'expiry_year': z.number().optional(), - 'last_digits': z.string(), - 'rich_card_art': z.string().optional(), - 'rich_text_description': z.string().optional(), + billing_address: PostalAddressSchema.optional(), + credential: PaymentCredentialSchema.optional(), + handler_id: z.string(), + id: z.string(), + type: CardPaymentInstrumentTypeSchema, + brand: z.string(), + expiry_month: z.number().optional(), + expiry_year: z.number().optional(), + last_digits: z.string(), + rich_card_art: z.string().optional(), + rich_text_description: z.string().optional(), }); export type CardPaymentInstrument = z.infer; export const FulfillmentDestinationRequestSchema = z.object({ - 'address_country': z.string().optional(), - 'address_locality': z.string().optional(), - 'address_region': z.string().optional(), - 'extended_address': z.string().optional(), - 'first_name': z.string().optional(), - 'full_name': z.string().optional(), - 'last_name': z.string().optional(), - 'phone_number': z.string().optional(), - 'postal_code': z.string().optional(), - 'street_address': z.string().optional(), - 'id': z.string().optional(), - 'address': PostalAddressSchema.optional(), - 'name': z.string().optional(), -}); -export type FulfillmentDestinationRequest = - z.infer; + address_country: z.string().optional(), + address_locality: z.string().optional(), + address_region: z.string().optional(), + extended_address: z.string().optional(), + first_name: z.string().optional(), + full_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), + postal_code: z.string().optional(), + street_address: z.string().optional(), + id: z.string().optional(), + address: PostalAddressSchema.optional(), + name: z.string().optional(), +}); +export type FulfillmentDestinationRequest = z.infer< + typeof FulfillmentDestinationRequestSchema +>; export const FulfillmentGroupCreateRequestSchema = z.object({ - 'selected_option_id': z.union([z.null(), z.string()]).optional(), + selected_option_id: z.union([z.null(), z.string()]).optional(), }); -export type FulfillmentGroupCreateRequest = - z.infer; +export type FulfillmentGroupCreateRequest = z.infer< + typeof FulfillmentGroupCreateRequestSchema +>; export const FulfillmentGroupUpdateRequestSchema = z.object({ - 'id': z.string(), - 'selected_option_id': z.union([z.null(), z.string()]).optional(), + id: z.string(), + selected_option_id: z.union([z.null(), z.string()]).optional(), }); -export type FulfillmentGroupUpdateRequest = - z.infer; +export type FulfillmentGroupUpdateRequest = z.infer< + typeof FulfillmentGroupUpdateRequestSchema +>; export const FulfillmentDestinationRequestElementSchema = z.object({ - 'address_country': z.string().optional(), - 'address_locality': z.string().optional(), - 'address_region': z.string().optional(), - 'extended_address': z.string().optional(), - 'first_name': z.string().optional(), - 'full_name': z.string().optional(), - 'last_name': z.string().optional(), - 'phone_number': z.string().optional(), - 'postal_code': z.string().optional(), - 'street_address': z.string().optional(), - 'id': z.string().optional(), - 'address': PostalAddressSchema.optional(), - 'name': z.string().optional(), -}); -export type FulfillmentDestinationRequestElement = - z.infer; + address_country: z.string().optional(), + address_locality: z.string().optional(), + address_region: z.string().optional(), + extended_address: z.string().optional(), + first_name: z.string().optional(), + full_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), + postal_code: z.string().optional(), + street_address: z.string().optional(), + id: z.string().optional(), + address: PostalAddressSchema.optional(), + name: z.string().optional(), +}); +export type FulfillmentDestinationRequestElement = z.infer< + typeof FulfillmentDestinationRequestElementSchema +>; export const GroupElementSchema = z.object({ - 'selected_option_id': z.union([z.null(), z.string()]).optional(), + selected_option_id: z.union([z.null(), z.string()]).optional(), }); export type GroupElement = z.infer; export const GroupClassSchema = z.object({ - 'id': z.string(), - 'selected_option_id': z.union([z.null(), z.string()]).optional(), + id: z.string(), + selected_option_id: z.union([z.null(), z.string()]).optional(), }); export type GroupClass = z.infer; export const AllowsMultiDestinationSchema = z.object({ - 'pickup': z.boolean().optional(), - 'shipping': z.boolean().optional(), + pickup: z.boolean().optional(), + shipping: z.boolean().optional(), }); -export type AllowsMultiDestination = - z.infer; +export type AllowsMultiDestination = z.infer< + typeof AllowsMultiDestinationSchema +>; export const MessageErrorSchema = z.object({ - 'code': z.string(), - 'content': z.string(), - 'content_type': ContentTypeSchema.optional(), - 'path': z.string().optional(), - 'severity': SeveritySchema, - 'type': MessageErrorTypeSchema, + code: z.string(), + content: z.string(), + content_type: ContentTypeSchema.optional(), + path: z.string().optional(), + severity: SeveritySchema, + type: MessageErrorTypeSchema, }); export type MessageError = z.infer; export const MessageInfoSchema = z.object({ - 'code': z.string().optional(), - 'content': z.string(), - 'content_type': ContentTypeSchema.optional(), - 'path': z.string().optional(), - 'type': MessageInfoTypeSchema, + code: z.string().optional(), + content: z.string(), + content_type: ContentTypeSchema.optional(), + path: z.string().optional(), + type: MessageInfoTypeSchema, }); export type MessageInfo = z.infer; export const MessageWarningSchema = z.object({ - 'code': z.string(), - 'content': z.string(), - 'content_type': ContentTypeSchema.optional(), - 'path': z.string().optional(), - 'type': MessageWarningTypeSchema, + code: z.string(), + content: z.string(), + content_type: ContentTypeSchema.optional(), + path: z.string().optional(), + type: MessageWarningTypeSchema, }); export type MessageWarning = z.infer; export const FulfillmentOptionResponseSchema = z.object({ - 'carrier': z.string().optional(), - 'description': z.string().optional(), - 'earliest_fulfillment_time': z.coerce.date().optional(), - 'id': z.string(), - 'latest_fulfillment_time': z.coerce.date().optional(), - 'subtotal': z.number().optional(), - 'tax': z.number().optional(), - 'title': z.string(), - 'total': z.number(), -}); -export type FulfillmentOptionResponse = - z.infer; + carrier: z.string().optional(), + description: z.string().optional(), + earliest_fulfillment_time: z.coerce.date().optional(), + id: z.string(), + latest_fulfillment_time: z.coerce.date().optional(), + subtotal: z.number().optional(), + tax: z.number().optional(), + title: z.string(), + total: z.number(), +}); +export type FulfillmentOptionResponse = z.infer< + typeof FulfillmentOptionResponseSchema +>; export const PaymentIdentitySchema = z.object({ - 'access_token': z.string(), + access_token: z.string(), }); export type PaymentIdentity = z.infer; export const PaymentInstrumentBaseSchema = z.object({ - 'billing_address': PostalAddressSchema.optional(), - 'credential': PaymentCredentialSchema.optional(), - 'handler_id': z.string(), - 'id': z.string(), - 'type': z.string(), + billing_address: PostalAddressSchema.optional(), + credential: PaymentCredentialSchema.optional(), + handler_id: z.string(), + id: z.string(), + type: z.string(), }); export type PaymentInstrumentBase = z.infer; export const PlatformFulfillmentConfigSchema = z.object({ - 'supports_multi_group': z.boolean().optional(), + supports_multi_group: z.boolean().optional(), }); -export type PlatformFulfillmentConfig = - z.infer; +export type PlatformFulfillmentConfig = z.infer< + typeof PlatformFulfillmentConfigSchema +>; export const RetailLocationRequestSchema = z.object({ - 'address': PostalAddressSchema.optional(), - 'name': z.string(), + address: PostalAddressSchema.optional(), + name: z.string(), }); export type RetailLocationRequest = z.infer; export const RetailLocationResponseSchema = z.object({ - 'address': PostalAddressSchema.optional(), - 'id': z.string(), - 'name': z.string(), + address: PostalAddressSchema.optional(), + id: z.string(), + name: z.string(), }); -export type RetailLocationResponse = - z.infer; +export type RetailLocationResponse = z.infer< + typeof RetailLocationResponseSchema +>; export const TokenCredentialCreateRequestSchema = z.object({ - 'token': z.string(), - 'type': z.string(), + token: z.string(), + type: z.string(), }); -export type TokenCredentialCreateRequest = - z.infer; +export type TokenCredentialCreateRequest = z.infer< + typeof TokenCredentialCreateRequestSchema +>; export const TokenCredentialResponseSchema = z.object({ - 'type': z.string(), + type: z.string(), }); -export type TokenCredentialResponse = - z.infer; +export type TokenCredentialResponse = z.infer< + typeof TokenCredentialResponseSchema +>; export const TokenCredentialUpdateRequestSchema = z.object({ - 'token': z.string(), - 'type': z.string(), + token: z.string(), + type: z.string(), }); -export type TokenCredentialUpdateRequest = - z.infer; +export type TokenCredentialUpdateRequest = z.infer< + typeof TokenCredentialUpdateRequestSchema +>; export const Ap2CompleteRequestObjectSchema = z.object({ - 'checkout_mandate': z.string(), + checkout_mandate: z.string(), }); -export type Ap2CompleteRequestObject = - z.infer; +export type Ap2CompleteRequestObject = z.infer< + typeof Ap2CompleteRequestObjectSchema +>; export const Ap2CheckoutResponseObjectSchema = z.object({ - 'merchant_authorization': z.string(), + merchant_authorization: z.string(), }); -export type Ap2CheckoutResponseObject = - z.infer; +export type Ap2CheckoutResponseObject = z.infer< + typeof Ap2CheckoutResponseObjectSchema +>; export const ConsentSchema = z.object({ - 'analytics': z.boolean().optional(), - 'marketing': z.boolean().optional(), - 'preferences': z.boolean().optional(), - 'sale_of_data': z.boolean().optional(), + analytics: z.boolean().optional(), + marketing: z.boolean().optional(), + preferences: z.boolean().optional(), + sale_of_data: z.boolean().optional(), }); export type Consent = z.infer; export const AllocationElementSchema = z.object({ - 'amount': z.number(), - 'path': z.string(), + amount: z.number(), + path: z.string(), }); export type AllocationElement = z.infer; export const MethodElementSchema = z.object({ - 'destinations': - z.array(FulfillmentDestinationRequestElementSchema).optional(), - 'groups': z.array(GroupElementSchema).optional(), - 'line_item_ids': z.array(z.string()).optional(), - 'selected_destination_id': z.union([z.null(), z.string()]).optional(), - 'type': TypeElementSchema, + destinations: z.array(FulfillmentDestinationRequestElementSchema).optional(), + groups: z.array(GroupElementSchema).optional(), + line_item_ids: z.array(z.string()).optional(), + selected_destination_id: z.union([z.null(), z.string()]).optional(), + type: TypeElementSchema, }); export type MethodElement = z.infer; export const FulfillmentAvailableMethodResponseSchema = z.object({ - 'description': z.string().optional(), - 'fulfillable_on': z.union([z.null(), z.string()]).optional(), - 'line_item_ids': z.array(z.string()), - 'type': TypeElementSchema, + description: z.string().optional(), + fulfillable_on: z.union([z.null(), z.string()]).optional(), + line_item_ids: z.array(z.string()), + type: TypeElementSchema, }); -export type FulfillmentAvailableMethodResponse = - z.infer; +export type FulfillmentAvailableMethodResponse = z.infer< + typeof FulfillmentAvailableMethodResponseSchema +>; export const FulfillmentDestinationResponseSchema = z.object({ - 'address_country': z.string().optional(), - 'address_locality': z.string().optional(), - 'address_region': z.string().optional(), - 'extended_address': z.string().optional(), - 'first_name': z.string().optional(), - 'full_name': z.string().optional(), - 'last_name': z.string().optional(), - 'phone_number': z.string().optional(), - 'postal_code': z.string().optional(), - 'street_address': z.string().optional(), - 'id': z.string(), - 'address': PostalAddressSchema.optional(), - 'name': z.string().optional(), -}); -export type FulfillmentDestinationResponse = - z.infer; + address_country: z.string().optional(), + address_locality: z.string().optional(), + address_region: z.string().optional(), + extended_address: z.string().optional(), + first_name: z.string().optional(), + full_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), + postal_code: z.string().optional(), + street_address: z.string().optional(), + id: z.string(), + address: PostalAddressSchema.optional(), + name: z.string().optional(), +}); +export type FulfillmentDestinationResponse = z.infer< + typeof FulfillmentDestinationResponseSchema +>; export const FulfillmentGroupResponseSchema = z.object({ - 'id': z.string(), - 'line_item_ids': z.array(z.string()), - 'options': z.array(FulfillmentOptionResponseSchema).optional(), - 'selected_option_id': z.union([z.null(), z.string()]).optional(), + id: z.string(), + line_item_ids: z.array(z.string()), + options: z.array(FulfillmentOptionResponseSchema).optional(), + selected_option_id: z.union([z.null(), z.string()]).optional(), }); -export type FulfillmentGroupResponse = - z.infer; +export type FulfillmentGroupResponse = z.infer< + typeof FulfillmentGroupResponseSchema +>; export const PaymentSchema = z.object({ - 'handlers': z.array(PaymentHandlerResponseSchema).optional(), + handlers: z.array(PaymentHandlerResponseSchema).optional(), }); export type Payment = z.infer; export const UcpServiceSchema = z.object({ - 'a2a': A2ASchema.optional(), - 'embedded': EmbeddedSchema.optional(), - 'mcp': McpSchema.optional(), - 'rest': RestSchema.optional(), - 'spec': z.string(), - 'version': z.string(), + a2a: A2ASchema.optional(), + embedded: EmbeddedSchema.optional(), + mcp: McpSchema.optional(), + rest: RestSchema.optional(), + spec: z.string(), + version: z.string(), }); export type UcpService = z.infer; export const LineItemCreateRequestSchema = z.object({ - 'item': ItemCreateRequestSchema, - 'quantity': z.number(), + item: ItemCreateRequestSchema, + quantity: z.number(), }); export type LineItemCreateRequest = z.infer; export const PaymentInstrumentSchema = z.object({ - 'billing_address': PostalAddressSchema.optional(), - 'credential': PaymentCredentialSchema.optional(), - 'handler_id': z.string(), - 'id': z.string(), - 'type': CardPaymentInstrumentTypeSchema, - 'brand': z.string(), - 'expiry_month': z.number().optional(), - 'expiry_year': z.number().optional(), - 'last_digits': z.string(), - 'rich_card_art': z.string().optional(), - 'rich_text_description': z.string().optional(), + billing_address: PostalAddressSchema.optional(), + credential: PaymentCredentialSchema.optional(), + handler_id: z.string(), + id: z.string(), + type: CardPaymentInstrumentTypeSchema, + brand: z.string(), + expiry_month: z.number().optional(), + expiry_year: z.number().optional(), + last_digits: z.string(), + rich_card_art: z.string().optional(), + rich_text_description: z.string().optional(), }); export type PaymentInstrument = z.infer; export const LineItemResponseSchema = z.object({ - 'id': z.string(), - 'item': ItemResponseSchema, - 'parent_id': z.string().optional(), - 'quantity': z.number(), - 'totals': z.array(TotalResponseSchema), + id: z.string(), + item: ItemResponseSchema, + parent_id: z.string().optional(), + quantity: z.number(), + totals: z.array(TotalResponseSchema), }); export type LineItemResponse = z.infer; export const PaymentResponseSchema = z.object({ - 'handlers': z.array(PaymentHandlerResponseSchema), - 'instruments': z.array(PaymentInstrumentSchema).optional(), - 'selected_instrument_id': z.string().optional(), + handlers: z.array(PaymentHandlerResponseSchema), + instruments: z.array(PaymentInstrumentSchema).optional(), + selected_instrument_id: z.string().optional(), }); export type PaymentResponse = z.infer; export const UcpCheckoutResponseSchema = z.object({ - 'capabilities': z.array(CapabilityResponseSchema), - 'version': z.string(), + capabilities: z.array(CapabilityResponseSchema), + version: z.string(), }); export type UcpCheckoutResponse = z.infer; export const LineItemUpdateRequestSchema = z.object({ - 'id': z.string().optional(), - 'item': ItemUpdateRequestSchema, - 'parent_id': z.string().optional(), - 'quantity': z.number(), + id: z.string().optional(), + item: ItemUpdateRequestSchema, + parent_id: z.string().optional(), + quantity: z.number(), }); export type LineItemUpdateRequest = z.infer; export const CheckoutUpdateRequestPaymentSchema = z.object({ - 'instruments': z.array(PaymentInstrumentSchema).optional(), - 'selected_instrument_id': z.string().optional(), + instruments: z.array(PaymentInstrumentSchema).optional(), + selected_instrument_id: z.string().optional(), }); -export type CheckoutUpdateRequestPayment = - z.infer; +export type CheckoutUpdateRequestPayment = z.infer< + typeof CheckoutUpdateRequestPaymentSchema +>; export const AdjustmentSchema = z.object({ - 'amount': z.number().optional(), - 'description': z.string().optional(), - 'id': z.string(), - 'line_items': z.array(AdjustmentLineItemSchema).optional(), - 'occurred_at': z.coerce.date(), - 'status': AdjustmentStatusSchema, - 'type': z.string(), + amount: z.number().optional(), + description: z.string().optional(), + id: z.string(), + line_items: z.array(AdjustmentLineItemSchema).optional(), + occurred_at: z.coerce.date(), + status: AdjustmentStatusSchema, + type: z.string(), }); export type Adjustment = z.infer; export const FulfillmentEventSchema = z.object({ - 'carrier': z.string().optional(), - 'description': z.string().optional(), - 'id': z.string(), - 'line_items': z.array(FulfillmentEventLineItemSchema), - 'occurred_at': z.coerce.date(), - 'tracking_number': z.string().optional(), - 'tracking_url': z.string().optional(), - 'type': z.string(), + carrier: z.string().optional(), + description: z.string().optional(), + id: z.string(), + line_items: z.array(FulfillmentEventLineItemSchema), + occurred_at: z.coerce.date(), + tracking_number: z.string().optional(), + tracking_url: z.string().optional(), + type: z.string(), }); export type FulfillmentEvent = z.infer; export const ExpectationSchema = z.object({ - 'description': z.string().optional(), - 'destination': PostalAddressSchema, - 'fulfillable_on': z.string().optional(), - 'id': z.string(), - 'line_items': z.array(ExpectationLineItemSchema), - 'method_type': MethodTypeSchema, + description: z.string().optional(), + destination: PostalAddressSchema, + fulfillable_on: z.string().optional(), + id: z.string(), + line_items: z.array(ExpectationLineItemSchema), + method_type: MethodTypeSchema, }); export type Expectation = z.infer; export const OrderLineItemSchema = z.object({ - 'id': z.string(), - 'item': ItemResponseSchema, - 'parent_id': z.string().optional(), - 'quantity': QuantitySchema, - 'status': OrderLineItemStatusSchema, - 'totals': z.array(TotalResponseSchema), + id: z.string(), + item: ItemResponseSchema, + parent_id: z.string().optional(), + quantity: QuantitySchema, + status: OrderLineItemStatusSchema, + totals: z.array(TotalResponseSchema), }); export type OrderLineItem = z.infer; export const PaymentCreateRequestSchema = z.object({ - 'instruments': z.array(PaymentInstrumentSchema).optional(), - 'selected_instrument_id': z.string().optional(), + instruments: z.array(PaymentInstrumentSchema).optional(), + selected_instrument_id: z.string().optional(), }); export type PaymentCreateRequest = z.infer; export const PaymentDataSchema = z.object({ - 'payment_data': PaymentInstrumentSchema, + payment_data: PaymentInstrumentSchema, }); export type PaymentData = z.infer; export const PaymentUpdateRequestSchema = z.object({ - 'instruments': z.array(PaymentInstrumentSchema).optional(), - 'selected_instrument_id': z.string().optional(), + instruments: z.array(PaymentInstrumentSchema).optional(), + selected_instrument_id: z.string().optional(), }); export type PaymentUpdateRequest = z.infer; export const BindingSchema = z.object({ - 'checkout_id': z.string(), - 'identity': IdentityClassSchema.optional(), + checkout_id: z.string(), + identity: IdentityClassSchema.optional(), }); export type Binding = z.infer; export const FulfillmentMethodCreateRequestSchema = z.object({ - 'destinations': - z.array(FulfillmentDestinationRequestElementSchema).optional(), - 'groups': z.array(GroupElementSchema).optional(), - 'line_item_ids': z.array(z.string()).optional(), - 'selected_destination_id': z.union([z.null(), z.string()]).optional(), - 'type': TypeElementSchema, + destinations: z.array(FulfillmentDestinationRequestElementSchema).optional(), + groups: z.array(GroupElementSchema).optional(), + line_item_ids: z.array(z.string()).optional(), + selected_destination_id: z.union([z.null(), z.string()]).optional(), + type: TypeElementSchema, }); -export type FulfillmentMethodCreateRequest = - z.infer; +export type FulfillmentMethodCreateRequest = z.infer< + typeof FulfillmentMethodCreateRequestSchema +>; export const FulfillmentMethodUpdateRequestSchema = z.object({ - 'destinations': - z.array(FulfillmentDestinationRequestElementSchema).optional(), - 'groups': z.array(GroupClassSchema).optional(), - 'id': z.string(), - 'line_item_ids': z.array(z.string()), - 'selected_destination_id': z.union([z.null(), z.string()]).optional(), + destinations: z.array(FulfillmentDestinationRequestElementSchema).optional(), + groups: z.array(GroupClassSchema).optional(), + id: z.string(), + line_item_ids: z.array(z.string()), + selected_destination_id: z.union([z.null(), z.string()]).optional(), }); -export type FulfillmentMethodUpdateRequest = - z.infer; +export type FulfillmentMethodUpdateRequest = z.infer< + typeof FulfillmentMethodUpdateRequestSchema +>; export const MerchantFulfillmentConfigSchema = z.object({ - 'allows_method_combinations': z.array(z.array(TypeElementSchema)).optional(), - 'allows_multi_destination': AllowsMultiDestinationSchema.optional(), + allows_method_combinations: z.array(z.array(TypeElementSchema)).optional(), + allows_multi_destination: AllowsMultiDestinationSchema.optional(), }); -export type MerchantFulfillmentConfig = - z.infer; +export type MerchantFulfillmentConfig = z.infer< + typeof MerchantFulfillmentConfigSchema +>; export const OrderFulfillmentDetailSchema = z.object({ - 'expected_fulfillment_time': z.string().optional(), - 'fulfillment_address': PostalAddressSchema.optional(), - 'fulfillment_option': FulfillmentOptionResponseSchema, - 'fulfillment_tracking_url': z.string().optional(), - 'id': z.string(), - 'status': OrderFulfillmentDetailStatusSchema, - 'tracking_identifier': z.string().optional(), -}); -export type OrderFulfillmentDetail = - z.infer; + expected_fulfillment_time: z.string().optional(), + fulfillment_address: PostalAddressSchema.optional(), + fulfillment_option: FulfillmentOptionResponseSchema, + fulfillment_tracking_url: z.string().optional(), + id: z.string(), + status: OrderFulfillmentDetailStatusSchema, + tracking_identifier: z.string().optional(), +}); +export type OrderFulfillmentDetail = z.infer< + typeof OrderFulfillmentDetailSchema +>; export const CompleteCheckoutRequestWithAp2Schema = z.object({ - 'ap2': Ap2CompleteRequestObjectSchema.optional(), + ap2: Ap2CompleteRequestObjectSchema.optional(), }); -export type CompleteCheckoutRequestWithAp2 = - z.infer; +export type CompleteCheckoutRequestWithAp2 = z.infer< + typeof CompleteCheckoutRequestWithAp2Schema +>; export const CheckoutWithAp2MandateSchema = z.object({ - 'buyer': BuyerSchema.optional(), - 'continue_url': z.string().optional(), - 'currency': z.string(), - 'expires_at': z.coerce.date().optional(), - 'id': z.string(), - 'line_items': z.array(LineItemResponseSchema), - 'links': z.array(LinkSchema), - 'messages': z.array(MessageSchema).optional(), - 'order_id': z.string().optional(), - 'order_permalink_url': z.string().optional(), - 'payment': PaymentResponseSchema, - 'status': CheckoutResponseStatusSchema, - 'totals': z.array(TotalResponseSchema), - 'ucp': UcpCheckoutResponseSchema, - 'ap2': Ap2CheckoutResponseObjectSchema.optional(), -}); -export type CheckoutWithAp2Mandate = - z.infer; + buyer: BuyerSchema.optional(), + continue_url: z.string().optional(), + currency: z.string(), + expires_at: z.coerce.date().optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkSchema), + messages: z.array(MessageSchema).optional(), + order_id: z.string().optional(), + order_permalink_url: z.string().optional(), + payment: PaymentResponseSchema, + status: CheckoutResponseStatusSchema, + totals: z.array(TotalResponseSchema), + ucp: UcpCheckoutResponseSchema, + ap2: Ap2CheckoutResponseObjectSchema.optional(), +}); +export type CheckoutWithAp2Mandate = z.infer< + typeof CheckoutWithAp2MandateSchema +>; export const BuyerWithConsentSchema = z.object({ - 'email': z.string().optional(), - 'first_name': z.string().optional(), - 'full_name': z.string().optional(), - 'last_name': z.string().optional(), - 'phone_number': z.string().optional(), - 'consent': ConsentSchema.optional(), + email: z.string().optional(), + first_name: z.string().optional(), + full_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), + consent: ConsentSchema.optional(), }); export type BuyerWithConsent = z.infer; export const AppliedElementSchema = z.object({ - 'allocations': z.array(AllocationElementSchema).optional(), - 'amount': z.number(), - 'automatic': z.boolean().optional(), - 'code': z.string().optional(), - 'method': MethodSchema.optional(), - 'priority': z.number().optional(), - 'title': z.string(), + allocations: z.array(AllocationElementSchema).optional(), + amount: z.number(), + automatic: z.boolean().optional(), + code: z.string().optional(), + method: MethodSchema.optional(), + priority: z.number().optional(), + title: z.string(), }); export type AppliedElement = z.infer; export const FulfillmentRequestSchema = z.object({ - 'methods': z.array(MethodElementSchema).optional(), + methods: z.array(MethodElementSchema).optional(), }); export type FulfillmentRequest = z.infer; export const CheckoutWithFulfillmentUpdateRequestSchema = z.object({ - 'buyer': BuyerSchema.optional(), - 'currency': z.string(), - 'id': z.string(), - 'line_items': z.array(LineItemUpdateRequestSchema), - 'payment': CheckoutUpdateRequestPaymentSchema, - 'fulfillment': FulfillmentRequestSchema.optional(), + buyer: BuyerSchema.optional(), + currency: z.string(), + id: z.string(), + line_items: z.array(LineItemUpdateRequestSchema), + payment: CheckoutUpdateRequestPaymentSchema, + fulfillment: FulfillmentRequestSchema.optional(), }); -export type CheckoutWithFulfillmentUpdateRequest = - z.infer; +export type CheckoutWithFulfillmentUpdateRequest = z.infer< + typeof CheckoutWithFulfillmentUpdateRequestSchema +>; export const FulfillmentMethodResponseSchema = z.object({ - 'destinations': z.array(FulfillmentDestinationResponseSchema).optional(), - 'groups': z.array(FulfillmentGroupResponseSchema).optional(), - 'id': z.string(), - 'line_item_ids': z.array(z.string()), - 'selected_destination_id': z.union([z.null(), z.string()]).optional(), - 'type': TypeElementSchema, + destinations: z.array(FulfillmentDestinationResponseSchema).optional(), + groups: z.array(FulfillmentGroupResponseSchema).optional(), + id: z.string(), + line_item_ids: z.array(z.string()), + selected_destination_id: z.union([z.null(), z.string()]).optional(), + type: TypeElementSchema, }); -export type FulfillmentMethodResponse = - z.infer; +export type FulfillmentMethodResponse = z.infer< + typeof FulfillmentMethodResponseSchema +>; export const UcpClassSchema = z.object({ - 'capabilities': z.array(CapabilityDiscoverySchema), - 'services': z.record(z.string(), UcpServiceSchema), - 'version': z.string(), + capabilities: z.array(CapabilityDiscoverySchema), + services: z.record(z.string(), UcpServiceSchema), + version: z.string(), }); export type UcpClass = z.infer; export const PaymentClassSchema = z.object({ - 'instruments': z.array(PaymentInstrumentSchema).optional(), - 'selected_instrument_id': z.string().optional(), + instruments: z.array(PaymentInstrumentSchema).optional(), + selected_instrument_id: z.string().optional(), }); export type PaymentClass = z.infer; export const CheckoutResponseSchema = z.object({ - 'buyer': BuyerSchema.optional(), - 'continue_url': z.string().optional(), - 'currency': z.string(), - 'expires_at': z.coerce.date().optional(), - 'id': z.string(), - 'line_items': z.array(LineItemResponseSchema), - 'links': z.array(LinkSchema), - 'messages': z.array(MessageSchema).optional(), - 'order_id': z.string().optional(), - 'order_permalink_url': z.string().optional(), - 'payment': PaymentResponseSchema, - 'status': CheckoutResponseStatusSchema, - 'totals': z.array(TotalResponseSchema), - 'ucp': UcpCheckoutResponseSchema, + buyer: BuyerSchema.optional(), + continue_url: z.string().optional(), + currency: z.string(), + expires_at: z.coerce.date().optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkSchema), + messages: z.array(MessageSchema).optional(), + order_id: z.string().optional(), + order_permalink_url: z.string().optional(), + payment: PaymentResponseSchema, + status: CheckoutResponseStatusSchema, + totals: z.array(TotalResponseSchema), + ucp: UcpCheckoutResponseSchema, }); export type CheckoutResponse = z.infer; export const CheckoutUpdateRequestSchema = z.object({ - 'buyer': BuyerSchema.optional(), - 'currency': z.string(), - 'id': z.string(), - 'line_items': z.array(LineItemUpdateRequestSchema), - 'payment': CheckoutUpdateRequestPaymentSchema, + buyer: BuyerSchema.optional(), + currency: z.string(), + id: z.string(), + line_items: z.array(LineItemUpdateRequestSchema), + payment: CheckoutUpdateRequestPaymentSchema, }); export type CheckoutUpdateRequest = z.infer; export const FulfillmentSchema = z.object({ - 'events': z.array(FulfillmentEventSchema).optional(), - 'expectations': z.array(ExpectationSchema).optional(), + events: z.array(FulfillmentEventSchema).optional(), + expectations: z.array(ExpectationSchema).optional(), }); export type Fulfillment = z.infer; export const CheckoutWithBuyerConsentSchema = z.object({ - 'buyer': BuyerWithConsentSchema.optional(), - 'continue_url': z.string().optional(), - 'currency': z.string(), - 'expires_at': z.coerce.date().optional(), - 'id': z.string(), - 'line_items': z.array(LineItemResponseSchema), - 'links': z.array(LinkSchema), - 'messages': z.array(MessageSchema).optional(), - 'order_id': z.string().optional(), - 'order_permalink_url': z.string().optional(), - 'payment': PaymentResponseSchema, - 'status': CheckoutResponseStatusSchema, - 'totals': z.array(TotalResponseSchema), - 'ucp': UcpCheckoutResponseSchema, -}); -export type CheckoutWithBuyerConsent = - z.infer; + buyer: BuyerWithConsentSchema.optional(), + continue_url: z.string().optional(), + currency: z.string(), + expires_at: z.coerce.date().optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkSchema), + messages: z.array(MessageSchema).optional(), + order_id: z.string().optional(), + order_permalink_url: z.string().optional(), + payment: PaymentResponseSchema, + status: CheckoutResponseStatusSchema, + totals: z.array(TotalResponseSchema), + ucp: UcpCheckoutResponseSchema, +}); +export type CheckoutWithBuyerConsent = z.infer< + typeof CheckoutWithBuyerConsentSchema +>; export const DiscountsClassSchema = z.object({ - 'applied': z.array(AppliedElementSchema).optional(), - 'codes': z.array(z.string()).optional(), + applied: z.array(AppliedElementSchema).optional(), + codes: z.array(z.string()).optional(), }); export type DiscountsClass = z.infer; export const CheckoutWithFulfillmentCreateRequestSchema = z.object({ - 'buyer': BuyerSchema.optional(), - 'currency': z.string(), - 'line_items': z.array(LineItemCreateRequestSchema), - 'payment': PaymentClassSchema, - 'fulfillment': FulfillmentRequestSchema.optional(), + buyer: BuyerSchema.optional(), + currency: z.string(), + line_items: z.array(LineItemCreateRequestSchema), + payment: PaymentClassSchema, + fulfillment: FulfillmentRequestSchema.optional(), }); -export type CheckoutWithFulfillmentCreateRequest = - z.infer; +export type CheckoutWithFulfillmentCreateRequest = z.infer< + typeof CheckoutWithFulfillmentCreateRequestSchema +>; export const FulfillmentResponseSchema = z.object({ - 'available_methods': - z.array(FulfillmentAvailableMethodResponseSchema).optional(), - 'methods': z.array(FulfillmentMethodResponseSchema).optional(), + available_methods: z + .array(FulfillmentAvailableMethodResponseSchema) + .optional(), + methods: z.array(FulfillmentMethodResponseSchema).optional(), }); export type FulfillmentResponse = z.infer; export const UcpDiscoveryProfileSchema = z.object({ - 'payment': PaymentSchema.optional(), - 'signing_keys': z.array(SigningKeySchema).optional(), - 'ucp': UcpClassSchema, + payment: PaymentSchema.optional(), + signing_keys: z.array(SigningKeySchema).optional(), + ucp: UcpClassSchema, }); export type UcpDiscoveryProfile = z.infer; export const CheckoutCreateRequestSchema = z.object({ - 'buyer': BuyerSchema.optional(), - 'currency': z.string(), - 'line_items': z.array(LineItemCreateRequestSchema), - 'payment': PaymentClassSchema, + buyer: BuyerSchema.optional(), + currency: z.string(), + line_items: z.array(LineItemCreateRequestSchema), + payment: PaymentClassSchema, }); export type CheckoutCreateRequest = z.infer; export const OrderSchema = z.object({ - 'adjustments': z.array(AdjustmentSchema).optional(), - 'checkout_id': z.string(), - 'fulfillment': FulfillmentSchema, - 'id': z.string(), - 'line_items': z.array(OrderLineItemSchema), - 'permalink_url': z.string(), - 'totals': z.array(TotalResponseSchema), - 'ucp': UcpOrderResponseSchema, + adjustments: z.array(AdjustmentSchema).optional(), + checkout_id: z.string(), + fulfillment: FulfillmentSchema, + id: z.string(), + line_items: z.array(OrderLineItemSchema), + permalink_url: z.string(), + totals: z.array(TotalResponseSchema), + ucp: UcpOrderResponseSchema, }); export type Order = z.infer; export const CheckoutWithDiscountSchema = z.object({ - 'buyer': BuyerSchema.optional(), - 'continue_url': z.string().optional(), - 'currency': z.string(), - 'expires_at': z.coerce.date().optional(), - 'id': z.string(), - 'line_items': z.array(LineItemResponseSchema), - 'links': z.array(LinkSchema), - 'messages': z.array(MessageSchema).optional(), - 'order_id': z.string().optional(), - 'order_permalink_url': z.string().optional(), - 'payment': PaymentResponseSchema, - 'status': CheckoutResponseStatusSchema, - 'totals': z.array(TotalResponseSchema), - 'ucp': UcpCheckoutResponseSchema, - 'discounts': DiscountsClassSchema.optional(), + buyer: BuyerSchema.optional(), + continue_url: z.string().optional(), + currency: z.string(), + expires_at: z.coerce.date().optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkSchema), + messages: z.array(MessageSchema).optional(), + order_id: z.string().optional(), + order_permalink_url: z.string().optional(), + payment: PaymentResponseSchema, + status: CheckoutResponseStatusSchema, + totals: z.array(TotalResponseSchema), + ucp: UcpCheckoutResponseSchema, + discounts: DiscountsClassSchema.optional(), }); export type CheckoutWithDiscount = z.infer; export const CheckoutWithFulfillmentResponseSchema = z.object({ - 'buyer': BuyerSchema.optional(), - 'continue_url': z.string().optional(), - 'currency': z.string(), - 'expires_at': z.coerce.date().optional(), - 'id': z.string(), - 'line_items': z.array(LineItemResponseSchema), - 'links': z.array(LinkSchema), - 'messages': z.array(MessageSchema).optional(), - 'order_id': z.string().optional(), - 'order_permalink_url': z.string().optional(), - 'payment': PaymentResponseSchema, - 'status': CheckoutResponseStatusSchema, - 'totals': z.array(TotalResponseSchema), - 'ucp': UcpCheckoutResponseSchema, - 'fulfillment': FulfillmentResponseSchema.optional(), -}); -export type CheckoutWithFulfillmentResponse = - z.infer; + buyer: BuyerSchema.optional(), + continue_url: z.string().optional(), + currency: z.string(), + expires_at: z.coerce.date().optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkSchema), + messages: z.array(MessageSchema).optional(), + order_id: z.string().optional(), + order_permalink_url: z.string().optional(), + payment: PaymentResponseSchema, + status: CheckoutResponseStatusSchema, + totals: z.array(TotalResponseSchema), + ucp: UcpCheckoutResponseSchema, + fulfillment: FulfillmentResponseSchema.optional(), +}); +export type CheckoutWithFulfillmentResponse = z.infer< + typeof CheckoutWithFulfillmentResponseSchema +>; diff --git a/rest/nodejs/src/utils/validation.ts b/rest/nodejs/src/utils/validation.ts index 3ebf6bf..093d18a 100644 --- a/rest/nodejs/src/utils/validation.ts +++ b/rest/nodejs/src/utils/validation.ts @@ -1,5 +1,5 @@ -import {type Context} from 'hono'; -import * as z from 'zod'; +import { type Context } from "hono"; +import * as z from "zod"; /** * Middleware to handle Zod validation results. @@ -11,8 +11,8 @@ import * as z from 'zod'; */ export function prettyValidation( result: - | {success: true; data: T; target: string} - | {success: false; error: any}, + | { success: true; data: T; target: string } + | { success: false; error: any }, c: Context, ) { if (result.success) { @@ -20,7 +20,7 @@ export function prettyValidation( `Request payload (${result.target}) passed validation:\n${JSON.stringify(result.data, null, 2)}`, ); } else { - c.var.logger.warn('Request payload failed validation'); + c.var.logger.warn("Request payload failed validation"); c.var.logger.warn( `Request payload:\n${JSON.stringify(c.req.json(), null, 2)}`, ); diff --git a/rest/nodejs/tsconfig.json b/rest/nodejs/tsconfig.json index dce08fe..ceb596f 100644 --- a/rest/nodejs/tsconfig.json +++ b/rest/nodejs/tsconfig.json @@ -6,9 +6,7 @@ "strict": true, "verbatimModuleSyntax": true, "skipLibCheck": true, - "types": [ - "node" - ], + "types": ["node"], "jsx": "react-jsx", "jsxImportSource": "hono/jsx", "outDir": "./dist" diff --git a/rest/python/client/flower_shop/README.md b/rest/python/client/flower_shop/README.md index a1c6b7c..cf4f2bf 100644 --- a/rest/python/client/flower_shop/README.md +++ b/rest/python/client/flower_shop/README.md @@ -35,7 +35,8 @@ checkout by processing a payment. execute this client against. Follow the instructions in the [Server README](../../server/README.md) to start the server on port 8182. - *Quick start (from `../../server/`):* `bash uv run server.py + _Quick start (from `../../server/`):_ `bash uv run server.py + --products_db_path=/tmp/ucp_test/products.db --transactions_db_path=/tmp/ucp_test/transactions.db --port=8182` @@ -49,14 +50,14 @@ uv run simple_happy_path_client.py --server_url=http://localhost:8182 ### Options -* `--server_url`: The base URL of the UCP Merchant Server (default: - `http://localhost:8182`). -* `--export_requests_to`: Path to a markdown file where the request/response - dialog will be logged. +- `--server_url`: The base URL of the UCP Merchant Server (default: + `http://localhost:8182`). +- `--export_requests_to`: Path to a markdown file where the request/response + dialog will be logged. - ```bash - uv run simple_happy_path_client.py --export_requests_to=interaction_log.md - ``` + ```bash + uv run simple_happy_path_client.py --export_requests_to=interaction_log.md + ``` ## Automated Demo (extract_json_dialog.sh) diff --git a/rest/python/client/flower_shop/sample_output/happy_path_dialog.md b/rest/python/client/flower_shop/sample_output/happy_path_dialog.md index 675b634..2f822f5 100644 --- a/rest/python/client/flower_shop/sample_output/happy_path_dialog.md +++ b/rest/python/client/flower_shop/sample_output/happy_path_dialog.md @@ -146,14 +146,8 @@ export RESPONSE=$(curl -s -X GET $SERVER_URL/.well-known/ucp) { "type": "CARD", "parameters": { - "allowedAuthMethods": [ - "PAN_ONLY", - "CRYPTOGRAM_3DS" - ], - "allowedCardNetworks": [ - "VISA", - "MASTERCARD" - ] + "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"], + "allowedCardNetworks": ["VISA", "MASTERCARD"] }, "tokenization_specification": [ { @@ -180,10 +174,7 @@ export RESPONSE=$(curl -s -X GET $SERVER_URL/.well-known/ucp) "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json" ], "config": { - "supported_tokens": [ - "success_token", - "fail_token" - ] + "supported_tokens": ["success_token", "fail_token"] } } ] @@ -613,9 +604,7 @@ export RESPONSE=$(curl -s -X PUT $SERVER_URL/checkout-sessions/$CHECKOUT_ID \ "instruments": [] }, "discounts": { - "codes": [ - "10OFF" - ], + "codes": ["10OFF"], "applied": [ { "code": "10OFF", @@ -760,9 +749,7 @@ export RESPONSE=$(curl -s -X PUT $SERVER_URL/checkout-sessions/$CHECKOUT_ID \ "instruments": [] }, "discounts": { - "codes": [ - "10OFF" - ], + "codes": ["10OFF"], "applied": [ { "code": "10OFF", @@ -969,9 +956,7 @@ export RESPONSE=$(curl -s -X PUT $SERVER_URL/checkout-sessions/$CHECKOUT_ID \ "instruments": [] }, "discounts": { - "codes": [ - "10OFF" - ], + "codes": ["10OFF"], "applied": [ { "code": "10OFF", @@ -1232,9 +1217,7 @@ export RESPONSE=$(curl -s -X PUT $SERVER_URL/checkout-sessions/$CHECKOUT_ID \ "instruments": [] }, "discounts": { - "codes": [ - "10OFF" - ], + "codes": ["10OFF"], "applied": [ { "code": "10OFF", @@ -1498,9 +1481,7 @@ export RESPONSE=$(curl -s -X POST $SERVER_URL/checkout-sessions/$CHECKOUT_ID/com "permalink_url": "http://localhost:8182/orders/5068a920-cc47-4304-b698-727c3cff9289" }, "discounts": { - "codes": [ - "10OFF" - ], + "codes": ["10OFF"], "applied": [ { "code": "10OFF", @@ -1643,4 +1624,3 @@ export RESPONSE=$(curl -s -X POST $SERVER_URL/checkout-sessions/$CHECKOUT_ID/com ```bash export ORDER_ID=$(echo $RESPONSE | jq -r '.order.id') ``` - diff --git a/rest/python/server/README.md b/rest/python/server/README.md index f6e7c73..28755a9 100644 --- a/rest/python/server/README.md +++ b/rest/python/server/README.md @@ -21,9 +21,9 @@ deployable both inside and outside of Google. ## Project Structure -* `server.py`: The entry point for the FastAPI application. -* `pyproject.toml`: Project configuration for external dependency management - and packaging. +- `server.py`: The entry point for the FastAPI application. +- `pyproject.toml`: Project configuration for external dependency management + and packaging. ## Prerequisites @@ -104,10 +104,10 @@ uv run simple_happy_path_client.py \ The server exposes an additional endpoint for simulation and testing purposes: -* `POST /testing/simulate-shipping/{id}`: Triggers a simulated "order shipped" - event for the specified order ID. This updates the order status and sends a - webhook notification if configured. This endpoint requires the - `Simulation-Secret` header to match the configured `--simulation_secret`. +- `POST /testing/simulate-shipping/{id}`: Triggers a simulated "order shipped" + event for the specified order ID. This updates the order status and sends a + webhook notification if configured. This endpoint requires the + `Simulation-Secret` header to match the configured `--simulation_secret`. ## Discovery @@ -204,14 +204,8 @@ Response: { "type": "CARD", "parameters": { - "allowedAuthMethods": [ - "PAN_ONLY", - "CRYPTOGRAM_3DS" - ], - "allowedCardNetworks": [ - "VISA", - "MASTERCARD" - ] + "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"], + "allowedCardNetworks": ["VISA", "MASTERCARD"] }, "tokenization_specification": [ { @@ -238,10 +232,7 @@ Response: "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json" ], "config": { - "supported_tokens": [ - "success_token", - "fail_token" - ] + "supported_tokens": ["success_token", "fail_token"] } } ] @@ -255,12 +246,12 @@ Full response ## Capabilities & Extensions -* Capabilities: Schema and Operations for commerce features identified via - reverse-domain notation to prevent conflicts. +- Capabilities: Schema and Operations for commerce features identified via + reverse-domain notation to prevent conflicts. -* Extensions: Modular additions (e.g., discounts, fulfillment etc) that - augment the schema of the base functionality of a capability. These use JSON - Schema’s allOf composition to modify capabilities predictably. +- Extensions: Modular additions (e.g., discounts, fulfillment etc) that + augment the schema of the base functionality of a capability. These use JSON + Schema’s allOf composition to modify capabilities predictably. ### Example of Checkout Capability @@ -577,9 +568,7 @@ curl -X PUT http://localhost:8182/checkout-sessions/$CHECKOUT_ID \ "order_permalink_url": null, "ap2": null, "discounts": { - "codes": [ - "10OFF" - ], + "codes": ["10OFF"], "applied": [ { "code": "10OFF", diff --git a/rest/python/server/routes/discovery_profile.json b/rest/python/server/routes/discovery_profile.json index af55c47..b5971d1 100644 --- a/rest/python/server/routes/discovery_profile.json +++ b/rest/python/server/routes/discovery_profile.json @@ -83,14 +83,8 @@ { "type": "CARD", "parameters": { - "allowedAuthMethods": [ - "PAN_ONLY", - "CRYPTOGRAM_3DS" - ], - "allowedCardNetworks": [ - "VISA", - "MASTERCARD" - ] + "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"], + "allowedCardNetworks": ["VISA", "MASTERCARD"] }, "tokenization_specification": [ { @@ -109,4 +103,4 @@ } ] } -} \ No newline at end of file +} diff --git a/rest/python/test_data/flower_shop/discounts.csv b/rest/python/test_data/flower_shop/discounts.csv index cf8bedd..9d613df 100644 --- a/rest/python/test_data/flower_shop/discounts.csv +++ b/rest/python/test_data/flower_shop/discounts.csv @@ -1,4 +1,4 @@ code,type,value,description 10OFF,percentage,10,10% Off WELCOME20,percentage,20,20% Off -FIXED500,fixed_amount,500,$5.00 Off \ No newline at end of file +FIXED500,fixed_amount,500,$5.00 Off diff --git a/rest/python/test_data/flower_shop/products.csv b/rest/python/test_data/flower_shop/products.csv index dcf6a4a..5182012 100644 --- a/rest/python/test_data/flower_shop/products.csv +++ b/rest/python/test_data/flower_shop/products.csv @@ -4,4 +4,4 @@ pot_ceramic,Ceramic Pot,1500,https://example.com/pot.jpg bouquet_sunflowers,Sunflower Bundle,2500,https://example.com/sunflowers.jpg bouquet_tulips,Spring Tulips,3000,https://example.com/tulips.jpg orchid_white,White Orchid,4500,https://example.com/orchid.jpg -gardenias,Gardenias,2000,https://example.com/gardenias.jpg \ No newline at end of file +gardenias,Gardenias,2000,https://example.com/gardenias.jpg