Hi Shopify Community,
I’m working on implementing custom discounts using Shopify’s order functions (the older API, not planning to migrate to the new discount functions yet). We have offers based on cart totals, where discounts should apply either at the cart level (order subtotal) or at the product level (individual variants/rows) depending on the offerType.
Specifically:
- If
offerTypeis “cart”, discount applies to the order subtotal. - If
offerTypeis “all-products”, it should apply to product variants (row-level).
From the schema.graphql, I see the targets support both orderSubtotal and productVariant, but in practice, even when I target productVariant, the discount is still getting applied to the order subtotal. It’s not behaving as expected for product-level discounts.
Here’s the code we’re using:
run.graphql
query Input {
shop{
rules: metafield(namespace: "oscp", key: "cartRules"){
value
}
}
cart {
cost {
subtotalAmount {
amount
}
}
deliverableLines {
quantity
}
lines {
quantity
cost {
amountPerQuantity {
amount
currencyCode
}
}
merchandise {
__typename
... on ProductVariant {
id
}
}
}
}
}
run.ts
/// Importing necessary types from the generated API
import {
FunctionResult,
FixedAmount,
DiscountApplicationStrategy,
} from "../generated/api";
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
* @typedef {import("../generated/api").Target} Target
* @typedef {import("../generated/api").ProductVariant} ProductVariant
*/
// Constant representing an empty discount
const EMPTY_DISCOUNT: FunctionResult = {
discountApplicationStrategy: DiscountApplicationStrategy.Maximum,
discounts: [],
};
// Configuration type (currently empty string, might be placeholder)
type Configuration = "";
/**
* Function to calculate discounts based on input rules and cart data.
* @param {RunInput} input - Input data containing shop rules and cart information.
* @returns {FunctionRunResult} - Result containing applicable discounts.
*/
export function run(input) {
// Parsing shop rules from input JSON
const cartRules = JSON.parse(input.shop.rules.value);
// Extracting cart amount from input
const cartAmount = parseFloat(input.cart.cost.subtotalAmount.amount);
let totalQuantity = 0;
// Calculating total quantity of items in the cart
for (const line of input.cart.deliverableLines) {
totalQuantity += line.quantity;
}
let applicableDiscount = null;
// Iterating through each rule in shop rules
cartRules.forEach(rule => {
const discountStatus = rule.discountStatus;
const discountLabel = rule.discountLabel;
const offerType = rule.offerType;
const offerValues = rule.offerValues || [];
offerValues.forEach(offer => {
const offerCondition = offer.offerCondition;
const minCartValue = offer.minCartValue;
const minCartQuantity = offer.minCartQuantity;
const type = offer.type;
const discountValue = offer.discountValue;
let conditionMet = false;
if (discountStatus) {
if (
(offerCondition === "order-amount-value" &&
cartAmount >= parseFloat(minCartValue) &&
parseFloat(minCartValue) !== 0) ||
(offerCondition === "order-quantity-value" &&
totalQuantity >= minCartQuantity &&
minCartQuantity !== 0)
) {
conditionMet = true;
}
}
if (conditionMet) {
let discountDetails;
if (type === "percent") {
discountDetails = { percentage: { value: parseFloat(discountValue) } };
} else if (type === "fixed") {
discountDetails = {
fixedAmount: { amount: parseFloat(discountValue).toFixed(2) },
};
}
let targets = [];
if (offerType === "all-products") {
// Apply discount to all product variants in the cart
targets = input.cart.lines
.filter((line: any) => line.merchandise.__typename === "ProductVariant")
.map((line: any) => ({
productVariant: {
id: line.merchandise.id,
quantity: line.quantity,
},
}));
} else if (offerType === "cart") {
// Apply discount to the whole cart subtotal
targets = [{
orderSubtotal: {
excludedVariantIds: [],
},
}];
}
// Log which offer and targets are applied
console.log("Applied Offer:", JSON.stringify({
discountLabel,
offerType,
discountDetails,
targets,
}));
applicableDiscount = {
targets,
message: discountLabel,
value: discountDetails,
};
}
});
});
// If applicable discount found, return it, otherwise return empty discount
if (applicableDiscount) {
return {
discounts: [applicableDiscount],
discountApplicationStrategy: DiscountApplicationStrategy.Maximum,
};
} else {
return EMPTY_DISCOUNT;
}
}
Example Input & Output
For better understanding, here’s a test example with two rules: one for “all-products” (fixed $10 off on variants if cart > $1000) and one for “cart” (12% off subtotal if quantity >=10). The cart subtotal is $1600 with 2 items.
Input (STDIN):
{
"shop": {
"rules": {
"value": "[{\"discountStatus\":\"1\",\"discountLabel\":\"Test 1\",\"offerType\":\"all-products\",\"offerResources\":[],\"offerCondition\":\"order-amount-value\",\"offerValues\":[{\"offerCondition\":\"order-amount-value\",\"minCartValue\":\"1000\",\"minCartQuantity\":\"0\",\"type\":\"fixed\",\"discountValue\":\"10\"}]},{\"discountStatus\":\"1\",\"discountLabel\":\"Test 2\",\"offerType\":\"cart\",\"offerResources\":[],\"offerCondition\":\"order-quantity-value\",\"offerValues\":[{\"offerCondition\":\"order-quantity-value\",\"minCartValue\":\"0\",\"minCartQuantity\":\"10\",\"type\":\"percent\",\"discountValue\":\"12\"}]}]"
}
},
"cart": {
"cost": {
"subtotalAmount": {
"amount": "1600.0"
}
},
"deliverableLines": [
{
"quantity": 2
}
],
"lines": [
{
"quantity": 2,
"cost": {
"amountPerQuantity": {
"amount": "800.0",
"currencyCode": "USD"
}
},
"merchandise": {
"__typename": "ProductVariant",
"id": "gid://shopify/ProductVariant/45112436162794"
}
}
]
}
}
Output (STDOUT):
{
"discounts": [
{
"targets": [
{
"productVariant": {
"id": "gid://shopify/ProductVariant/45112436162794",
"quantity": 2
}
}
],
"message": "Test 1",
"value": {
"fixedAmount": {
"amount": "10.00"
}
}
}
],
"discountApplicationStrategy": "MAXIMUM"
}
Logs:
Applied Offer: {"discountLabel":"Test 1","offerType":"all-products","discountDetails":{"fixedAmount":{"amount":"10.00"}},"targets":[{"productVariant":{"id":"gid://shopify/ProductVariant/45112436162794","quantity":2}}]}
In this example, the “Test 1” rule (all-products, fixed $10) meets the condition (cart > $1000), and we’re targeting productVariant. But in the actual checkout, the discount applies to the subtotal, not the product row. The “Test 2” rule doesn’t apply here since quantity is only 2 (<10).
To clarify the rules and expected vs. actual behavior, here’s a table:
| Rule Name | Offer Type | Condition | Discount Type | Value | Expected Target | Actual Target Applied |
|---|---|---|---|---|---|---|
| Test 1 | all-products | Cart Amount >= $1000 | Fixed | $10 | Product Variant (row-level, e.g., $10 off per qualifying item) | Order Subtotal |
| Test 2 | cart | Cart Quantity >= 10 | Percent | 12% | Order Subtotal | Order Subtotal (as expected, but not triggered in example) |
Relevant schema.graphql Snippet
"""
The method for applying the discount to an order. This argument accepts either a single
`OrderSubtotalTarget` or one or more `ProductVariantTarget`s, but not both.
"""
input Target @oneOf {
"""
A method for applying a discount to the entire order subtotal. The subtotal is the total amount of the
order before any taxes, shipping fees, or discounts are applied. For example, if a customer places an order
for a t-shirt and a pair of shoes, then the subtotal is the sum of the prices of those items.
"""
orderSubtotal: OrderSubtotalTarget
"""
A method for applying a discount to a
[product variant](https://help.shopify.com/manual/products/variants). Product variants
are the different versions of a product that can be purchased. For example, if a customer orders a t-shirt
in a specific size and color, the t-shirt is a product variant. The discount can be applied to all
product variants in the order, or to specific product variants that meet certain criteria.
"""
productVariant: ProductVariantTarget
}
"""
The target type of a condition.
"""
enum TargetType {
"""
The target is the subtotal of the order.
"""
ORDER_SUBTOTAL
"""
The target is a product variant.
"""
PRODUCT_VARIANT
}
Has anyone encountered this issue? Is there something wrong with how I’m structuring the targets for productVariant? Or a limitation in the older functions API? Any workarounds without migrating would be appreciated!
Thanks in advance!