Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions rest/python/server/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,69 @@ def test_webhook_delivers_the_bare_order_as_body(self) -> None:
self.assertNotIn("event_type", body)
self.assertNotIn("order", body)

def test_order_advertises_the_order_capability_not_checkout(self) -> None:
"""An order, fetched or delivered by webhook, names the order capability.

overview.md, Response Capability Selection: a business MUST include in
ucp.capabilities only the capabilities relevant to the response's
operation type, and an order response includes dev.ucp.shopping.order and
not checkout. The order was built from the completed checkout's ucp block,
so GET /orders/{id} and the order webhook body both advertised
dev.ucp.shopping.checkout.
"""
with self.client:
payload = self._create_checkout_payload(
"order_capability", [("rose", "Red Rose", 1000, 1)]
)
response = self.client.post(
"/checkout-sessions",
headers=self._get_headers(idempotency_key="oc1", request_id="oc1"),
json=payload.model_dump(mode="json", exclude_none=True),
)
self.assertEqual(response.status_code, 201, response.text)
checkout_sid = self.get_resource_id(response.json()["id"])

response = self.client.post(
f"/checkout-sessions/{checkout_sid}/complete",
headers=self._get_headers(idempotency_key="oc2", request_id="oc2"),
json=self._create_payment_payload(),
)
self.assertEqual(response.status_code, 200, response.text)
# The completed checkout response keeps the checkout capability.
self.assertEqual(
list(response.json()["ucp"]["capabilities"]),
["dev.ucp.shopping.checkout"],
)
checkout = UnifiedCheckout.model_validate(response.json())
order_id = response.json()["order"]["id"]

response = self.client.get(
f"/orders/{order_id}", headers=self._get_headers()
)
self.assertEqual(response.status_code, 200, response.text)
fetched = response.json()

checkout.platform = PlatformSchema(
webhook_url="https://platform.example/ucp-webhook"
)

captured = self._notify_and_capture(checkout, "order_placed")
self.assertEqual(len(captured), 1, "exactly one webhook must be delivered")

delivered = captured[0]["json"]
for label, order in (("GET /orders/{id}", fetched), ("webhook", delivered)):
capabilities = order["ucp"]["capabilities"]
self.assertEqual(
list(capabilities),
["dev.ucp.shopping.order"],
f"{label} must advertise only the order capability",
)
self.assertEqual(
capabilities["dev.ucp.shopping.order"][0]["version"],
order["ucp"]["version"],
)
Order.model_validate(order)

def test_webhook_is_skipped_when_there_is_no_order(self) -> None:
"""No webhook is delivered when the checkout has no order to send.

Expand Down
17 changes: 13 additions & 4 deletions rest/python/server/services/checkout_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,12 +891,21 @@ async def complete_checkout(
)
order_line_items.append(oli)

version = config.get_server_version()
order = Order(
ucp=ResponseOrder(
version=getattr(checkout.ucp, "version", "2026-04-08"),
capabilities=dict(checkout.ucp.capabilities)
if hasattr(checkout.ucp, "capabilities") and checkout.ucp.capabilities
else {},
version=version,
# An order response names the order capability, not the checkout
# capabilities it was created from (overview.md, Response
# Capability Selection).
capabilities={
"dev.ucp.shopping.order": [
Response(
name="dev.ucp.shopping.order",
version=version,
)
]
},
),
id=checkout.order.id,
checkout_id=checkout.id,
Expand Down
Loading