Skip to content

Instantly share code, notes, and snippets.

@CodeBrotha
Last active March 20, 2023 22:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CodeBrotha/4a9977adb7efbe76dbfe4fdf9d8dbae9 to your computer and use it in GitHub Desktop.
Save CodeBrotha/4a9977adb7efbe76dbfe4fdf9d8dbae9 to your computer and use it in GitHub Desktop.
Shopify Scripts - Combined Line Item Script
# ================================================================
# ================================================================
# LINE ITEM COMBO SCRIPT
#
# This script runs multiple line item scripts.
#
# Currently contains:
#
# 1: PRODUCT QUANTITY LIMITS
# (Limits line item quantities per order)
# 2: LINE ITEM PROPERTY DISCOUNT
# (Applies discount to each line item based on a line item property's value.)
# ================================================================
# ================================================================
# ================================================================
# PRODUCT QUANTITY LIMITS
# ================================================================
# ================ Customizable Settings ================
# If the quantity of any matching items is greater than the
# entered threshold, the excess items are removed from the cart.
# It should be noted that there will be no notice to the customer
# when this happens.
#
# - 'enable' determines whether the campaign will run. Can be:
# - 'true' to run
# - 'false' to not run
# - '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
# - ':subscription' to find subscription products
# - ':all' for all products
# - 'product_selectors' 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'.
# - 'variant_level_limit' determines whether the below limit
# is applied on a variant, or a total quantity, level. For
# example, can I have X number of individual matching items,
# or can I only have X number total of matching items?
# Can be:
# - 'true' to limit at a variant level
# - 'false' to limit total quantity
# - 'quantity_allowed' is the number of products allowed
# ================================================================
## Check if any line items have a tag that starts with "restrict:qty-limit"
## If so create a restricted quantity campaign for that tag
restricted_qty_campaigns = [
{
product_selector_match_type: :include,
product_selector_type: :all,
variant_level_limit: true,
quantity_allowed: 100,
},
{
product_selector_match_type: :include,
product_selector_type: :tag,
product_selectors: ["special-tag"],
variant_level_limit: true,
quantity_allowed: 10,
}
]
has_restrict_qty_tag = false
## Give your quantity limit tags the same prefix, example "restrict:qty-5" or "restrict:qty-10" etc.
## Then you can easily use the following to capture any quantity limit tag with the same prefix as shown here.
Input.cart.line_items.each do |item|
item.variant.product.tags.each do |tag|
if tag.start_with?('restrict:qty')
next if restricted_qty_campaigns.any? {|h| h[:product_selectors] and h[:product_selectors].include? tag}
has_restrict_qty_tag = true
restrict_qty_tag = tag
split_tag = tag.split('-', 2)
restrict_qty = split_tag.last.to_i
puts 'restrict:qty tag found'
puts 'restrict_qty_tag is ' + restrict_qty_tag
puts 'restrict_qty is = ' + split_tag.last
## If the tag's restrict quantity is the same as that of an existing campaign, combine with the existing campaign.
if restricted_qty_campaigns.any? {|h| h[:quantity_allowed] == restrict_qty}
existing_hash = restricted_qty_campaigns.find { |h| h[:quantity_allowed] == restrict_qty }
restrict_qty_tag_array = [restrict_qty_tag]
existing_hash[:product_selectors] += restrict_qty_tag_array
else
restricted_qty_campaign = [
{
product_selector_match_type: :include,
product_selector_type: :tag,
product_selectors: [restrict_qty_tag],
variant_level_limit: true,
quantity_allowed: restrict_qty,
}
]
restricted_qty_campaigns += restricted_qty_campaign
end
end
end
end
puts 'restricted_qty_campaigns is ' + restricted_qty_campaigns.to_s
QUANTITY_LIMITS = {
enable: true,
campaigns: restricted_qty_campaigns
}
# ================================================================
# Script Code (do not edit)
# ================================================================
# ProductSelector
#
# Finds matching products by the entered criteria.
# ================================================================
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 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 subscription(line_item)
!line_item.selling_plan_id.nil?
end
def all(line_item)
true
end
end
# ================================================================
# ProductQuantityLimitCampaign
#
# If the quantity of any matching items is greater than the
# entered threshold, the excess items are removed from the cart.
# ================================================================
class ProductQuantityLimitCampaign
def initialize(enable, campaigns)
@enable = enable
@campaigns = campaigns
end
def run(cart)
return unless @enable
@campaigns.each do |campaign|
product_selector = ProductSelector.new(
campaign[:product_selector_match_type],
campaign[:product_selector_type],
campaign[:product_selectors]
)
if campaign[:variant_level_limit]
applicable_items = {}
cart.line_items.each_with_index do |line_item, idx|
next unless product_selector.match?(line_item)
id = idx
if applicable_items[id].nil?
applicable_items[id] = {
items: [],
total_quantity: 0
}
end
applicable_items[id][:items].push(line_item)
applicable_items[id][:total_quantity] += line_item.quantity
end
next if applicable_items.nil?
applicable_items.each do |id, info|
next unless info[:total_quantity] > campaign[:quantity_allowed]
num_to_remove = info[:total_quantity] - campaign[:quantity_allowed]
self.loop_items(cart, info[:items], num_to_remove)
end
else
applicable_items = cart.line_items.select { |line_item| product_selector.match?(line_item) }
next if applicable_items.nil?
total_quantity = applicable_items.map(&:quantity).reduce(0, :+)
next unless total_quantity > campaign[:quantity_allowed]
num_to_remove = total_quantity - campaign[:quantity_allowed]
self.loop_items(cart, applicable_items, num_to_remove)
end
end
end
def loop_items(cart, line_items, num_to_remove)
line_items.each do |line_item|
if line_item.quantity > num_to_remove
split_line_item = line_item.split(take: num_to_remove)
break
else
index = cart.line_items.find_index(line_item)
cart.line_items.delete_at(index)
num_to_remove -= line_item.quantity
end
break if num_to_remove <= 0
end
end
end
# ================================================================
# END PRODUCT QUANTITY LIMITS
# ================================================================
# ================================================================
# RUN THE CAMPAIGNS
# ================================================================
CAMPAIGNS = [
ProductQuantityLimitCampaign.new(
QUANTITY_LIMITS[:enable],
QUANTITY_LIMITS[:campaigns],
),
]
CAMPAIGNS.each do |campaign|
campaign.run(Input.cart)
end
# ================================================================
# END RUN THE CAMPAIGNS
# ================================================================
# ================================================================
# LINE ITEM PROPERTY DISCOUNT
# ================================================================
## This script applies discount to each line item based on a line item property's value.
##
# ================================================================
# ================================================================
Input.cart.line_items.each do |item|
# Set the line item property in Shopify theme When adding item to cart
#
# line item property is a key/value pair. Example: {"_someCoolPromo": "1"}
#
# In this example:
# "_someCoolPromo" is the key we look for here and must ALWAYS start with an underscore.
# "1" is the value and respresents a value of $1.00 ("1.25" for $1.25, "1.5" for $1.50 etc.)
#
# ================================================================
## Check if line item has the specific line item property and if so apply the relevant discount (if greater than sale discount)
if item.properties.has_key?("_lineItemPropertyKeyHere") && (item.properties["_lineItemPropertyKeyHere"].to_f > 0.00)
line_item_discount_value = Float(item.properties["_lineItemPropertyKeyHere"])
discount_amount = Money.new(cents: 100) * line_item_discount_value
discount_message = "Some Cool Promo"
## Check if line item is currently on sale and if so get the sale discount
if !item.variant.compare_at_price.nil? && item.variant.compare_at_price.cents > 0
## Item has compare_at_price, check if is on sale
if item.variant.compare_at_price > item.variant.price
## Item is on sale, apply bundle item discount based on item.variant.compare_at_price
sale_discount = item.variant.compare_at_price - item.variant.price
if line_item_discount_value > (Float(sale_discount.cents.to_s) / 100)
new_line_price = [item.variant.compare_at_price - (discount_amount * item.quantity), Money.zero].max
item.change_line_price(new_line_price, { message: discount_message })
end
else
## Item is not on sale, apply line item discount based on item.line_price
new_line_price = [item.line_price - (discount_amount * item.quantity), Money.zero].max
item.change_line_price(new_line_price, { message: discount_message })
end
else
## Item does not have compare_at_price, apply line item discount based on item.line_price
new_line_price = [item.line_price - (discount_amount * item.quantity), Money.zero].max
item.change_line_price(new_line_price, { message: discount_message })
end
end
end
# ================================================================
# END LINE ITEM PROPERTY DISCOUNT
# ================================================================
Output.cart = Input.cart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment