FROM CACHE - en_header

Complex Discount based on cart total AND product types

dkrat
New Member
2 0 0

We are attempting to use Scripts for our B2B store, but our discounts are a little more complex than most stores would typically use.

 

When a customer is approved and purchases over a certain dollar amount, new pricing kicks in. But the percentage of each isn't the same, so a flat percentage-based discount won't work.

 

All the products are tagged by type, and then received a percentage discount based on the cart subtotal. So if a customer buys $400+ worth of goods, they should get the correct discounts on the TYPES of products in the cart. But if they purchase over $2500, then the next level price should kick-in.

I am able to get the $400 tier working, but I can't seem to get it to register the next tier and its respective discount. I will just stay with the lowest subtotal discounts.

 

I'm using the https://jgodson.github.io/shopify-script-creator/ to create my scripts, and I'll paste the script I am currently using as a comment below.

Reply 1 (1)
dkrat
New Member
2 0 0

The code below is hard to read, here is a pastbin

https://pastebin.com/Bi2kj256

# GENERATED BY THE SHOPIFY SCRIPT CREATOR APP
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) }
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 }
next 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)
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 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 CustomerTagQualifier < Qualifier
def initialize(match_type, match_condition, tags)
@match_condition = match_condition
@invert = match_type == :does_not
@Tags = tags.map(&:downcase)
end

def match?(cart, selector = nil)
return true if cart.customer.nil? && @invert
return false if cart.customer.nil?
customer_tags = cart.customer.tags.to_a.map(&:downcase)
case @match_condition
when :match
return @invert ^ ((@tags & customer_tags).length > 0)
else
return @invert ^ partial_match(@match_condition, customer_tags, @Tags)
end
end
end

class CartAmountQualifier < Qualifier
def initialize(behaviour, comparison_type, amount)
@behaviour = behaviour
@comparison_type = comparison_type
@amount = Money.new(cents: amount * 100)
end

def match?(cart, selector = nil)
total = cart.subtotal_price
if @behaviour == :item || @behaviour == :diff_item
total = cart.line_items.reduce(Money.zero) do |total, item|
total + (selector&.match?(item) ? item.line_price : Money.zero)
end
end
case @behaviour
when :cart, :item
compare_amounts(total, @comparison_type, @amount)
when :diff_cart
compare_amounts(cart.subtotal_price_was - @amount, @comparison_type, total)
when :diff_item
original_line_total = cart.line_items.reduce(Money.zero) do |total, item|
total + (selector&.match?(item) ? item.original_line_price : Money.zero)
end
compare_amounts(original_line_total - @amount, @comparison_type, total)
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 ProductTypeSelector < Selector
def initialize(match_type, product_types)
@invert = match_type == :not_one
@Product_types = product_types.map(&:downcase)
end

def match?(line_item)
@invert ^ @Product_types.include?(line_item.variant.product.product_type.downcase)
end
end

CAMPAIGNS = [
TieredDiscount.new(
:all,
CustomerTagQualifier.new(
:does,
:match,
["flexdealer"]
),
CartAmountQualifier.new(
:cart,
:greater_than_or_equal,
400
),
ProductTypeSelector.new(
:is_one,
["ratcat"]
),
:percent,
:cart_subtotal,
[{:tier => "ratcat", :discount => "29", :message => "$400 column pricing -RatCAT"}]
),
TieredDiscount.new(
:all,
CustomerTagQualifier.new(
:does,
:match,
["flexdealer"]
),
CartAmountQualifier.new(
:cart,
:greater_than_or_equal,
400
),
ProductTypeSelector.new(
:is_one,
["cabletester"]
),
:percent,
:cart_subtotal,
[{:tier => "cabletesters", :discount => "29", :message => "$400 column pricing - Testers"}]
),
TieredDiscount.new(
:all,
CustomerTagQualifier.new(
:does,
:match,
["flexdealer"]
),
CartAmountQualifier.new(
:cart,
:greater_than_or_equal,
400
),
ProductTypeSelector.new(
:is_one,
["cable"]
),
:percent,
:cart_subtotal,
[{:tier => "cable", :discount => "15", :message => "$400 column pricing - cable"}]
),
TieredDiscount.new(
:all,
CustomerTagQualifier.new(
:does,
:match,
["flexdealer"]
),
CartAmountQualifier.new(
:cart,
:greater_than_or_equal,
400
),
ProductTypeSelector.new(
:is_one,
["essentials"]
),
:percent,
:cart_subtotal,
[{:tier => "essentials", :discount => "25", :message => "$400 column pricing - essentials"}]
),
TieredDiscount.new(
:all,
CustomerTagQualifier.new(
:does,
:match,
["flexdealer"]
),
CartAmountQualifier.new(
:cart,
:greater_than_or_equal,
2500
),
ProductTypeSelector.new(
:is_one,
["ratcat"]
),
:percent,
:cart_subtotal,
[{:tier => "ratcat", :discount => "39", :message => "$2500 column pricing -RatCAT"}]
),
TieredDiscount.new(
:all,
CustomerTagQualifier.new(
:does,
:match,
["flexdealer"]
),
CartAmountQualifier.new(
:cart,
:greater_than_or_equal,
2500
),
ProductTypeSelector.new(
:is_one,
["cabletester"]
),
:percent,
:cart_subtotal,
[{:tier => "cabletesters", :discount => "39", :message => "$2500 column pricing - Testers"}]
),
TieredDiscount.new(
:all,
CustomerTagQualifier.new(
:does,
:match,
["flexdealer"]
),
CartAmountQualifier.new(
:cart,
:greater_than_or_equal,
2500
),
ProductTypeSelector.new(
:is_one,
["cable"]
),
:percent,
:cart_subtotal,
[{:tier => "cable", :discount => "25", :message => "$2500 column pricing - cable"}]
),
TieredDiscount.new(
:all,
CustomerTagQualifier.new(
:does,
:match,
["flexdealer"]
),
CartAmountQualifier.new(
:cart,
:greater_than_or_equal,
2500
),
ProductTypeSelector.new(
:is_one,
["essentials"]
),
:percent,
:cart_subtotal,
[{:tier => "essentials", :discount => "25", :message => "$2500 column pricing - essentials"}]
)
].freeze

CAMPAIGNS.each do |campaign|
campaign.run_with_hooks(Input.cart)
end

Output.cart = Input.cart