Have your say in Community Polls: What was/is your greatest motivation to start your own business?

Exclude Route Insurance From Scripts Tired Discount

Solved

Exclude Route Insurance From Scripts Tired Discount

witmade
Shopify Partner
17 1 5

How do I exclude a product (Route Shipping Insurance) from being included in a "tired discount by spend" total?
Currently, at checkout the script applies a discount to the route charge, but I need the script to exclude it and not discount it. 

 

Current Script:

"# ================================ Customizable Settings ================================
# ================================================================
# Tiered Discounts by Spend Threshold
#
# If the cart total is greater than (or equal to) an entered
# threshold, the associated discount is applied to each item.
#
# - 'threshold' is the spend amount needed to qualify
# - 'discount_type' is the type of discount to provide. Can be
# either:
# - ':percent'
# - ':dollar'
# - 'discount_amount' is the percentage/dollar discount to
# apply (per item)
# - 'discount_message' is the message to show when a discount
# is applied
# ================================================================
SPENDING_THRESHOLDS = [
{
threshold: 75,
discount_type: :percent,
discount_amount: 20,
discount_message: '20% off $75',
},
{
threshold: 100,
discount_type: :percent,
discount_amount: 25,
discount_message: '25% off $100',
},
{
threshold: 125,
discount_type: :percent,
discount_amount: 30,
discount_message: '30% off $125',
},
]

# ================================ Script Code (do not edit) ================================
# ================================================================
# DiscountApplicator
#
# Applies the entered discount to the supplied line item.
# ================================================================
class DiscountApplicator
def initialize(discount_type, discount_amount, discount_message)
@discount_type = discount_type
@discount_message = discount_message

@discount_amount = if discount_type == :percent
1 - (discount_amount * 0.01)
else
Money.new(cents: 100) * discount_amount
end
end

def apply(line_item)
new_line_price = if @discount_type == :percent
line_item.line_price * @discount_amount
else
[line_item.line_price - (@discount_amount * line_item.quantity), Money.zero].max
end

line_item.change_line_price(new_line_price, message: @discount_message)
end
end

# ================================================================
# TieredDiscountBySpendCampaign
#
# If the cart total is greater than (or equal to) an entered
# threshold, the associated discount is applied to each item.
# ================================================================
class TieredDiscountBySpendCampaign
def initialize(tiers)
@tiers = tiers.sort_by { |tier| tier[:threshold] }.reverse
end

def run(cart)
applicable_tier = @tiers.find { |tier| cart.subtotal_price >= (Money.new(cents: 100) * tier[:threshold]) }
return if applicable_tier.nil?

discount_applicator = DiscountApplicator.new(
applicable_tier[:discount_type],
applicable_tier[:discount_amount],
applicable_tier[:discount_message]
)

cart.line_items.each do |line_item|
next if line_item.variant.product.gift_card?
discount_applicator.apply(line_item)
end

if Input.cart.discount_code
Input.cart.discount_code.reject(
message: "Discount codes cannot be combined with promotional discounts"
)
end
end
end

CAMPAIGNS = [
TieredDiscountBySpendCampaign.new(SPENDING_THRESHOLDS),
]

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

Output.cart = Input.cart  "

 

Thank you.

Accepted Solution (1)
playwright-mike
Shopify Partner
72 18 33

This is an accepted solution.

Hi Witmade, 

 

I generated a script for you based on your request. It applies the tiered discount like normal, but it excludes the Route Insurance product from the discount (although it does still count toward total spend). However, that means that you will need to verify one thing first:

  • Go to Shopify Admin > Product and find the Route Insurance product
  • Check that the Product Vendor is "Route" (if it's something else just modify the end of the supplied script)
    Screen Shot 2021-11-24 at 1.15.12 AM.png

After you've verified that you can use/modify this script below. I'll copy and paste the script below as well as attach it as a file:

 

########################################################################
##  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
    _item_selector = qualifiers.last unless _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 _match_type.nil?
        cart.line_items.send(@li_match_type) { |item| qualifier.match?(item) }
      else
        qualifier.match?(cart, _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)
    _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| _item_selector.nil? || _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
    _items = []
    _split = behaviour == :split
  end

  def apply(line_item)
    if _split
      _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 _items.length == 0
    total_items = _items.length
    total_quantity = 0
    total_cost = Money.zero
    _items.each do |item|
      total_quantity += item.quantity
      total_cost += item.line_price
    end
    _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 ProductVendorSelector < Selector
  def initialize(match_type, vendors)
    @invert = match_type != :is_one
     = vendors.map(&:downcase)
  end

  def match?(line_item)
    @invert ^ .include?(line_item.variant.product.vendor.downcase)
  end
end

CAMPAIGNS = [
  TieredDiscount.new(
    :all,
    nil,
    nil,
    ProductVendorSelector.new(
      :not_one,
      ["Route"]
    ),
    :percent,
    :cart_subtotal,
    [
      {:tier => "75", :discount => "20", :message => "20% off $75"},
      {:tier => "100", :discount => "25", :message => "25% off $100"},
      {:tier => "125", :discount => "30", :message => "30% off $125"}
    ]
  )
].freeze

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

Output.cart = Input.cart

 

 

Let me know if that is helpful!

 

Best,
Matthew

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

View solution in original post

Replies 3 (3)

witmade
Shopify Partner
17 1 5

*Tiered Discount not tired. Although I am tired..... 😉

playwright-mike
Shopify Partner
72 18 33

This is an accepted solution.

Hi Witmade, 

 

I generated a script for you based on your request. It applies the tiered discount like normal, but it excludes the Route Insurance product from the discount (although it does still count toward total spend). However, that means that you will need to verify one thing first:

  • Go to Shopify Admin > Product and find the Route Insurance product
  • Check that the Product Vendor is "Route" (if it's something else just modify the end of the supplied script)
    Screen Shot 2021-11-24 at 1.15.12 AM.png

After you've verified that you can use/modify this script below. I'll copy and paste the script below as well as attach it as a file:

 

########################################################################
##  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
    _item_selector = qualifiers.last unless _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 _match_type.nil?
        cart.line_items.send(@li_match_type) { |item| qualifier.match?(item) }
      else
        qualifier.match?(cart, _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)
    _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| _item_selector.nil? || _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
    _items = []
    _split = behaviour == :split
  end

  def apply(line_item)
    if _split
      _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 _items.length == 0
    total_items = _items.length
    total_quantity = 0
    total_cost = Money.zero
    _items.each do |item|
      total_quantity += item.quantity
      total_cost += item.line_price
    end
    _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 ProductVendorSelector < Selector
  def initialize(match_type, vendors)
    @invert = match_type != :is_one
     = vendors.map(&:downcase)
  end

  def match?(line_item)
    @invert ^ .include?(line_item.variant.product.vendor.downcase)
  end
end

CAMPAIGNS = [
  TieredDiscount.new(
    :all,
    nil,
    nil,
    ProductVendorSelector.new(
      :not_one,
      ["Route"]
    ),
    :percent,
    :cart_subtotal,
    [
      {:tier => "75", :discount => "20", :message => "20% off $75"},
      {:tier => "100", :discount => "25", :message => "25% off $100"},
      {:tier => "125", :discount => "30", :message => "30% off $125"}
    ]
  )
].freeze

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

Output.cart = Input.cart

 

 

Let me know if that is helpful!

 

Best,
Matthew

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

witmade
Shopify Partner
17 1 5

Hi, Thank you for your help on this. I ended up excluding it based on tag, and just gave route insurance a tag that was then excluded, but your solution would be better as in the future I will need to exclude variants and not just whole products based on tags. Thank you for your help.