Skip to content

Instantly share code, notes, and snippets.

@jimmijazz
Last active January 21, 2024 16:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jimmijazz/516d127f23bfbb9e136bbfbae9294a7b to your computer and use it in GitHub Desktop.
Save jimmijazz/516d127f23bfbb9e136bbfbae9294a7b to your computer and use it in GitHub Desktop.
Shopify Script Discounting example
# ================================ Customizable Settings ================================
# ================================================================
# Tiered Discounts by Quantity
#
# A list of discount tier offers where:
# - '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
# - ':id' to find products by ID
# - ':type' to find products by type
# - 'product_selector' is a list of tags or IDs to identify
# qualifying products
# - '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'
# - ':dollar'
# - 'discount_amount' is the percentage/dollar 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: :type,
product_selectors: ["Socks"],
tiers: [
{
quantity: 3,
discount_type: :dollar,
discount_amount: 8.333,
discount_message: '3 pairs of Socks for $59!',
},
],
},
]
# ================================================================
# Bundle Cart Discounts (ie. 'Buy a bib and a jersey, and get 15%
# off all items)
#
# A list of bundle discounts where:
# - 'bundle_items' is a list of the items that comprise the
# bundle, where:
# - 'product_id' is the ID of the product
# - 'quantity_needed' is the quantity necessary to complete
# the bundle
# - '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
# ================================================================
BUNDLE_DISCOUNTS = [
{
bundle_items: [
{
product_selector_match_type: :include,
product_selector_type: :type,
product_selectors: ["Men's Bib Shorts", "Women's Bib Shorts"],
quantity_needed: 1
},
{
product_selector_match_type: :include,
product_selector_type: :type,
product_selectors: ["Jersey", "Men's Jersey", "Women's Jersey"],
quantity_needed: 1
},
],
discount_type: :percent,
discount_amount: 15,
discount_message: "15% off when buying a bib and a jersey!",
},
]
# ================================ Script Code (do not edit) ================================
# ================================================================
# ProductSelector
#
# Finds matching products - either by product ID or tag
# ================================================================
class ProductSelector
def initialize(match_type, selector_type, selectors)
@match_type = match_type
@comparator = match_type == :include ? 'any?' : 'none?'
@selector_type = selector_type
@selectors = selectors
end
def match?(line_item)
if @selector_type == :tag
product_tags = line_item.variant.product.tags.map { |tag| tag.downcase.strip }
@selectors = @selectors.map { |selector| selector.downcase.strip }
(@selectors & product_tags).send(@comparator)
elsif @selector_type == :id
if @match_type == :include
@selectors.include?(line_item.variant.product.id)
else
!@selectors.include?(line_item.variant.product.id)
end
else
@selectors = @selectors.map { |selector| selector.downcase.strip }
if @match_type == :include
@selectors.include?(line_item.variant.product.product_type.downcase.strip)
else
!@selectors.include?(line_item.variant.product.product_type.downcase.strip)
end
end
end
end
# ================================================================
# BundleSelector
#
# Finds any items that are part of the entered bundle and saves
# them
# ================================================================
class BundleSelector
def initialize(bundle_items)
@bundle_items = bundle_items.reduce([]) do |acc, bundle_item|
product_selector = ProductSelector.new(
bundle_item[:product_selector_match_type],
bundle_item[:product_selector_type],
bundle_item[:product_selectors],
)
bundle_item_info = {
product_selector: product_selector,
quantity_needed: bundle_item[:quantity_needed],
total_quantity: 0,
}
acc.push(bundle_item_info)
acc
end
end
def build(cart)
cart.line_items.each do |line_item|
@bundle_items.each do |bundle_item|
next unless bundle_item[:product_selector].match?(line_item)
bundle_item[:total_quantity] += line_item.quantity
end
end
@bundle_items
end
end
# ================================================================
# 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
# ================================================================
# TieredPricingCampaign
#
# Finds any products of the entered type and applies the
# associated tier discount
# ================================================================
class TieredPricingCampaign
def initialize(discounts)
@discounts = discounts
end
def run(cart)
@discounts.each do |discount|
product_selector = ProductSelector.new(
discount[:product_selector_match_type],
discount[:product_selector_type],
discount[:product_selectors],
)
tiers = discount[:tiers].sort_by { |tier| tier[:quantity] }.reverse
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, :+)
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|
next if line_item.discounted?
discount_applicator.apply(line_item)
end
end
end
end
# ================================================================
# BundleCartDiscountCampaign
#
# If the entered bundle is present, the entered discount is
# applied to each item in the cart
# ================================================================
class BundleCartDiscountCampaign
def initialize(discounts)
@discounts = discounts
end
def run(cart)
@discounts.each do |discount|
bundle_selector = BundleSelector.new(discount[:bundle_items])
bundle_items = bundle_selector.build(cart)
next if bundle_items.any? do |product_info|
product_info[:total_quantity] < product_info[:quantity_needed]
end
discount_applicator = DiscountApplicator.new(
discount[:discount_type],
discount[:discount_amount],
discount[:discount_message]
)
cart.line_items.each do |line_item|
next if line_item.discounted?
discount_applicator.apply(line_item)
end
end
end
end
CAMPAIGNS = [
TieredPricingCampaign.new(PRODUCT_DISCOUNT_TIERS),
BundleCartDiscountCampaign.new(BUNDLE_DISCOUNTS),
]
CAMPAIGNS.each do |campaign|
campaign.run(Input.cart)
end
Output.cart = Input.cart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment