Skip to content

feat(cart): add terminal status to the cart resource - #740

Draft
prateek-ct wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
commercetools:feat/cart-terminal-status
Draft

feat(cart): add terminal status to the cart resource#740
prateek-ct wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
commercetools:feat/cart-terminal-status

Conversation

@prateek-ct

@prateek-ct prateek-ct commented Aug 18, 2026

Copy link
Copy Markdown

Problem

Cart handoff via continue_url is 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", and cart.md describes cart status as "Binary (exists/not found)". So the only observable signal is Get Cart returning not_found — which covers ordered, canceled, expired and never-existed alike.

Two things make that signal not merely coarse but unreliable:

  1. Clearing is MAY. cart.md says 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.

  2. 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_found cannot tell them apart.

ucp.status does not help — its enum is success | error, an envelope discriminator, not a resource lifecycle.

What this changes

Adds status to 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.

ordered is independent of the flow that placed the order — a UCP checkout session, the business's own web checkout after a continue_url handoff, 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 call create_checkout.

Also in this PR:

  • Expiry vs retention, which cart previously conflated. expires_at bounds usability and drives the expired status. Retention bounds how long a terminal status stays readable. Cart defines only the first, so platforms are told not to assume a terminal status is retrievable indefinitely.
  • cart_not_active for mutating operations on terminal carts, added to the common/types/error_code.json examples. Cancel Cart stays idempotent on already-canceled carts.
  • not_found separated from terminal status in both bindings, with an "Ordered" example tab for Get Cart in REST and MCP.
  • The Actions section's sentence "The cart has no status lifecycle" amended to "no intermediate status lifecycle", plus an explicit Actions MUST NOT change status. Actions semantics are otherwise untouched.
  • Because status is required, the example corpus needed two follow-on edits to stay green: "status": "active" added to scripts/scaffolds/shopping_cart_response.json (so partial cart examples validated via extract=/target= still pass through scaffold merge), and to six full cart response examples in discount.md (which the coverage walk requires explicitly).

Breaking change

status is added to required, matching checkout.json, so a business advertising this cart version always answers the question. An optional status would reproduce today's ambiguity for any business that omits it, which defeats the purpose.

The alternative is optional status with "absent MUST be treated as active". That path reproduces today's ambiguity for any business that omits the field: ordered is 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 status is 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:

Concept Question it answers UCP cart today
Expiry When does the cart stop being usable? expires_at — absolute RFC 3339 instant, optional, no default (checkout defaults to 6h; cart states none)
Retention When does the record stop existing, and so when does a terminal status stop being readable? no field, no default

This PR keeps the two distinct in the docs and leaves the retention window to the business, with a SHOULD to 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 $id warnings 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-requiring status and re-running.
  • scripts/test_validate_examples.py — 50 passed.
  • markdownlint and cspell clean on all changed files.
  • New examples carry ucp:example annotations per the current convention, and the MCP content.text placeholder matches the updated "{\"ucp\":{…},…}" form.
  • rest.openapi.json and mcp.openrpc.json $ref cart.json directly, so no transport spec needed updating. No sdk/ directory on main, 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.

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.
@prateek-ct prateek-ct changed the title feat(cart)!: add terminal status to the cart resource feat(cart): add terminal status to the cart resource Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant