Personalized checkout and custom promotions with Shopify Scripts
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.
Solved! Go to the solution
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:
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
*Tiered Discount not tired. Although I am tired..... 😉
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:
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
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.
Dropshipping, a high-growth, $226 billion-dollar industry, remains a highly dynamic bus...
By JasonH Nov 27, 2024Hey Community! It’s time to share some appreciation and celebrate what we have accomplis...
By JasonH Nov 14, 2024In today’s interview, we sat down with @BSS-Commerce to discuss practical strategies f...
By JasonH Nov 13, 2024