Discount Function - wrong line item order and error message

Topic summary

Issue: A Shopify Functions discount extension (“Get X off every Y products” with product selection) causes cart line items to flip order and show error messages on the wrong line when quantity changes split a line into discounted vs. non-discounted portions. The misordering appears on each increment.

Implementation: Built from the Shopify discount functions tutorial. The function aggregates eligible variant quantities, computes target quantities in groups of Y, and returns a single discounts payload with targets and a percentage value (DiscountApplicationStrategy.First). No direct control over cart line rendering is available, unlike legacy Ruby Checkout Scripts.

Troubleshooting: One reply suggested array splice during forEach could shift indices. The author notes the issue persists even with a single variant and a single target, indicating it’s unlikely to be caused by local splice logic.

Status/Update: Shopify acknowledged the behavior as a known issue and stated the team is working on it. No timeline or fix has been provided yet.

Notes: A screenshot and an attached video are central to demonstrating the misordering and error placement. Discussion remains open pending a resolution or ETA.

Summarized with AI on January 12. AI used: gpt-5.

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?

Hey @winterpkvtg

Could the splice be causing the issue? When you remove an element with splice(), all subsequent elements move one index down, causing unintended forEach behavior.

Let me know if it’s still behaving weird and I’ll dig in further!

No its not splice because in the example video I uploaded I’m using a single variant. The data returned from the discount function only has 1 single variant so there is no way to order them. Basically all I can do is tell Shopify what variants to discount, how many and how much. I know it was possible to modify the cart with checkout scripts but I don’t see a way to do that with this discount function.

Ah I understand! Thanks, checking with the Functions folks.

Hey @winterpkvtg

Thanks for reporting this. The team are aware of the issue and working on it.

1 Like

Any update here? I have a fully working app now and just waiting for this to release it.