diff --git a/docs/specification/loyalty.md b/docs/specification/loyalty.md new file mode 100644 index 00000000..624e99f6 --- /dev/null +++ b/docs/specification/loyalty.md @@ -0,0 +1,243 @@ + + +# Loyalty Extension + +## Overview + +The loyalty extension is designed to facilitate high-fidelity loyalty experiences across various commerce journeys, ensuring that shoppers can for example sign-up for membership, access personalized benefits, redeem rewards, and manage memberships seamlessly across various capabilities. + +In its current format, it can be used to decorate the following Capabilities for example: + +* **Checkout**: For applying member-specific discounts/fulfillment benefits, calculating point earnings, or initiating reward redemptions, in conjunction with discount extension to specify the monetary effect of applicable loyalty benefits +* **Cart**: For displaying the loyalty benefits for potential usage on subsequent checkout (e.g. redeemable balance, tier evaluations), with or without the help of discount extension to upsell potential savings and non-monetary benefits from membership + +In the future, for membership lifecycle management by agents (e.g. membership status fetching, program sign-up, balance transfer, etc.). it is possible that loyalty could be considered an independent capability. The other direction is fitting them within higher-order account and profile management capabilities that allow for broader operations, with loyalty being a subset and hence potentially represented by the same extension as it. This is not yet finalized among community discussion and pending future updates to achieve. + +## Key Concepts + +Loyalty has four main components: + +**Memberships**: the overarching framework of a loyalty program, as well as the specific enrollment status and standing of a customer within it + +* Usually represents a customer lifecycle solution the businesses have, which offer a tiered ecosystem of access, convenience, and identity. +* Encompasses the metadata of the concrete offerings as well as the customer level information associated with (if applicable). +* Customers can have multiple memberships that are relevant to the business (e.g. self-owned program and third-party program). + +**Tiers**: progressive achievement levels within a membership that unlock increasing value based on activity or spend/fee payment + +* Defines achievable levels within a membership for a customer, with various types of qualification methods and conditions. +* Customers can simultaneously be activated on and eligible for multiple tiers if the program allows. + +**Benefits**: ongoing perks and privileges granted to a customer based on their current tier or membership status + +* Contains both delayed (e.g. “Members have access to dedicated customer service”) and immediate-value (e.g. “Members get 5% off”) benefits. + +**Rewards**: the accumulated balances and/or stored value available for the customer to redeem on transactions + +* One membership can offer multiple types of accumulable/collectable rewards, each having its own usage and redemption rules. + +## Discovery + +Businesses can follow standard advertisement mechanism to advertise loyalty support in the Business profile + +```json +{ + "ucp": { + "version": "2026-01-23", + "capabilities": { + "dev.ucp.common.loyalty": [ + { + "version": "2026-01-23", + "extends": "dev.ucp.shopping.checkout", + "spec": "https://ucp.dev/2026-01-23/specification/loyalty", + "schema": "https://ucp.dev/2026-01-23/schemas/common/loyalty.json" + } + ] + } + } +} +``` + +## Schema + +### Entities + +#### Loyalty + +{{ schema_fields('types/loyalty', 'loyalty') }} + +#### Loyalty Membership + +{{ schema_fields('types/loyalty_membership', 'loyalty') }} + +#### Membership Tier + +{{ schema_fields('types/membership_tier', 'loyalty') }} + +#### Membership Tier Condition + +{{ schema_fields('types/membership_tier_condition', 'loyalty') }} + +#### Membership Tier Benefit + +{{ schema_fields('types/membership_tier_benefit', 'loyalty') }} + +#### Membership Reward + +{{ schema_fields('types/membership_reward', 'loyalty') }} + +#### Balance Currency + +{{ schema_fields('types/balance_currency', 'loyalty') }} + +#### Membership Balance + +{{ schema_fields('types/membership_balance', 'loyalty') }} + +#### Expiring Balance + +{{ schema_fields('types/expiring_balance', 'loyalty') }} + +#### Balance Redemption + +{{ schema_fields('types/balance_redemption', 'loyalty') }} + +### Example + +```json +{ + "memberships": [ + { + "id": "membership_1", + "member_id": "member_id_1", + "name": "My Loyalty Program", + "enrollment_date": "2022-11-12T00:00:00Z", + "last_activity_date": "2026-01-12T00:00:00Z", + "end_date": "2036-01-12T00:00:00Z", + "tiers": [ + { + "id": "tier_1", + "name": "GOLD", + "level": 1, + "benefits": [ + { + "id": "BEN_001", + "title": "24-hour Early access", + "description": "24-hour early access to seasonal sales" + } + ], + "conditions": [ + { + "id": "CON_001", + "description": "Free to join", + "condition_type": "free" + } + ], + "links": [ + { + "type": "terms_of_service", + "url": "loyalty.com/terms", + "title": "Sign-up Terms" + } + ] + }, + { + "id": "tier_2", + "name": "PLATINUM", + "level": 2, + "benefits": [ + { + "id": "BEN_001", + "title": "Free shipping", + "description": "Complimentary standard shipping on all orders", + "applies_on": ["$.discounts.applied[0]"] + }, + { + "id": "BEN_002", + "title": "48-hour Early access", + "description": "48-hour early access to seasonal sales" + }, + { + "id": "BEN_003", + "title": "Member discount", + "description": "Members get $5 off", + "applies_on": ["$.discounts.applied[1]"] + } + ], + "enrollment_conditions": [ + { + "id": "CON_002", + "description": "$99/yr membership fee", + "condition_type": "fee" + } + ], + "links": [ + { + "type": "terms_of_service", + "url": "loyalty.com/terms", + "title": "Sign-up Terms" + } + ] + } + ], + "activated_tiers": ["tier_1"], + "rewards": [ + { + "currency": { + "name": "LoyaltyStars", + "code": "LST", + }, + "balance": { + "available": 4500, + "pending": 250, + "lifetime_earned": 25000, + "lifetime_redeemed": 1000, + "expiring": [ + { + "amount": 500, + "expiry_date": "2026-12-31T23:59:59Z", + "reason": "ANNUAL_EXPIRATION" + } + ] + }, + "redemption": { + "amount": 100, + "applies_on": ["$.discounts.applied[0]"] + } + } + ] + } + ], + "activated_memberships": ["membership_1"] +} +``` + +## Behavior and Expectations + +### Request + +* Non-empty `activated_memberships` and `activated_tiers` implies membership sign-up intention from the customer. No eligibility verification needed, but can rely on the `condition_type` of `membership_tier_condition` within each `membership_tier`to provide selections for customers (e.g. only offer `free` typed tier for sign up). +* Render applicable loyalty benefits based on the human-readable `title` and `description` provided, and highlight the source of immediate-value benefits alongside where they are applied based on the back referencing from `applies_on` to provide necessary disclosure or explanation. +* Only request rewards redemption when there is non-zero available balance but no need to run any extra check as redemption rules can be complicated even if there is sufficient available balance for example. + +### Response + +* Check and determine user’s eligibility in context (e.g. checkout with member benefits applied, checkout with rewards redemption, sign-up new membership etc) for both existing and prospective customers. When eligibility condition is not met, response clear ERROR message to indicate that. +* Populate `applies_on` when immediate-value membership benefits are applicable to the transaction and reference them to the corresponding line items. +* Provide a comprehensive list of tiers that the membership offers, but at the minimum it MUST contain the ones that the customer is activated with (if applicable). +* For memberships that do not have a tiered system (i.e. loyalty level can not be upgraded or downgraded), it MUST still be treated as a single-tiered membership. +* Businesses MUST respond with at least one benefit and SHOULD aim to provide a comprehensive list. diff --git a/generate_ts_schema_types.js b/generate_ts_schema_types.js index 1b0120d9..0867acfb 100644 --- a/generate_ts_schema_types.js +++ b/generate_ts_schema_types.js @@ -28,6 +28,18 @@ async function generate() { } } + // Add common schemas + const commonDir = path.join(SOURCE_ROOT, 'schemas/common'); + if (fs.existsSync(commonDir)) { + for (const file of fs.readdirSync(commonDir)) { + if (file.endsWith('.json')) { + properties[path.basename(file, '.json')] = { + $ref: path.join(commonDir, file) + }; + } + } + } + // Add handler schemas const handlersDir = path.join(SOURCE_ROOT, 'handlers'); if (fs.existsSync(handlersDir)) { diff --git a/main.py b/main.py index 642305da..3b285638 100644 --- a/main.py +++ b/main.py @@ -29,12 +29,15 @@ # Base directories for schema resolution OPENAPI_DIR = Path("source/services/shopping") SHOPPING_SCHEMAS_DIR = Path("source/schemas/shopping") +COMMON_SCHEMAS_DIR = Path("source/schemas/common") UCP_SCHEMA_PATH = Path("source/schemas/ucp.json") SCHEMAS_DIRS = [ Path("source/handlers/google_pay"), Path("source/schemas"), SHOPPING_SCHEMAS_DIR, SHOPPING_SCHEMAS_DIR / "types", + COMMON_SCHEMAS_DIR, + COMMON_SCHEMAS_DIR / "types", ] # Cache for resolved schemas to avoid repeated subprocess calls diff --git a/mkdocs.yml b/mkdocs.yml index 563de73a..19171fdb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,10 +47,12 @@ nav: - Buyer Consent Extension: specification/buyer-consent.md - Discounts Extension: specification/discount.md - Fulfillment Extension: specification/fulfillment.md + - Loyalty Extension: specification/loyalty.md - Cart Capability: - Overview: specification/cart.md - HTTP/REST Binding: specification/cart-rest.md - MCP Binding: specification/cart-mcp.md + - Loyalty Extension: specification/loyalty.md - Catalog Capability: - Overview: specification/catalog/index.md - Search: specification/catalog/search.md @@ -222,6 +224,7 @@ plugins: - specification/buyer-consent.md - specification/discount.md - specification/fulfillment.md + - specification/loyalty.md - specification/cart.md - specification/cart-rest.md - specification/cart-mcp.md diff --git a/source/schemas/common/loyalty.json b/source/schemas/common/loyalty.json new file mode 100644 index 00000000..0859d2a8 --- /dev/null +++ b/source/schemas/common/loyalty.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/loyalty.json", + "name": "dev.ucp.common.loyalty", + "title": "Loyalty Extension", + "description": "Extends various Capabilities with loyalty support using memberships info.", + "$defs": { + "loyalty": { + "$ref": "types/loyalty.json" + }, + "dev.ucp.shopping.cart": { + "title": "Cart with Loyalty", + "description": "Cart extended with Loyalty capability.", + "allOf": [ + { + "$ref": "cart.json" + }, + { + "type": "object", + "properties": { + "loyalty": { + "$ref": "#/$defs/loyalty", + "ucp_request": { + "create": "optional", + "update": "optional" + } + } + } + } + ] + }, + "dev.ucp.shopping.checkout": { + "title": "Checkout with Loyalty", + "description": "Checkout extended with Loyalty capability.", + "allOf": [ + { + "$ref": "../shopping/checkout.json" + }, + { + "type": "object", + "properties": { + "loyalty": { + "$ref": "#/$defs/loyalty", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "omit" + } + } + } + } + ] + } + } +} diff --git a/source/schemas/common/types/balance_currency.json b/source/schemas/common/types/balance_currency.json new file mode 100644 index 00000000..fe9ee79e --- /dev/null +++ b/source/schemas/common/types/balance_currency.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/balance_currency.json", + "title": "Balance Currency", + "description": "The currency of the balance.", + "type": "object", + "required": ["name", "code"], + "properties": { + "name": { + "type": "string", + "description": "Human-readable name of the currency (e.g. “LoyaltyStars”).", + "ucp_request": "omit" + }, + "code": { + "type": "string", + "description": "Business-specific representation of the currency (e.g. “LST”)." + } + } +} diff --git a/source/schemas/common/types/balance_redemption.json b/source/schemas/common/types/balance_redemption.json new file mode 100644 index 00000000..5acf57ba --- /dev/null +++ b/source/schemas/common/types/balance_redemption.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/balance_redemption.json", + "title": "Balance Redemption", + "description": "The membership balance to be redeemed.", + "type": "object", + "required": ["amount"], + "properties": { + "amount": { + "type": "integer", + "minimum": 0, + "description": "The amount of available balance to be redeemed." + }, + "applies_on": { + "type": "array", + "items": { "type": "string" }, + "description": "JSONPaths to the discount target representing the redemption (e.g. `$.discounts.applied[0]`).", + "ucp_request": "omit" + } + } +} diff --git a/source/schemas/common/types/expiring_balance.json b/source/schemas/common/types/expiring_balance.json new file mode 100644 index 00000000..63e74371 --- /dev/null +++ b/source/schemas/common/types/expiring_balance.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/expiring_balance.json", + "title": "Expiring Balance", + "description": "The balance that are expiring.", + "type": "object", + "required": ["amount", "expiry_time"], + "properties": { + "amount": { + "type": "integer", + "minimum": 0, + "description": "The amount of expiring balance." + }, + "expiry_time": { + "type": "string", + "format": "date-time", + "description": "Balance expiration date (RFC 3339)." + }, + "reason": { + "type": "string", + "description": "Human-readable explanation of the reason for the expiring balance." + } + } +} diff --git a/source/schemas/common/types/loyalty.json b/source/schemas/common/types/loyalty.json new file mode 100644 index 00000000..1e00a0fa --- /dev/null +++ b/source/schemas/common/types/loyalty.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/loyalty.json", + "title": "Loyalty", + "description": "Container for loyalty memberships info.", + "type": "object", + "properties": { + "memberships": { + "type": "array", + "items": { "$ref": "loyalty_membership.json" }, + "description": "List of memberships that the business offers or associated with." + }, + "activated_memberships": { + "type": "array", + "items": { "type": "string" }, + "description": "List of memberships that the customer is activated with, identified by the `id` of the Loyalty Membership object." + } + } +} diff --git a/source/schemas/common/types/loyalty_membership.json b/source/schemas/common/types/loyalty_membership.json new file mode 100644 index 00000000..1d9210b9 --- /dev/null +++ b/source/schemas/common/types/loyalty_membership.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/loyalty_membership.json", + "title": "Loyalty Membership", + "description": "Loyalty membership that the business offers or associated with.", + "type": "object", + "required": ["id", "name", "tiers", "activated_tiers"], + "properties": { + "id": { + "type": "string", + "description": "Unique loyalty membership identifier." + }, + "member_id": { + "type": "string", + "description": "User's loyalty membership ID (e.g. loyalty card #, membership #)." + }, + "name": { + "type": "string", + "description": "Business specific name of the loyalty membership/program.", + "ucp_request": "omit" + }, + "enrollment_time": { + "type": "string", + "format": "date-time", + "description": "Membership enrollment date (RFC 3339).", + "ucp_request": "omit" + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Membership end/expiration date (RFC 3339).", + "ucp_request": "omit" + }, + "tiers": { + "type": "array", + "items": { + "$ref": "membership_tier.json" + }, + "description": "List of tiers associated with the loyalty membership.", + "ucp_request": "omit" + }, + "activated_tiers": { + "type": "array", + "items": { "type": "string" }, + "description": "List of tiers that the customer is activated for or wish to activate, identified by the `id` of the Membership Tier object." + }, + "rewards": { + "type": "array", + "items": { + "$ref": "membership_reward.json" + }, + "description": "List of quantifiable rewards value the user holds. Each object encapsulates one type of reward the membership offers." + } + } +} diff --git a/source/schemas/common/types/membership_balance.json b/source/schemas/common/types/membership_balance.json new file mode 100644 index 00000000..c7edfa2b --- /dev/null +++ b/source/schemas/common/types/membership_balance.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/membership_balance.json", + "title": "Membership Balance", + "description": "The overall balance information of the membership.", + "type": "object", + "required": ["available"], + "properties": { + "available": { + "type": "integer", + "minimum": 0, + "description": "The amount of available/usable balance." + }, + "pending": { + "type": "integer", + "minimum": 0, + "description": "The amount of pending balance." + }, + "lifetime_earned": { + "type": "integer", + "minimum": 0, + "description": "The amount of balance accumulated since the membership enrollment time." + }, + "lifetime_redeemed": { + "type": "integer", + "minimum": 0, + "description": "The amount of balance used since the membership enrollment time." + }, + "expiring": { + "type": "array", + "items": { + "$ref": "expiring_balance.json" + }, + "description": "List of balances that are expiring." + } + } +} diff --git a/source/schemas/common/types/membership_reward.json b/source/schemas/common/types/membership_reward.json new file mode 100644 index 00000000..3733c45d --- /dev/null +++ b/source/schemas/common/types/membership_reward.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/membership_reward.json", + "title": "Membership Reward", + "description": "Quantifiable and collectable reward of loyalty membership of the customer.", + "type": "object", + "required": ["currency", "balance"], + "properties": { + "currency": { + "type": "object", + "$ref": "balance_currency.json", + "description": "A unit of value that customers can accumulate through various commercial activities." + }, + "balance": { + "type": "object", + "$ref": "membership_balance.json", + "description": "The balance information of the membership, in the unit of `currency`.", + "ucp_request": "omit" + }, + "redemption": { + "type": "object", + "$ref": "balance_redemption.json", + "description": "The balance to be redeemed." + } + } +} diff --git a/source/schemas/common/types/membership_tier.json b/source/schemas/common/types/membership_tier.json new file mode 100644 index 00000000..c6c770da --- /dev/null +++ b/source/schemas/common/types/membership_tier.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/membership_tier.json", + "title": "Membership Tier", + "description": "A level within a loyalty program structured ranking system that offers escalating benefits and exclusive perks.", + "type": "object", + "required": ["id", "name", "level"], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the membership tier." + }, + "name": { + "type": "string", + "description": "The human-readable name of the tier (e.g., “Platinum”)." + }, + "level": { + "type": "integer", + "minimum": 1, + "description": "The relative ranking or numerical level of the tier within the loyalty program." + }, + "enrollment_conditions": { + "type": "array", + "items": { + "$ref": "membership_tier_condition.json" + }, + "description": "List of conditions to enroll in the tier." + }, + "benefits": { + "type": "array", + "items": { + "$ref": "membership_tier_benefit.json" + }, + "description": "List of possible benefits associated with this tier." + }, + "links": { + "type": "array", + "items": { + "$ref": "../../shopping/types/link.json" + }, + "description": "URLs for detailed fine prints of tier (including sign-up requirements, benefits summary, terms of service, etc.)." + } + } +} diff --git a/source/schemas/common/types/membership_tier_benefit.json b/source/schemas/common/types/membership_tier_benefit.json new file mode 100644 index 00000000..0209a379 --- /dev/null +++ b/source/schemas/common/types/membership_tier_benefit.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/membership_tier_benefit.json", + "title": "Membership Tier Benefit", + "description": "Benefits associated with a membership tier.", + "type": "object", + "required": ["id", "title"], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the tier benefit." + }, + "title": { + "type": "string", + "description": "Short label-like summary of the benefit (e.g. “Free shipping”)." + }, + "description": { + "type": "string", + "description": "Detailed complete explanation of the benefit (e.g. “Complimentary standard shipping on all orders”)." + }, + "applies_on": { + "type": "array", + "items": { "type": "string" }, + "description": "JSONPaths to the eligible benefit target represented by a discount (e.g. `$.discounts.applied[0]`)." + } + } +} diff --git a/source/schemas/common/types/membership_tier_condition.json b/source/schemas/common/types/membership_tier_condition.json new file mode 100644 index 00000000..f70226af --- /dev/null +++ b/source/schemas/common/types/membership_tier_condition.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/membership_tier_condition.json", + "title": "Membership Tier Condition", + "description": "Condition to gain eligibility for the membership tier.", + "type": "object", + "required": ["id", "description"], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the tier condition." + }, + "description": { + "type": "string", + "description": "Short and succinct explanation of the tier participation condition (e.g. “Free to join”)." + }, + "condition_type": { + "type": "string", + "enum": [ + "free", + "fee", + "spend", + "brand_card" + ], + "description": "The type of condition is about." + } + } +}