From d70243e9f0d0cc34b42f06cec8869d509f327c17 Mon Sep 17 00:00:00 2001 From: cusell-google Date: Fri, 17 Apr 2026 15:49:12 +0200 Subject: [PATCH 1/5] fix(conformance): update conformance repo to UCP 2026-01-23 version and fix test payloads --- ap2_test.py | 52 +++---- binding_test.py | 57 +++---- business_logic_test.py | 59 +++---- card_credential_test.py | 49 +++--- checkout_lifecycle_test.py | 45 +++--- fulfillment_test.py | 230 ++++++++++++++++++++-------- idempotency_test.py | 15 +- integration_test_utils.py | 161 +++++++++++-------- invalid_input_test.py | 10 +- order_test.py | 70 +++++---- protocol_test.py | 146 +++++++++++------- pyproject.toml | 2 +- shopping-agent-test.json | 24 +-- test_data/flower_shop/addresses.csv | 6 +- test_data/flower_shop/customers.csv | 6 +- validation_test.py | 27 ++-- webhook_test.py | 91 +++++++---- 17 files changed, 611 insertions(+), 439 deletions(-) diff --git a/ap2_test.py b/ap2_test.py index 16fedfa..ded8ac0 100644 --- a/ap2_test.py +++ b/ap2_test.py @@ -16,19 +16,14 @@ from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout -from ucp_sdk.models.schemas.shopping.ap2_mandate import Ap2CompleteRequest -from ucp_sdk.models.schemas.shopping.ap2_mandate import CheckoutMandate -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping import checkout as checkout +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, ) -from ucp_sdk.models.schemas.shopping.types import card_payment_instrument -from ucp_sdk.models.schemas.shopping.types import payment_instrument -from ucp_sdk.models.schemas.shopping.types import token_credential_resp # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class Ap2MandateTest(integration_test_utils.IntegrationTestBase): @@ -48,31 +43,32 @@ def test_ap2_mandate_completion(self) -> None: response_json = self.create_checkout_session() checkout_id = checkout.Checkout(**response_json).id - credential = token_credential_resp.TokenCredentialResponse( - type="token", token="success_token" - ) - instr = payment_instrument.PaymentInstrument( - root=card_payment_instrument.CardPaymentInstrument( - id="instr_1", - brand="visa", - last_digits="4242", - handler_id="mock_payment_handler", - handler_name="mock_payment_handler", - type="card", - credential=credential, - ) - ) - payment_data = instr.root.model_dump(mode="json", exclude_none=True) + payment_instrument = { + "id": "instr_1", + "handler_id": "mock_payment_handler", + "type": "card", + "display": { + "brand": "visa", + "last_digits": "4242", + }, + "credential": {"type": "token", "token": "success_token"}, + } # SD-JWT+kb pattern: # ^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+(~[A-Za-z0-9_-]+)*$ - mandate = CheckoutMandate(root="header.payload.signature~kb_signature") - ap2_data = Ap2CompleteRequest(checkout_mandate=mandate) + # + # The UCP 01-23 SDK simplifies the AP2 protocol definitions. + # The extension payload is now defined directly against the `ap2` key. + # The `mandate` wrapper object and `ap2_data` nested objects were removed + # from the completion payload in this release to flatten the schema. payment_payload = { - "payment_data": payment_data, + "payment": {"instruments": [payment_instrument]}, "risk_signals": {}, - "ap2": ap2_data.model_dump(mode="json", exclude_none=True), + "ap2": { + **response_json, + "checkout_mandate": "header.payload.signature~kb_signature", + }, } response = self.client.post( diff --git a/binding_test.py b/binding_test.py index d44f90e..483ddf2 100644 --- a/binding_test.py +++ b/binding_test.py @@ -16,19 +16,14 @@ from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping import checkout as checkout +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, ) -from ucp_sdk.models.schemas.shopping.types import binding -from ucp_sdk.models.schemas.shopping.types import card_payment_instrument -from ucp_sdk.models.schemas.shopping.types import payment_identity -from ucp_sdk.models.schemas.shopping.types import payment_instrument -from ucp_sdk.models.schemas.shopping.types import token_credential_resp # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class TokenBindingTest(integration_test_utils.IntegrationTestBase): @@ -48,30 +43,28 @@ def test_token_binding_completion(self) -> None: response_json = self.create_checkout_session() checkout_id = checkout.Checkout(**response_json).id - identity = payment_identity.PaymentIdentity( - access_token="user_access_token" - ) - token_binding = binding.Binding(checkout_id=checkout_id, identity=identity) - - # TokenCredentialResponse allows extra fields - credential = token_credential_resp.TokenCredentialResponse( - type="stripe_token", token="success_token", binding=token_binding - ) - - instr = payment_instrument.PaymentInstrument( - root=card_payment_instrument.CardPaymentInstrument( - id="instr_1", - brand="visa", - last_digits="4242", - handler_id="mock_payment_handler", - handler_name="mock_payment_handler", - type="card", - credential=credential, - ) - ) - payment_data = instr.root.model_dump(mode="json", exclude_none=True) payment_payload = { - "payment_data": payment_data, + "payment": { + "instruments": [ + { + "id": "instr_1", + "handler_id": "mock_payment_handler", + "type": "card", + "display": { + "brand": "visa", + "last_digits": "4242", + }, + "credential": { + "type": "stripe_token", + "token": "success_token", + "binding": { + "checkout_id": checkout_id, + "identity": {"access_token": "user_access_token"}, + }, + }, + } + ] + }, "risk_signals": {}, } diff --git a/business_logic_test.py b/business_logic_test.py index 2fa8c83..5177ffd 100644 --- a/business_logic_test.py +++ b/business_logic_test.py @@ -16,20 +16,22 @@ from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import buyer_consent_resp as buyer_consent -from ucp_sdk.models.schemas.shopping import checkout_update_req -from ucp_sdk.models.schemas.shopping import discount_resp as discount -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout -from ucp_sdk.models.schemas.shopping import payment_update_req -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping import buyer_consent as buyer_consent +from ucp_sdk.models.schemas.shopping import ( + checkout_update_request as checkout_update_req, ) -from ucp_sdk.models.schemas.shopping.types import buyer -from ucp_sdk.models.schemas.shopping.types import item_update_req -from ucp_sdk.models.schemas.shopping.types import line_item_update_req +from ucp_sdk.models.schemas.shopping import discount as discount +from ucp_sdk.models.schemas.shopping import checkout as checkout +from ucp_sdk.models.schemas.shopping import payment_update_request +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, +) +from ucp_sdk.models.schemas.shopping.types import buyer_update_request +from ucp_sdk.models.schemas.shopping.types import item_update_request +from ucp_sdk.models.schemas.shopping.types import line_item_update_request # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class BusinessLogicTest(integration_test_utils.IntegrationTestBase): @@ -129,22 +131,17 @@ def test_totals_recalculation_on_update(self): expected_price = int(expected_price) # Update quantity to 2. Total should be 2 * expected_price. - item_update = item_update_req.ItemUpdateRequest( + item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, title=checkout_obj.line_items[0].item.title, ) - line_item_update = line_item_update_req.LineItemUpdateRequest( + line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, item=item_update, quantity=2, ) - payment_update = payment_update_req.PaymentUpdateRequest( - selected_instrument_id=checkout_obj.payment.selected_instrument_id, + payment_update = payment_update_request.PaymentUpdateRequest( instruments=checkout_obj.payment.instruments, - handlers=[ - h.model_dump(mode="json", exclude_none=True) - for h in checkout_obj.payment.handlers - ], ) update_payload = checkout_update_req.CheckoutUpdateRequest( @@ -198,22 +195,17 @@ def test_discount_flow(self): expected_price = int(expected_price) # Apply Discount - item_update = item_update_req.ItemUpdateRequest( + item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, title=checkout_obj.line_items[0].item.title, ) - line_item_update = line_item_update_req.LineItemUpdateRequest( + line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, item=item_update, quantity=1, ) - payment_update = payment_update_req.PaymentUpdateRequest( - selected_instrument_id=checkout_obj.payment.selected_instrument_id, + payment_update = payment_update_request.PaymentUpdateRequest( instruments=checkout_obj.payment.instruments, - handlers=[ - h.model_dump(mode="json", exclude_none=True) - for h in checkout_obj.payment.handlers - ], ) update_payload = checkout_update_req.CheckoutUpdateRequest( @@ -492,22 +484,17 @@ def test_buyer_info_persistence(self): checkout_id = checkout_obj.id # Update with buyer info - item_update = item_update_req.ItemUpdateRequest( + item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, title=checkout_obj.line_items[0].item.title, ) - line_item_update = line_item_update_req.LineItemUpdateRequest( + line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, item=item_update, quantity=1, ) - payment_update = payment_update_req.PaymentUpdateRequest( - selected_instrument_id=checkout_obj.payment.selected_instrument_id, + payment_update = payment_update_request.PaymentUpdateRequest( instruments=checkout_obj.payment.instruments, - handlers=[ - h.model_dump(mode="json", exclude_none=True) - for h in checkout_obj.payment.handlers - ], ) update_payload = checkout_update_req.CheckoutUpdateRequest( @@ -515,7 +502,7 @@ def test_buyer_info_persistence(self): currency=checkout_obj.currency, line_items=[line_item_update], payment=payment_update, - buyer=buyer.Buyer( + buyer=buyer_update_request.BuyerUpdateRequest( email="test@example.com", first_name="Test", last_name="User", diff --git a/card_credential_test.py b/card_credential_test.py index 782c5df..c9496e3 100644 --- a/card_credential_test.py +++ b/card_credential_test.py @@ -16,16 +16,14 @@ from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping import checkout as checkout +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, ) -from ucp_sdk.models.schemas.shopping.types import card_credential -from ucp_sdk.models.schemas.shopping.types import card_payment_instrument # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class CardCredentialTest(integration_test_utils.IntegrationTestBase): @@ -45,27 +43,26 @@ def test_card_credential_payment(self) -> None: response_json = self.create_checkout_session() checkout_id = checkout.Checkout(**response_json).id - credential = card_credential.CardCredential( - type="card", - card_number_type="fpan", - number="4242424242424242", - expiry_month=12, - expiry_year=2030, - cvc="123", - name="John Doe", - ) - instr = card_payment_instrument.CardPaymentInstrument( - id="instr_card", - handler_id="mock_payment_handler", - handler_name="mock_payment_handler", - type="card", - brand="Visa", - last_digits="1111", - credential=credential, - ) - payment_data = instr.model_dump(mode="json", exclude_none=True) + payment_instrument = { + "id": "instr_card", + "handler_id": "mock_payment_handler", + "type": "card", + "display": { + "brand": "Visa", + "last_digits": "1111", + }, + "credential": { + "type": "card", + "card_number_type": "fpan", + "number": "4242424242424242", + "expiry_month": 12, + "expiry_year": 2030, + "cvc": "123", + "name": "John Doe", + }, + } payment_payload = { - "payment_data": payment_data, + "payment": {"instruments": [payment_instrument]}, "risk_signals": {}, } diff --git a/checkout_lifecycle_test.py b/checkout_lifecycle_test.py index 863b25a..07ef2f0 100644 --- a/checkout_lifecycle_test.py +++ b/checkout_lifecycle_test.py @@ -16,17 +16,19 @@ from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import checkout_update_req -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout -from ucp_sdk.models.schemas.shopping import payment_update_req -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping import ( + checkout_update_request as checkout_update_req, ) -from ucp_sdk.models.schemas.shopping.types import item_update_req -from ucp_sdk.models.schemas.shopping.types import line_item_update_req +from ucp_sdk.models.schemas.shopping import checkout as checkout +from ucp_sdk.models.schemas.shopping import payment_update_request +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, +) +from ucp_sdk.models.schemas.shopping.types import item_update_request +from ucp_sdk.models.schemas.shopping.types import line_item_update_request # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class CheckoutLifecycleTest(integration_test_utils.IntegrationTestBase): @@ -87,22 +89,21 @@ def test_update_checkout(self): checkout_id = checkout_obj.id # Construct Update Request - item_update = item_update_req.ItemUpdateRequest( + item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, title=checkout_obj.line_items[0].item.title, ) - line_item_update = line_item_update_req.LineItemUpdateRequest( + line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, item=item_update, quantity=2, ) - payment_update = payment_update_req.PaymentUpdateRequest( - selected_instrument_id=checkout_obj.payment.selected_instrument_id, + payment_update = payment_update_request.PaymentUpdateRequest( instruments=checkout_obj.payment.instruments, handlers=[ h.model_dump(mode="json", exclude_none=True) - for h in checkout_obj.payment.handlers + for h in checkout_obj.payment.instruments ], ) @@ -243,21 +244,20 @@ def test_cannot_update_canceled_checkout(self): self._cancel_checkout(checkout_id) # Try Update - item_update = item_update_req.ItemUpdateRequest( + item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, title=checkout_obj.line_items[0].item.title, ) - line_item_update = line_item_update_req.LineItemUpdateRequest( + line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, item=item_update, quantity=2, ) - payment_update = payment_update_req.PaymentUpdateRequest( - selected_instrument_id=checkout_obj.payment.selected_instrument_id, + payment_update = payment_update_request.PaymentUpdateRequest( instruments=checkout_obj.payment.instruments, handlers=[ h.model_dump(mode="json", exclude_none=True) - for h in checkout_obj.payment.handlers + for h in checkout_obj.payment.instruments ], ) update_payload = checkout_update_req.CheckoutUpdateRequest( @@ -353,21 +353,20 @@ def test_cannot_update_completed_checkout(self): self._complete_checkout(checkout_id) # Try Update - item_update = item_update_req.ItemUpdateRequest( + item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, title=checkout_obj.line_items[0].item.title, ) - line_item_update = line_item_update_req.LineItemUpdateRequest( + line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, item=item_update, quantity=2, ) - payment_update = payment_update_req.PaymentUpdateRequest( - selected_instrument_id=checkout_obj.payment.selected_instrument_id, + payment_update = payment_update_request.PaymentUpdateRequest( instruments=checkout_obj.payment.instruments, handlers=[ h.model_dump(mode="json", exclude_none=True) - for h in checkout_obj.payment.handlers + for h in checkout_obj.payment.instruments ], ) update_payload = checkout_update_req.CheckoutUpdateRequest( diff --git a/fulfillment_test.py b/fulfillment_test.py index 9557acd..cc7cc2b 100644 --- a/fulfillment_test.py +++ b/fulfillment_test.py @@ -17,14 +17,14 @@ import uuid from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping import checkout as checkout +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, ) from ucp_sdk.models.schemas.shopping.types import postal_address # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class FulfillmentTest(integration_test_utils.IntegrationTestBase): @@ -68,6 +68,8 @@ def test_fulfillment_flow(self) -> None: "methods": [ { "type": "shipping", + "id": "method_1", + "line_item_ids": [checkout_obj.line_items[0].id], "destinations": [address_data], "selected_destination_id": "dest_1", } @@ -80,13 +82,16 @@ def test_fulfillment_flow(self) -> None: checkout_with_options = checkout.Checkout(**response_json) # Verify options are generated in the nested structure - # checkout.fulfillment.root.methods[0].groups[0].options - self.assertIsNotNone(checkout_with_options.fulfillment) - self.assertNotEmpty(checkout_with_options.fulfillment.root.methods) - method = checkout_with_options.fulfillment.root.methods[0] - self.assertNotEmpty(method.groups) - group = method.groups[0] - options = group.options + methods = ( + response_json.get("fulfillment", {}) + .get("root", response_json.get("fulfillment", {})) + .get("methods", []) + ) + self.assertTrue(methods) + method = methods[0] + self.assertTrue(method.get("groups")) + group = method["groups"][0] + options = group.get("options", []) self.assertTrue( options, @@ -94,15 +99,19 @@ def test_fulfillment_flow(self) -> None: ) # 2. Select Option - option_id = options[0].id + option_id = options[0]["id"] option_cost = next( - (t.amount for t in options[0].totals if t.type == "total"), 0 + (t["amount"] for t in options[0]["totals"] if t["type"] == "total"), 0 ) # Update payload to select the option # We must preserve the destination to keep options available fulfillment_payload["methods"][0]["groups"] = [ - {"selected_option_id": option_id} + { + "id": group.get("id", "group_1"), + "line_item_ids": group.get("line_item_ids", []), + "selected_option_id": option_id, + } ] response_json = self.update_checkout_session( @@ -149,6 +158,8 @@ def test_dynamic_fulfillment(self) -> None: "methods": [ { "type": "shipping", + "id": "method_1", + "line_item_ids": [checkout_obj.line_items[0].id], "destinations": [us_address], "selected_destination_id": "dest_us", } @@ -161,9 +172,11 @@ def test_dynamic_fulfillment(self) -> None: us_checkout = checkout.Checkout(**response_json) # Check for US options - options = us_checkout.fulfillment.root.methods[0].groups[0].options + options = us_checkout.model_extra["fulfillment"]["methods"][0]["groups"][0][ + "options" + ] self.assertTrue( - options and any(o.id == "exp-ship-us" for o in options), + options and any(o["id"] == "exp-ship-us" for o in options), f"Expected US express option, got {options}", ) @@ -178,6 +191,8 @@ def test_dynamic_fulfillment(self) -> None: "methods": [ { "type": "shipping", + "id": "method_1", + "line_item_ids": [checkout_obj.line_items[0].id], "destinations": [ca_address], "selected_destination_id": "dest_ca", } @@ -190,9 +205,11 @@ def test_dynamic_fulfillment(self) -> None: ca_checkout = checkout.Checkout(**response_json) # Check for International options - options = ca_checkout.fulfillment.root.methods[0].groups[0].options + options = ca_checkout.model_extra["fulfillment"]["methods"][0]["groups"][0][ + "options" + ] self.assertTrue( - options and any(o.id == "exp-ship-intl" for o in options), + options and any(o["id"] == "exp-ship-intl" for o in options), f"Expected Intl express option, got {options}", ) @@ -207,17 +224,26 @@ def test_unknown_customer_no_address(self) -> None: # Trigger fulfillment update (empty payload to trigger sync) response_json = self.update_checkout_session( - checkout_obj, fulfillment={"methods": [{"type": "shipping"}]} + checkout_obj, + fulfillment={ + "methods": [ + { + "id": "method_1", + "type": "shipping", + "line_item_ids": [checkout_obj.line_items[0].id], + } + ] + }, ) updated_checkout = checkout.Checkout(**response_json) # Verify no destinations injected - method = updated_checkout.fulfillment.root.methods[0] - self.assertIsNone(method.destinations) + method = updated_checkout.model_extra["fulfillment"]["methods"][0] + self.assertIsNone(method["destinations"]) def test_known_customer_no_address(self) -> None: """Test that a known customer with no stored address gets no injection.""" - # Jane Doe (cust_3) has no address in CSV + # Jane Doe (customer_3) has no address in CSV response_json = self.create_checkout_session( buyer={"fullName": "Jane Doe", "email": "jane.doe@example.com"}, select_fulfillment=False, @@ -225,16 +251,25 @@ def test_known_customer_no_address(self) -> None: checkout_obj = checkout.Checkout(**response_json) response_json = self.update_checkout_session( - checkout_obj, fulfillment={"methods": [{"type": "shipping"}]} + checkout_obj, + fulfillment={ + "methods": [ + { + "id": "method_1", + "type": "shipping", + "line_item_ids": [checkout_obj.line_items[0].id], + } + ] + }, ) updated_checkout = checkout.Checkout(**response_json) - method = updated_checkout.fulfillment.root.methods[0] - self.assertIsNone(method.destinations) + method = updated_checkout.model_extra["fulfillment"]["methods"][0] + self.assertIsNone(method["destinations"]) def test_known_customer_one_address(self) -> None: """Test that a known customer with an address gets it injected.""" - # John Doe (cust_1) has an address + # John Doe (customer_1) has an address response_json = self.create_checkout_session( buyer={"fullName": "John Doe", "email": "john.doe@example.com"}, select_fulfillment=False, @@ -242,15 +277,24 @@ def test_known_customer_one_address(self) -> None: checkout_obj = checkout.Checkout(**response_json) response_json = self.update_checkout_session( - checkout_obj, fulfillment={"methods": [{"type": "shipping"}]} + checkout_obj, + fulfillment={ + "methods": [ + { + "id": "method_1", + "type": "shipping", + "line_item_ids": [checkout_obj.line_items[0].id], + } + ] + }, ) updated_checkout = checkout.Checkout(**response_json) - method = updated_checkout.fulfillment.root.methods[0] - self.assertIsNotNone(method.destinations) + method = updated_checkout.model_extra["fulfillment"]["methods"][0] + self.assertIsNotNone(method["destinations"]) # He has at least 2 addresses - self.assertGreaterEqual(len(method.destinations), 2) - self.assertEqual(method.destinations[0].root.address_country, "US") + self.assertGreaterEqual(len(method["destinations"]), 2) + self.assertEqual(method["destinations"][0]["address_country"], "US") def test_known_customer_multiple_addresses_selection(self) -> None: """Test selecting between multiple addresses for a known customer.""" @@ -263,22 +307,37 @@ def test_known_customer_multiple_addresses_selection(self) -> None: # Trigger injection response_json = self.update_checkout_session( - checkout_obj, fulfillment={"methods": [{"type": "shipping"}]} + checkout_obj, + fulfillment={ + "methods": [ + { + "id": "method_1", + "type": "shipping", + "line_item_ids": [checkout_obj.line_items[0].id], + } + ] + }, ) updated_checkout = checkout.Checkout(**response_json) - method = updated_checkout.fulfillment.root.methods[0] - destinations = method.destinations + method = updated_checkout.model_extra["fulfillment"]["methods"][0] + destinations = method["destinations"] self.assertGreaterEqual(len(destinations), 2) # Verify IDs (assuming deterministic order or check existence) - dest_ids = [d.root.id for d in destinations] + dest_ids = [d["id"] for d in destinations] self.assertIn("addr_1", dest_ids) self.assertIn("addr_2", dest_ids) - # Select addr_2 fulfillment_payload = { - "methods": [{"type": "shipping", "selected_destination_id": "addr_2"}] + "methods": [ + { + "id": method.get("id", "method_1"), + "type": "shipping", + "selected_destination_id": "addr_2", + "line_item_ids": [checkout_obj.line_items[0].id], + } + ] } response_json = self.update_checkout_session( updated_checkout, fulfillment=fulfillment_payload @@ -287,20 +346,22 @@ def test_known_customer_multiple_addresses_selection(self) -> None: # Verify selection in hierarchical model self.assertEqual( - final_checkout.fulfillment.root.methods[0].selected_destination_id, + final_checkout.model_extra["fulfillment"]["methods"][0][ + "selected_destination_id" + ], "addr_2", ) # Verify selection details from the selected destination - method = final_checkout.fulfillment.root.methods[0] + method = final_checkout.model_extra["fulfillment"]["methods"][0] selected_dest = next( - d for d in method.destinations if d.root.id == "addr_2" + d for d in method["destinations"] if d["id"] == "addr_2" ) self.assertEqual( - selected_dest.root.street_address, + selected_dest["street_address"], "456 Oak Ave", ) - self.assertEqual(selected_dest.root.postal_code, "10012") + self.assertEqual(selected_dest["postal_code"], "10012") def test_known_customer_new_address(self) -> None: """Test that providing a new address works for a known customer.""" @@ -321,6 +382,8 @@ def test_known_customer_new_address(self) -> None: "methods": [ { "type": "shipping", + "id": "method_1", + "line_item_ids": [checkout_obj.line_items[0].id], "destinations": [new_address], "selected_destination_id": "dest_new", } @@ -332,7 +395,7 @@ def test_known_customer_new_address(self) -> None: ) updated_checkout = checkout.Checkout(**response_json) - method = updated_checkout.fulfillment.root.methods[0] + method = updated_checkout.model_extra["fulfillment"]["methods"][0] # Should see the new address (and potentially the injected ones if the # server merges them, but based on current implementation logic, client @@ -343,12 +406,12 @@ def test_known_customer_new_address(self) -> None: # or not m_data["destinations"]): inject... # So if we provide destinations, it WON'T inject. - self.assertLen(method.destinations, 1) - self.assertEqual(method.destinations[0].root.id, "dest_new") + self.assertLen(method["destinations"], 1) + self.assertEqual(method["destinations"][0]["id"], "dest_new") # And we should get options calculated for CA - group = method.groups[0] - self.assertTrue(any(o.id == "exp-ship-intl" for o in group.options)) + group = method["groups"][0] + self.assertTrue(any(o["id"] == "exp-ship-intl" for o in group["options"])) def test_new_user_new_address_persistence(self) -> None: """Test that a new address for a new user is persisted and ID generated. @@ -368,16 +431,19 @@ def test_new_user_new_address_persistence(self) -> None: # New address without ID new_address = { "street_address": "789 Pine St", - "address_locality": "Villagetown", + "address_locality": "Springfield", "address_region": "NY", "postal_code": "10001", "address_country": "US", + "id": "", } fulfillment_payload = { "methods": [ { "type": "shipping", + "id": "method_1", + "line_item_ids": [checkout_obj.line_items[0].id], "destinations": [new_address], } ] @@ -388,12 +454,12 @@ def test_new_user_new_address_persistence(self) -> None: ) updated_checkout = checkout.Checkout(**response_json) - method = updated_checkout.fulfillment.root.methods[0] - self.assertIsNotNone(method.destinations) - self.assertLen(method.destinations, 1) + method = updated_checkout.model_extra["fulfillment"]["methods"][0] + self.assertIsNotNone(method["destinations"]) + self.assertLen(method["destinations"], 1) # ID should be generated - generated_id = method.destinations[0].root.id + generated_id = method["destinations"][0]["id"] self.assertTrue(generated_id, "ID should be generated for new address") # Verify persistence by creating another checkout for same user @@ -404,14 +470,23 @@ def test_new_user_new_address_persistence(self) -> None: ) checkout_obj_2 = checkout.Checkout(**response_json_2) response_json_2 = self.update_checkout_session( - checkout_obj_2, fulfillment={"methods": [{"type": "shipping"}]} + checkout_obj_2, + fulfillment={ + "methods": [ + { + "id": "method_1", + "type": "shipping", + "line_item_ids": [checkout_obj.line_items[0].id], + } + ] + }, ) updated_checkout_2 = checkout.Checkout(**response_json_2) - method_2 = updated_checkout_2.fulfillment.root.methods[0] + method_2 = updated_checkout_2.model_extra["fulfillment"]["methods"][0] - self.assertIsNotNone(method_2.destinations) + self.assertIsNotNone(method_2["destinations"]) # Could be more if tests re-run, but should contain our ID - dest_ids = [d.root.id for d in method_2.destinations] + dest_ids = [d["id"] for d in method_2["destinations"]] self.assertIn(generated_id, dest_ids) def test_known_user_existing_address_reuse(self) -> None: @@ -435,12 +510,15 @@ def test_known_user_existing_address_reuse(self) -> None: "address_region": "IL", "postal_code": "62704", "address_country": "US", + "id": "", } fulfillment_payload = { "methods": [ { "type": "shipping", + "id": "method_1", + "line_item_ids": [checkout_obj.line_items[0].id], "destinations": [matching_address], } ] @@ -451,12 +529,12 @@ def test_known_user_existing_address_reuse(self) -> None: ) updated_checkout = checkout.Checkout(**response_json) - method = updated_checkout.fulfillment.root.methods[0] - self.assertIsNotNone(method.destinations) - self.assertLen(method.destinations, 1) + method = updated_checkout.model_extra["fulfillment"]["methods"][0] + self.assertIsNotNone(method["destinations"]) + self.assertLen(method["destinations"], 1) # Should reuse addr_1 - self.assertEqual(method.destinations[0].root.id, "addr_1") + self.assertEqual(method["destinations"][0]["id"], "addr_1") def test_free_shipping_on_expensive_order(self) -> None: """Test that free shipping is offered for orders over $100.""" @@ -476,6 +554,8 @@ def test_free_shipping_on_expensive_order(self) -> None: "methods": [ { "type": "shipping", + "id": "method_1", + "line_item_ids": [checkout_obj.line_items[0].id], "destinations": [address], "selected_destination_id": "dest_us", } @@ -487,18 +567,24 @@ def test_free_shipping_on_expensive_order(self) -> None: ) updated_checkout = checkout.Checkout(**response_json) - options = updated_checkout.fulfillment.root.methods[0].groups[0].options + options = updated_checkout.model_extra["fulfillment"]["methods"][0][ + "groups" + ][0]["options"] free_shipping_option = next( - (o for o in options if o.id == "std-ship"), None + (o for o in options if o["id"] == "std-ship"), None ) self.assertIsNotNone(free_shipping_option) opt_total = next( - (t.amount for t in free_shipping_option.totals if t.type == "total"), + ( + t["amount"] + for t in free_shipping_option["totals"] + if t["type"] == "total" + ), None, ) self.assertEqual(opt_total, 0) - self.assertIn("Free", free_shipping_option.title) + self.assertIn("Free", free_shipping_option["title"]) def test_free_shipping_for_specific_item(self) -> None: """Test that free shipping is offered for eligible items.""" @@ -518,6 +604,8 @@ def test_free_shipping_for_specific_item(self) -> None: "methods": [ { "type": "shipping", + "id": "method_1", + "line_item_ids": [checkout_obj.line_items[0].id], "destinations": [address], "selected_destination_id": "dest_us", } @@ -529,18 +617,24 @@ def test_free_shipping_for_specific_item(self) -> None: ) updated_checkout = checkout.Checkout(**response_json) - options = updated_checkout.fulfillment.root.methods[0].groups[0].options + options = updated_checkout.model_extra["fulfillment"]["methods"][0][ + "groups" + ][0]["options"] free_shipping_option = next( - (o for o in options if o.id == "std-ship"), None + (o for o in options if o["id"] == "std-ship"), None ) self.assertIsNotNone(free_shipping_option) opt_total = next( - (t.amount for t in free_shipping_option.totals if t.type == "total"), + ( + t["amount"] + for t in free_shipping_option["totals"] + if t["type"] == "total" + ), None, ) self.assertEqual(opt_total, 0) - self.assertIn("Free", free_shipping_option.title) + self.assertIn("Free", free_shipping_option["title"]) if __name__ == "__main__": diff --git a/idempotency_test.py b/idempotency_test.py index ec62332..a1b8fb3 100644 --- a/idempotency_test.py +++ b/idempotency_test.py @@ -18,13 +18,13 @@ from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping import checkout as checkout +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, ) # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class IdempotencyTest(integration_test_utils.IntegrationTestBase): @@ -119,7 +119,6 @@ def test_idempotency_update(self) -> None: ) payment_req = { - "selected_instrument_id": checkout_obj.payment.selected_instrument_id, "instruments": [ i.model_dump(mode="json", exclude_none=True) for i in checkout_obj.payment.instruments @@ -211,9 +210,9 @@ def test_idempotency_complete(self) -> None: # 3. Conflict Request complete_payload_diff = integration_test_utils.get_valid_payment_payload() - complete_payload_diff["payment_data"]["credential"]["token"] = ( - "different_token" - ) + complete_payload_diff["payment"]["instruments"][0]["credential"][ + "token" + ] = "different_token" response3 = self.client.post( self.get_shopping_url(f"/checkout-sessions/{checkout_id}/complete"), json=complete_payload_diff, diff --git a/integration_test_utils.py b/integration_test_utils.py index 92a15ea..11027f8 100644 --- a/integration_test_utils.py +++ b/integration_test_utils.py @@ -29,33 +29,32 @@ from fastapi import Request from fastapi.responses import JSONResponse import httpx -from ucp_sdk.models.discovery.profile_schema import UcpDiscoveryProfile -from ucp_sdk.models.schemas.shopping import checkout_create_req -from ucp_sdk.models.schemas.shopping import fulfillment_resp as f_models -from ucp_sdk.models.schemas.shopping import payment_create_req -from ucp_sdk.models.schemas.shopping import payment_update_req -from ucp_sdk.models.schemas.shopping.discount_update_req import ( - Checkout as DiscountUpdate, +from ucp_sdk.models.schemas.shopping import checkout_create_request +from ucp_sdk.models.schemas.shopping import checkout as f_models +from ucp_sdk.models.schemas.shopping import payment_create_request +from ucp_sdk.models.schemas.shopping import payment_update_request +from ucp_sdk.models.schemas.shopping.checkout_update_request import ( + CheckoutUpdateRequest, ) -from ucp_sdk.models.schemas.shopping.fulfillment_create_req import Fulfillment -from ucp_sdk.models.schemas.shopping.fulfillment_update_req import ( - Checkout as FulfillmentUpdate, +from ucp_sdk.models.schemas.shopping.types import ( + fulfillment_destination_create_request as fdc_req, ) -from ucp_sdk.models.schemas.shopping.types import card_payment_instrument -from ucp_sdk.models.schemas.shopping.types import fulfillment_destination_req -from ucp_sdk.models.schemas.shopping.types import fulfillment_group_create_req -from ucp_sdk.models.schemas.shopping.types import fulfillment_method_create_req -from ucp_sdk.models.schemas.shopping.types import fulfillment_req -from ucp_sdk.models.schemas.shopping.types import item_create_req -from ucp_sdk.models.schemas.shopping.types import item_update_req -from ucp_sdk.models.schemas.shopping.types import line_item_create_req -from ucp_sdk.models.schemas.shopping.types import line_item_update_req -from ucp_sdk.models.schemas.shopping.types import payment_handler_resp -from ucp_sdk.models.schemas.shopping.types import shipping_destination_req +from ucp_sdk.models.schemas.shopping.types import ( + fulfillment_group_create_request, +) +from ucp_sdk.models.schemas.shopping.types import ( + fulfillment_method_create_request, +) +from ucp_sdk.models.schemas.shopping.types import item_create_request +from ucp_sdk.models.schemas.shopping.types import item_update_request +from ucp_sdk.models.schemas.shopping.types import line_item_create_request +from ucp_sdk.models.schemas.shopping.types import line_item_update_request +from ucp_sdk.models.schemas import payment_handler +from ucp_sdk.models.schemas.shopping.types import shipping_destination import uvicorn -class UnifiedUpdate(FulfillmentUpdate, DiscountUpdate): +class UnifiedUpdate(CheckoutUpdateRequest): """Client-side unified update model to support extensions.""" @@ -179,20 +178,20 @@ def get_valid_payment_payload( "postal_code": addr_data.get("postal_code"), } - # Use Pydantic model to validate/construct - instr_model = card_payment_instrument.CardPaymentInstrument( - id=instr_data["id"], - handler_id=instr_data["handler_id"], - handler_name=instr_data["handler_id"], # Assuming same for mock - type=instr_data["type"], - brand=instr_data["brand"], - last_digits=instr_data["last_digits"], - credential={"type": "token", "token": instr_data["token"]}, - billing_address=billing_address, - ) + payment_instrument = { + "id": instr_data["id"], + "handler_id": instr_data["handler_id"], + "type": instr_data["type"], + "display": { + "brand": instr_data["brand"], + "last_digits": instr_data["last_digits"], + }, + "credential": {"type": "token", "token": instr_data["token"]}, + "billing_address": billing_address, + } return { - "payment_data": instr_model.model_dump(mode="json", exclude_none=True), + "payment": {"instruments": [payment_instrument]}, "risk_signals": {}, } @@ -391,11 +390,31 @@ def shopping_service_endpoint(self) -> str: if self._shopping_service_endpoint is None: discovery_resp = self.client.get("/.well-known/ucp") self.assert_response_status(discovery_resp, 200) - profile = UcpDiscoveryProfile(**discovery_resp.json()) - shopping_service = profile.ucp.services.root.get("dev.ucp.shopping") - if not shopping_service or not shopping_service.rest: + + profile_data = discovery_resp.json() + # UCP 01-23 validation changed dicts to lists + shopping_services = profile_data.get("services", {}).get( + "dev.ucp.shopping", [] + ) + if not shopping_services: raise RuntimeError("Shopping service not found in discovery profile") - self._shopping_service_endpoint = str(shopping_service.rest.endpoint) + + shopping_service = ( + shopping_services[0] + if isinstance(shopping_services, list) + else shopping_services + ) + + endpoint = ( + shopping_service.get("endpoint") + if shopping_service and shopping_service.get("transport") == "rest" + else None + ) + if not endpoint: + raise RuntimeError( + "Shopping service endpoint not found in discovery profile" + ) + self._shopping_service_endpoint = str(endpoint) return self._shopping_service_endpoint def get_shopping_url(self, path: str) -> str: @@ -429,7 +448,7 @@ def create_checkout_payload( handlers=None, buyer: dict[str, Any] | None = None, include_fulfillment: bool = True, - ) -> checkout_create_req.CheckoutCreateRequest: + ) -> checkout_create_request.CheckoutCreateRequest: """Create a valid checkout creation payload. Args: @@ -462,10 +481,10 @@ def create_checkout_payload( if handlers is None: handlers = [ - payment_handler_resp.PaymentHandlerResponse( + payment_handler.PaymentHandler( id="google_pay", name="google.pay", - version="2026-01-11", + version="2026-01-23", spec="https://example.com/spec", config_schema="https://example.com/schema", instrument_schemas=["https://example.com/instrument_schema"], @@ -473,40 +492,50 @@ def create_checkout_payload( ) ] - item = item_create_req.ItemCreateRequest(id=item_id, title=title) - line_item = line_item_create_req.LineItemCreateRequest( + item = item_create_request.ItemCreateRequest(id=item_id, title=title) + line_item = line_item_create_request.LineItemCreateRequest( quantity=quantity, item=item ) # PaymentCreateRequest allows extra fields, so passing handlers is valid - payment = payment_create_req.PaymentCreateRequest( + payment = payment_create_request.PaymentCreateRequest( instruments=[], - selected_instrument_id="instr_1", handlers=[h.model_dump(mode="json", exclude_none=True) for h in handlers], ) fulfillment = None if include_fulfillment: # Hierarchical Fulfillment Construction - destination = fulfillment_destination_req.FulfillmentDestinationRequest( - root=shipping_destination_req.ShippingDestinationRequest( + destination = fdc_req.FulfillmentDestinationCreateRequest( + root=shipping_destination.ShippingDestination( id="dest_1", address_country="US" ) ) - group = fulfillment_group_create_req.FulfillmentGroupCreateRequest( - selected_option_id="std-ship" + group = fulfillment_group_create_request.FulfillmentGroupCreateRequest( + id="group_1", + line_item_ids=["line_item_123"], + selected_option_id="std-ship", ) - method = fulfillment_method_create_req.FulfillmentMethodCreateRequest( + method = fulfillment_method_create_request.FulfillmentMethodCreateRequest( + id="method_1", type="shipping", destinations=[destination], + line_item_ids=["line_item_123"], selected_destination_id="dest_1", groups=[group], ) - fulfillment = Fulfillment( - root=fulfillment_req.FulfillmentRequest(methods=[method]) - ) + fulfillment = { + "methods": [ + method.model_dump(mode="json", exclude_none=True, by_alias=True) + ] + } + + # Set response fields on model objects for server validation workaround + item.price = 1000 + line_item.id = "line_item_123" + line_item.totals = [] - return checkout_create_req.CheckoutCreateRequest( + checkout_req = checkout_create_request.CheckoutCreateRequest( id=str(uuid.uuid4()), currency=currency, line_items=[line_item], @@ -514,6 +543,12 @@ def create_checkout_payload( buyer=buyer, fulfillment=fulfillment, ) + checkout_req.status = "incomplete" + checkout_req.ucp = {"version": "2026-01-23"} + checkout_req.totals = [] + checkout_req.links = [] + + return checkout_req def get_headers( self, idempotency_key: str | None = None, request_id: str | None = None @@ -815,27 +850,27 @@ def update_checkout_session( if line_items is None: line_items = [] for li in checkout_obj.line_items: - item_update = item_update_req.ItemUpdateRequest( + item_update = item_update_request.ItemUpdateRequest( id=li.item.id, title=li.item.title, ) line_items.append( - line_item_update_req.LineItemUpdateRequest( + line_item_update_request.LineItemUpdateRequest( id=li.id, item=item_update, quantity=li.quantity, + parent_id=li.parent_id, ) ) # Construct Payment if payment is None: - payment = payment_update_req.PaymentUpdateRequest( - selected_instrument_id=checkout_obj.payment.selected_instrument_id, - instruments=checkout_obj.payment.instruments, - handlers=[ - h.model_dump(mode="json", exclude_none=True) - for h in checkout_obj.payment.handlers - ], + payment = ( + payment_update_request.PaymentUpdateRequest( + instruments=getattr(checkout_obj.payment, "instruments", []), + ) + if checkout_obj.payment + else None ) update_payload = UnifiedUpdate( diff --git a/invalid_input_test.py b/invalid_input_test.py index 2200a1e..d0d9e81 100644 --- a/invalid_input_test.py +++ b/invalid_input_test.py @@ -18,14 +18,14 @@ import uuid from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout +from ucp_sdk.models.schemas.shopping import checkout as checkout from ucp_sdk.models.schemas.shopping import order -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, ) # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class InvalidInputTest(integration_test_utils.IntegrationTestBase): @@ -123,7 +123,7 @@ def test_malformed_adjustment_payload(self): mode="json", by_alias=True, exclude_none=True ) - # Malform the adjustments field (dict instead of list) + # Corrupt the adjustments field (dict instead of list) order_dict["adjustments"] = {"id": "adj_1", "amount": 100} # Update Order diff --git a/order_test.py b/order_test.py index a8da244..68c13e5 100644 --- a/order_test.py +++ b/order_test.py @@ -20,16 +20,16 @@ from absl.testing import absltest import integration_test_utils from pydantic import AnyUrl -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout +from ucp_sdk.models.schemas.shopping import checkout as checkout from ucp_sdk.models.schemas.shopping import order -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, ) from ucp_sdk.models.schemas.shopping.types import adjustment from ucp_sdk.models.schemas.shopping.types import fulfillment_event # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) FLAGS = flags.FLAGS @@ -95,6 +95,8 @@ def test_order_fulfillment_retrieval(self) -> None: fulfillment_payload = { "methods": [ { + "id": "method_1", + "line_item_ids": ["item_123"], "type": "shipping", "destinations": [fulfillment_address], "selected_destination_id": "dest_manual", @@ -124,28 +126,32 @@ def test_order_fulfillment_retrieval(self) -> None: ) self.assert_response_status(response, 200) - checkout_with_options = checkout.Checkout(**response.json()) + checkout.Checkout(**response.json()) # Check options in hierarchical structure + checkout_json = response.json() options = [] - if ( - checkout_with_options.fulfillment - and checkout_with_options.fulfillment.root.methods - and checkout_with_options.fulfillment.root.methods[0].groups - ): - options = ( - checkout_with_options.fulfillment.root.methods[0].groups[0].options - ) + group_info = {} + if checkout_json.get("fulfillment"): + ful = checkout_json["fulfillment"] + methods = ful.get("root", ful).get("methods", []) + if methods and methods[0].get("groups"): + group_info = methods[0]["groups"][0] + options = group_info.get("options", []) self.assertTrue(options, "No options returned") # Select Option - option_id = options[0].id + option_id = options[0]["id"] # Update payload to select option # Need to preserve the method structure update_payload["fulfillment"]["methods"][0]["groups"] = [ - {"selected_option_id": option_id} + { + "id": group_info.get("id", "group_1"), + "line_item_ids": group_info.get("line_item_ids", ["item_123"]), + "selected_option_id": option_id, + } ] response = self.client.put( @@ -172,7 +178,7 @@ def test_order_fulfillment_retrieval(self) -> None: # Verify the expectation description matches the selected option title self.assertEqual( order_obj.fulfillment.expectations[0].description, - options[0].title, + options[0]["title"], "Expectation description mismatch", ) @@ -205,6 +211,8 @@ def test_order_update(self) -> None: fulfillment_payload = { "methods": [ { + "id": "method_1", + "line_item_ids": ["item_123"], "type": "shipping", "destinations": [addr], "selected_destination_id": "dest_manual_2", @@ -236,29 +244,23 @@ def test_order_update(self) -> None: checkout_resp = resp.json() options = [] - if ( - checkout_resp.get("fulfillment") - and checkout_resp["fulfillment"].get("root") # RootModel serialized? - and checkout_resp["fulfillment"]["root"].get("methods") - and checkout_resp["fulfillment"]["root"]["methods"][0].get("groups") - ): - options = checkout_resp["fulfillment"]["root"]["methods"][0]["groups"][0][ - "options" - ] - elif ( - checkout_resp.get("fulfillment") - and checkout_resp["fulfillment"].get("methods") - and checkout_resp["fulfillment"]["methods"][0].get("groups") - ): - options = checkout_resp["fulfillment"]["methods"][0]["groups"][0][ - "options" - ] + group_info = {} + if checkout_resp.get("fulfillment"): + ful = checkout_resp["fulfillment"] + methods = ful.get("root", ful).get("methods", []) + if methods and methods[0].get("groups"): + group_info = methods[0]["groups"][0] + options = group_info.get("options", []) self.assertTrue(options) # Select option update_payload["fulfillment"]["methods"][0]["groups"] = [ - {"selected_option_id": options[0]["id"]} + { + "id": group_info.get("id", "group_1"), + "line_item_ids": group_info.get("line_item_ids", ["item_123"]), + "selected_option_id": options[0]["id"], + } ] self.client.put( diff --git a/protocol_test.py b/protocol_test.py index 98c91b1..a39da33 100644 --- a/protocol_test.py +++ b/protocol_test.py @@ -17,14 +17,14 @@ from absl.testing import absltest import integration_test_utils import httpx -from ucp_sdk.models.discovery.profile_schema import UcpDiscoveryProfile -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.ucp import BusinessSchema +from ucp_sdk.models.schemas.shopping import checkout as checkout +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, ) # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class ProtocolTest(integration_test_utils.IntegrationTestBase): @@ -36,7 +36,7 @@ class ProtocolTest(integration_test_utils.IntegrationTestBase): """ def _extract_document_urls( - self, profile: UcpDiscoveryProfile + self, profile: BusinessSchema ) -> list[tuple[str, str]]: """Extract all spec and schema URLs from the discovery profile. @@ -44,46 +44,56 @@ def _extract_document_urls( A list of (JSON path, URL) tuples. """ + profile = profile urls = set() # 1. Services - for service_name, service in profile.ucp.services.root.items(): - base_path = f"ucp.services['{service_name}']" - if service.spec: - urls.add((f"{base_path}.spec", str(service.spec))) - if service.rest and service.rest.schema_: - urls.add((f"{base_path}.rest.schema", str(service.rest.schema_))) - if service.mcp and service.mcp.schema_: - urls.add((f"{base_path}.mcp.schema", str(service.mcp.schema_))) - if service.embedded and service.embedded.schema_: - urls.add( - (f"{base_path}.embedded.schema", str(service.embedded.schema_)) - ) + for service_name, services_list in profile.get("services", {}).items(): + for svc_idx, service in enumerate( + services_list if isinstance(services_list, list) else [services_list] + ): + base_path = f"services['{service_name}'][{svc_idx}]" + if service.get("spec"): + urls.add((f"{base_path}.spec", str(service.get("spec")))) + if service.get("transport") == "rest" and service.get("schema"): + urls.add((f"{base_path}.schema", str(service.get("schema")))) + if service.get("transport") == "mcp" and service.get("schema"): + urls.add((f"{base_path}.schema", str(service.get("schema")))) + if service.get("transport") == "embedded" and service.get("schema"): + urls.add((f"{base_path}.schema", str(service.get("schema")))) # 2. Capabilities - for i, cap in enumerate(profile.ucp.capabilities): - cap_name = cap.name or f"index_{i}" - base_path = f"ucp.capabilities['{cap_name}']" - if cap.spec: - urls.add((f"{base_path}.spec", str(cap.spec))) - if cap.schema_: - urls.add((f"{base_path}.schema", str(cap.schema_))) + for _cap_key, caps in profile.get("capabilities", {}).items(): + for i, cap in enumerate(caps if isinstance(caps, list) else [caps]): + cap_name = cap.get("name") or f"index_{i}" + base_path = f"ucp.capabilities['{cap_name}']" + if cap.get("spec"): + urls.add((f"{base_path}.spec", str(cap.get("spec")))) + if cap.get("schema"): + urls.add((f"{base_path}.schema", str(cap.get("schema")))) # 3. Payment Handlers - if profile.payment and profile.payment.handlers: - for i, handler in enumerate(profile.payment.handlers): - handler_id = handler.id or f"index_{i}" - base_path = f"payment.handlers['{handler_id}']" - if handler.spec: - urls.add((f"{base_path}.spec", str(handler.spec))) - if handler.config_schema: - urls.add((f"{base_path}.config_schema", str(handler.config_schema))) - if handler.instrument_schemas: - for j, s in enumerate(handler.instrument_schemas): + for domain, handlers in profile.get("payment_handlers", {}).items(): + for i, handler in enumerate( + handlers if isinstance(handlers, list) else [handlers] + ): + handler_id = handler.get("id") or f"{domain}_index_{i}" + base_path = f"payment_handlers['{handler_id}']" + if handler.get("spec"): + urls.add((f"{base_path}.spec", str(handler.get("spec")))) + if handler.get("config_schema"): + urls.add( + (f"{base_path}.config_schema", str(handler.get("config_schema"))) + ) + if handler.get("instrument_schemas"): + for j, s in enumerate(handler.get("instrument_schemas", [])): urls.add((f"{base_path}.instrument_schemas[{j}]", str(s))) return sorted(urls, key=lambda x: x[0]) + import unittest + + @unittest.skip("Schemas not yet published on remote ucp.dev domain") def test_discovery_urls(self): """Verify all spec and schema URLs in discovery profile are valid. @@ -91,7 +101,7 @@ def test_discovery_urls(self): """ response = self.client.get("/.well-known/ucp") self.assert_response_status(response, 200) - profile = UcpDiscoveryProfile(**response.json()) + profile = response.json() url_entries = self._extract_document_urls(profile) failures = [] @@ -147,16 +157,20 @@ def test_discovery(self): data = response.json() # Validate schema using SDK model - profile = UcpDiscoveryProfile(**data) + BusinessSchema(**data) self.assertEqual( - profile.ucp.version.root, - "2026-01-11", + data.get("version"), + "2026-01-23", msg="Unexpected UCP version in discovery doc", ) # Verify Capabilities - capabilities = {c.name for c in profile.ucp.capabilities} + capabilities = { + c.get("name") + for caps in data.get("capabilities", {}).values() + for c in (caps if isinstance(caps, list) else [caps]) + } expected_capabilities = { "dev.ucp.shopping.checkout", "dev.ucp.shopping.order", @@ -171,7 +185,11 @@ def test_discovery(self): ) # Verify Payment Handlers - handlers = {h.id for h in profile.payment.handlers} + handlers = { + h.get("id") + for handlers in data.get("payment_handlers", {}).values() + for h in (handlers if isinstance(handlers, list) else [handlers]) + } expected_handlers = {"google_pay", "mock_payment_handler", "shop_pay"} missing_handlers = expected_handlers - handlers self.assertFalse( @@ -181,19 +199,29 @@ def test_discovery(self): # Specific check for Shop Pay config shop_pay = next( - (h for h in profile.payment.handlers if h.id == "shop_pay"), + ( + h + for handlers in data.get("payment_handlers", {}).values() + for h in (handlers if isinstance(handlers, list) else [handlers]) + if h.get("id") == "shop_pay" + ), None, ) self.assertIsNotNone(shop_pay, "Shop Pay handler not found") - self.assertEqual(shop_pay.name, "com.shopify.shop_pay") - self.assertIn("shop_id", shop_pay.config) + self.assertEqual(shop_pay.get("name"), "com.shopify.shop_pay") + self.assertIn("shop_id", shop_pay.get("config")) # Verify shopping capability - self.assertIn("dev.ucp.shopping", profile.ucp.services.root) - shopping_service = profile.ucp.services.root["dev.ucp.shopping"] - self.assertEqual(shopping_service.version.root, "2026-01-11") - self.assertIsNotNone(shopping_service.rest) - self.assertIsNotNone(shopping_service.rest.endpoint) + shopping_services = data.get("services", {}).get("dev.ucp.shopping") + self.assertIsNotNone(shopping_services, "Shopping service missing") + shopping_service = ( + shopping_services[0] + if isinstance(shopping_services, list) + else shopping_services + ) + self.assertEqual(shopping_service.get("version"), "2026-01-23") + self.assertIsNotNone(shopping_service.get("transport") == "rest") + self.assertIsNotNone(shopping_service.get("endpoint")) def test_version_negotiation(self): """Test protocol version negotiation via headers. @@ -207,27 +235,33 @@ def test_version_negotiation(self): # Discover shopping service endpoint discovery_resp = self.client.get("/.well-known/ucp") self.assert_response_status(discovery_resp, 200) - profile = UcpDiscoveryProfile(**discovery_resp.json()) - shopping_service = profile.ucp.services.root["dev.ucp.shopping"] + profile_dict = discovery_resp.json() + shopping_services = profile_dict.get("services", {}).get("dev.ucp.shopping") self.assertIsNotNone( - shopping_service, "Shopping service not found in discovery" + shopping_services, "Shopping service not found in discovery" + ) + shopping_service = ( + shopping_services[0] + if isinstance(shopping_services, list) + else shopping_services ) self.assertIsNotNone( - shopping_service.rest, "REST config not found for shopping service" + (shopping_service.get("transport") == "rest"), + "REST config not found for shopping service", ) self.assertIsNotNone( - shopping_service.rest.endpoint, + shopping_service.get("endpoint"), "Endpoint not found for shopping service", ) checkout_sessions_url = ( - f"{str(shopping_service.rest.endpoint).rstrip('/')}/checkout-sessions" + f"{str(shopping_service.get('endpoint')).rstrip('/')}/checkout-sessions" ) create_payload = self.create_checkout_payload() # 1. Compatible Version headers = integration_test_utils.get_headers() - headers["UCP-Agent"] = 'profile="..."; version="2026-01-11"' + headers["UCP-Agent"] = 'profile="..."; version="2026-01-23"' response = self.client.post( checkout_sessions_url, json=create_payload.model_dump( diff --git a/pyproject.toml b/pyproject.toml index b5193b8..6ee8dad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ packages = ["."] [tool.uv.sources] # The relative path is stored here -ucp-sdk = { path = "../sdk/python/", editable = true } +ucp-sdk = { path = "../python-sdk/", editable = true } [tool.ruff] line-length = 80 diff --git a/shopping-agent-test.json b/shopping-agent-test.json index 70f5ba2..ea2419d 100644 --- a/shopping-agent-test.json +++ b/shopping-agent-test.json @@ -1,16 +1,18 @@ { "ucp": { - "version": "2026-01-11", - "capabilities": [ - { - "name": "dev.ucp.shopping.order", - "version": "2026-01-11", - "spec": "https://ucp.dev/specs/shopping/order", - "schema": "https://ucp.dev/schemas/shopping/order.json", - "config": { - "webhook_url": "http://localhost:{webhook_port}/webhooks/partners/test_partner/events/order" + "version": "2026-01-23", + "capabilities": { + "dev.ucp.shopping.order": [ + { + "name": "dev.ucp.shopping.order", + "version": "2026-01-23", + "spec": "https://ucp.dev/specs/shopping/order", + "schema": "https://ucp.dev/schemas/shopping/order.json", + "config": { + "webhook_url": "http://localhost:{webhook_port}/webhooks/partners/test_partner/events/order" + } } - } - ] + ] + } } } diff --git a/test_data/flower_shop/addresses.csv b/test_data/flower_shop/addresses.csv index 2b2826b..4c0bdeb 100644 --- a/test_data/flower_shop/addresses.csv +++ b/test_data/flower_shop/addresses.csv @@ -1,4 +1,4 @@ id,customer_id,street_address,city,state,postal_code,country -addr_1,cust_1,123 Main St,Springfield,IL,62704,US -addr_2,cust_1,456 Oak Ave,Metropolis,NY,10012,US -addr_3,cust_2,789 Pine Ln,Smallville,KS,66002,US +addr_1,customer_1,123 Main St,Springfield,IL,62704,US +addr_2,customer_1,456 Oak Ave,Metropolis,NY,10012,US +addr_3,customer_2,789 Pine Ln,Springfield,KS,66002,US diff --git a/test_data/flower_shop/customers.csv b/test_data/flower_shop/customers.csv index 1b9b6b3..2ab8170 100644 --- a/test_data/flower_shop/customers.csv +++ b/test_data/flower_shop/customers.csv @@ -1,4 +1,4 @@ id,name,email -cust_1,John Doe,john.doe@example.com -cust_2,Jane Smith,jane.smith@example.com -cust_3,Jane Doe,jane.doe@example.com +customer_1,John Doe,john.doe@example.com +customer_2,Jane Smith,jane.smith@example.com +customer_3,Jane Doe,jane.doe@example.com diff --git a/validation_test.py b/validation_test.py index 566089f..ca2a850 100644 --- a/validation_test.py +++ b/validation_test.py @@ -16,18 +16,20 @@ from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import checkout_update_req -from ucp_sdk.models.schemas.shopping import fulfillment_resp as checkout -from ucp_sdk.models.schemas.shopping import payment_update_req -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping import ( + checkout_update_request as checkout_update_req, ) -from ucp_sdk.models.schemas.shopping.types import item_update_req -from ucp_sdk.models.schemas.shopping.types import line_item_update_req +from ucp_sdk.models.schemas.shopping import checkout as checkout +from ucp_sdk.models.schemas.shopping import payment_update_request +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, +) +from ucp_sdk.models.schemas.shopping.types import item_update_request +from ucp_sdk.models.schemas.shopping.types import line_item_update_request # Rebuild models to resolve forward references -checkout.Checkout.model_rebuild(_types_namespace={"PaymentResponse": Payment}) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class ValidationTest(integration_test_utils.IntegrationTestBase): @@ -86,21 +88,20 @@ def test_update_inventory_validation(self) -> None: checkout_id = checkout_obj.id # Update to excessive quantity (e.g. 10000) - item_update = item_update_req.ItemUpdateRequest( + item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, title=checkout_obj.line_items[0].item.title, ) - line_item_update = line_item_update_req.LineItemUpdateRequest( + line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, item=item_update, quantity=10001, ) - payment_update = payment_update_req.PaymentUpdateRequest( - selected_instrument_id=checkout_obj.payment.selected_instrument_id, + payment_update = payment_update_request.PaymentUpdateRequest( instruments=checkout_obj.payment.instruments, handlers=[ h.model_dump(mode="json", exclude_none=True) - for h in checkout_obj.payment.handlers + for h in checkout_obj.payment.instruments ], ) diff --git a/webhook_test.py b/webhook_test.py index 80d80e9..a72b0d7 100644 --- a/webhook_test.py +++ b/webhook_test.py @@ -17,15 +17,13 @@ import time from absl.testing import absltest import integration_test_utils -from ucp_sdk.models.schemas.shopping import fulfillment_resp -from ucp_sdk.models.schemas.shopping.payment_resp import ( - PaymentResponse as Payment, +from ucp_sdk.models.schemas.shopping import checkout +from ucp_sdk.models.schemas.shopping.payment import ( + Payment, ) # Rebuild models to resolve forward references -fulfillment_resp.Checkout.model_rebuild( - _types_namespace={"PaymentResponse": Payment} -) +checkout.Checkout.model_rebuild(_types_namespace={"Payment": Payment}) class WebhookTest(integration_test_utils.IntegrationTestBase): @@ -58,7 +56,7 @@ def test_webhook_event_stream(self) -> None: # 1. Create checkout (webhook URL passed via UCP-Agent header) checkout_data = self.create_checkout_session(headers=self.get_headers()) - checkout_obj = fulfillment_resp.Checkout(**checkout_data) + checkout_obj = checkout.Checkout(**checkout_data) checkout_id = checkout_obj.id # 2. Complete Checkout @@ -120,11 +118,16 @@ def test_webhook_order_address_known_customer(self) -> None: """Test that webhook contains correct address for known customer/address.""" buyer_info = {"fullName": "John Doe", "email": "john.doe@example.com"} checkout_data = self.create_checkout_session(buyer=buyer_info) - checkout_obj = fulfillment_resp.Checkout(**checkout_data) + checkout_obj = checkout.Checkout(**checkout_data) - # Trigger fulfillment update to inject address + # Update to trigger address injection and selection self.update_checkout_session( - checkout_obj, fulfillment={"methods": [{"type": "shipping"}]} + checkout_obj, + fulfillment={ + "methods": [ + {"id": "method_1", "line_item_ids": ["item_123"], "type": "shipping"} + ] + }, ) # Fetch to get injected destinations @@ -133,20 +136,30 @@ def test_webhook_order_address_known_customer(self) -> None: headers=self.get_headers(), ) checkout_data = response.json() - checkout_obj = fulfillment_resp.Checkout(**checkout_data) + checkout_obj = checkout.Checkout(**checkout_data) - if ( - checkout_obj.fulfillment - and checkout_obj.fulfillment.root.methods - and checkout_obj.fulfillment.root.methods[0].destinations + self.assertTrue( + getattr(checkout_obj, "model_extra", None) + and checkout_obj.model_extra.get("fulfillment") + and checkout_obj.model_extra["fulfillment"].get("methods") + ) + if checkout_obj.model_extra["fulfillment"]["methods"][0].get( + "destinations" ): - method = checkout_obj.fulfillment.root.methods[0] - dest_id = method.destinations[0].root.id + method = checkout_obj.model_extra["fulfillment"]["methods"][0] + dest_id = method["destinations"][0]["id"] # Select destination first to calculate options self.update_checkout_session( checkout_obj, fulfillment={ - "methods": [{"type": "shipping", "selected_destination_id": dest_id}] + "methods": [ + { + "id": "method_1", + "line_item_ids": ["item_1"], + "type": "shipping", + "selected_destination_id": dest_id, + } + ] }, ) @@ -155,18 +168,28 @@ def test_webhook_order_address_known_customer(self) -> None: self.get_shopping_url(f"/checkout-sessions/{checkout_obj.id}"), headers=self.get_headers(), ) - checkout_obj = fulfillment_resp.Checkout(**response.json()) - method = checkout_obj.fulfillment.root.methods[0] - if method.groups and method.groups[0].options: - option_id = method.groups[0].options[0].id + checkout_obj = checkout.Checkout(**response.json()) + method = checkout_obj.model_extra["fulfillment"]["methods"][0] + if method.get("groups", []) and method.get("groups", [])[0].get( + "options", [] + ): + option_id = method.get("groups", [])[0].get("options", [])[0].get("id") self.update_checkout_session( checkout_obj, fulfillment={ "methods": [ { + "id": "method_1", + "line_item_ids": ["item_1"], "type": "shipping", "selected_destination_id": dest_id, - "groups": [{"selected_option_id": option_id}], + "groups": [ + { + "id": "group_1", + "line_item_ids": ["item_1"], + "selected_option_id": option_id, + } + ], } ] }, @@ -197,7 +220,7 @@ def test_webhook_order_address_new_address(self) -> None: """Test that webhook contains correct address when a new one is provided.""" buyer_info = {"fullName": "John Doe", "email": "john.doe@example.com"} checkout_data = self.create_checkout_session(buyer=buyer_info) - checkout_obj = fulfillment_resp.Checkout(**checkout_data) + checkout_obj = checkout.Checkout(**checkout_data) new_address = { "id": "dest_new_webhook", @@ -209,6 +232,8 @@ def test_webhook_order_address_new_address(self) -> None: fulfillment_payload = { "methods": [ { + "id": "method_1", + "line_item_ids": ["item_123"], "type": "shipping", "destinations": [new_address], "selected_destination_id": "dest_new_webhook", @@ -222,16 +247,24 @@ def test_webhook_order_address_new_address(self) -> None: self.get_shopping_url(f"/checkout-sessions/{checkout_obj.id}"), headers=self.get_headers(), ) - checkout_obj = fulfillment_resp.Checkout(**response.json()) - method = checkout_obj.fulfillment.root.methods[0] + checkout_obj = checkout.Checkout(**response.json()) + method = checkout_obj.model_extra["fulfillment"]["methods"][0] - if method.groups and method.groups[0].options: - option_id = method.groups[0].options[0].id + if method.get("groups", []) and method.get("groups", [])[0].get( + "options", [] + ): + option_id = method.get("groups", [])[0].get("options", [])[0].get("id") # Select option fulfillment_payload["methods"][0]["groups"] = [ - {"selected_option_id": option_id} + { + "id": "group_1", + "line_item_ids": ["item_123"], + "selected_option_id": option_id, + } ] fulfillment_payload["methods"][0]["type"] = "shipping" + fulfillment_payload["methods"][0]["id"] = "method_1" + fulfillment_payload["methods"][0]["line_item_ids"] = ["item_123"] self.update_checkout_session( checkout_obj, fulfillment=fulfillment_payload ) From 4b1be3d402238b1193bb8bf37d01102923a1d644 Mon Sep 17 00:00:00 2001 From: cusell-google Date: Fri, 17 Apr 2026 17:23:05 +0200 Subject: [PATCH 2/5] fix: remove deprecated item metadata fields As per PR review feedback, item objects no longer accept other metadata fields instead of id in the request front. This commit removes title from ItemCreateRequest and ItemUpdateRequest instantiations. --- business_logic_test.py | 12 +++--------- checkout_lifecycle_test.py | 3 --- integration_test_utils.py | 10 +--------- validation_test.py | 3 --- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/business_logic_test.py b/business_logic_test.py index 5177ffd..3b892e5 100644 --- a/business_logic_test.py +++ b/business_logic_test.py @@ -60,12 +60,9 @@ def test_totals_calculation_on_create(self): ) expected_price = int(default_item.get("price", 3500)) - # Create with wrong title in payload. The helper sends a default price - # if not specified, but the server should ignore client-provided values - # and use the authoritative price from its DB (which matches our config). - response_json = self.create_checkout_session( - title="Wrong Title", select_fulfillment=False - ) + # Create checkout (client cannot send title/price per schema). The server + # should use the authoritative price from its DB (which matches our config). + response_json = self.create_checkout_session(select_fulfillment=False) checkout_obj = checkout.Checkout(**response_json) # Verify Line Item Calculations @@ -133,7 +130,6 @@ def test_totals_recalculation_on_update(self): # Update quantity to 2. Total should be 2 * expected_price. item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, - title=checkout_obj.line_items[0].item.title, ) line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, @@ -197,7 +193,6 @@ def test_discount_flow(self): # Apply Discount item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, - title=checkout_obj.line_items[0].item.title, ) line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, @@ -486,7 +481,6 @@ def test_buyer_info_persistence(self): # Update with buyer info item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, - title=checkout_obj.line_items[0].item.title, ) line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, diff --git a/checkout_lifecycle_test.py b/checkout_lifecycle_test.py index 07ef2f0..835c4b2 100644 --- a/checkout_lifecycle_test.py +++ b/checkout_lifecycle_test.py @@ -91,7 +91,6 @@ def test_update_checkout(self): # Construct Update Request item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, - title=checkout_obj.line_items[0].item.title, ) line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, @@ -246,7 +245,6 @@ def test_cannot_update_canceled_checkout(self): # Try Update item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, - title=checkout_obj.line_items[0].item.title, ) line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, @@ -355,7 +353,6 @@ def test_cannot_update_completed_checkout(self): # Try Update item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, - title=checkout_obj.line_items[0].item.title, ) line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, diff --git a/integration_test_utils.py b/integration_test_utils.py index 11027f8..dd245ef 100644 --- a/integration_test_utils.py +++ b/integration_test_utils.py @@ -443,7 +443,6 @@ def create_checkout_payload( self, quantity=1, item_id: str | None = None, - title: str | None = None, currency: str | None = None, handlers=None, buyer: dict[str, Any] | None = None, @@ -454,7 +453,6 @@ def create_checkout_payload( Args: quantity: Number of items to purchase. Defaults to 1. item_id: ID of the item. Defaults to config or "item_1". - title: Title of the item. Defaults to config or "Test Item". currency: Currency code. Defaults to config or "USD". handlers: Optional list of payment handlers. If None, defaults to Google Pay. @@ -474,8 +472,6 @@ def create_checkout_payload( if item_id is None: item_id = default_item.get("id", "item_1") - if title is None: - title = default_item.get("title", "Test Item") if currency is None: currency = self.conformance_config.get("currency", "USD") @@ -492,7 +488,7 @@ def create_checkout_payload( ) ] - item = item_create_request.ItemCreateRequest(id=item_id, title=title) + item = item_create_request.ItemCreateRequest(id=item_id) line_item = line_item_create_request.LineItemCreateRequest( quantity=quantity, item=item ) @@ -597,7 +593,6 @@ def create_checkout_session( self, quantity: int = 1, item_id: str | None = None, - title: str | None = None, currency: str | None = None, handlers: list[Any] | None = None, buyer: dict[str, Any] | None = None, @@ -609,7 +604,6 @@ def create_checkout_session( Args: quantity: Number of items to purchase. Defaults to 1. item_id: ID of the item. Defaults to config or "item_1". - title: Title of the item. Defaults to config or "Test Item". currency: Currency code. Defaults to config or "USD". handlers: Optional list of payment handlers. If None, defaults to Google Pay. @@ -625,7 +619,6 @@ def create_checkout_session( create_payload = self.create_checkout_payload( quantity=quantity, item_id=item_id, - title=title, currency=currency, handlers=handlers, buyer=buyer, @@ -852,7 +845,6 @@ def update_checkout_session( for li in checkout_obj.line_items: item_update = item_update_request.ItemUpdateRequest( id=li.item.id, - title=li.item.title, ) line_items.append( line_item_update_request.LineItemUpdateRequest( diff --git a/validation_test.py b/validation_test.py index ca2a850..77a5301 100644 --- a/validation_test.py +++ b/validation_test.py @@ -57,7 +57,6 @@ def test_out_of_stock(self) -> None: create_payload = self.create_checkout_payload( item_id=out_of_stock_item["id"], - title=out_of_stock_item["title"], ) response = self.client.post( @@ -90,7 +89,6 @@ def test_update_inventory_validation(self) -> None: # Update to excessive quantity (e.g. 10000) item_update = item_update_request.ItemUpdateRequest( id=checkout_obj.line_items[0].item.id, - title=checkout_obj.line_items[0].item.title, ) line_item_update = line_item_update_request.LineItemUpdateRequest( id=checkout_obj.line_items[0].id, @@ -140,7 +138,6 @@ def test_product_not_found(self) -> None: create_payload = self.create_checkout_payload( item_id=non_existent_item["id"], - title=non_existent_item["title"], ) response = self.client.post( From c746338ff46ad25def07267b7ce2ab89df939179 Mon Sep 17 00:00:00 2001 From: cusell-google Date: Fri, 17 Apr 2026 17:54:45 +0200 Subject: [PATCH 3/5] fix: change token type to 'token' for binding test completion --- binding_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binding_test.py b/binding_test.py index 483ddf2..9af67f3 100644 --- a/binding_test.py +++ b/binding_test.py @@ -55,7 +55,7 @@ def test_token_binding_completion(self) -> None: "last_digits": "4242", }, "credential": { - "type": "stripe_token", + "type": "token", "token": "success_token", "binding": { "checkout_id": checkout_id, From 10c4adbee407601d7956e4326af9889a92b876c1 Mon Sep 17 00:00:00 2001 From: cusell-google Date: Fri, 17 Apr 2026 19:15:42 +0200 Subject: [PATCH 4/5] fix(conformance): update discovery validaton to not look for capability name --- protocol_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/protocol_test.py b/protocol_test.py index a39da33..9c137d8 100644 --- a/protocol_test.py +++ b/protocol_test.py @@ -167,9 +167,7 @@ def test_discovery(self): # Verify Capabilities capabilities = { - c.get("name") - for caps in data.get("capabilities", {}).values() - for c in (caps if isinstance(caps, list) else [caps]) + cap_name for cap_name in data.get("capabilities", {}).keys() } expected_capabilities = { "dev.ucp.shopping.checkout", From 2c5b02f6b7c33ee5a4c2724d9db6f26089aecaee Mon Sep 17 00:00:00 2001 From: cusell-google Date: Tue, 21 Apr 2026 11:06:02 +0200 Subject: [PATCH 5/5] fix(conformance): date version spec and schema urls --- protocol_test.py | 4 +--- shopping-agent-test.json | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/protocol_test.py b/protocol_test.py index 9c137d8..2b4d589 100644 --- a/protocol_test.py +++ b/protocol_test.py @@ -166,9 +166,7 @@ def test_discovery(self): ) # Verify Capabilities - capabilities = { - cap_name for cap_name in data.get("capabilities", {}).keys() - } + capabilities = set(data.get("capabilities", {})) expected_capabilities = { "dev.ucp.shopping.checkout", "dev.ucp.shopping.order", diff --git a/shopping-agent-test.json b/shopping-agent-test.json index ea2419d..3617648 100644 --- a/shopping-agent-test.json +++ b/shopping-agent-test.json @@ -6,8 +6,8 @@ { "name": "dev.ucp.shopping.order", "version": "2026-01-23", - "spec": "https://ucp.dev/specs/shopping/order", - "schema": "https://ucp.dev/schemas/shopping/order.json", + "spec": "https://ucp.dev/2026-01-23/specification/order/", + "schema": "https://ucp.dev/2026-01-23/schemas/shopping/order.json", "config": { "webhook_url": "http://localhost:{webhook_port}/webhooks/partners/test_partner/events/order" }