negotiate payment terms as a checkout selection - #692
Conversation
Takes #603's direction to its destination. Three of #603's four changes stand: delete schedule.json, require a schedule description, aggregate totals per schedule. Only due_at returns, as an absolute RFC 3339 timestamp rather than a recurrence engine. A payment term is negotiated. The Business offers a list, the Buyer picks one, and the Business reprices. selected_term_id makes that choice a first-class field, so the term is what determines the price, the disclosure duties, and which instruments are eligible — eligibility follows from the term rather than the term following from the instrument. instruments[].term_refs is superseded and removed. UCP already has this shape. fulfillment_group is a list of priced options with exactly one selection, and nearly everything below follows from copying it rather than inventing a second shape for the same problem. fulfillment_group is the structural twin: a list of priced options with exactly one selection, which the Buyer changes and the Business prices. Rather than invent a second shape for the same problem, this copies it. - available_terms[] is response-only (ucp_request: omit); selected_term_id is a request-writable scalar sibling. Exactly how options[] pairs with selected_option_id. - Schedules carry a plain total.json array like fulfillment_option, not totals.json, which would mandate a meaningless subtotal per schedule. - due_at is absolute, like earliest_fulfillment_time. No recurrence rules, so month-end, DST, and rounding-residual decisions stay with the party that already makes them. - Selecting a term is a Checkout mutation, and the Business response is authoritative for all derived state. The blast radius is deliberately not enumerated, because fulfillment already establishes that it need not be. - The baseline render contract is title + description + total, mirroring "title + description + total is sufficient to render any option". Recognizing a type only enables optional enhancement; it never changes what must be rendered. The mandatory floor is title, description, schedules[].description, and schedules[].totals, read against the Checkout currency. Above it, `immediate`, exact amounts, and absolute due_at are supplementary. Only one well-known type value exists; anything else means "not captured at completion". A second value would imply a closed vocabulary. Normative acceptance test: a Platform that recognizes no type and reads no other field renders a correct presentation of every term. A subscription passes it with zero subscription vocabulary. That floor covers amounts and timing. It does not discharge a jurisdictional disclosure duty; those travel through policies[] paired with disclosure warnings, and a Platform that cannot honor the rendering contract escalates via continue_url rather than dropping the notice. Display is unconditional: the contract forbids hiding, collapsing, and dismissing. payment.accepted_term carries the accepted term onto the Order, with its schedules and their totals. The other terms are checkout state and are not projected. A Checkout-only disclosure evaporates at completion, which is exactly when a deferred obligation starts mattering: without this, a Buyer who owes $900 at check-in has no protocol record of it. This also gives term-scoped policies a node to survive against. A disclosure that governed the accepted term MUST be returned on the Order targeting $.payment.accepted_term, and disclosures for terms the Buyer declined MUST NOT be. That is stricter than order.md, which leaves the policy snapshot at MAY; the extension tightens its own domain because the obligation is the point of the feature. Deliberately excluded: per-schedule payment status. What was agreed lives here; what was charged lives in #584's payment.instruments[].amount. #584 adds payment to core order.json and this composes with it via allOf.
available_ is not a generic prefix in UCP. It marks a secondary, availability-flavored list sitting beside the real one. shopping/types/fulfillment.json carries both: methods[] is the list, while available_methods[] is "inventory availability hints" with its own type and ucp_request: omit. payment_handler.json uses available_instruments the same way, for discovery-time support. The selectable list is a bare plural wherever it appears: options, methods, groups, instruments. available_terms therefore claimed the wrong slot, reading as hints about terms rather than as the terms themselves. payment.terms[] also states the domain term directly, and lines up with payment.selected_term_id on the Checkout and payment.accepted_term on the Order, all resolving to one type, payment_term.json.
|
Read through this from the conformance side and validated the schemas against our oracle — the schema-expressible constraints all enforce cleanly, and using an open-vocabulary On your MUST-vs-MAY question — I'd keep the MUST, with a framing that makes it consistent with core rather than stricter than it. core's MAYs (the policy and attribution snapshots) are context — duplicated information whose absence harms no one. That does raise the projection's lifetime, though. The Order Payment MUST ("its schedule Two of the invariants are inherently beyond JSON Schema — Minor: the lodging Order example ( Happy to contribute a conformance module for the wire-checkable invariant (selected term's schedule totals == checkout |
Takes #603's direction to its final destination. Three of #603's four changes stand: delete
schedule.json, require a scheduledescription, aggregatetotalsper schedule. Onlydue_atreturns, as an absolute RFC 3339 timestamp rather than a recurrence engine.The main change here is that a payment term is negotiated. The Business offers a list, the Buyer picks one, and the Business reprices.
payment.selected_term_idmakes that choice a first-class field, so the term determines the price, the disclosure duties, and which instruments are eligible — eligibility follows from the term rather than the term following from the instrument.instruments[].term_refsis superseded and removed.UCP already has this shape.
fulfillment_groupis a list of priced options with exactly one selection, so this copies it rather than inventing a second shape for the same problem:options[]terms[]selected_option_idselected_term_iducp_request: omit)total.jsonarraytotal.jsonarrayearliest_fulfillment_time)due_at)title+description+totaltitle+description+totalOrder projection
payment.accepted_termcarries the accepted term onto the Order with its schedules and totals. The other terms are checkout state and are not projected. This exists because a Checkout-only disclosure evaporates at completion, which is exactly when a deferred obligation starts mattering — without it, a Buyer who owes $900 at check-in has no protocol record of it. It also gives term-scoped policies a node to survive against, soapplies_to: ["$.payment.accepted_term"]resolves on the Order.Note/dicussion point: this is stricter than core.
order.mdleaves the policy snapshot at MAY; here, a disclosure that governed the accepted term MUST be returned on the Order. Right choice, or should it remain a MAY?Deliberately excluded: per-schedule payment status. What was agreed lives here; what was charged lives in #584's
payment.instruments[].amount. #584 addspaymentto coreorder.jsonand this composes with it viaallOf— the two are additive, not competing, but they should land in a known order.