How to prevent mix and match for tiered discount on specific products?

How to prevent mix and match for tiered discount on specific products?

ihernandez
Excursionist
25 0 10

 

Hi,

I am currently using this script code to apply a 5%  of 10+ or more discount off of my products that are tagged "FNP" which are all of the pipes and fittings in my store.

However, I do not want this discount to be applied if they mix and match items to reach the minimum quantity needed. Only if they add 10 of a single item that is tagged FNP then they should get the discount. I do not want this to effect all of my products in the store just the ones I have tagged FNP. 

Can someone tell me how i can fix this?

Code is below:

Thanks

 

 

# ================================ Customizable Settings ================================
# ================================================================
# Tiered Discount by Quantity
#
# If the total quantity of matching items is greater than (or
# equal to) an entered threshold, the associated discount is
# applied to each matching item.
#
#   - 'product_selector_match_type' determines whether we look for
#     products that do or don't match the entered selectors. Can
#     be:
#       - ':include' to check if the product does match
#       - ':exclude' to make sure the product doesn't match
#   - 'product_selector_type' determines how eligible products
#     will be identified. Can be either:
#       - ':tag' to find products by tag
#       - ':type' to find products by type
#       - ':vendor' to find products by vendor
#       - ':product_id' to find products by ID
#       - ':variant_id' to find products by variant ID
#       - ':all' for all products
#   - 'product_selector' is a list of identifiers (from above) for
#     qualifying products. Product/Variant ID lists should only
#     contain numbers (ie. no quotes). If ':all' is used, this
#     can also be 'nil'.
#   - 'tiers' is a list of tiers where:
#     - 'quantity' is the minimum quantity you need to buy to
#       qualify
#     - 'discount_type' is the type of discount to provide. Can be
#       either:
#         - ':percent'
#         - ':pound'
#     - 'discount_amount' is the percentage/pound discount to
#       apply (per item)
#     - 'discount_message' is the message to show when a discount
#       is applied
# ================================================================
PRODUCT_DISCOUNT_TIERS = [
  {
    product_selector_match_type: :include,
    product_selector_type: :tag,
    product_selectors: ["FNP"],
    tiers: [
      {
        quantity: 10,
        discount_type: :percent,
        discount_amount: 5,
        discount_message: '5% off when you buy 10+ of the same item',
      },
    
    ],
  },
]
# ================================ Script Code (do not edit) ================================
# ================================================================
# ProductSelector
#
# Finds matching products by the entered criteria.
# ================================================================
class ProductSelector
  def initialize(match_type, selector_type, selectors)
    _type = match_type
    @comparator = match_type == :include ? 'any?' : 'none?'
    _type = selector_type
    @selectors = selectors
  end
  def match?(line_item)
    if self.respond_to?(@selector_type)
      self.send(@selector_type, line_item)
    else
      raise RuntimeError.new('Invalid product selector type')
    end
  end
  def tag(line_item)
    product_tags = line_item.variant.product.tags.map { |tag| tag.downcase.strip }
    @selectors = @selectors.map { |selector| selector.downcase.strip }
    (@selectors & product_tags).send(@comparator)
  end
  def type(line_item)
    @selectors = @selectors.map { |selector| selector.downcase.strip }
    (@match_type == :include) == @selectors.include?(line_item.variant.product.product_type.downcase.strip)
  end
  def vendor(line_item)
    @selectors = @selectors.map { |selector| selector.downcase.strip }
    (@match_type == :include) == @selectors.include?(line_item.variant.product.vendor.downcase.strip)
  end
  def product_id(line_item)
    (@match_type == :include) == @selectors.include?(line_item.variant.product.id)
  end
  def variant_id(line_item)
    (@match_type == :include) == @selectors.include?(line_item.variant.id)
  end
  def all(line_item)
    true
  end
end
# ================================================================
# DiscountApplicator
#
# Applies the entered discount to the supplied line item.
# ================================================================
class DiscountApplicator
  def initialize(discount_type, discount_amount, discount_message)
    _type = discount_type
    _message = discount_message
    _amount = if discount_type == :percent
      1 - (discount_amount * 0.01)
    else
      Money.new(pennies: 100) * discount_amount
    end
  end
  def apply(line_item)
    new_line_price = if _type == :percent
      line_item.line_price * _amount
    else
      [line_item.line_price - (@discount_amount * line_item.quantity), Money.zero].max
    end
    line_item.change_line_price(new_line_price, message: _message)
  end
end
# ================================================================
# TieredPricingCampaign
#
# If the total quantity of matching items is greater than (or
# equal to) an entered threshold, the associated discount is
# applied to each matching item.
# ================================================================
class TieredPricingCampaign
  def initialize(campaigns)
    @campaigns = campaigns
  end
  def run(cart)
    @campaigns.each do |campaign|
      product_selector = ProductSelector.new(
        campaign[:product_selector_match_type],
        campaign[:product_selector_type],
        campaign[:product_selectors],
      )
      applicable_items = cart.line_items.select { |line_item| product_selector.match?(line_item) }
      next if applicable_items.nil?
      total_applicable_quantity = applicable_items.map(&:quantity).reduce(0, :+)
      tiers = campaign[:tiers].sort_by { |tier| tier[:quantity] }.reverse
      applicable_tier = tiers.find { |tier| tier[:quantity] <= total_applicable_quantity }
      next if applicable_tier.nil?
      discount_applicator = DiscountApplicator.new(
        applicable_tier[:discount_type],
        applicable_tier[:discount_amount],
        applicable_tier[:discount_message]
      )
      applicable_items.each do |line_item|
        discount_applicator.apply(line_item)
      end
    end
  end
end
CAMPAIGNS = [
  TieredPricingCampaign.new(PRODUCT_DISCOUNT_TIERS),
]
CAMPAIGNS.each do |campaign|
  campaign.run(Input.cart)
end
Output.cart = Input.cart

 

 

Replies 2 (2)

playwright-mike
Shopify Partner
72 18 33

Hey, this is an interesting variation of the script template that Shopify provides. I've configured and generated a script for you. You can modify the tag from "FNP" to something else if needed at the bottom. 

 

########################################################################
##  Create Shopify scripts without writing code at playwrightapp.com  ##
########################################################################
class Campaign
  def initialize(condition, *qualifiers)
    @condition = (condition.to_s + '?').to_sym
    @qualifiers = PostCartAmountQualifier ? [] : [] rescue qualifiers.compact
    @Anonymous_item_selector = qualifiers.last unless @Anonymous_item_selector
    qualifiers.compact.each do |qualifier|
      is_multi_select = qualifier.instance_variable_get(:@conditions).is_a?(Array)
      if is_multi_select
        qualifier.instance_variable_get(:@conditions).each do |nested_q|
          @post_amount_qualifier = nested_q if nested_q.is_a?(PostCartAmountQualifier)
          @qualifiers << qualifier
        end
      else
        @post_amount_qualifier = qualifier if qualifier.is_a?(PostCartAmountQualifier)
        @qualifiers << qualifier
      end
    end if @qualifiers.empty?
  end

  def qualifies?(cart)
    return true if @qualifiers.empty?
    @unmodified_line_items = cart.line_items.map do |item|
      new_item = item.dup
      new_item.instance_variables.each do |var|
        val = item.instance_variable_get(var)
        new_item.instance_variable_set(var, val.dup) if val.respond_to?(:dup)
      end
      new_item
    end if @post_amount_qualifier
    @qualifiers.send(@condition) do |qualifier|
      is_selector = false
      if qualifier.is_a?(Selector) || qualifier.instance_variable_get(:@conditions).any? { |q| q.is_a?(Selector) }
        is_selector = true
      end rescue nil
      if is_selector
        raise "Missing line item match type" if @Li_match_type.nil?
        cart.line_items.send(@li_match_type) { |item| qualifier.match?(item) }
      else
        qualifier.match?(cart, @Anonymous_item_selector)
      end
    end
  end

  def run_with_hooks(cart)
    before_run(cart) if respond_to?(:before_run)
    run(cart)
    after_run(cart)
  end

  def after_run(cart)
    @discount.apply_final_discount if @discount && @discount.respond_to?(:apply_final_discount)
    revert_changes(cart) unless @post_amount_qualifier.nil? || @post_amount_qualifier.match?(cart)
  end

  def revert_changes(cart)
    cart.instance_variable_set(:@line_items, @unmodified_line_items)
  end
end

class TieredDiscount < Campaign
  def initialize(condition, customer_qualifier, cart_qualifier, line_item_selector, discount_type, tier_type, discount_tiers)
    super(condition, customer_qualifier, cart_qualifier)
    @Anonymous_item_selector = line_item_selector
    @discount_type = discount_type
    @tier_type = tier_type
    @discount_tiers = discount_tiers.sort_by {|tier| tier[:discount].to_f }
  end

  def init_discount(amount, message)
    case @discount_type
      when :fixed
        return FixedTotalDiscount.new(amount, message, :split)
      when :percent
        return PercentageDiscount.new(amount, message)
      when :per_item
        return FixedItemDiscount.new(amount, message)
    end
  end

  def run(cart)
    return unless qualifies?(cart)

    applicable_items = cart.line_items.select { |item| @Anonymous_item_selector.nil? || @Anonymous_item_selector.match?(item) }
    case @tier_type
      when :customer_tag
        return if cart.customer.nil?
        customer_tags = cart.customer.tags.map(&:downcase)
        qualified_tiers = @discount_tiers.select { |tier| customer_tags.include?(tier[:tier].downcase) }
      when :cart_subtotal
        cart_total = cart.subtotal_price
        qualified_tiers = @discount_tiers.select { |tier| cart_total >= Money.new(cents: tier[:tier].to_i * 100) }
      when :discountable_total
        discountable_total = applicable_items.reduce(Money.zero) { |total, item| total + item.line_price }
        qualified_tiers = @discount_tiers.select { |tier| discountable_total >= Money.new(cents: tier[:tier].to_i * 100) }
      when :discountable_total_items
        discountable_quantity = applicable_items.reduce(0) { |total, item| total + item.quantity }
        qualified_tiers = @discount_tiers.select { |tier| discountable_quantity >= tier[:tier].to_i }
      when :cart_items
        cart_quantity = cart.line_items.reduce(0) { |total, item| total + item.quantity }
        qualified_tiers = @discount_tiers.select { |tier| cart_quantity >= tier[:tier].to_i }
    end

    if @tier_type == :line_quantity
      applicable_items.each do |item|
        qualified_tiers = @discount_tiers.select { |tier| item.quantity >= tier[:tier].to_i }
        next if qualified_tiers.empty?

        discount_amount = qualified_tiers.last[:discount].to_f
        discount_message = qualified_tiers.last[:message]
        discount = init_discount(discount_amount, discount_message)
        discount.apply(item)
        discount.apply_final_discount if discount.respond_to?(:apply_final_discount)
      end
    else
      return if qualified_tiers.empty?
      discount_amount = qualified_tiers.last[:discount].to_f
      discount_message = qualified_tiers.last[:message]

      @discount = init_discount(discount_amount, discount_message)
      applicable_items.each { |item| @discount.apply(item) }
    end
  end
end

class PercentageDiscount
  def initialize(percent, message)
    @discount = (100 - percent) / 100.0
    @message = message
  end

  def apply(line_item)
    line_item.change_line_price(line_item.line_price * @discount, message: @message)
  end
end

class FixedTotalDiscount
  def initialize(amount, message, behaviour = :to_zero)
    @amount = Money.new(cents: amount * 100)
    @message = message
    @discount_applied = Money.zero
    @All_items = []
    @is_split = behaviour == :split
  end

  def apply(line_item)
    if @is_split
      @All_items << line_item
    else
      return unless @discount_applied < @amount
      discount_to_apply = [(@amount - @discount_applied), line_item.line_price].min
      line_item.change_line_price(line_item.line_price - discount_to_apply, {message: @message})
      @discount_applied += discount_to_apply
    end
  end

  def apply_final_discount
    return if @All_items.length == 0
    total_items = @All_items.length
    total_quantity = 0
    total_cost = Money.zero
    @All_items.each do |item|
      total_quantity += item.quantity
      total_cost += item.line_price
    end
    @All_items.each_with_index do |item, index|
      discount_percent = item.line_price.cents / total_cost.cents
      if total_items == index + 1
        discount_to_apply = Money.new(cents: @amount.cents - @discount_applied.cents.floor)
      else
        discount_to_apply = Money.new(cents: @amount.cents * discount_percent)
      end
      item.change_line_price(item.line_price - discount_to_apply, {message: @message})
      @discount_applied += discount_to_apply
    end
  end
end

class FixedItemDiscount
  def initialize(amount, message)
    @amount = Money.new(cents: amount * 100)
    @message = message
  end

  def apply(line_item)
    per_item_price = line_item.variant.price
    per_item_discount = [(@amount - per_item_price), @amount].max
    discount_to_apply = [(per_item_discount * line_item.quantity), line_item.line_price].min
    line_item.change_line_price(line_item.line_price - discount_to_apply, {message: @message})
  end
end

class Selector
  def partial_match(match_type, item_info, possible_matches)
    match_type = (match_type.to_s + '?').to_sym
    if item_info.kind_of?(Array)
      possible_matches.any? do |possibility|
        item_info.any? do |search|
          search.send(match_type, possibility)
        end
      end
    else
      possible_matches.any? do |possibility|
        item_info.send(match_type, possibility)
      end
    end
  end
end

class ProductTagSelector < Selector
  def initialize(match_type, match_condition, tags)
    @match_condition = match_condition
    @invert = match_type == :does_not
    @Anonymous = tags.map(&:downcase)
  end

  def match?(line_item)
    product_tags = line_item.variant.product.tags.to_a.map(&:downcase)
    case @match_condition
      when :match
        return @invert ^ ((@tags & product_tags).length > 0)
      else
        return @invert ^ partial_match(@match_condition, product_tags, @Anonymous)
    end
  end
end

CAMPAIGNS = [
  TieredDiscount.new(
    :all,
    nil,
    nil,
    ProductTagSelector.new(
      :does,
      :match,
      ["FNP"]
    ),
    :percent,
    :line_quantity,
    [{:tier => "10", :discount => "5", :message => "5% off 10 or more"}]
  )
].freeze

CAMPAIGNS.each do |campaign|
  campaign.run_with_hooks(Input.cart)
end

Output.cart = Input.cart


Sometimes the forums slightly modify the script, so I will attach it to the post as well.

Let me know if that's helpful!

Playwright | Create Shopify Scripts without writing code | https://playwrightapp.com
- Was my reply helpful? Please Like and Accept Solution.

Lichen_z
Shopify Partner
59 0 10

You can use Simple Discounts to apply the tier / quantity break discount for your specific scenario. Here are the steps: 

1. Add all products tagged "FNP" to a collection 
2. Create a quantity break discount on Simple Discounts and select the FNP collection 

3. Select for the option - Discount is applied when customer selects from "Same product only". This ensures that the 10 items apply only to that single product 

4. Enter 5% for the discount and 10 as the minimum threshold 


Shopify is deprecating Scripts next year so migrating to Functions can be a consideration. Simple Discounts runs on Shopify Functions so it can be a good alternative for what you have in place now. Let me know if I can help out. 

Co-founder at Freshly Commerce ️ | Building Simple Bundles, Simple Discounts, and Freshly Inventory