Personalized checkout and custom promotions with Shopify Scripts
Background
I have markets set up as such:
Converted prices are rounded up to the nearest whole dollar.
Using shopify native currency conversion, no other app.
I have a script running to give 100% off items tagged "FreeGift" if the cart is at least $100 CAD, so a free gift w purchase basically.
Script:
# GENERATED BY THE SHOPIFY SCRIPT CREATOR APP
class Campaign
def initialize(condition, *qualifiers)
@condition = (condition.to_s + '?').to_sym
@qualifiers = PostCartAmountQualifier ? [] : [] rescue qualifiers.compact
_item_selector = qualifiers.last unless _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 _match_type.nil?
cart.line_items.send(@li_match_type) { |item| qualifier.match?(item) }
else
qualifier.match?(cart, _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)
.apply_final_discount if && .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 ConditionalDiscount < Campaign
def initialize(condition, customer_qualifier, cart_qualifier, line_item_selector, discount, max_discounts)
super(condition, customer_qualifier, cart_qualifier)
_item_selector = line_item_selector
= discount
@items_to_discount = max_discounts == 0 ? nil : max_discounts
end
def run(cart)
raise "Campaign requires a discount" unless
return unless qualifies?(cart)
applicable_items = cart.line_items.select { |item| _item_selector.nil? || _item_selector.match?(item) }
applicable_items = applicable_items.sort_by { |item| item.variant.price }
applicable_items.each do |item|
break if @items_to_discount == 0
if (!@items_to_discount.nil? && item.quantity > @items_to_discount)
discounted_items = item.split(take: @items_to_discount)
.apply(discounted_items)
cart.line_items << discounted_items
@items_to_discount = 0
else
.apply(item)
@items_to_discount -= item.quantity if !@items_to_discount.nil?
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 PostCartAmountQualifier < Qualifier
def initialize(comparison_type, amount)
@comparison_type = comparison_type
@amount = Money.new(cents: amount * 100)
end
def match?(cart, selector = nil)
total = cart.subtotal_price
compare_amounts(total, @comparison_type, @amount)
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)
_condition = match_condition
@invert = match_type == :does_not
= tags.map(&:downcase)
end
def match?(line_item)
product_tags = line_item.variant.product.tags.to_a.map(&:downcase)
case _condition
when :match
return @invert ^ ((@tags & product_tags).length > 0)
else
return @invert ^ partial_match(@match_condition, product_tags, )
end
end
end
class PercentageDiscount
def initialize(percent, message)
= (100 - percent) / 100.0
@message = message
end
def apply(line_item)
line_item.change_line_price(line_item.line_price * , message: @message)
end
end
CAMPAIGNS = [
ConditionalDiscount.new(
:all,
nil,
PostCartAmountQualifier.new(
:greater_than_or_equal,
100
),
ProductTagSelector.new(
:does,
:match,
["FreeGift"]
),
PercentageDiscount.new(
100,
"Free Gift"
),
1
)
].freeze
CAMPAIGNS.each do |campaign|
campaign.run_with_hooks(Input.cart)
end
Output.cart = Input.cart
The sample free gift product is a Mask, which is $15 CAD and $11.15 USD rounded to $12 at the manual conversion rate.
Issue
On a CAD cart of over $100 it's fine, item is discounted 100% to free:
However, on a USD converted cart...
There is a residual amount of money.
I changed the USD exchange rate to automatic (which at this time is about 78 cents to 1CAD, making the mask $11.83 USD rounded to $12 USD)
And sure enough, the discount takes off 100% to free.
I would expect the discount to take 100% off the price achieved by the manual rate, not the automatic rate!
Hey Community! As we jump into 2025, we want to give a big shout-out to all of you wh...
By JasonH Jan 7, 2025Hey Community! As the holiday season unfolds, we want to extend heartfelt thanks to a...
By JasonH Dec 6, 2024Dropshipping, a high-growth, $226 billion-dollar industry, remains a highly dynamic bus...
By JasonH Nov 27, 2024