I have built a discount function extension by following the tutorial at https://shopify.dev/docs/apps/selling-strategies/discounts/experience. I’m attempting to create a discount that lets you “Get X off every Y products” where X could be a percentage and Y is a numeric quantity. I’ve also added a product selector and filtered the target products to only use the ones selected. I have everything working almost perfectly but for some reason when I adjust the cart quantity I’m getting strange behavior with the line item order and unexpected error messages on the wrong lines. The admin UI looks like this:
This is my run.js file that does the logic for discounting cart items in groups of Y products.
// @ts-check
import { 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
*/
/**
* @type {FunctionRunResult}
*/
const EMPTY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: [],
};
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
// Define a type for your configuration, and parse it from the metafield
/**
* @type {{
* quantity: number
* percentage: number,
* products: object
* }}
*/
const configuration = JSON.parse(
input?.discountNode?.metafield?.value ?? "{}"
);
// Make sure we have required config settings for the discount
if (!configuration.quantity || !configuration.percentage || !configuration.products) {
return EMPTY_DISCOUNT;
}
const variantIds = configuration.products.map((product) => { return product.variantId; });
const targets = [];
input.cart.lines.forEach((line) => {
if (line.merchandise.__typename !== 'ProductVariant' || !variantIds.includes(line.merchandise.id)) {
return;
}
const variant = /** @type {ProductVariant} */ (line.merchandise);
const existing = targets.filter((target) => target.productVariant.id == variant.id);
if (existing.length) {
// Add up line quantity
existing[0].productVariant.quantity += line.quantity;
} else {
// Create a new target
const target = /** @type {Target} */ ({
productVariant: {
id: variant.id,
quantity: line.quantity
}
});
targets.push(target);
}
})
// Loop through targets to determine target quantity
targets.forEach((target, index) => {
// Calculate target quantity based on configuration settings
target.productVariant.quantity = Math.floor(target.productVariant.quantity / configuration.quantity) * configuration.quantity;
// Remove target if quantity is zero
if (target.productVariant.quantity === 0) {
targets.splice(index, 1);
}
});
if (!targets.length) {
console.error("No cart lines qualify for discount.");
return EMPTY_DISCOUNT;
}
return {
discounts: [
{
targets,
value: {
percentage: {
value: configuration.percentage.toString()
}
}
}
],
discountApplicationStrategy: DiscountApplicationStrategy.First
};
};
I’ve attached a video of the problem in hopes that someone can offer some help with this bug. The best way to say it is that the cart line items AND error message reverses every time a quantity is added that would results in the line items to split into two, one with the discount and one without. I’m porting this over from the Shopify Ruby Checkout Scripts where I was able to adjust the Output.cart. With the new Discount Functions I just return a discounts object with targets and quantities and don’t have the same control over the cart output. Can anyone point me in the right direction on how I can fix this bug or how I can control the cart output in the scope of this discount function?
