Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Amex
Ant
Anytown
Backordered
BOGO
Braintree
Carrefour
Centricity
Expand Down
149 changes: 149 additions & 0 deletions docs/specification/discount.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,155 @@ via the `messages[]` array:
| `discount_code_user_not_logged_in` | Code requires authenticated user |
| `discount_code_user_ineligible` | User does not meet eligibility criteria |

### Inapplicable Discounts

In addition to `messages[]` warnings, businesses SHOULD include rejected codes
in the `discounts.inapplicable` array. This provides structured, code-scoped
rejection feedback directly on the discounts object.

{{ extension_schema_fields('discount.json#/$defs/inapplicable_discount', 'discount') }}

**Relationship to `messages[]`:**

- `inapplicable` complements `messages[]` — it does not replace it
- Businesses SHOULD include both `inapplicable` entries AND `messages[]`
warnings for backward compatibility
- `inapplicable` is scoped to code-based rejections; automatic discount issues
remain in `messages[]` only

**Agent benefit:** The `inapplicable` array gives agents a clean success/failure
partition without parsing JSONPath in `messages[].path`. This is especially
valuable when agents try multiple codes in sequence and need to understand which
succeeded and which failed.

**Response requirements:**

- When a submitted code cannot be applied, businesses SHOULD include it in
`discounts.inapplicable` with a machine-readable `reason` and human-readable
`content`.
- Businesses SHOULD use standard `discount_code_*` error codes for the `reason`
field. Freeform codes are permitted for business-specific rejection reasons.
- For backward compatibility, businesses SHOULD also include a corresponding
`messages[]` warning entry.
- `inapplicable` MUST only contain code-based rejections. Issues with automatic
discounts (e.g., automatic discount no longer applicable due to cart change)
MUST be communicated via `messages[]` only.

**Invariant:** Every code in `discounts.codes` appears in exactly one of
`discounts.applied[].code` or `discounts.inapplicable[].code` (or neither, if
the code was silently ignored).

### Example: Mixed Applied and Inapplicable Codes

=== "Request"

```json
{
"discounts": {
"codes": ["SUMMER20", "EXPIRED50", "VIP_ONLY"]
}
}
```

=== "Response"

```json
{
"discounts": {
"codes": ["SUMMER20", "EXPIRED50", "VIP_ONLY"],
"applied": [
{
"code": "SUMMER20",
"title": "Summer Sale 20% Off",
"amount": 800,
"method": "each",
"allocations": [
{"path": "$.line_items[0]", "amount": 800}
]
}
],
"inapplicable": [
{
"code": "EXPIRED50",
"reason": "discount_code_expired",
"content": "Code 'EXPIRED50' expired on December 1, 2025"
},
{
"code": "VIP_ONLY",
"reason": "discount_code_user_ineligible",
"content": "This code is available to VIP members only"
}
]
},
"totals": [
{"type": "subtotal", "display_text": "Subtotal", "amount": 4000},
{"type": "items_discount", "display_text": "Item Discounts", "amount": -800},
{"type": "total", "display_text": "Total", "amount": 3200}
],
"messages": [
{
"type": "warning",
"code": "discount_code_expired",
"path": "$.discounts.codes[1]",
"content": "Code 'EXPIRED50' expired on December 1, 2025"
},
{
"type": "warning",
"code": "discount_code_user_ineligible",
"path": "$.discounts.codes[2]",
"content": "This code is available to VIP members only"
}
]
}
```

Note: Both `inapplicable` and `messages[]` are populated for backward
compatibility. Platforms supporting `inapplicable` can use the structured
objects directly; older platforms continue using `messages[]`.

### Example: Combination Conflict

When codes conflict with each other or with automatic discounts:

=== "Request"

```json
{
"discounts": {
"codes": ["BOGO50", "FLAT20"]
}
}
```

=== "Response"

```json
{
"discounts": {
"codes": ["BOGO50", "FLAT20"],
"applied": [
{
"code": "BOGO50",
"title": "Buy One Get One 50% Off",
"amount": 1500,
"priority": 1
}
],
"inapplicable": [
{
"code": "FLAT20",
"reason": "discount_code_combination_disallowed",
"content": "Cannot combine 'FLAT20' with 'BOGO50' — only one promotional code allowed per order"
}
]
}
}
```

An agent can communicate: "I applied the Buy One Get One 50% Off (-$15.00).
Unfortunately, the 20% off code can't be combined with it — promotional codes
are limited to one per order. The BOGO deal gives you the bigger savings here."

## Automatic Discounts

Businesses may apply discounts automatically based on cart contents, customer
Expand Down
11 changes: 11 additions & 0 deletions source/schemas/shopping/discount.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
}
}
},
"inapplicable_discount": {
"$ref": "types/inapplicable_discount.json"
},
"discounts_object": {
"type": "object",
"description": "Discount codes input and applied discounts output.",
Expand All @@ -97,6 +100,14 @@
"$ref": "#/$defs/applied_discount"
},
"description": "Discounts successfully applied (code-based and automatic)."
},
"inapplicable": {
"type": "array",
"readOnly": true,
"items": {
"$ref": "#/$defs/inapplicable_discount"
},
"description": "Submitted discount codes that could not be applied, with structured rejection reasons. Complements messages[] by providing code-scoped rejection objects directly on the discounts object."
}
}
},
Expand Down
22 changes: 22 additions & 0 deletions source/schemas/shopping/types/inapplicable_discount.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/types/inapplicable_discount.json",
"title": "Inapplicable Discount",
"description": "A submitted discount code that could not be applied, with a structured rejection reason.",
"type": "object",
"required": ["code", "reason", "content"],
"properties": {
"code": {
"type": "string",
"description": "The discount code that was rejected."
},
"reason": {
"$ref": "error_code.json",
"description": "Machine-readable rejection reason. SHOULD use standard discount error codes: discount_code_expired, discount_code_invalid, discount_code_already_applied, discount_code_combination_disallowed, discount_code_user_not_logged_in, discount_code_user_ineligible."
},
"content": {
"type": "string",
"description": "Human-readable rejection explanation (e.g., \"Code 'SUMMER20' expired on December 1st\")."
}
}
}