feat(cart): add terminal status to the cart resource - #740
Draft
prateek-ct wants to merge 1 commit into
Draft
Conversation
Adds status (active | ordered | canceled | expired) so a platform can learn how a cart ended after a one-way continue_url handoff, instead of inferring it from an ambiguous not_found. Also separates expiry from retention, and adds cart_not_active for mutations on terminal carts. BREAKING CHANGE: status is required on cart responses, matching checkout.
Open
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Cart handoff via
continue_urlis one-way. The buyer finishes in the business's own UI, and the platform does not participate in that flow — it may hold no checkout session and issue no further calls. But the platform is still surfacing that cart, and today it has no way to learn whether the cart was purchased.Cart carries no
status. The schema says so explicitly: "no payment info or complex status states", andcart.mddescribes cart status as "Binary (exists/not found)". So the only observable signal is Get Cart returningnot_found— which covers ordered, canceled, expired and never-existed alike.Two things make that signal not merely coarse but unreliable:
Clearing is
MAY.cart.mdsays the business may clear the cart after checkout completion. A business that clears aggressively makes an abandoned cart look purchased; one that retains makes a purchased cart look untouched. The platform cannot calibrate without per-merchant knowledge, which is the thing a protocol is supposed to remove.The two cases prescribe opposite handling. A purchased cart should stop being prompted; a lapsed cart is a reasonable thing to offer to rebuild.
not_foundcannot tell them apart.ucp.statusdoes not help — its enum issuccess | error, an envelope discriminator, not a resource lifecycle.What this changes
Adds
statusto the cart resource:active | ordered | canceled | expired.Cart keeps its deliberately status-free active life. There are no intermediate states, because every state between purchase intent and order placement belongs to Checkout. What is added is a terminal disposition, so how a cart ended is reported rather than inferred. Terminal values are immutable.
orderedis independent of the flow that placed the order — a UCP checkout session, the business's own web checkout after acontinue_urlhandoff, or anything else. The business sets it from its own record of the purchase, so no UCP checkout session need exist. This matters for platforms whose cart integration is one-way handoff only and which never callcreate_checkout.Also in this PR:
expires_atbounds usability and drives theexpiredstatus. Retention bounds how long a terminalstatusstays readable. Cart defines only the first, so platforms are told not to assume a terminal status is retrievable indefinitely.cart_not_activefor mutating operations on terminal carts, added to thecommon/types/error_code.jsonexamples. Cancel Cart stays idempotent on already-canceledcarts.not_foundseparated from terminal status in both bindings, with an "Ordered" example tab for Get Cart in REST and MCP.Actions MUST NOT change status. Actions semantics are otherwise untouched.statusis required, the example corpus needed two follow-on edits to stay green:"status": "active"added toscripts/scaffolds/shopping_cart_response.json(so partial cart examples validated viaextract=/target=still pass through scaffold merge), and to six full cart response examples indiscount.md(which the coverage walk requires explicitly).Breaking change
statusis added torequired, matchingcheckout.json, so a business advertising this cart version always answers the question. An optionalstatuswould reproduce today's ambiguity for any business that omits it, which defeats the purpose.The alternative is optional
statuswith "absent MUST be treated asactive". That path reproduces today's ambiguity for any business that omits the field:orderedis a new value only new implementations emit, so absence is indistinguishable from a business that never sets it.Open question: retention TTL
This PR names the retention gap but does not close it, because a terminal
statusis only useful for as long as the record survives cleanup — and UCP has no vocabulary for that window.Two distinct concepts are involved, and cart currently conflates them:
expires_at— absolute RFC 3339 instant, optional, no default (checkout defaults to 6h; cart states none)statusstop being readable?This PR keeps the two distinct in the docs and leaves the retention window to the business, with a
SHOULDto retain terminal carts long enough for a handed-off platform to read the outcome. It deliberately does not introduce a retention field.Worth deciding whether UCP wants a retention field at all, or only non-normative guidance — most naturally in #344 (Cart sessions need persistence guidance), which already asks for TTL ranges by use case.
Validation
Run against upstream
main(this branch is rebased onto it):ucp-schema lint source/— 107 files checked, all passed. The five warnings are pre-existing$idwarnings on the openrpc/openapi service files.scripts/validate_examples.py --schema-base source/schemas/— 311 passed, 0 failed, 50 skipped, matching the pre-change baseline exactly. Verified the baseline by temporarily un-requiringstatusand re-running.scripts/test_validate_examples.py— 50 passed.markdownlintandcspellclean on all changed files.ucp:exampleannotations per the current convention, and the MCPcontent.textplaceholder matches the updated"{\"ucp\":{…},…}"form.rest.openapi.jsonandmcp.openrpc.json$refcart.jsondirectly, so no transport spec needed updating. Nosdk/directory onmain, so no model regeneration applies.Process note
Per CONTRIBUTING.md — significant changes, core schema edits and breaking changes require an Enhancement Proposal approved by the Tech Council.
Proposal: #739.
This PR is deliberately opened as a draft — it is the reference implementation to review alongside that proposal, not a request to merge ahead of the TC process.