Shopify – Tiered Discount Script

Photo of author
Written By Ryan Rana

 

 

 

 

Shopify Plus users are able to install the internal Scripts App. It’s one of the benefits of upgrading.

You are going to have to learn a bit of Ruby if you want to get into scripts in depth, but the editor in the app gives you a split code/input development IDE to build yours.

There is also a github repo with prebuilt scripts here -> https://github.com/Shopify/shopify-scripts but be aware that these are not maintained and may not work perfectly.

The example below works, but will only work for a specific product. So if you add 1 item of one product and one item of another to your cart you won’t get 5% applied. You will only get 5% applied if you add two items of the same product and the discount will apply only to that product.

product_types doesn’t appear to have any effect on this script.

# Define a list of price tiers.
PRICE_TIERS = [
  # Pricing tiers for Shoes
  {
    product_types: [],
    group_by: :product, # :product or :variant
    tiers: [
      {
        quantity: 2,
        discount_percentage: 5,
        discount_message: '5% saving'
      },
      {
        quantity: 3,
        discount_percentage: 15,
        discount_message: '10% saving'
      }
    ]
  }
]

# You shouldn't need to edit anything below this line, unless you're a developer
# and know what you're doing :).

##
# Tiered pricing campaign.
class TieredPricingCampaign

  def initialize(partitioner, tiers)
    @partitioner = partitioner
    @tiers = tiers.sort_by { |tier| tier[:quantity] }
  end

  def run(cart)
    @partitioner.partition(cart).each do |k, items|
      total_quantity = items.map(&:quantity).reduce(0, :+)
      applicable_tier = find_tier_for_quantity(total_quantity)
      unless applicable_tier.nil?
        apply_tier_discount(items, applicable_tier)
      end
    end
  end

  private

    def find_tier_for_quantity(quantity)
      @tiers.select { |tier| tier[:quantity] <= quantity }.last
    end

    def apply_tier_discount(items, tier)
      discount = get_tier_discount(tier)
      items.each do |item|
        discount.apply(item)
      end
    end

    def get_tier_discount(tier)
      PercentageDiscount.new(tier[:discount_percentage], tier[:discount_message])
    end

end

##
# Select line items by product type.
class ProductTypeSelector

  def initialize(product_types)
    @product_types = Array(product_types).map(&:upcase)
  end

  def match?(line_item)
    @product_types.include?(line_item.variant.product.product_type.upcase)
  end

  def group_key
    @product_types.join(',')
  end

end

##
# Apply a percentage discount to a line item.
class PercentageDiscount

  def initialize(percent, message = '')
    @percent = Decimal.new(percent) / 100.0
    @message = message
  end

  def apply(item)
    line_discount = item.original_line_price * @percent
    new_line_price = item.original_line_price - line_discount
    if new_line_price < item.line_price
      item.change_line_price(new_line_price, message: @message)
    end
  end

end

##
# A pricing tier partition.
class TierPartitioner

  def initialize(selector, group_by)
    @selector = selector
    @group_by = group_by
  end

  def partition(cart)
    # Filter items
    items = cart.line_items.select { |item| @selector.match?(item) }

    # Group items using the appropriate key.
    cart.line_items.group_by { |item| group_key(item) }
  end

  private

    def group_key(line_item)
      case @group_by
        when :product
          line_item.variant.product.id
        when :variant
          line_item.variant.id
        else
          @selector.group_key
      end
    end

end

##
# Instantiate and run Price Tiers.
PRICE_TIERS.each do |pt|
  TieredPricingCampaign.new(
    TierPartitioner.new(
      ProductTypeSelector.new(pt[:product_types]),
      pt[:group_by]
    ),
    pt[:tiers]
  ).run(Input.cart)
end

##
# Export changes.
Output.cart = Input.cart

The script below works for all products. A very useful script if you want tier prices to apply across the board in your store;

# Define a list of price tiers.
PRICE_TIERS = [
  # Pricing tiers for Shoes
  {
    product_types: ['All'], # set to 'All' if you don't want to limit to type/product
    group_by: :none, # :product or :variant or :none(for 'All' products only)
    tiers: [
      {
        quantity: 2,
        discount_percentage: 5,
        discount_message: '5% saving'
      },
      {
        quantity: 3,
        discount_percentage: 15,
        discount_message: '10% saving'
      }
    ]
  }
]

# You shouldn't need to edit anything below this line, unless you're a developer
# and know what you're doing :).

##
# Tiered pricing campaign.
class TieredPricingCampaign

  def initialize(partitioner, tiers)
    @partitioner = partitioner
    @tiers = tiers.sort_by { |tier| tier[:quantity] }
  end

  def run(cart)
    @partitioner.partition(cart).each do |k, items|
      puts items
      total_quantity = items.map(&:quantity).reduce(0, :+)
      applicable_tier = find_tier_for_quantity(total_quantity)
      puts total_quantity
      unless applicable_tier.nil?
        apply_tier_discount(items, applicable_tier)
      end
    end
  end

  private

    def find_tier_for_quantity(quantity)
      @tiers.select { |tier| tier[:quantity] <= quantity }.last
    end

    def apply_tier_discount(items, tier)
      discount = get_tier_discount(tier)
      items.each do |item|
        puts item
        discount.apply(item)
      end
    end

    def get_tier_discount(tier)
      PercentageDiscount.new(tier[:discount_percentage], tier[:discount_message])
    end

end

##
# Select line items by product type.
class ProductTypeSelector

  def initialize(product_types)
    @product_types = Array(product_types).map(&:upcase)
  end

  def match?(line_item)
    if @product_types.length == 1 && @product_types[0] == 'ALL'
      line_item
    else
      @product_types.include?(line_item.variant.product.product_type.upcase)
    end
  end

  def group_key
    @product_types.join(',')
  end

end

##
# Apply a percentage discount to a line item.
class PercentageDiscount

  def initialize(percent, message = '')
    @percent = Decimal.new(percent) / 100.0
    @message = message
  end

  def apply(item)
    line_discount = item.original_line_price * @percent
    new_line_price = item.original_line_price - line_discount
    if new_line_price < item.line_price
      item.change_line_price(new_line_price, message: @message)
    end
  end

end

##
# A pricing tier partition.
class TierPartitioner

  def initialize(selector, group_by)
    @selector = selector
    @group_by = group_by
  end

  def partition(cart)
    # Filter items
    items = cart.line_items.select { |item| @selector.match?(item) }
    # Group items using the appropriate key.
    cart.line_items.group_by { |item| group_key(item) }
  end

  private

    def group_key(line_item)
      case @group_by
        when :product
          line_item.variant.product.id
        when :variant
          line_item.variant.id
        when :none
        else
          @selector.group_key
      end
    end

end

##
# Instantiate and run Price Tiers.
PRICE_TIERS.each do |pt|
  TieredPricingCampaign.new(
    TierPartitioner.new(
      ProductTypeSelector.new(pt[:product_types]),
      pt[:group_by]
    ),
    pt[:tiers]
  ).run(Input.cart)
end

##
# Export changes.
Output.cart = Input.cart

Leave a Comment