Last active
October 15, 2021 20:05
-
-
Save KamGraphica/cb7f2f58be9d3df502b19fa9e59fdddd to your computer and use it in GitHub Desktop.
Shopify Plus - Shopify Script - Buy 4 get 1 Free - Products tagged "BOGO"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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