bundle-offers-app/spec/offer-config.schema.json
metatroncubeswdev 365e45406a Bundle Offers Shopify app: discount function, admin, storefront, deploy stack
Rust discount function (one automatic discount evaluating all offers in priority
order, parity with the Odoo engine), React Router admin with Polaris, theme app
extension and app proxy pages, webhooks incl. GDPR, managed-pricing limits, and
a Docker Compose stack (Postgres + Caddy HTTPS) for a single VPS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:35:58 -04:00

343 lines
9.0 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "bundle-offers/function-config.schema.json",
"title": "Bundle Offers - Discount Function configuration",
"description": "Stored as JSON in the metafield $app:configuration on the ONE automatic app discount the app owns per shop. The app regenerates it whenever an offer, a plan, or a schedule boundary changes. Keep it compact: function metafields over ~10 kB are reportedly not delivered to functions (verify against current Shopify limits). Offers are listed in PRIORITY ORDER (lower sequence first); the function evaluates them in this order and every cart item counts towards at most one offer.",
"type": "object",
"required": [
"v",
"offers"
],
"additionalProperties": false,
"properties": {
"v": {
"const": 1,
"description": "Config format version."
},
"cur": {
"type": "string",
"description": "Shop base currency (ISO 4217). 'amount' and 'fixed_price' values are in this currency; the function converts them with presentmentCurrencyRate."
},
"ex": {
"type": "object",
"description": "Global exclusions: lines matching any of these never count towards a bundle.",
"additionalProperties": false,
"properties": {
"tag": {
"type": "string",
"description": "Product tag that excludes a product, e.g. 'no-bundle' (Odoo: marketplace sellers' products)."
},
"subs": {
"type": "boolean",
"default": true,
"description": "Exclude lines bought with a selling plan (subscriptions)."
},
"gc": {
"type": "boolean",
"default": true,
"description": "Exclude gift cards."
}
}
},
"offers": {
"type": "array",
"maxItems": 200,
"items": {
"$ref": "#/$defs/offer"
}
},
"ba": {
"enum": [
"all",
"get"
],
"description": "Shopify addition: bxgy allocation. 'all' (default, Odoo parity) spreads over buy+get units; 'get' discounts only the get units."
},
"g": {
"type": "string",
"format": "date-time",
"description": "Shopify addition: guard time (UTC) = earliest 'e' of the listed offers. The app passes it, in shop local time, as the guardAt input variable; once shop.localTime is past it, the function drops offers with e <= g (covers a late scheduler)."
}
},
"$defs": {
"gidList": {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"scope": {
"type": "object",
"description": "A cart item matches if its variant, OR its product (any variant), OR any of its collections is listed.",
"additionalProperties": false,
"properties": {
"v": {
"$ref": "#/$defs/gidList",
"description": "Variant ids (numeric part of gid) "
},
"p": {
"$ref": "#/$defs/gidList",
"description": "Product ids (any variant)"
},
"c": {
"$ref": "#/$defs/gidList",
"description": "Collection ids (Odoo: eCommerce categories incl. sub-categories; Shopify collections are flat, so the merchant picks every collection that should count)"
}
}
},
"offer": {
"type": "object",
"required": [
"id",
"t",
"n"
],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "App offer id (short)."
},
"n": {
"type": "string",
"maxLength": 80,
"description": "Offer name; used as the discount message shown in cart/checkout."
},
"t": {
"enum": [
"fixed",
"mix",
"bxgy"
],
"description": "Bundle type."
},
"s": {
"type": "string",
"format": "date-time",
"description": "Starts (UTC). Omitted = already started."
},
"e": {
"type": "string",
"format": "date-time",
"description": "Ends (UTC). Omitted = no end."
},
"mu": {
"type": "integer",
"minimum": 0,
"default": 0,
"description": "Max uses per order, 0 = unlimited."
},
"dt": {
"enum": [
"percent",
"amount",
"fixed_price"
],
"description": "fixed/mix only: % off, $ off, or bundle price."
},
"dv": {
"type": "number",
"exclusiveMinimum": 0,
"description": "fixed/mix only: the discount value (percent <= 100)."
},
"l": {
"type": "array",
"minItems": 1,
"description": "fixed only: bundle lines. Each line is a variant OR a product (any variant) with a quantity.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"q"
],
"properties": {
"v": {
"type": "string"
},
"p": {
"type": "string"
},
"q": {
"type": "integer",
"minimum": 1
}
},
"oneOf": [
{
"required": [
"v"
]
},
{
"required": [
"p"
]
}
]
}
},
"sc": {
"$ref": "#/$defs/scope",
"description": "mix only: eligible items."
},
"q": {
"type": "integer",
"minimum": 1,
"description": "mix only: number of items."
},
"bs": {
"$ref": "#/$defs/scope",
"description": "bxgy only: what the customer buys."
},
"bq": {
"type": "integer",
"minimum": 1,
"description": "bxgy only: buy quantity."
},
"gs": {
"$ref": "#/$defs/scope",
"description": "bxgy only: what the customer gets."
},
"gq": {
"type": "integer",
"minimum": 1,
"description": "bxgy only: get quantity."
},
"gp": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 100,
"description": "bxgy only: % off the get items (100 = free)."
},
"cc": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[A-Z]{2}$"
},
"description": "Shopify addition (F08): ISO country codes; the offer applies only when localization.country is listed. Empty/omitted = everywhere. (localization.market is deprecated in the function input.)"
}
},
"allOf": [
{
"if": {
"properties": {
"t": {
"const": "fixed"
}
}
},
"then": {
"required": [
"l",
"dt",
"dv"
]
}
},
{
"if": {
"properties": {
"t": {
"const": "mix"
}
}
},
"then": {
"required": [
"sc",
"q",
"dt",
"dv"
]
}
},
{
"if": {
"properties": {
"t": {
"const": "bxgy"
}
}
},
"then": {
"required": [
"bs",
"bq",
"gs",
"gq",
"gp"
]
}
}
]
}
},
"examples": [
{
"v": 1,
"cur": "CAD",
"ex": {
"tag": "no-bundle",
"subs": true,
"gc": true
},
"offers": [
{
"id": "o1",
"n": "Diwali Puja Kit",
"t": "fixed",
"dt": "percent",
"dv": 20,
"mu": 0,
"s": "2026-10-20T04:00:00Z",
"e": "2026-11-09T04:59:59Z",
"l": [
{
"v": "44001",
"q": 1
},
{
"p": "8001",
"q": 2
}
]
},
{
"id": "o2",
"n": "Any 3 Sweets for $20",
"t": "mix",
"dt": "fixed_price",
"dv": 20,
"q": 3,
"sc": {
"c": [
"4501",
"4502"
]
}
},
{
"id": "o3",
"n": "Buy 2 Garlands, Get 1 Free",
"t": "bxgy",
"bq": 2,
"gq": 1,
"gp": 100,
"bs": {
"c": [
"4600"
]
},
"gs": {
"c": [
"4600"
]
}
}
]
}
]
}