feat(rest/python): implement cart capability and discount extension - #159
feat(rest/python): implement cart capability and discount extension#159damaz91 wants to merge 11 commits into
Conversation
…o payment handlers)
…pability extends cart
…h default handlers
# Conflicts: # rest/python/server/services/checkout_service.py
vishkaty
left a comment
There was a problem hiding this comment.
Tested this end-to-end — it holds up very well. Since this is the reference implementation for a released capability, I ran it rather than just reading it:
- Full suite on the PR head: 165 passed (131 at the merge-base — the delta is the new cart test file; no regressions in the checkout/webhook/signature tests).
- On the wire (server seeded with the flower-shop data): full cart lifecycle works — create/get/update/cancel,
10OFFapplied astotals: [subtotal 3500, discount −350, total 3150]with a correctallocationsentry, and conversion viacart_idonly correctly inherits the cart's line items and discounts, populatespayment_handlers, is idempotent (a second create with the samecart_idreturns the same checkout), and clears the cart after/complete. The happy-path client runs the whole flow green. - Schema-validated every response against the official v2026-04-08 schemas (pinned at
a2d8bf0): cart responses vsshopping/cart.json+ the discount extension, conversion checkouts vscheckout.json+cart.json#/$defs/checkout, and/.well-known/ucpvsucp.json#/$defs/business_schema— all pass. Nice touch getting thediscounttotal negative, whichtotal.json's conditional (exclusiveMaximum: 0) requires and which is easy to miss. - Edge behavior is right too: 409 on idempotency-key reuse with a different body, 400 with the UCP error envelope for unknown products, 422 when neither
cart_idnorline_itemsis provided.
One logistical note from actually performing the rebase: the conflict with main is in create_checkout, where #167's server-assigned checkout id collides with the checkout_id = getattr(checkout_req, "id", ...) line inside the new cart-initialization block. Resolving in favor of the server-assigned id is mechanical, but the two cart-conversion tests then need a matching update — they assert client-chosen checkout ids, which #167 now (correctly) forbids. The cart-clearing in complete_checkout merges cleanly alongside #169's webhook dispatch.
This also closes #134 as scoped there (lifecycle routes, discovery declaration, conversion semantics per cart.md, tests in the existing style — plus the discount extension beyond it), and it gives cart conformance coverage something real to run against, which the ecosystem has been missing. Great to see this land.
Description
Implement the Cart capability (
dev.ucp.shopping.cart) and the Cart Discount extension (dev.ucp.shopping.discount) in the Python REST sample server.Key changes:
CartServicefor managing cart lifecycle (create, get, update, cancel) and calculating estimated totals.models.pywithUnifiedCart,UnifiedCartCreateRequest, andUnifiedCartUpdateRequestto support the discount extension fields on cart.cart_test.pycovering cart CRUD operations and discount application.CheckoutService: inherits all cart data (line items, buyer, context, signals, attribution, and discounts) and deletes the cart session upon successful checkout completion.cart_id.UnifiedCheckoutCreateRequest(models.py) to makeline_itemsoptional, and added a model validator enforcing that eithercart_idorline_itemsis provided. This allows clients to convert by sending only thecart_idas per the UCP specification.CheckoutService.create_checkoutto handle missing optional fields safely.simple_happy_path_client.pyto execute the full Cart-to-Checkout flow, verifying the conversion with a minimal payload.idfield for new line items during updates, ensuring it only sends known IDs or leaves them unset, conforming to UCP specifications.discovery_profile.jsonto expose the cart capability and advertise that thedev.ucp.shopping.discountcapability extends bothcheckoutandcart.ucp.payment_handlersin the checkout response (services/checkout_service.py) with default supported handlers loaded from the discovery profile via a new cached helper inconfig.py. This ensures full compliance with the required fields in the response schema._recalculate_totalsto_enrich_and_recalculatein bothcart_service.pyandcheckout_service.pyto clarify that the method performs product details enrichment from the database (like updating titles and prices) before computing totals.db.pyby using.as_string()on the JSON index to avoid quote mismatch during cart lookup.Category (Required)
ucp-schematool (resolver, linter, validator). (Requires Maintainer approval)Related Issues
Fixes #134
Checklist
!for breaking changes).