Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/specification/cart.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,11 @@ requirements.

### Total

{{ schema_fields('types/total_resp', 'checkout') }}
The same totals contract applies to cart and checkout. See
[Checkout Totals](checkout.md#totals) for the rendering contract, accounting
identity, well-known types, repeating types, and sub-line semantics.

{{ schema_fields('types/total_line_resp', 'checkout') }}

Taxes MAY be included where calculable. Platforms SHOULD assume cart totals
are estimates; accurate taxes are computed at checkout.
Expand Down
131 changes: 128 additions & 3 deletions docs/specification/checkout.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,11 +610,136 @@ field or omitting them.

{{ extension_schema_fields('capability.json#/$defs/response_schema', 'checkout') }}

### Total
### Total {: #totals }

#### Total
{{ schema_fields('types/total_line_resp', 'checkout') }}

{{ schema_fields('types/total_resp', 'checkout') }}
#### Rendering Contract

Businesses are the authoritative source for presented totals — their content
and order — because the correct presentation is subject to regional, product,
and regulatory requirements that the business is obligated to satisfy (e.g.,
multi-jurisdiction tax itemization, mandatory fee disclosures).

Platforms MUST render all top-level entries in the order provided:

```python
for entry in totals:
render_line(entry.display_text, entry.amount)
```

Platforms MAY render sub-lines as supplementary detail:

```python
for entry in totals:
render_line(entry.display_text, entry.amount)
if entry.lines:
for sub in entry.lines:
render_detail_line(sub.display_text, sub.amount)
```

Platforms MUST NOT interpret, filter, reorder, aggregate, or apply display
logic of their own.

Invariants of `totals[]`:

* Every entry carries a `display_text` and an `amount`.
* Amounts are signed integers — negative values are subtractive (e.g.,
discounts), positive values are additive. The sign IS the direction.
* Exactly one `type: "subtotal"` MUST be present.
* Exactly one `type: "total"` MUST be present.

#### Verification

Platforms MUST NOT substitute their own computed totals for the business's
values. Platforms MAY verify the provided totals:

```python
assert sum(e.amount for e in totals if e.type != "total") == total_entry.amount
```

If the computed sum does not match the `type: "total"` entry, the platform
MUST NOT alter the rendered output — the business's total is authoritative.
Platforms MAY prevent checkout completion or surface the discrepancy to the
buyer.

#### Well-Known Types

| Type | Sign | Meaning |
| ----------------- | ---------- | ---------------------------------------------- |
| `subtotal` | + | Sum of line item prices |
| `discount` | − | Order or line-item level discount |
| `items_discount` | − | Rollup of line-item discounts |
| `fulfillment` | + | Shipping, delivery, or pickup charges |
| `tax` | + | Tax charges |
| `fee` | + | Fees and surcharges |
| `total` | = | Authoritative grand total (exactly one) |

The sign convention for well-known types is schema-enforced: subtractive
types (discount, items_discount) MUST have negative amounts; additive types
(subtotal, fulfillment, tax, fee) MUST have non-negative amounts. The `type`
field is an open string — businesses MAY use values beyond the well-known
set, where the sign on the amount is self-describing.

#### Repeating Types

All types except `subtotal` and `total` MAY appear multiple times —
for example, multi-jurisdiction tax lines or itemized fees.

#### Sub-Lines (`lines`)

Each top-level entry MAY include a `lines` array. Sub-lines share the same
base shape as top-level entries — `display_text` and `amount` — providing an
itemized breakdown under the parent.

**Invariant:** `sum(lines[].amount)` MUST equal the parent entry's `amount`.

The business controls what MUST be rendered (top-level entries) versus what
MAY be optionally surfaced (sub-lines). Platforms SHOULD render sub-lines
when provided.

#### Examples

**Split tax, itemized at top-level:**

```json
"totals": [
{ "type": "subtotal", "display_text": "Subtotal", "amount": 5750 },
{ "type": "fulfillment", "display_text": "Shipping", "amount": 899 },
{ "type": "tax", "display_text": "Federal Tax", "amount": 332 },
{ "type": "tax", "display_text": "State Tax", "amount": 465 },
{ "type": "total", "display_text": "Total", "amount": 7446 }
]
```

**Collapsed fees with optional breakdown:**

```json
"totals": [
{ "type": "subtotal", "display_text": "Subtotal", "amount": 4999 },
{
"type": "fee", "display_text": "Fees", "amount": 549,
"lines": [
{ "display_text": "Service Fee", "amount": 399 },
{ "display_text": "Recycling Fee", "amount": 150 }
]
},
{ "type": "tax", "display_text": "Tax", "amount": 444 },
{ "type": "total", "display_text": "Total", "amount": 5992 }
]
```

**Discount and account credit — negative amounts:**

```json
"totals": [
{ "type": "subtotal", "display_text": "Subtotal", "amount": 10000 },
{ "type": "discount", "display_text": "Summer Sale", "amount": -1500 },
{ "type": "tax", "display_text": "Tax", "amount": 680 },
{ "type": "account_credit", "display_text": "Account Credit", "amount": -2500 },
{ "type": "total", "display_text": "Amount Due", "amount": 6680 }
]
```

### UCP Response Checkout {: #ucp-response-checkout-schema }

Expand Down
2 changes: 1 addition & 1 deletion docs/specification/fulfillment.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ method.

#### Total

{{ schema_fields('types/total_resp', 'fulfillment') }}
{{ schema_fields('types/total_line_resp', 'fulfillment') }}

#### Postal Address

Expand Down
2 changes: 1 addition & 1 deletion docs/specification/order.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ zero-downtime key rotation procedures.

### Total

{{ schema_fields('types/total_resp', 'order') }}
{{ schema_fields('types/total_line_resp', 'order') }}

### UCP Response Order Schema <span id="ucp"></span> {: #ucp-response-order-schema }

Expand Down
5 changes: 1 addition & 4 deletions source/schemas/shopping/cart.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,7 @@
"ucp_request": "omit"
},
"totals": {
"type": "array",
"items": {
"$ref": "types/total.json"
},
"$ref": "types/totals.json",
"description": "Estimated cost breakdown. May be partial if shipping/tax not yet calculable.",
"ucp_request": "omit"
},
Expand Down
5 changes: 1 addition & 4 deletions source/schemas/shopping/checkout.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,7 @@
}
},
"totals": {
"type": "array",
"items": {
"$ref": "types/total.json"
},
"$ref": "types/totals.json",
"description": "Different cart totals.",
"ucp_request": "omit"
},
Expand Down
5 changes: 1 addition & 4 deletions source/schemas/shopping/order.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,7 @@
"description": "ISO 4217 currency code. MUST match the currency from the originating checkout session."
},
"totals": {
"type": "array",
"items": {
"$ref": "types/total.json"
},
"$ref": "types/totals.json",
"description": "Different totals for the order."
}
}
Expand Down
2 changes: 1 addition & 1 deletion source/schemas/shopping/types/fulfillment_option.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"totals": {
"type": "array",
"items": {
"$ref": "total.json"
"$ref": "total_line.json"
},
"description": "Fulfillment option totals breakdown.",
"ucp_request": "omit"
Expand Down
2 changes: 1 addition & 1 deletion source/schemas/shopping/types/line_item.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"totals": {
"type": "array",
"items": {
"$ref": "total.json"
"$ref": "total_line.json"
},
"description": "Line item totals breakdown.",
"ucp_request": "omit"
Expand Down
2 changes: 1 addition & 1 deletion source/schemas/shopping/types/order_line_item.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"totals": {
"type": "array",
"items": {
"$ref": "total.json"
"$ref": "total_line.json"
},
"description": "Line item totals breakdown."
},
Expand Down
7 changes: 7 additions & 0 deletions source/schemas/shopping/types/signed_amount.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/types/signed_amount.json",
"title": "Signed Amount",
"description": "Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD). May be negative — the sign is intrinsic to the value (e.g., discounts are negative, charges are positive).",
"type": "integer"
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/types/total.json",
"title": "Total",
"$id": "https://ucp.dev/schemas/shopping/types/total_line.json",
"title": "Total Line",
"description": "A cost breakdown entry with a category, amount, and optional display text.",
"type": "object",
"required": [
"type",
Expand All @@ -10,16 +11,7 @@
"properties": {
"type": {
"type": "string",
"enum": [
"items_discount",
"subtotal",
"discount",
"fulfillment",
"tax",
"fee",
"total"
],
"description": "Type of total categorization.",
"description": "Cost category. Well-known values: subtotal, items_discount, discount, fulfillment, tax, fee, total. Businesses MAY use additional values.",
"ucp_request": "omit"
},
"display_text": {
Expand All @@ -28,8 +20,7 @@
"ucp_request": "omit"
},
"amount": {
"$ref": "amount.json",
"description": "If type == total, sums subtotal - discount + fulfillment + tax + fee. Should be >= 0. Amount in ISO 4217 minor units.",
"$ref": "signed_amount.json",
"ucp_request": "omit"
}
}
Expand Down
64 changes: 64 additions & 0 deletions source/schemas/shopping/types/totals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/types/totals.json",
"title": "Totals",
"description": "Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.",
"type": "array",
"items": {
"allOf": [
{
"$ref": "total_line.json"
},
{
"type": "object",
"required": [
"display_text"
],
"properties": {
"lines": {
"type": "array",
"items": {
"type": "object",
"required": [
"display_text",
"amount"
],
"properties": {
"display_text": {
"type": "string",
"description": "Human-readable label for this sub-line."
},
"amount": {
"$ref": "signed_amount.json"
}
},
"description": "Sub-line entry. Additional metadata MAY be included."
},
"description": "Optional itemized breakdown. The parent entry is always rendered; lines are supplementary. Sum of line amounts MUST equal the parent entry amount.",
"ucp_request": "omit"
}
}
},
{
"if": { "properties": { "type": { "enum": ["discount", "items_discount"] } }, "required": ["type"] },
"then": { "properties": { "amount": { "exclusiveMaximum": 0 } } }
},
{
"if": { "properties": { "type": { "enum": ["subtotal", "fulfillment", "tax", "fee"] } }, "required": ["type"] },
"then": { "properties": { "amount": { "minimum": 0 } } }
}
]
},
"allOf": [
{
"contains": { "properties": { "type": { "const": "subtotal" } }, "required": ["type"] },
"minContains": 1,
"maxContains": 1
},
{
"contains": { "properties": { "type": { "const": "total" } }, "required": ["type"] },
"minContains": 1,
"maxContains": 1
}
]
}
Loading