Combine free shipping with product or order discounts

Is it possible to offer free shipping only when a product discount or order discount created in the application is applied?
I would like to add a free shipping option to my product discounts and order discounts.

I tried this problem by rewriting shopify.extension.toml as follows but it did not solve the problem.

api_version = "2024-07"

  [[extensions.targeting]]
  target = "purchase.shipping-discount.run"
  input_query = "src/run-shipping.graphql"
  export = "run-shipping"

  [[extensions.targeting]]
  target = "purchase.product-discount.run"
  input_query = "src/run.graphql"
  export = "run"

Message log:
Error while updating drafts: Targeting multiple Function APIs is not supported.

You can currently only have one target for discount functions. Your use case is not technically feasible with functions. One alternative is to automatically add a manual shipping code at checkout with an extension (our application Stackable now does this!).

2 Likes

I found an article I would like to read on this issue and share it here.

Why You Can’t Use a Single Code for Both Product and Shipping Discounts in Shopify

Thanks for sharing my article; however, it is now outdated. Now that Shopify has released a new Discount Functions API, which allows multiple discount classes on the same Function instance, it should be possible to combine product/shipping discounts into the same code.

Best,
Tobe

1 Like

@tobebuilds I just encountered an issue with the new Discount Function API. It was working fine last week, but today when I tried to create a new one, it returned a product-discount instead.

When I checked the functions, I noticed that it only includes PRODUCT classes. I believe this issue started last Saturday or Sunday, because everything was still working correctly on Friday’s morning.

Could you please check this issue?

  • Image 1 shows how it looked last 10AM Friday
  • Image 2 shows the current situation

Thanks!

Hello Pemond,

I do not work for Shopify. Therefore, I can’t look into this for you.

Hopefully you find the answer you are looking for!

Oh, Thank for let me know.
Hope you have a good day.

Does someone knows some example of a multiple discount classes on the same function?

I want to apply free shipping discounts only when discounts are applied to products or orders. Is this possible?

I think the same logic needs to be implemented in cart_delivery_options_discounts_generate_run and cart_lines_discounts_generate_run.

@Hirano :
This will give you an approach idea :

How Shopify Discount Functions Work

Shopify’s Discount Function API processes discounts in a specific order during cart evaluation:

  1. Cart Lines Discounts (cart_lines_discounts_generate_run): This function applies discounts to individual cart lines (products) or the order subtotal. It outputs discount operations that Shopify applies to the cart.
  2. Delivery Discounts (cart_delivery_options_discounts_generate_run): This function applies discounts to shipping options. It runs after cart line discounts are calculated and can access the cart’s state, including any applied discounts via the discountAllocations field.

The input to cart_delivery_options_discounts_generate_run includes the cart’s current state, which reflects discounts applied by cart_lines_discounts_generate_run. Specifically, the discountAllocations field in the cart or cart lines will contain details about discounts applied to products or the order subtotal.

using this you can decide on providing free shipping

using before query for cart_delivery_options_discounts_generate_run will provide data

query RunInput {
  cart {
    lines {
      id
      discountAllocations {
        discountedAmount {
          amount
        }
      }
    }
    cost {
      subtotalAmount {
        amount
      }
      discountAllocations {
        discountedAmount {
          amount
        }
      }
    }
    deliveryGroups {
      id
      deliveryOptions {
        handle
        cost {
          amount
        }
      }
    }
  }
}

@Ankit_Shrivastava

Thank you for your advise.
But the documentation does not mention a discountAllocations field.
https://shopify.dev/docs/api/functions/latest/discount#Input

Am I missing something?

I am still searching for the answer to this question.
I want to apply free shipping discounts only when discounts are applied to products or orders. Is this possible?

@Hirano : Using Discount function API
api_version = “2025-04”
[[extensions.targeting]]
target = “cart.lines.discounts.generate.run”

within this you can handle product discount and shipping discount both.

@Ankit_Shrivastava
Thank you for your response.

According to the documentation, cart.lines.discounts.generate.run cannot handle shipping discounts. Do you have any specific advice?

@Hirano : I generated this code using AI but this should work as I checked with the documentation :

// @ts-check

/**
 * @typedef {import("../generated/api").RunInput} RunInput
 * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
 */

/**
 * @type {FunctionRunResult}
 */
const NO_CHANGES = {
  operations: [],
};

/**
 * Single discount function that handles both product and shipping discounts
 * @param {RunInput} input
 * @returns {FunctionRunResult}
 */
export function cart_lines_discounts_generate_run(input) {
  const operations = [];

  // Apply 20% discount to products with discount metafield = "yes"
  const productTargets = [];
  
  input.cart.lines.forEach(line => {
    const discountMetafield = line.merchandise.metafield;
    if (discountMetafield && discountMetafield.value === "yes") {
      productTargets.push({
        productVariant: {
          id: line.merchandise.id
        }
      });
    }
  });

  if (productTargets.length > 0) {
    operations.push({
      add: {
        discountClass: "PRODUCT",
        targets: productTargets,
        value: {
          percentage: {
            value: 20.0
          }
        },
        title: "20% off eligible products"
      }
    });
  }

  // Apply $20 off shipping only for pickup delivery method
  const deliveryTargets = [];
  
  input.cart.deliveryGroups.forEach(deliveryGroup => {
    if (deliveryGroup.selectedDeliveryOption && 
        deliveryGroup.selectedDeliveryOption.deliveryMethod === "PICK_UP") {
      deliveryTargets.push({
        deliveryOption: {
          handle: deliveryGroup.selectedDeliveryOption.handle
        }
      });
    }
  });

  if (deliveryTargets.length > 0) {
    operations.push({
      add: {
        discountClass: "SHIPPING",
        targets: deliveryTargets,
        value: {
          fixedAmount: {
            amount: 20.0
          }
        },
        title: "$20 off pickup delivery"
      }
    });
  }

  if (operations.length === 0) {
    return NO_CHANGES;
  }

  return {
    operations: operations,
  };
}

@Ankit_Shrivastava
I am developing in Rust, and I realized that I first need to update my environment. I will try that next.

@Ankit_Shrivastava
In Rust, it is not possible to implement the shipping discount logic depending on the return type of cart.lines.discounts.generate.run, but is it possible in JavaScript?

https://crates.io/crates/shopify_function

I cannot add DeliveryOperation::DeliveryDiscountsAdd to the CartLinesDiscountsGenerateRunResult type.

cart_lines_discounts_generate_run.rs

use crate::schema::CartLineTarget;
use crate::schema::CartLinesDiscountsGenerateRunResult;
use crate::schema::CartOperation;
use crate::schema::DiscountClass;
use crate::schema::OrderDiscountCandidate;
use crate::schema::OrderDiscountCandidateTarget;
use crate::schema::OrderDiscountCandidateValue;
use crate::schema::OrderDiscountSelectionStrategy;
use crate::schema::OrderDiscountsAddOperation;
use crate::schema::OrderSubtotalTarget;
use crate::schema::Percentage;
use crate::schema::ProductDiscountCandidate;
use crate::schema::ProductDiscountCandidateTarget;
use crate::schema::ProductDiscountCandidateValue;
use crate::schema::ProductDiscountSelectionStrategy;
use crate::schema::ProductDiscountsAddOperation;

use super::schema;
use shopify_function::prelude::*;
use shopify_function::Result;

#[shopify_function]
fn cart_lines_discounts_generate_run(
    input: schema::cart_lines_discounts_generate_run::Input,
) -> Result<CartLinesDiscountsGenerateRunResult> {
    let max_cart_line = input
        .cart()
        .lines()
        .iter()
        .max_by(|a, b| {
            a.cost()
                .subtotal_amount()
                .amount()
                .partial_cmp(b.cost().subtotal_amount().amount())
                .unwrap_or(std::cmp::Ordering::Equal)
        })
        .ok_or("No cart lines found")?;

    let has_order_discount_class = input
        .discount()
        .discount_classes()
        .contains(&DiscountClass::Order);
    let has_product_discount_class = input
        .discount()
        .discount_classes()
        .contains(&DiscountClass::Product);

    if !has_order_discount_class && !has_product_discount_class {
        return Ok(CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }

    let mut operations = vec![];

    // Check if the discount has the ORDER class
    if has_order_discount_class {
        operations.push(CartOperation::OrderDiscountsAdd(
            OrderDiscountsAddOperation {
                selection_strategy: OrderDiscountSelectionStrategy::First,
                candidates: vec![OrderDiscountCandidate {
                    targets: vec![OrderDiscountCandidateTarget::OrderSubtotal(
                        OrderSubtotalTarget {
                            excluded_cart_line_ids: vec![],
                        },
                    )],
                    message: Some("10% OFF ORDER".to_string()),
                    value: OrderDiscountCandidateValue::Percentage(Percentage {
                        value: Decimal(10.0),
                    }),
                    conditions: None,
                    associated_discount_code: None,
                }],
            },
        ));
    }

    // Check if the discount has the PRODUCT class
    if has_product_discount_class {
        operations.push(CartOperation::ProductDiscountsAdd(
            ProductDiscountsAddOperation {
                selection_strategy: ProductDiscountSelectionStrategy::First,
                candidates: vec![ProductDiscountCandidate {
                    targets: vec![ProductDiscountCandidateTarget::CartLine(CartLineTarget {
                        id: max_cart_line.id().clone(),
                        quantity: None,
                    })],
                    message: Some("20% OFF PRODUCT".to_string()),
                    value: ProductDiscountCandidateValue::Percentage(Percentage {
                        value: Decimal(20.0),
                    }),
                    associated_discount_code: None,
                }],
            },
        ));
    }

    Ok(CartLinesDiscountsGenerateRunResult { operations })
}