Skip to content

test: cover the payment-terms cross-field invariants (terms.md) - #89

Open
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:test/payment-terms-conformance
Open

test: cover the payment-terms cross-field invariants (terms.md)#89
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:test/payment-terms-conformance

Conversation

@vishkaty

@vishkaty vishkaty commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Adds a conformance module for the payment-terms capability that landed in ucp#602, covering the cross-field invariants the JSON schema cannot express. Offered in the #602 review thread (ucp#692 discussion) once the shape settled.

What it checks (all against the raw wire payload, following the totals_test.py stance):

Test Invariant Spec anchor (terms.md)
selected_term_names_an_offered_term terms non empty when present; selected_term_id present and names exactly one terms[].id Where terms is present it MUST contain at least one term, and selected_term_id MUST name one of them
term_ids_unique_within_checkout terms[].id unique A Business MUST make terms[].id unique within a Checkout
selected_term_schedules_sum_to_checkout_total selected term schedule amounts sum to the checkout total For the selected term, the Business MUST ensure that sum equals the checkout total
selecting_each_term_recomputes_the_checkout each selection is honored, or the change is reported with a payment_term_changed warning — including when the requested term stays selected but is rewritten in place (schedules, due dates, amounts); the responses selected term always resolves and keeps the sum invariant; selections drawn from the latest response terms[] a business resolving a selection conflict MUST report the change as a payment_term_changed warning, and MUST also report it when changing the selected term in place without naming a different term; a Platform can therefore detect a changed selection from the code alone
unresolvable_selection_is_not_silently_substituted an unknown selected_term_id is either rejected or answered with a payment_term_changed warning, never a silent substitute; a control update with a valid id must succeed first so a 4xx is attributable to the id MUST NOT silently substitute a term, and MUST report the change as a payment_term_changed warning in messages[]
accepted_term_travels_to_the_order order carries payment.accepted_term for the term selected once the checkout REACHED ready_for_complete (skips honestly if readiness is not observed); its schedules sum to the order total at creation; asserts on the embedded order or follows the id to GET /orders/{id} (the order_test pattern) Carry the accepted term onto the Order; MUST ensure its schedule amounts sum to the Order total; the term is already agreed before ready_for_complete
due_at_when_present_is_rfc3339 every stated due_at matches the RFC 3339 shape (lowercase t/z and leap seconds accepted per the RFC; naive datetimes without a UTC offset rejected as not absolute) with calendar fields range checked due_at is an absolute RFC 3339 date-time

Conformant variability is accepted, not just the happy path. A business that answers a valid selection by keeping the previous term with a payment_term_changed warning (a selection conflict), changes its default term during fulfillment updates (warned), embeds only an order reference at completion, changes which terms are offered across responses, or rejects an unresolvable selection with a 4xx instead of warning: all pass. Each of these is exercised as its own verification mode.

Gating and current CI behavior. The module gates on dev.ucp.common.payment.terms in /.well-known/ucp (the discount_test.py pattern) and additionally skips when the driven checkout carries no terms, since offering terms on a given checkout is a business decision. Against the current sample server the module skips cleanly (verified against samples main); the checks activate the day a business or the reference advertises the capability.

Verification. Exercised against a minimal golden business implementing the extension: 2 answer postures and 6 additional spec-sanctioned behavior variants all PASS (including an in-place term rewrite that is correctly warned); 11 violation mutants across 14 matrix rows each fail the intended test (wrong schedule sum, duplicate term ids, dangling and missing selected_term_id, ignored selection, silent substitution, missing and mis-summing accepted_term with both full and reference-only order embeds, malformed and naive due_at). Broad violations (a dangling selection, duplicate ids) legitimately fail every test that resolves the selected term; the selection, silence, order, and due_at tests each also have a surgical killer that fails them alone. Ruff lint and format clean per the repo config.

Notes and limits.

  • Assertions are wire level dict reads, so nothing here depends on the SDK carrying payment_terms models.
  • Not asserted: that checkout terms are absent from the order (stated by the spec but additionalProperties true makes an extra field schema legal rather than a MUST violation); the disclosure travel MUSTs (they need a business with a disclosure bearing term to drive; follow-up once a reference implements one); the totals difference entry MUST (whether a term changes what the purchase costs is not observable server-agnostically); selected_term_id absent when terms absent (unreachable given the no-terms skip).
  • A business that churns term ids on every response without reporting payment_term_changed would still fail the selection test; the spec's own detection-from-the-code-alone sentence is read as making the warning the required signal for any changed selection.

@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Aug 14, 2026
@damaz91

damaz91 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Thanks for working on this. Let's put this on-hold until the new UCP version + SDK is launched

@damaz91 damaz91 added status:blocked and removed status:needs-triage Signal that the PR is ready for human triage labels Aug 17, 2026
@vishkaty

Copy link
Copy Markdown
Contributor Author

Sounds good, and once the new version and SDK are out I will re verify the module against the released text and refresh this PR to match.

Rebases the held payment-terms conformance module (payment_terms_test.py)
onto upstream main and brings it forward to the text UCP actually
released as v2026-08-25, replacing the pre-release draft text it was
originally built against in mid August.

Import fix (ucp#741): the payment constructs moved from
ucp_sdk.models.schemas.shopping to ucp_sdk.models.schemas.common.types.
The bare Payment import is now a try/except: the common.types.payment
location first (ucp-sdk 0.5.0 and newer), falling back to the old
shopping.payment location (ucp-sdk 0.4.6 and older, including the
0.4.4 the repo currently pins). There is no existing version-tolerant
import idiom anywhere else in this repo, so this introduces the
try/except pattern locally to this one file rather than a repo wide
convention.

Capability identifier fix: the module checked discovery for
dev.ucp.shopping.payment_terms. The released terms.md names the
capability dev.ucp.common.payment.terms (see its Capability Name line
and the schema name in source/schemas/common/payment_terms.json), and
this also lines up with the ucp#741 shopping to common move above.
Left uncorrected, the module would have silently skipped every test
forever, even once a reference merchant ships the capability, since
_advertises_payment_terms() would never match the real name on the
wire.

Doc relocation (#723 reorg): the spec doc moved to
docs/specification/payment/extensions/terms.md. Updated every
payment-terms.md citation in the module docstrings and comments to
terms.md, matching the bare filename citation style already used by
sibling modules (order.md, discount.md, fulfillment.md).

Assertion diff against the released text: did a sentence by sentence
comparison of the released docs/specification/payment/extensions/terms.md
plus source/schemas/common/payment_terms.json,
source/schemas/common/types/payment_term.json, and
source/schemas/common/types/payment_schedule.json against every PT-001
through PT-007 assertion. All seven normative quotes the tests cite are
verbatim unchanged in the released text (terms present implies at
least one term and a resolving selected_term_id; unique terms[].id;
selected term schedule sum equals checkout total; the
payment_term_changed warning on a changed or in place rewritten
selection; no silent substitution on an unresolvable selection; carry
the accepted term onto the Order with its schedules summing to the
order total; due_at is an absolute RFC 3339 date-time). Nothing was
removed or reworded, so no assertion needed rewriting and none was
deleted.

Not added: source/schemas/common/types/payment_schedule.json now lists
type as a required field alongside id, description, and amount. That
is schema expressible single field requiredness, which this module
explicitly scopes out (its stated purpose is the cross-field
invariants the JSON schema cannot express), so it is left uncovered
here rather than expanding this refresh into new coverage.

Verified locally against both ucp-sdk 0.4.4 (the repo pin) and
ucp-sdk 0.5.0: the module imports and runs cleanly in both, correctly
skips all seven tests against the Flower Shop reference (which does
not yet implement this capability), and a standalone kill test
confirms both the capability name fix and an unchanged cross field
invariant actually fail when mutated and pass when restored. Repo
pre-commit hooks (ruff, ruff format, codespell, whitespace) pass clean
on the changed file.
@vishkaty

Copy link
Copy Markdown
Contributor Author

The hold condition here has cleared: v2026-08-25 is released and ucp-sdk 0.5.0 is published. Refreshing the module against the released text, and noting the payment constructs moved from shopping to common in ucp#741, so the import this module uses is absent in 0.5.0 while this repo still pins 0.4.4. Will push the refreshed branch shortly.

@vishkaty
vishkaty force-pushed the test/payment-terms-conformance branch from 80ca776 to 5bf15b1 Compare August 31, 2026 00:21
@vishkaty vishkaty changed the title test: cover the payment-terms cross-field invariants (payment-terms.md) test: cover the payment-terms cross-field invariants (terms.md) Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants