Skip to content

feat: add approaching discounts to discount extension.#482

Open
sdedeo2025 wants to merge 4 commits into
Universal-Commerce-Protocol:mainfrom
sdedeo2025:feat/approaching_discount
Open

feat: add approaching discounts to discount extension.#482
sdedeo2025 wants to merge 4 commits into
Universal-Commerce-Protocol:mainfrom
sdedeo2025:feat/approaching_discount

Conversation

@sdedeo2025

Copy link
Copy Markdown

Description

Updates the Discount extension (dev.ucp.shopping.discount) to support returning details of approaching discounts.

Approaching discounts enables buyers to be informed when they are approaching a cart ($10 off orders >= $100) or shipping discount (free shipping on orders >= $100).

Approaching discounts are for promotions that trigger based on a qualifying amount and contain an upsell threshold, enabling the merchant to specify when the approaching discount message is shown to the buyer.

How it works

Cart
Expanding on the above example, $10 off orders >= $100, the qualifying amount is $100. If the upsell threshold is set to $25, the message will be returned once the cart exceeds $75 in value (but less than $100 when the promotion applies).

Shipping
Similarly, shipping approaching discounts are used to convey discounted or free shipping. Shipping approaching discounts are calculated based on the amount of qualifying products within the fulfillment group.
i.e. Free shipping on orders >= $100, upsell threshold $25

Schema

The approaching discounts are returned as part of the discount object in the cart and checkout response. Example below.

 "discounts": {
        "approaching": {
            "cart": [
                {
                    "threshold": 10000,
                    "title": "$10 off orders over $100",
                    "total": 7500
                }
            ]
        }
    }

Design Decisions

  • Add on to the discount extension as approaching and applied discounts are tightly related.
  • Backwards compatible as this is a new section within the discount response and only returned when applicable.

Files

  • source/schemas/shopping/types/approaching_discount.json - Schema for the approaching discount object
  • source/schemas/shopping/discount.json - Modified schema to include approaching discount object within the discount object
  • docs/specification/approaching_discounts.md - Documentation with schema definitions and examples.
  • docs/specification/discount.md - Changes to include approaching discounts / link to approaching_discounts.md.

Category (Required)

  • Core Protocol: Changes to the base communication layer, global context, or breaking refactors. (Requires Technical Council approval)
  • Governance/Contributing: Updates to GOVERNANCE.md, CONTRIBUTING.md, or CODEOWNERS. (Requires Governance Council approval)
  • Capability: New schemas (Discovery, Cart, etc.) or extensions. (Requires Maintainer approval)
  • Documentation: Updates to README, or documentations regarding schema or capabilities. (Requires Maintainer approval)
  • Infrastructure: CI/CD, Linters, or build scripts. (Requires DevOps Maintainer approval)
  • Maintenance: Version bumps, lockfile updates, or minor bug fixes. (Requires DevOps Maintainer approval)
  • SDK: Language-specific SDK updates and releases. (Requires DevOps Maintainer approval)
  • Samples / Conformance: Maintaining samples and the conformance suite. (Requires Maintainer approval)
  • UCP Schema: Changes to the ucp-schema tool (resolver, linter, validator). (Requires Maintainer approval)
  • Community Health (.github): Updates to templates, workflows, or org-level configs. (Requires DevOps Maintainer approval)

Related Issues

NA

Checklist

  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk.

@sdedeo2025 sdedeo2025 requested review from a team as code owners May 27, 2026 14:00
@google-cla

google-cla Bot commented May 27, 2026

Copy link
Copy Markdown

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.

@sdedeo2025 sdedeo2025 changed the title Feat/approaching discount feat: add approaching discounts to discount extension. May 27, 2026
@karangoel16

Copy link
Copy Markdown

Design question: should approaching discounts live in a new incentives extension instead?

First — really glad to see this. Approaching-discount messaging is high-leverage for conversion and AOV, and the schema is clean. Before we land it under dev.ucp.shopping.discount, I'd like to surface a design alternative for the working group to weigh in on, because I think where this lives will be hard to change once implementers ship against it.

The concern with placing it in the discount extension

The discount capability today is consistent about one thing: it represents money that has moved — amounts, allocations, methods, eligibility for adjustments that are actually applied. Approaching discounts are a different kind of object: no money has changed, nothing is allocated, and the buyer hasn't done anything yet. It's a marketing/upsell signal about a possible future state.

The shape generalizes naturally to other upsell signals merchants already run today:

  • "Free gift at $50"
  • "Free sample at $75"
  • "BOGO unlocks at qty 2"
  • "Add gift wrap for $5"
  • Loyalty points progress ("50 points from Gold")

None of those are discounts, but they all share the same structure: here is a threshold, here is where you are, here is the message. If we put approaching discounts under discounts.approaching now, the second upsell shape either gets jammed in awkwardly or lives somewhere inconsistent.

There's also a coupling cost: businesses can't advertise upsell-messaging support without claiming the full discount surface, and any change to upsell semantics churns the discount extension's version.

Proposal

A new extension — dev.ucp.shopping.incentives (name TBD; upsells or promotions_preview also work) — with approaching discounts as its first concrete shape, leaving room for future upsell types:

"incentives": {
  "approaching_discounts": [
    { "title": "...", "remaining": 2500, "scope": {...} }
  ]
  // future: free_gifts, samples, bogo_triggers, loyalty_progress
}

Why this fits cleanly with identity linking

This is actually the strongest argument for the split. Incentives are deeply identity-aware:

  • Anonymous: generic upsells ("Free shipping over $100")
  • Identified loyalty member: personalized ("Free shipping over $50 for Gold members", "Welcome back, $10 off", loyalty progress)

A separate extension can define its own scope (dev.ucp.shopping.incentives:read), which businesses then choose to list in their dev.ucp.common.identity_linking config if they want personalized incentives gated behind login. Merchants who are happy to show generic upsells to anonymous traffic simply don't list it. Same identity-linking mechanism used elsewhere — no new integration surface, and we can reuse the existing identity_optional notice pattern to prompt "Sign in for member offers."

Folding it into discount means inheriting whatever auth posture discounts use, even when the upsell signal could be safely public. A dedicated extension lets the per-capability identity gating do what it's designed to do.

Two smaller things on the current schema (worth addressing either way)

  1. Use the shared monetary type. threshold and total are declared inline as "type": "integer", but the sibling applied[].amount in discount.json uses "\$ref": "../common/types/amount.json". Worth \$ref'ing the shared type for consistency.
  2. Consider returning remaining instead of (or alongside) threshold + total. Forces no client-side math, and removes the ambiguity around what "total" means — cart total, qualifying subtotal, fulfillment-group total? Only the merchant knows what counts toward the threshold.

Honest take

If the appetite is "ship now, refactor when the second upsell shape arrives," the PR is fine as-is — the field is additive and ucp_request: omit, so removable. But schema decisions calcify fast, and I'd rather have this conversation before we have one implementer shipping. Curious what others think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants