Skip to content

Instantly share code, notes, and snippets.

@KamGraphica
Last active October 15, 2021 20:05
Show Gist options
  • Save KamGraphica/cb7f2f58be9d3df502b19fa9e59fdddd to your computer and use it in GitHub Desktop.
Save KamGraphica/cb7f2f58be9d3df502b19fa9e59fdddd to your computer and use it in GitHub Desktop.
Shopify Plus - Shopify Script - Buy 4 get 1 Free - Products tagged "BOGO"
class TagSelector
def initialize(tag)
@tag = tag
end
# Returns whether a line item matches this selector or not.
#
# Arguments
# ---------
#
# * line_item
# The item to check for matching.
#
# Example
# -------
# Given `TagSelector.new("sale")` and
# a line_item with a variant with tags = ["sale", "boat", "hat"]
#
# selector.match?(line_item) # returns true
#
def match?(line_item)
line_item.variant.product.tags.include?(@tag)
end
end
# PercentageDiscount
# ==================
#
# The `PercentageDiscount` gives percentage discounts to item prices.
#
# Example
# -------
# * 15% off
#
class PercentageDiscount
# Initializes the discount.
#
# Arguments
# ---------
#
# * percent
# The percentage by which the item will be discounted.
#
# * message
# The message to show for the discount.
#
def initialize(percent, message)
# Calculate the percentage, while ensuring that Decimal values are used in
# order to maintain precision.
@percent = Decimal.new(percent) / 100.0
@message = message
end
# Applies the discount on a line item.
#
# Arguments
# ---------
#
# * line_item
# The item on which the discount will be applied.
#
# Example
# -------
# Given `PercentageDiscount.new(10, "Great discount")` and the following line item:
#
# * Quantity = 2, Price = 10
#
# The discount will give $1 off per quantity, for a total of $2 off.
#
def apply(line_item)
# Calculate the discount for this line item
line_discount = line_item.line_price * @percent
# Calculated the discounted line price
new_line_price = line_item.line_price - line_discount
# Apply the new line price to this line item with a given message
# describing the discount, which may be displayed in cart pages and
# confirmation emails to describe the applied discount.
line_item.change_line_price(new_line_price, message: @message)
# Print a debugging line to the console
puts "Discounted line item with variant #{line_item.variant.id} by #{line_discount}."
end
end
# LowToHighPartitioner
# ====================
#
# The `LowToHighPartitioner` is used by campaigns for which not all items
# are discounted, such as `BogoCampaign`. It tries to discount items so that
# the cheaper items are prioritized for discounting.
#
# Example
# -------
# Given `LowToHighPartitioner.new(2,1)` and a cart containing the following
# line items:
#
# * (A) Quantity = 2, Price = 5
# * (B) Quantity = 3, Price = 10
#
# The partitioner will:
#
# * Sort them by ascending price (A, B)
# * Count the total items to be discounted (1)
# * Take 1 of A to be discounted
#
# The items to be discounted will be (before discount):
#
# * (A) Quantity = 1, Price = 5
#
class LowToHighPartitioner
# Initializes the partitioner.
#
# Arguments
# ---------
#
# * paid_item_count
# The number of items to skip before selecting items to discount.
#
# * discounted_item_count
# The number of items to return for discounting.
#
# Example
# -------
# To create a campaign such as "Buy two, the 3rd item is discounted"
#
# LowToHighPartitioner.new(2,1)
#
def initialize(paid_item_count, discounted_item_count)
@paid_item_count = paid_item_count
@discounted_item_count = discounted_item_count
end
# Partitions the items and returns the items that are to be discounted.
#
# Arguments
# ---------
#
# * cart
# The cart to which split items will be added (typically Input.cart).
#
# * line_items
# The selected items that are applicable for the campaign.
#
# Example
# -------
#
# To create a campaign such that for all items under $5, the 3rd one is discounted:
#
# selected_items = Input.cart.line_items.select{|item| item.variant.price < Money.new(cents: 5_00)}
# partitioner = LowToHighPartitioner.new(2,1)
# items_to_discount = partitioner.partition(Input.cart, selected_items)
#
# After this, the campaign has to apply discounts to `items_to_discount`.
#
def partition(cart, applicable_line_items)
# Sort the items by price from low to high
sorted_items = applicable_line_items.sort_by{|line_item| line_item.variant.price}
# Find the total quantity of items
total_applicable_quantity = sorted_items.map(&:quantity).reduce(0, :+)
# Find the quantity of items that must be discounted
discounted_items_remaining = Integer(total_applicable_quantity / (@paid_item_count + @discounted_item_count) * @discounted_item_count)
# Create an array of items to return
discounted_items = []
# Loop over all the items and find those to be discounted
sorted_items.each do |line_item|
# Exit the loop if all discounted items have been found
break if discounted_items_remaining == 0
# The item will be discounted
discounted_item = line_item
if line_item.quantity > discounted_items_remaining
# If the item has more quantity than what must be discounted, split it
discounted_item = line_item.split(take: discounted_items_remaining)
# Insert the newly-created item in the cart, right after the original item
position = cart.line_items.find_index(line_item)
cart.line_items.insert(position + 1, discounted_item)
end
# Decrement the items left to be discounted
discounted_items_remaining -= discounted_item.quantity
# Add the item to be returned
discounted_items.push(discounted_item)
end
# Example
# ------- Check to see if additional discount code is used. If used, add customer discount code and remove BOGO. Same applies if discount code is removed. Apply BOGO instead.
cart_discounted_subtotal =
case cart.discount_code
when CartDiscount::Percentage
if cart.subtotal_price >= cart.discount_code.minimum_order_amount
cart.subtotal_price * ((Decimal.new(100) - cart.discount_code.percentage) / 100)
else
cart.subtotal_price
end
when CartDiscount::FixedAmount
if cart.subtotal_price >= cart.discount_code.minimum_order_amount
[cart.subtotal_price - cart.discount_code.amount, Money.new(0)].max
else
cart.subtotal_price
end
else
cart.subtotal_price
end
#---
# Return the items to be discounted
discounted_items
end
end
# BogoCampaign
# ============
#
# Example campaigns
# -----------------
#
# * Buy one, get one free
# * Buy one, get one 50% off
# * Buy two items and get a third for $5 off
#
class BogoCampaign
# Initializes the campaign.
#
# Arguments
# ---------
#
# * selector
# The selector finds eligible items for this campaign.
#
# * discount
# The discount changes the prices of the items returned by the partitioner.
#
# * partitioner
# The partitioner takes all applicable items, and returns only those that
# are to be discounted. In a "Buy two items, get the third for free"
# campaign, the partitioner would skip two items and return the third item.
#
def initialize(selector, discount, partitioner)
@selector = selector
@discount = discount
@partitioner = partitioner
end
# Runs the campaign on the given cart.
#
# Arguments
# ---------
#
# * cart
# The cart to which the campaign is applied.
#
# Example
# -------
# To run the campaign on the input cart:
#
# campaign.run(Input.cart)
#
def run(cart)
applicable_items = cart.line_items.select do |line_item|
@selector.match?(line_item)
end
discounted_items = @partitioner.partition(cart, applicable_items)
discounted_items.each do |line_item|
@discount.apply(line_item)
end
end
end
# Use an array to keep track of the discount campaigns desired.
CAMPAIGNS = [
# Give every 5th item with the tag "BOGO" for free.
BogoCampaign.new(
TagSelector.new("Bogo"),
PercentageDiscount.new(100, "5TH ITEM IS FREE!"),
LowToHighPartitioner.new(4,1),
)
]
# Iterate through each of the discount campaigns.
CAMPAIGNS.each do |campaign|
# Apply the campaign onto the cart.
campaign.run(Input.cart)
end
# In order to have the changes to the line items be reflected, the output of
# the script needs to be specified.
Output.cart = Input.cart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment