Skip to content

Commit b08458e

Browse files
authored
Merge branch 'main' into main
2 parents 021316d + 89727fb commit b08458e

8 files changed

Lines changed: 66 additions & 48 deletions

File tree

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,8 @@ image: assets/banner.png
367367
"type": "refund",
368368
"occurred_at": "2026-01-12T14:30:00Z",
369369
"status": "completed",
370-
"line_items": [{ "id": "li_1", "quantity": 1 }],
371-
"amount": 26550,
370+
"line_items": [{ "id": "li_1", "quantity": -1 }],
371+
"totals": [{ "type": "total", "amount": -26550 }],
372372
"description": "Defective item"
373373
}
374374
],

docs/specification/discount.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ operations. The same semantics apply to both resources.
138138

139139
- `discounts.applied` contains all active discounts (code-based + automatic)
140140
- Rejected codes communicated via `messages[]` (see below)
141-
- Discount amounts reflected in `totals[]` and `line_items[].discount`
141+
- Discount amounts reflected in `totals[]` and `line_items[].totals[]`
142142

143143
**Cart-to-checkout continuity:** When a cart is converted to a checkout via the
144144
cart capability's `cart_id` field, businesses MUST carry forward any discount
@@ -302,18 +302,18 @@ with allocations to shipping/fees, contribute to `discount`.
302302

303303
| Discount Type | Where Reflected |
304304
| -------------------- | ------------------------------------------ |
305-
| Line-item discount | `line_items[].discount` + `items_discount` |
306-
| Order-level discount | `totals[]` with `type: "discount"` |
305+
| Line-item discount | `line_items[].totals[type=items_discount]` |
306+
| Order-level discount | `totals[type=discount]` |
307307

308308
**Invariant:** `totals[type=items_discount].amount` equals
309-
`sum(line_items[].discount)`.
309+
`sum(line_items[].totals[type=items_discount].amount)`.
310310

311311
The `discounts.applied` array shows **what** was applied. The `totals[]` and
312-
`line_items[].discount` show **where** and **how much**.
312+
`line_items[].totals[]` show **where** and **how much**.
313313

314-
**Amount convention:** All discount amounts are positive integers in minor
315-
currency units. When presenting totals to users, display discount types as
316-
subtractive (e.g., "-$13.99").
314+
**Amount convention:** Discount amounts in `discounts.applied` are positive
315+
integers (the value of the discount). Discount entries in `totals[]` are
316+
negative (the effect on the receipt) — the sign is schema-enforced.
317317

318318
## Examples
319319

docs/specification/order.md

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,23 @@ Orders have three main components:
3131
**Line Items** — what was purchased at checkout:
3232

3333
* Includes current quantity counts (total, fulfilled)
34+
* Can change post-order (e.g. order edits, exchanges); **MUST** include all
35+
line items that ever existed on the order regardless of edits or alterations
3436

3537
**Fulfillment** — how items get delivered:
3638

3739
* **Expectations** — buyer-facing *promises* about when/how items will arrive
3840
* **Events** (append-only log) — what actually happened (e.g. 👕 was shipped)
3941

40-
**Adjustments** (append-only log) — post-order events independent of fulfillment:
42+
**Adjustments** — post-order events independent of fulfillment:
4143

4244
* Typically money movements (refunds, returns, credits, disputes, cancellations)
4345
* Can be any post-order change
4446
* Can happen before, during, or after fulfillment
47+
* Businesses SHOULD append new entries rather than mutating existing ones;
48+
append-only ledger is preferred. Businesses that do not maintain adjustment
49+
history MAY perform in-place updates of existing entries
50+
(e.g. a single `return` adjustment can transition from `pending` to `completed`)
4551

4652
## Data Model
4753

@@ -50,7 +56,7 @@ Orders have three main components:
5056
Line items reflect what was purchased at checkout and their current state:
5157

5258
* Item details (product, price, quantity ordered)
53-
* Quantity counts and status are derived
59+
* Quantity counts and fulfillment status
5460

5561
### Fulfillment
5662

@@ -83,15 +89,17 @@ Expectations can be split, merged, or adjusted post-order. For example:
8389

8490
### Adjustments
8591

86-
**Adjustments** are an append-only log of events that exist independently of
92+
**Adjustments** are post-order events that exist independently of
8793
fulfillment:
8894

8995
* Type is an open string field - businesses can use any values that make sense
9096
(typically money movements like `refund`, `return`, `credit`,
9197
`price_adjustment`, `dispute`, `cancellation`)
9298
* Can be any post-order change
9399
* Optionally link to line items (or order-level for things like shipping refunds)
94-
* Include amount when relevant
100+
* Quantities and amounts are signed—negative for reductions (returns, refunds),
101+
positive for additions (exchanges)
102+
* Include totals breakdown when relevant
95103
* Can happen at any time regardless of fulfillment status
96104

97105
## Schema
@@ -103,23 +111,24 @@ fulfillment:
103111
### Order Line Item
104112

105113
Line items reflect what was purchased at checkout and their current state.
106-
Status and quantity counts should reflect the event logs.
107114

108115
{{ schema_fields('order_line_item', 'order') }}
109116

110117
**Quantity Structure:**
111118

112119
```json
113120
{
114-
"total": 3, // Current total quantity
121+
"original": 3, // Quantity from the original checkout
122+
"total": 3, // Current total (may differ after edits/exchanges)
115123
"fulfilled": 2 // What has been fulfilled
116124
}
117125
```
118126

119127
**Status Derivation:**
120128

121129
```text
122-
if (fulfilled == total) → "fulfilled"
130+
if (total == 0) → "removed"
131+
else if (fulfilled == total) → "fulfilled"
123132
else if (fulfilled > 0) → "partial"
124133
else → "processing"
125134
```
@@ -171,7 +180,7 @@ Examples: `refund`, `return`, `credit`, `price_adjustment`, `dispute`,
171180
{
172181
"id": "li_shoes",
173182
"item": { "id": "prod_shoes", "title": "Running Shoes", "price": 3000 },
174-
"quantity": { "total": 3, "fulfilled": 3 },
183+
"quantity": { "original": 3, "total": 3, "fulfilled": 3 },
175184
"totals": [
176185
{"type": "subtotal", "amount": 9000},
177186
{"type": "total", "amount": 9000}
@@ -181,7 +190,7 @@ Examples: `refund`, `return`, `credit`, `price_adjustment`, `dispute`,
181190
{
182191
"id": "li_shirts",
183192
"item": { "id": "prod_shirts", "title": "Cotton T-Shirt", "price": 2000 },
184-
"quantity": { "total": 2, "fulfilled": 0 },
193+
"quantity": { "original": 2, "total": 2, "fulfilled": 0 },
185194
"totals": [
186195
{"type": "subtotal", "amount": 4000},
187196
{"type": "total", "amount": 4000}
@@ -238,8 +247,10 @@ Examples: `refund`, `return`, `credit`, `price_adjustment`, `dispute`,
238247
"type": "refund",
239248
"occurred_at": "2025-01-10T14:30:00Z",
240249
"status": "completed",
241-
"line_items": [{ "id": "li_shoes", "quantity": 1 }],
242-
"amount": 3000,
250+
"line_items": [{ "id": "li_shoes", "quantity": -1 }],
251+
"totals": [
252+
{ "type": "total", "amount": -3000 }
253+
],
243254
"description": "Defective item"
244255
}
245256
],

source/schemas/shopping/order.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"$id": "https://ucp.dev/schemas/shopping/order.json",
44
"name": "dev.ucp.shopping.order",
55
"title": "Order",
6-
"description": "Order schema with immutable line items, buyer-facing fulfillment expectations, and append-only event logs.",
6+
"description": "Order schema with line items, buyer-facing fulfillment expectations, and event logs.",
77
"$defs": {
88
"platform_schema": {
99
"title": "Platform Order Schema",
@@ -53,7 +53,7 @@
5353
"items": {
5454
"$ref": "types/order_line_item.json"
5555
},
56-
"description": "Immutable line items — source of truth for what was ordered."
56+
"description": "Line items representing what was purchased — can change post-order via edits or exchanges."
5757
},
5858
"fulfillment": {
5959
"type": "object",
@@ -80,7 +80,7 @@
8080
"items": {
8181
"$ref": "types/adjustment.json"
8282
},
83-
"description": "Append-only event log of money movements (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment."
83+
"description": "Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment."
8484
},
8585
"currency": {
8686
"type": "string",

source/schemas/shopping/types/adjustment.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://json-schema.org/draft/2020-12/schema",
33
"$id": "https://ucp.dev/schemas/shopping/types/adjustment.json",
44
"title": "Adjustment",
5-
"description": "Append-only event that exists independently of fulfillment. Typically represents money movements but can be any post-order change. Polymorphic type that can optionally reference line items.",
5+
"description": "Post-order event that exists independently of fulfillment. Typically represents money movements but can be any post-order change. Polymorphic type that can optionally reference line items.",
66
"type": "object",
77
"required": [
88
"id",
@@ -45,16 +45,18 @@
4545
},
4646
"quantity": {
4747
"type": "integer",
48-
"minimum": 1,
49-
"description": "Quantity affected by this adjustment."
48+
"description": "Signed quantity affected by this adjustment. Negative values represent reductions (e.g. returns); positive values represent additions (e.g. exchanges)."
5049
}
5150
}
5251
},
5352
"description": "Which line items and quantities are affected (optional)."
5453
},
55-
"amount": {
56-
"$ref": "amount.json",
57-
"description": "Amount in ISO 4217 minor units for refunds, credits, or price adjustments."
54+
"totals": {
55+
"type": "array",
56+
"items": {
57+
"$ref": "total.json"
58+
},
59+
"description": "Adjustment totals breakdown. Signed values - negative for money returned to buyer (refunds, credits), positive for additional charges (exchanges)."
5860
},
5961
"description": {
6062
"type": "string",

source/schemas/shopping/types/order_line_item.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,23 @@
2323
"type": "object",
2424
"required": ["total", "fulfilled"],
2525
"properties": {
26+
"original": {
27+
"type": "integer",
28+
"minimum": 0,
29+
"description": "Quantity from the original checkout."
30+
},
2631
"total": {
2732
"type": "integer",
2833
"minimum": 0,
29-
"description": "Current total quantity."
34+
"description": "Current total active quantity. May differ from original due to post-order modifications (e.g., returns or cancellations)."
3035
},
3136
"fulfilled": {
3237
"type": "integer",
3338
"minimum": 0,
34-
"description": "Quantity fulfilled (sum from fulfillment events)."
39+
"description": "Quantity fulfilled so far."
3540
}
3641
},
37-
"description": "Quantity tracking. Both total and fulfilled are derived from events."
42+
"description": "Quantity tracking for the line item."
3843
},
3944
"totals": {
4045
"type": "array",
@@ -48,9 +53,10 @@
4853
"enum": [
4954
"processing",
5055
"partial",
51-
"fulfilled"
56+
"fulfilled",
57+
"removed"
5258
],
53-
"description": "Derived status: fulfilled if quantity.fulfilled == quantity.total, partial if quantity.fulfilled > 0, otherwise processing."
59+
"description": "Derived status: removed if quantity.total == 0, fulfilled if quantity.total > 0 and quantity.fulfilled == quantity.total, partial if quantity.total > 0 and quantity.fulfilled > 0, otherwise processing."
5460
},
5561
"parent_id": {
5662
"type": "string",

source/schemas/shopping/types/total.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,18 @@
2020
"ucp_request": "omit"
2121
},
2222
"amount": {
23-
"$ref": "amount.json",
23+
"$ref": "signed_amount.json",
2424
"ucp_request": "omit"
2525
}
26-
}
26+
},
27+
"allOf": [
28+
{
29+
"if": { "properties": { "type": { "enum": ["discount", "items_discount"] } }, "required": ["type"] },
30+
"then": { "properties": { "amount": { "exclusiveMaximum": 0 } } }
31+
},
32+
{
33+
"if": { "properties": { "type": { "enum": ["subtotal", "fulfillment", "tax", "fee"] } }, "required": ["type"] },
34+
"then": { "properties": { "amount": { "minimum": 0 } } }
35+
}
36+
]
2737
}

source/schemas/shopping/types/totals.json

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212
{
1313
"type": "object",
1414
"properties": {
15-
"amount": {
16-
"$ref": "signed_amount.json"
17-
},
1815
"lines": {
1916
"type": "array",
2017
"items": {
@@ -39,14 +36,6 @@
3936
}
4037
}
4138
},
42-
{
43-
"if": { "properties": { "type": { "enum": ["discount", "items_discount"] } }, "required": ["type"] },
44-
"then": { "properties": { "amount": { "exclusiveMaximum": 0 } } }
45-
},
46-
{
47-
"if": { "properties": { "type": { "enum": ["subtotal", "fulfillment", "tax", "fee"] } }, "required": ["type"] },
48-
"then": { "properties": { "amount": { "minimum": 0 } } }
49-
},
5039
{
5140
"if": {
5241
"properties": {

0 commit comments

Comments
 (0)