Personalized checkout and custom promotions with Shopify Scripts
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
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!
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.
By investing 30 minutes of your time, you can unlock the potential for increased sales,...
By Jacqui Sep 11, 2024We appreciate the diverse ways you participate in and engage with the Shopify Communi...
By JasonH Sep 9, 2024Thanks to everyone who participated in our AMA with 2H Media: Marketing Your Shopify St...
By Jacqui Sep 6, 2024