Discounts per product, based on product tag
If the product has tag "Tiered Discount" and qty is between 50-100 the discount code will apply 10% off for this specific product.
The custom code part will be per product instead of all products that have the tag above. This is custom made because the script editor will take all tagged products above and create the discount. Put another way: the script used to take the total qty of all products with the tag, but now will be based on the product.
class Campaign
def initialize(condition, *qualifiers)
@condition = (condition.to_s + '?').to_sym
@qualifiers = PostCartAmountQualifier ? [] : [] rescue qualifiers.compact
@line_item_selector = qualifiers.last unless @line_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, @line_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)
@line_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| @line_item_selector.nil? || @line_item_selector.match?(item) }
puts "l"
puts @line_iem_selector
puts "d"
puts @discount_tiers
puts "t"
puts @tier_type
puts "a"
puts applicable_items
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 }
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 AndSelector
def initialize(*conditions)
@conditions = conditions.compact
end
def match?(item, selector = nil)
@conditions.all? do |condition|
if selector
condition.match?(item, selector)
else
condition.match?(item)
end
end
end
end
class Qualifier
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
def compare_amounts(compare, comparison_type, compare_to)
case comparison_type
when :greater_than
return compare > compare_to
when :greater_than_or_equal
return compare >= compare_to
when :less_than
return compare < compare_to
when :less_than_or_equal
return compare <= compare_to
when :equal_to
return compare == compare_to
else
raise "Invalid comparison type"
end
end
end
class CartQuantityQualifier < Qualifier
def initialize(total_method, comparison_type, quantity)
@total_method = total_method
@comparison_type = comparison_type
@quantity = quantity
end
def match?(cart, selector = nil)
case @total_method
when :item
total = cart.line_items.reduce(0) do |total, item|
total + ((selector ? selector.match?(item) : true) ? item.quantity : 0)
end
when :cart
total = cart.line_items.reduce(0) { |total, item| total + item.quantity }
end
if @total_method == :line_any || @total_method == :line_all
method = @total_method == :line_any ? :any? : :all?
qualified_items = cart.line_items.select { |item| selector ? selector.match?(item) : true }
qualified_items.send(method) { |item| compare_amounts(item.quantity, @comparison_type, @quantity) }
else
compare_amounts(total, @comparison_type, @quantity)
end
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
@tags = 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, @tags)
end
end
end
class ProductQuantitySelector < Qualifier
def initialize(comparison_type, quantity)
@comparison_type = comparison_type
@quantity = quantity
end
def match?(line_item)
total = line_item.quantity
compare_amounts(total, @comparison_type, @quantity)
end
end
CAMPAIGNS = [
TieredDiscount.new(
:all,
nil,
AndSelector.new(
CartQuantityQualifier.new(
:item,
:greater_than_or_equal,
50
),
nil,
nil
),
AndSelector.new(
ProductTagSelector.new(
:does,
:include,
["Tiered Discount"]
),
ProductQuantitySelector.new(
:greater_than_or_equal,
50
),
ProductQuantitySelector.new(
:less_than,
100
)
),
:percent,
:line_quantity,
[{:tier => "Line Item", :discount => "10", :message => "buy 50 get 10% off MSRP"}]
)
].freeze
CAMPAIGNS.each do |campaign|
campaign.run_with_hooks(Input.cart)
end
Output.cart = Input.cart