From 640bdd96f5ded242b75ad923e0095a8c2dfb8d82 Mon Sep 17 00:00:00 2001 From: Paul Carey Date: Wed, 9 Sep 2026 10:12:35 +0100 Subject: [PATCH] feat!: define checkout payment finality and order reporting Allow a Business to mark the aggregate Checkout total as final or provisional. Provisional totals require the maximum amount approved by the Buyer; final totals use the aggregate total. Carry that amount onto the Order and include it in the AP2 mandate. Report current authorization and total captured amounts separately on Order payment instruments. Keep Checkout credentials, routing, and selection fields out of the Order representation, and preserve the selected-instrument requirements for Split Payments. --- .../payment/extensions/ap2-mandates.md | 7 ++ .../payment/extensions/split-payments.md | 17 +-- .../specification/payment/extensions/terms.md | 1 + docs/specification/payment/guide.md | 4 +- docs/specification/reference.md | 20 ++++ docs/specification/shopping/checkout/index.md | 88 ++++++++++++++- docs/specification/shopping/order/index.md | 77 ++++++++++++- docs/specification/shopping/order/mcp.md | 1 + docs/specification/shopping/order/rest.md | 1 + .../scaffolds/shopping_order_response.json | 5 +- .../common/payment_split_payments.json | 2 +- source/schemas/common/payment_terms.json | 3 +- .../common/types/card_payment_instrument.json | 2 +- .../common/types/payment_instrument.json | 103 ++++++++++++------ source/schemas/shopping/checkout.json | 98 ++++++++++++++++- source/schemas/shopping/order.json | 25 +++++ 16 files changed, 394 insertions(+), 60 deletions(-) diff --git a/docs/specification/payment/extensions/ap2-mandates.md b/docs/specification/payment/extensions/ap2-mandates.md index 1ba806d41..600f3395c 100644 --- a/docs/specification/payment/extensions/ap2-mandates.md +++ b/docs/specification/payment/extensions/ap2-mandates.md @@ -231,6 +231,13 @@ The checkout mandate **MUST** contain the full checkout response including the `ap2.merchant_authorization` field. This creates a nested cryptographic binding where the platform's signature covers the business's signature. +When the Checkout aggregate total is provisional, that signed response includes +both `totals[].amount_finality` and `payment.maximum_amount`. The Checkout +mandate therefore records the amount shown to the Buyer and the maximum they +approved. The Business MUST NOT authorize or collect more than that maximum +under the mandate. A higher amount requires separate Buyer approval and cannot +rely on the original mandate. + **Specification Boundary:** This extension defines *where* mandates are placed in UCP requests and responses. The mandate credential structure (claims, selective disclosure, key binding) is defined by the diff --git a/docs/specification/payment/extensions/split-payments.md b/docs/specification/payment/extensions/split-payments.md index 542bf32cb..b70fae73c 100644 --- a/docs/specification/payment/extensions/split-payments.md +++ b/docs/specification/payment/extensions/split-payments.md @@ -47,6 +47,10 @@ charged for each successfully-processed instrument in the same `amount` field. Response `amount` is informational only; see "Response: Actual Charges" below. +Split Payments `amount` records the instrument's contribution to the Checkout. +Order instruments use `authorized_amount` and `captured_amount` to report +authorization and capture separately. + All `amount` values are expressed in `checkout.currency` minor units (ISO 4217). Handlers using foreign-currency-denominated instruments (e.g., a CAD gift card in a USD checkout) or non-currency instruments @@ -284,14 +288,12 @@ Neither instrument includes `amount` — the business determines both. "id": "pi_gc_1", "handler_id": "example_handler_1", "type": "gift_card", - "credential": { "type": "gift_card", "token": "gc_abc123" }, "amount": 1000 }, { "id": "pi_card_1", "handler_id": "example_handler_1", "type": "card", - "credential": { "type": "card", "token": "tok_visa_xxxx" }, "amount": 4000 } ] @@ -346,14 +348,12 @@ the rest. "id": "pi_lp_1", "handler_id": "example_handler_1", "type": "loyalty", - "credential": { "type": "loyalty", "token": "lp_abc123" }, "amount": 500 }, { "id": "pi_card_1", "handler_id": "example_handler_1", "type": "card", - "credential": { "type": "card", "token": "tok_visa_xxxx" }, "amount": 4500 } ] @@ -409,21 +409,18 @@ credit card covers the remaining $45. "id": "pi_gc_1", "handler_id": "handler_gc", "type": "gift_card", - "credential": { "type": "gift_card", "token": "gc_abc123" }, "amount": 2500 }, { "id": "pi_gc_2", "handler_id": "handler_gc", "type": "gift_card", - "credential": { "type": "gift_card", "token": "gc_def456" }, "amount": 0 }, { "id": "pi_card_1", "handler_id": "handler_card", "type": "card", - "credential": { "type": "card", "token": "tok_visa_xxxx" }, "amount": 7500 } ] @@ -454,14 +451,12 @@ per the atomic invariant):** { "id": "pi_gc_1", "handler_id": "example_handler_1", - "type": "gift_card", - "credential": { "type": "gift_card", "token": "gc_abc123" } + "type": "gift_card" }, { "id": "pi_card_1", "handler_id": "example_handler_1", - "type": "card", - "credential": { "type": "card", "token": "tok_visa_xxxx" } + "type": "card" } ] }, diff --git a/docs/specification/payment/extensions/terms.md b/docs/specification/payment/extensions/terms.md index 6449b825a..9c84f4caa 100644 --- a/docs/specification/payment/extensions/terms.md +++ b/docs/specification/payment/extensions/terms.md @@ -535,6 +535,7 @@ disclosure moves with the term it governs: { "type": "total", "amount": 120000 } ], "payment": { + "maximum_amount": 120000, "accepted_term": { "id": "pt_deposit_balance", "title": "First night now, balance at check-in", diff --git a/docs/specification/payment/guide.md b/docs/specification/payment/guide.md index 111ebcc10..305e69ff6 100644 --- a/docs/specification/payment/guide.md +++ b/docs/specification/payment/guide.md @@ -574,13 +574,15 @@ Each variant has its own config schema tailored to its context: | Schema | Description | | :----------------------------------------------------------------------------------------- | :--------------------------------------------------------------- | -| [`payment_instrument.json`](site:schemas/common/types/payment_instrument.json) | Base: id, handler_id, type, billing_address, credential, display | +| [`payment_instrument.json`](site:schemas/common/types/payment_instrument.json) | Base: id, type, billing_address, credential, display | | [`card_payment_instrument.json`](site:schemas/common/types/card_payment_instrument.json) | Extends base with display: brand, last_digits, expiry, card art | UCP provides base schemas for universal payment instruments like `card`. Spec authors **MAY** extend any of the base instruments to add handler-specific display data or customize the credential reference. Handlers **MAY** define multiple instrument types for different payment flows. +Checkout adds `handler_id` and `selected` through +`selected_payment_instrument`. **Constraint Targets:** diff --git a/docs/specification/reference.md b/docs/specification/reference.md index 6b1fb7147..9c0e4d58d 100644 --- a/docs/specification/reference.md +++ b/docs/specification/reference.md @@ -25,6 +25,22 @@ within the UCP. {{ auto_generate_schema_reference('.', 'reference', include_extensions=False) }} +### Checkout Totals + +An ordered array of [Checkout Total](#checkout-total) entries. + +### Checkout Total + +{{ extension_schema_fields('../shopping/checkout.json#/$defs/checkout_total', 'reference') }} + +### Checkout Payment + +{{ extension_schema_fields('../shopping/checkout.json#/$defs/checkout_payment', 'reference') }} + +### Order Payment + +{{ extension_schema_fields('../shopping/order.json#/$defs/order_payment', 'reference') }} + ## Type Schemas {{ auto_generate_schema_reference('types', 'reference', include_extensions=False, base_dir='source/schemas/common') }} @@ -35,6 +51,10 @@ within the UCP. {{ extension_schema_fields('types/payment_instrument.json#/$defs/selected_payment_instrument', 'reference') }} +### Order Payment Instrument {: #payment-instrument-order-payment-instrument } + +{{ extension_schema_fields('types/payment_instrument.json#/$defs/order_payment_instrument', 'reference') }} + ### Pagination Request {: #pagination-request } {{ extension_schema_fields('types/pagination.json#/$defs/request', 'reference') }} diff --git a/docs/specification/shopping/checkout/index.md b/docs/specification/shopping/checkout/index.md index 0aba1d4dd..a056ea4fd 100644 --- a/docs/specification/shopping/checkout/index.md +++ b/docs/specification/shopping/checkout/index.md @@ -1262,9 +1262,11 @@ this response root (e.g., `$.line_items[0]`). See {{ schema_fields('types/message_warning', 'shopping/checkout') }} -### Payment + -{{ schema_fields('payment', 'shopping/checkout') }} +### Checkout Payment + +{{ extension_schema_fields('checkout.json#/$defs/checkout_payment', 'shopping/checkout') }} #### Selected Payment Instrument @@ -1282,9 +1284,87 @@ this response root (e.g., `$.line_items[0]`). See {{ extension_schema_fields('capability.json#/$defs/response_schema', 'shopping/checkout') }} -### Total {: #totals } + + +### Checkout Totals + +`totals` is an ordered array of Checkout Total entries. + +#### Checkout Total + +{{ extension_schema_fields('checkout.json#/$defs/checkout_total', 'shopping/checkout') }} + +#### Final and Provisional Totals + +The aggregate Checkout total is the single `totals[]` entry whose `type` is +`total`. It MAY include `amount_finality` as `final` or `provisional`; omission +means `final`. Other total entries MUST NOT include `amount_finality`. + +Every Checkout sets the maximum amount the Buyer approves: + +* For a final total, the aggregate total is the approved maximum and + `payment.maximum_amount` MUST be omitted. +* For a provisional total, `payment.maximum_amount` is required and MUST be + greater than or equal to the displayed aggregate total. + +JSON Schema cannot compare the two values. Implementations MUST ensure that +`payment.maximum_amount` is at least the aggregate total. + +Before completing a provisional Checkout, the Platform MUST disclose that the +amount may change up to `payment.maximum_amount` and obtain the Buyer's +approval. Across all instruments, the Business MUST NOT authorize or collect +more than that amount under the approval given at Checkout. Increasing the +payment for a post-purchase Order change requires separate Buyer approval. + +`maximum_amount` applies to the Checkout as a whole rather than to each +instrument. It is also separate from the amount a payment provider will still +allow the Business to capture against an authorization. The sum of all +historical authorization and capture records can be larger than +`maximum_amount` because an authorization can be replaced or a reversed capture +can be retried. + +Within UCP, the Business is responsible for enforcing the approved maximum. A +payment provider may also enforce a limit passed to it by the Business, but UCP +does not define that downstream exchange. The authorization and capture +summaries on the Order are not enough to verify the limit: one reports current +state and the other reports total activity. When AP2 is used, the Checkout +mandate provides a signed record of the amount the Buyer approved. + +The Business MAY change the total and its finality while the Checkout remains +open. Once Complete Checkout is accepted, the approved maximum no longer +changes. The resulting Order MUST record that amount in +`order.payment.maximum_amount`. -{{ schema_fields('types/total_resp', 'shopping/checkout') }} +For example, this Checkout can increase from $54.00 to no more than $60.00 +after completion: + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "status": "success", + "payment_handlers": {} + }, + "id": "chk_provisional_total", + "status": "ready_for_complete", + "currency": "USD", + "line_items": [ ... ], + "totals": [ + { "type": "subtotal", "amount": 5000 }, + { "type": "tax", "amount": 400 }, + { + "type": "total", + "amount": 5400, + "amount_finality": "provisional" + } + ], + "links": [], + "payment": { + "maximum_amount": 6000 + } +} +``` #### Rendering Contract diff --git a/docs/specification/shopping/order/index.md b/docs/specification/shopping/order/index.md index 76edff575..8e0b53897 100644 --- a/docs/specification/shopping/order/index.md +++ b/docs/specification/shopping/order/index.md @@ -26,7 +26,7 @@ it will be delivered, and what has happened since order placement. ### Key Concepts -Orders have three main components: +Orders have four main components: **Line Items** — what was purchased at checkout: @@ -39,6 +39,11 @@ Orders have three main components: * **Expectations** — buyer-facing *promises* about when/how items will arrive * **Events** (append-only log) — what actually happened (e.g. 👕 was shipped) +**Payment** — what the Buyer approved and what the Business reports: + +* Records the maximum amount approved at Checkout +* May include the amount authorized and captured for each instrument + **Adjustments** — post-order events independent of fulfillment: * Typically money movements (refunds, returns, credits, disputes, cancellations) @@ -100,6 +105,41 @@ Expectations can be split, merged, or adjusted post-order. For example: (common examples: `processing`, `shipped`, `in_transit`, `delivered`, `failed_attempt`, `canceled`, `undeliverable`, `returned_to_sender`) +### Payment + +**Payment** records the payment instruments used for the Order and the maximum +amount the Buyer approved at Checkout. The instruments may change after +purchase, for example when an Order modification adds or replaces a payment +method. + +Each instrument uses the shared Payment Instrument in its Order context: + +* `display` is required. +* Checkout-only `handler_id` and `selected` fields are absent. +* `credential` is request-only and is omitted from responses. +* `authorized_amount` is the amount currently authorized, in the Order + currency's minor units. Released and expired authorizations are excluded; + remaining capture capacity is a separate concept. +* `captured_amount` is the total amount successfully captured or collected, in + the Order currency's minor units, before refunds, credits, disputes, or + capture reversals. This measures capture rather than settlement or payout. + +Either amount may be omitted when it does not apply or is not reported. +Platforms MUST accept `captured_amount` greater than `authorized_amount`; this +can happen when capture above the authorized amount is supported or when an +authorization is later released. Neither field sets a maximum for the +instrument. + +Every Order includes `payment.maximum_amount`, which records the maximum amount +the Buyer approved when completing Checkout. For a provisional Checkout total, +it equals `checkout.payment.maximum_amount`; for a final total, it equals the +aggregate Checkout total. It remains unchanged if the Buyer later approves an +Order modification. + +Platforms MUST NOT compare `maximum_amount` with the sum of historical +authorization or capture records. Replaced authorizations and retried captures +can cause those sums to exceed the amount approved by the Buyer. + ### Attribution Businesses MAY surface a snapshot of the originating checkout's @@ -133,6 +173,14 @@ fulfillment: {{ schema_fields('order', 'shopping/order') }} +### Order Payment + +{{ extension_schema_fields('order.json#/$defs/order_payment', 'shopping/order') }} + +### Order Payment Instrument + +{{ extension_schema_fields('../common/types/payment_instrument.json#/$defs/order_payment_instrument', 'shopping/order') }} + ### Order Line Item Line items reflect what was purchased at checkout and their current state. @@ -277,6 +325,30 @@ Examples: `refund`, `return`, `credit`, `price_adjustment`, `dispute`, } ] }, + "payment": { + "maximum_amount": 15342, + "instruments": [ + { + "id": "pi_gift", + "type": "gift_card", + "captured_amount": 5000, + "display": { + "description": "Gift card", + "last_digits": "9821" + } + }, + { + "id": "pi_card", + "type": "card", + "captured_amount": 10342, + "display": { + "brand": "visa", + "last_digits": "4242", + "description": "Visa ending in 4242" + } + } + ] + }, "adjustments": [ { "id": "adj_1", @@ -321,6 +393,7 @@ rounded to `119`, and the return credits `79 × 0.25 = 19.75`, rounded to "checkout_id": "chk_bananas_1", "permalink_url": "https://business.example.com/orders/bananas1", "currency": "USD", + "payment": { "maximum_amount": 119 }, "line_items": [ { "id": "li_bananas", @@ -400,6 +473,7 @@ nominal steps `3 × 40 = 120`, settled `114`, delta `6` steps "checkout_id": "chk_apples_1", "permalink_url": "https://business.example.com/orders/apples1", "currency": "USD", + "payment": { "maximum_amount": 240 }, "line_items": [ { "id": "li_apples", @@ -490,6 +564,7 @@ then holds exactly (`190 == 190`) and the line derives `fulfilled`: "checkout_id": "chk_bananas_2", "permalink_url": "https://business.example.com/orders/bananas2", "currency": "USD", + "payment": { "maximum_amount": 158 }, "line_items": [ { "id": "li_bananas", diff --git a/docs/specification/shopping/order/mcp.md b/docs/specification/shopping/order/mcp.md index ffd27f898..0ea706a6c 100644 --- a/docs/specification/shopping/order/mcp.md +++ b/docs/specification/shopping/order/mcp.md @@ -150,6 +150,7 @@ current-state snapshot of an order. "checkout_id": "checkout_xyz789", "permalink_url": "https://business.example.com/orders/abc123", "currency": "USD", + "payment": { "maximum_amount": 4104 }, "line_items": [ { "id": "li_shoes", diff --git a/docs/specification/shopping/order/rest.md b/docs/specification/shopping/order/rest.md index fbd7e6120..1a87e79d8 100644 --- a/docs/specification/shopping/order/rest.md +++ b/docs/specification/shopping/order/rest.md @@ -123,6 +123,7 @@ Returns the current-state snapshot of an order. "checkout_id": "checkout_xyz789", "permalink_url": "https://business.example.com/orders/abc123", "currency": "USD", + "payment": { "maximum_amount": 4104 }, "line_items": [ { "id": "li_shoes", diff --git a/scripts/scaffolds/shopping_order_response.json b/scripts/scaffolds/shopping_order_response.json index 4f631403d..3f90227f8 100644 --- a/scripts/scaffolds/shopping_order_response.json +++ b/scripts/scaffolds/shopping_order_response.json @@ -23,5 +23,8 @@ "totals": [ { "type": "subtotal", "amount": 1000 }, { "type": "total", "amount": 1000 } - ] + ], + "payment": { + "maximum_amount": 1000 + } } diff --git a/source/schemas/common/payment_split_payments.json b/source/schemas/common/payment_split_payments.json index adcb77cef..5bc7044ff 100644 --- a/source/schemas/common/payment_split_payments.json +++ b/source/schemas/common/payment_split_payments.json @@ -12,7 +12,7 @@ "title": "Payment Instrument (Split Payments)", "description": "Payment instrument extended with an optional per-instrument amount for split payments.", "allOf": [ - { "$ref": "types/payment_instrument.json" }, + { "$ref": "types/payment_instrument.json#/$defs/selected_payment_instrument" }, { "type": "object", "properties": { diff --git a/source/schemas/common/payment_terms.json b/source/schemas/common/payment_terms.json index f5e246d78..0c0b09861 100644 --- a/source/schemas/common/payment_terms.json +++ b/source/schemas/common/payment_terms.json @@ -74,8 +74,7 @@ "properties": { "payment": { "$ref": "#/$defs/order_payment", - "description": "Payment details for the Order, including the accepted payment term.", - "ucp_response": "optional" + "description": "Payment details for the Order, including the accepted payment term." } } } diff --git a/source/schemas/common/types/card_payment_instrument.json b/source/schemas/common/types/card_payment_instrument.json index cf35f228b..4cc519106 100644 --- a/source/schemas/common/types/card_payment_instrument.json +++ b/source/schemas/common/types/card_payment_instrument.json @@ -23,7 +23,7 @@ }, "allOf": [ { - "$ref": "payment_instrument.json" + "$ref": "payment_instrument.json#/$defs/base_payment_instrument" }, { "type": "object", diff --git a/source/schemas/common/types/payment_instrument.json b/source/schemas/common/types/payment_instrument.json index 830188577..cf0998bac 100644 --- a/source/schemas/common/types/payment_instrument.json +++ b/source/schemas/common/types/payment_instrument.json @@ -2,48 +2,53 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/payment_instrument.json", "title": "Payment Instrument", - "description": "The base definition for any payment instrument. It links the instrument to a specific payment handler.", - "type": "object", - "required": [ - "id", - "handler_id", - "type" - ], - "properties": { - "id": { - "type": "string", - "description": "A unique identifier for this instrument instance. Typically assigned by the platform for instruments it collects. For a business-owned saved instrument returned on an identity-linked response, this identifier is assigned by the business; the platform MUST treat it as an opaque, business-scoped reference, and the business resolves it server-side when the buyer selects it." - }, - "handler_id": { - "type": "string", - "description": "The unique identifier for the handler instance that produced this instrument. This corresponds to the 'id' field in the Payment Handler definition." - }, - "type": { - "type": "string", - "description": "The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value." - }, - "billing_address": { - "$ref": "postal_address.json", - "description": "The billing address associated with this payment method." - }, - "credential": { - "$ref": "payment_credential.json" - }, - "display": { - "type": "object", - "description": "Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler." - } - }, - "additionalProperties": true, + "description": "Fields shared by payment instruments in Checkout and Order. Payment handlers add instrument-specific credential and display fields, while each capability adds the fields it needs for routing, selection, or reporting.", + "$ref": "payment_instrument.json#/$defs/base_payment_instrument", "$defs": { + "base_payment_instrument": { + "type": "object", + "required": [ + "id", + "type" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this instrument, assigned by the participant that introduces it. Identifiers assigned by another participant MUST be treated as opaque." + }, + "type": { + "type": "string", + "description": "The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value." + }, + "billing_address": { + "$ref": "postal_address.json", + "description": "The billing address associated with this payment method." + }, + "credential": { + "$ref": "payment_credential.json", + "description": "Credential used to execute payment. This field is request-only and MUST NOT be returned in responses.", + "ucp_response": "omit" + }, + "display": { + "type": "object", + "description": "Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler." + } + }, + "additionalProperties": true + }, "selected_payment_instrument": { "title": "Selected Payment Instrument", - "description": "A payment instrument with selection state.", + "description": "A Checkout payment instrument with handler routing and selection state.", "allOf": [ - { "$ref": "#" }, + { "$ref": "payment_instrument.json#/$defs/base_payment_instrument" }, { "type": "object", + "required": ["handler_id"], "properties": { + "handler_id": { + "type": "string", + "description": "The unique identifier for the handler instance that produced this instrument. This corresponds to the `id` field in the Payment Handler definition." + }, "selected": { "type": "boolean", "description": "Whether this instrument is selected by the user." @@ -51,6 +56,34 @@ } } ] + }, + "order_payment_instrument": { + "title": "Order Payment Instrument", + "description": "Payment instrument as returned on an Order, which may include current authorization and total captured amounts.", + "allOf": [ + { "$ref": "payment_instrument.json#/$defs/base_payment_instrument" }, + { + "type": "object", + "required": ["display"], + "properties": { + "authorized_amount": { + "$ref": "amount.json", + "description": "Amount currently authorized for this instrument, in the Order currency's minor units. Released and expired authorizations are excluded. Remaining capture capacity is a separate concept." + }, + "captured_amount": { + "$ref": "amount.json", + "description": "Total amount successfully captured or collected for this instrument, in the Order currency's minor units, before refunds, credits, disputes, or capture reversals. This measures capture rather than settlement or payout." + } + }, + "not": { + "anyOf": [ + { "required": ["credential"] }, + { "required": ["handler_id"] }, + { "required": ["selected"] } + ] + } + } + ] } } } diff --git a/source/schemas/shopping/checkout.json b/source/schemas/shopping/checkout.json index 983e42972..479109c36 100644 --- a/source/schemas/shopping/checkout.json +++ b/source/schemas/shopping/checkout.json @@ -4,6 +4,65 @@ "name": "dev.ucp.shopping.checkout", "title": "Checkout", "description": "Base checkout schema. Extensions compose onto this using allOf.", + "$defs": { + "checkout_total": { + "title": "Checkout Total", + "description": "Checkout pricing entry. Only the aggregate total may state whether its amount is final.", + "allOf": [ + { "$ref": "../common/types/total.json" }, + { + "type": "object", + "properties": { + "amount_finality": { + "type": "string", + "enum": ["final", "provisional"], + "description": "Whether the aggregate total may change after the Buyer completes the Checkout. Omission means `final`. When `provisional`, `payment.maximum_amount` is the most the Business may authorize or collect under the Buyer's approval of this Checkout.", + "ucp_request": "omit" + } + }, + "if": { + "properties": { + "type": { "const": "total" } + }, + "required": ["type"] + }, + "else": { + "not": { + "required": ["amount_finality"] + } + } + } + ] + }, + "checkout_totals": { + "title": "Checkout Totals", + "description": "Checkout pricing breakdown in which only the aggregate total may state whether its amount is final.", + "allOf": [ + { "$ref": "../common/types/totals.json" }, + { + "type": "array", + "items": { "$ref": "#/$defs/checkout_total" } + } + ] + }, + "checkout_payment": { + "title": "Checkout Payment", + "description": "Checkout payment details, including the maximum amount approved by the Buyer when the total is provisional.", + "allOf": [ + { "$ref": "../common/types/payment.json" }, + { + "type": "object", + "properties": { + "maximum_amount": { + "$ref": "../common/types/amount.json", + "description": "Maximum amount, across all payment instruments and in the Checkout currency's minor units, that the Business may authorize or collect under the Buyer's approval of this Checkout. Required for a provisional total and omitted for a final total, where the aggregate total is the approved maximum.", + "ucp_request": "omit" + } + } + } + ] + } + }, "type": "object", "required": [ "ucp", @@ -81,7 +140,7 @@ "ucp_request": "omit" }, "totals": { - "$ref": "../common/types/totals.json", + "$ref": "#/$defs/checkout_totals", "description": "Different cart totals.", "ucp_request": "omit" }, @@ -127,7 +186,7 @@ "ucp_request": "omit" }, "payment": { - "$ref": "../common/types/payment.json", + "$ref": "#/$defs/checkout_payment", "ucp_request": { "create": "optional", "update": "optional", @@ -139,5 +198,38 @@ "description": "Details about an order created for this checkout session.", "ucp_request": "omit" } - } + }, + "allOf": [ + { + "if": { + "properties": { + "totals": { + "contains": { + "properties": { + "type": { "const": "total" }, + "amount_finality": { "const": "provisional" } + }, + "required": ["type", "amount_finality"] + } + } + }, + "required": ["totals"] + }, + "then": { + "properties": { + "payment": { + "required": ["maximum_amount"] + } + }, + "required": ["payment"] + }, + "else": { + "properties": { + "payment": { + "not": { "required": ["maximum_amount"] } + } + } + } + } + ] } diff --git a/source/schemas/shopping/order.json b/source/schemas/shopping/order.json index f5ade18a9..7e79da6ee 100644 --- a/source/schemas/shopping/order.json +++ b/source/schemas/shopping/order.json @@ -19,6 +19,25 @@ "description": "URL where merchant sends order lifecycle events (webhooks)." } } + }, + "order_payment": { + "title": "Order Payment", + "description": "Payment details returned on the Order, including the maximum amount the Buyer approved at Checkout.", + "type": "object", + "required": ["maximum_amount"], + "properties": { + "instruments": { + "type": "array", + "items": { + "$ref": "../common/types/payment_instrument.json#/$defs/order_payment_instrument" + }, + "description": "Payment instruments used on the Order." + }, + "maximum_amount": { + "$ref": "../common/types/amount.json", + "description": "Maximum amount the Buyer approved when completing the Checkout, in the Order currency's minor units. For a provisional Checkout total, this equals `checkout.payment.maximum_amount`; for a final total, it equals the aggregate Checkout total. It remains unchanged if the Buyer later approves an Order modification." + } + } } }, "type": "object", @@ -29,6 +48,7 @@ "permalink_url", "line_items", "fulfillment", + "payment", "currency", "totals" ], @@ -80,6 +100,11 @@ }, "description": "Fulfillment data: buyer expectations and what actually happened." }, + "payment": { + "$ref": "#/$defs/order_payment", + "description": "Payment details returned on the Order. The instruments may change after purchase, but `maximum_amount` continues to record what the Buyer approved at Checkout.", + "ucp_request": "omit" + }, "adjustments": { "type": "array", "items": {