How To Create a 2 for 25 discount

I have successfully created a 2 for $25 discount, to mirror the sort of discounts available in the soon to be going away Shopify Functions.

The Gist can be found here:

This allows a discount for products with the tags “2FOR25” that have different prices, all discounted to 12.25 each in multiples of two.

Features: removes a discount from the highest price when the total quantity of eligble products in cart is odd. Allows different products to be purchased with full discount as long as total is even. Call to action message for customer on line item to add one to get full discount. Unique discount per cartline per product.

Here is the Run.graphql:

query RunInput {
  cart {
    lines {
      id
      quantity
      cost {
        amountPerQuantity {
          amount
          currencyCode
        }
        subtotalAmount {
          amount
          currencyCode
        }
      }
      
      merchandise {
        __typename
        ...on ProductVariant {
            id
            product {
              two_for_tag: hasAnyTag(tags: ["2FOR25"])
            }
        }
      }
    }
  }
}

Here is the run.js:

// @ts-check
import { DiscountApplicationStrategy } from "../generated/api";

// Use JSDoc annotations for type safety
/**
 * @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.All,
  discounts: [],
};

const DISCOUNT_MESSAGE = "2 for $25 Discount limited time only!";

var customDiscount = {
  discountApplicationStrategy: DiscountApplicationStrategy.All,
  discounts: [],
  //message: DISCOUNT_MESSAGE,

};

var bigArray = [];

/**
 * @param {RunInput} input
 * @returns {FunctionRunResult}
 */
export function run(input) {
  //console.error("function running");

  const targets = input.cart.lines
    // Only include cart lines with a quantity of two or more
    .filter((line) =>  line.merchandise.__typename == "ProductVariant" && line.merchandise.product.two_for_tag == true)
    .map((line) => {
      return /** @type {Target} */ ({
        // Use the cart line ID to create a discount target
        cartLine: {
          id: line.id,
        },
        lineitemPrice: line.cost.amountPerQuantity.amount,
        lineitemQty: line.quantity,
        // value: {
        //   fixedAmount: {
        //     amount: "10.0",
        //   },
        // },
      });
    });
  if (!targets.length) {
    // You can use STDERR for debug logs in your function
    //console.error("No cart lines qualify for volume discount.");
    return EMPTY_DISCOUNT;
  }

  // is it even

  var totalIsEven = false;

  var myTotalEligible = targets.reduce((sum, cartli) => sum + cartli.lineitemQty, 0);

  totalIsEven = isEven(myTotalEligible);

 
  function isEven(n) {
    return n % 2 == 0;
  }

  //console.error("total is " + myTotalEligible);
  //console.error("totalIsEven = " + totalIsEven);

  // Sort targets here.
  // only if totalIsEven == false

  if ( totalIsEven == false ) {
    // Sort array

    function compare( a, b ) {
      if ( parseFloat(a.lineitemPrice).toFixed(2) > parseFloat(b.lineitemPrice) ){
        return -1;
      }
      if ( parseFloat(a.lineitemPrice).toFixed(2) < parseFloat(b.lineitemPrice).toFixed(2) ){
        return 1;
      }
      return 0;
    }

    targets.sort(compare);

    // targets.sort((a, b) => {

    //   console.error("a is " + JSON.stringify(a));
    //   console.error("b is " + JSON.stringify(b));
    //   const titleA = parseFloat(a.lineitemPrice).toFixed(2); 
    //   const titleB = parseFloat(b.lineitemPrice).toFixed(2); 
    //   if (titleA > titleB) {
    //     return -1;
    //   }
    //   if (titleA < titleB) {
    //     return 1;
    //   }
    
    //   // must be equal
    //   return 0;
    // });


  }


  targets.forEach(tempFunc);
  
  function tempFunc(value, index) {

    //console.error("value is " + JSON.stringify(value));
    //console.error("index is " + index);

    

    if (totalIsEven == true ) {

    var obj = new Object();
    //console.error("starting even branch ...");

    obj.targets = [{ cartLine: value.cartLine}] ;
    //parseFloat(yourString).toFixed(2)
    var temp_discount = ( (parseFloat(value.lineitemPrice).toFixed(2) - 12.50) * value.lineitemQty ).toFixed(2);
    //num.toLocaleString('en-US', {minimumFractionDigits: 8, useGrouping: false})
    var temp_discount_str = temp_discount.toLocaleString('en-US', {minimumFractionDigits: 2, useGrouping: false});
    obj.message = DISCOUNT_MESSAGE;
    obj.value = { "fixedAmount": {amount: temp_discount_str}};

    

    //obj.targets[value]
    // var temp_json = JSON.stringify(obj);
    // console.error("temp_json = " + JSON.parse(temp_json));
    customDiscount.discounts.push(obj);
    } else {

      //console.error("not even branch ...");

      
      
      if (index == 0 && value.lineitemQty > 1 ){

        //console.error("first index branch ... ");

        var obj = new Object();
        obj.targets = [{ cartLine: value.cartLine}] ;

        var temp_discount = ( (parseFloat(value.lineitemPrice).toFixed(2) - 12.50) * ( value.lineitemQty - 1) ).toFixed(2);
        //num.toLocaleString('en-US', {minimumFractionDigits: 8, useGrouping: false})
        var temp_discount_str = temp_discount.toLocaleString('en-US', {minimumFractionDigits: 2, useGrouping: false});
        obj.message = "Add one more to get full discount!";
        obj.value = { "fixedAmount": {amount: temp_discount_str}};
        
        customDiscount.discounts.push(obj);

        


      } else if (index == 0 && value.lineitemQty == 1 ) {
        // Do nothing, do not push to the array.
        //console.error("the quantity for this is one, quantity = " + value.lineitemQty);

        var obj = new Object();
        obj.targets = [{ cartLine: value.cartLine}] ;

        // var temp_discount = ( (parseFloat(value.lineitemPrice).toFixed(2) - 12.50) * (value.lineitemQty - 1 ) ).toFixed(2);
        //num.toLocaleString('en-US', {minimumFractionDigits: 8, useGrouping: false})
        // var temp_discount_str = temp_discount.toLocaleString('en-US', {minimumFractionDigits: 2, useGrouping: false});
        // Note we are not needing to calculate the discount here as it will always be zero and we are just calling to action
        // customer to andd one more to get full discount

        obj.message = "Add one more to get full discount!"
        obj.value = { "fixedAmount": {amount: "0.00"}};
        
        customDiscount.discounts.push(obj);
        

      } else {

        //console.error("not first index branch ... ");

        var obj = new Object();
        obj.targets = [{ cartLine: value.cartLine}] ;

        var temp_discount = ( (parseFloat(value.lineitemPrice).toFixed(2) - 12.50) * value.lineitemQty ).toFixed(2);
        //num.toLocaleString('en-US', {minimumFractionDigits: 8, useGrouping: false})
        var temp_discount_str = temp_discount.toLocaleString('en-US', {minimumFractionDigits: 2, useGrouping: false});
        
        obj.message = DISCOUNT_MESSAGE;
        obj.value = { "fixedAmount": {amount: temp_discount_str}};
        customDiscount.discounts.push(obj);


      }


    }

  };

  


  return {
    //customDiscount,

    discounts: customDiscount.discounts,

    // discounts: [
    //   {
    //     // Apply the discount to the collected targets
        
        
    //      targets,
    //     // // Define a percentage-based discount
        
        
    //     // value: {
    //     //   fixedAmount: {
    //     //     amount: "10.0",
    //     //   },
    //     // },
        
        
        
    //   },
    // ],
   // message: customDiscount.message,
    discountApplicationStrategy: DiscountApplicationStrategy.All,
  };
}```
3 Likes

Thanks for sharing @Floyd_Wallace!

Hey @Floyd_Wallace

This is awesome! Thanks for sharing this technique with the developer community here!!

Thanks for sharing this example! One small note: I think you meant to say “soon to be going away Shopify Scripts” - Shopify Functions isn’t going away! :slight_smile:

(Marking as solved)

1 Like

Hi Yes, was a typo!

Jonathon – can you tell me if the startsAt and endsAt datetime object in a Graphql mutation to install the discount is UTC or my shop’s local time zone (for me, Pacific).

Urgent need this for pre-black friday stuff.

Thanks.

Sorry for not seeing this earlier, hopefully it’s still useful: The API docs for the DateTime type indicate how to format the value unambiguously for UTC.