feat(shopping): add Fee Extension for checkout and cart (Option B)#245
feat(shopping): add Fee Extension for checkout and cart (Option B)#245maximenajim wants to merge 1 commit intomainfrom
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
Implement structured fee support via dev.ucp.shopping.fee extension, enabling businesses to surface itemized fees (service, handling, recycling, etc.) with allocation breakdowns, taxability, and waivability metadata. Extracts shared allocation type to decouple fee and discount extensions. Closes #219.
080e343 to
c491a72
Compare
| !!! note "Partial adoption" | ||
| A business MAY support fees on checkout only, cart only, or both. Each | ||
| `extends` entry is independent. Platforms should check which base | ||
| capabilities the fee extension extends before expecting `fees` in responses. |
There was a problem hiding this comment.
I'm curious if this needs to be in docs/specification/overview.md, as this should be true for all extensions, correct?
| | Total Type | Description | | ||
| | ---------------- | ------------------------------------------------------------- | | ||
| | `subtotal` | Sum of line item prices before any adjustments | | ||
| | `items_discount` | Discounts allocated to line items | | ||
| | `discount` | Order-level discounts (shipping, flat amount) | | ||
| | `fulfillment` | Shipping, delivery, or pickup costs | | ||
| | `tax` | Tax amount | | ||
| | `fee` | **Single aggregated fee total** — sum of all `fees.applied[]` | | ||
| | `total` | Grand total: `subtotal - discount + fulfillment + tax + fee` | |
There was a problem hiding this comment.
Should this table be here under docs/specification/fee.md or should fee.md just call out the new total type? I ask because it could make the docs harder to maintain if every extension that affects totals recreates the totals table (e.g. Discount also could have this table).
| | `discount` | Order-level discounts (shipping, flat amount) | | ||
| | `fulfillment` | Shipping, delivery, or pickup costs | | ||
| | `tax` | Tax amount | | ||
| | `fee` | **Single aggregated fee total** — sum of all `fees.applied[]` | |
There was a problem hiding this comment.
This language is helpful, and we should consider being this crisp with the other total types.
| Businesses MUST ensure this invariant holds. If a platform detects a mismatch | ||
| between the aggregated fee total and the sum of itemized fees, the platform | ||
| SHOULD treat the response as invalid and MUST NOT complete the checkout without | ||
| surfacing the discrepancy to the user. |
There was a problem hiding this comment.
Even if the discrepancy is surfaced, is it appropriate for the platform to proceed? Should the Platform use the continue_url is such a case? If yes, should the continue_url be a required field? Currently, it doesn't seem like we have a good way for platforms to handle these cases.
| SHOULD treat the response as invalid and MUST NOT complete the checkout without | ||
| surfacing the discrepancy to the user. | ||
|
|
||
| !!! note "When the Fee Extension is absent" |
There was a problem hiding this comment.
This still feels a bit ambiguous. Does this note deserve the same normative language as the Invariant above? That is, "totals[type=fee] MUST represent the aggregated fee total"; or alternatively, something about there MAY be n number of totals[type=fee] objects in totals (the question that sparked issue #219 ).
| }, | ||
| "amount": { | ||
| "$ref": "amount.json", | ||
| "description": "Amount allocated to this target in ISO 4217 minor units." |
There was a problem hiding this comment.
We should adopt this across the spec. In other places we are simply saying "minor units (cents)" which is inadequate for all currencies.
| "properties": { | ||
| "id": { | ||
| "type": "string", | ||
| "description": "Unique fee identifier. Unlike applied discounts, fees always require an id to enable stable referencing across updates." |
There was a problem hiding this comment.
Are fee ids expected to be consistent across checkout sessions? Or just across requests within a checkout session or cart?
| }, | ||
| "description": { | ||
| "type": "string", | ||
| "description": "Optional explanation of why the fee is charged." |
There was a problem hiding this comment.
Also human-readable, I assume. As with conversations around disclosure, I wonder how Platforms should navigate Businesses potentially wanting richer text, or even images.
| | `service` | General service fee for order processing | Platform service fee | | ||
| | `handling` | Physical handling and packaging of goods | Oversized item handling fee | | ||
| | `recycling` | Disposal or recycling of materials | Electronics recycling fee | | ||
| | `processing` | Payment or order processing surcharge | Credit card processing fee | | ||
| | `regulatory` | Government-mandated fee or compliance charge | Mattress recycling surcharge | | ||
| | `convenience` | Fee for using a particular ordering channel | Online ordering convenience fee | | ||
| | `restocking` | Fee for processing returns or exchanges | Return restocking fee | | ||
| | `environmental` | Environmental impact or sustainability surcharge | Carbon offset fee | |
There was a problem hiding this comment.
What is the value of this spec defining types, especially so many of them? Does a Platform need to know the various types?
|
|
||
| 3. **Positive amounts only:** All fee amounts use `exclusiveMinimum: 0` — | ||
| zero-amount fees are not permitted. If a fee does not apply, it MUST be | ||
| omitted from the `applied` array entirely. This includes waived fees: when a |
There was a problem hiding this comment.
Is there perhaps a need to communicate to the Platform / User that a fee was waived? If we do not allow zero-amount fees, how should this be communicated in the response?
| operations, meaning platforms never send fee data in requests — fees are | ||
| determined entirely by the business and returned in responses. | ||
|
|
||
| Business receivers MUST reject any `fees` fields provided by platforms in |
There was a problem hiding this comment.
Shall we add an error code to source/schemas/shopping/types/error_code.json for this?
| "amount": 500, | ||
| "fee_type": "recycling", | ||
| "taxable": true, | ||
| "description": "State-mandated electronics recycling fee." |
There was a problem hiding this comment.
This made me think of #222 . Is a plain-text description adequate, or are there fees that may need to be more expressive; perhaps we handle those with disclosures?
Summary
Implements the Fee Extension (
dev.ucp.shopping.fee) per Issue #219 (Option B), enabling businesses to surface itemized fees on checkout sessions and carts.Key design decisions
types/allocation.jsonas a reusable type referenced by both discount and fee extensions — eliminates cross-extension coupling (fee no longer depends on discount's$defs)fee_typerequired: Fees without a type are ambiguous for platform rendering/categorizationexclusiveMinimum: 0: Zero-amount fees are not permitted — waived fees are omitted from the array entirelyadditionalProperties: falseon fee type for strict validationreadOnly: trueonfees.appliedfor generic tooling/codegen compatibility$reftoamount.jsonon all monetary fields to preserve the canonical amount type chaintotal.jsondescriptions ("When the Fee Extension is present…")Files changed (8)
New (4):
source/schemas/shopping/types/allocation.json— Shared allocation typesource/schemas/shopping/types/fee.json— Individual fee schemasource/schemas/shopping/fee.json— Fee Extension schema (checkout + cart)docs/specification/fee.md— Full specification pageModified (4):
source/schemas/shopping/discount.json— Allocation now$refs shared typesource/schemas/shopping/types/total.json— Conditional fee language in descriptionsmkdocs.yml— Nav + llmstxt entries.cspell/custom-words.txt— New termsSpec highlights
totals[type=fee]diverges fromsum(fees.applied[].amount)title/description/display_textare plain text — renderers MUST escapeCloses #219
Related: #220