Personalized checkout and custom promotions with Shopify Scripts
I've been seeing production errors my every script I run in the Script Editor. Everything tested fine but I'm unable to resolve this. Any help would be much obliged.
Below is the script I am running.
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 BuyXGetX < Campaign def initialize(condition, customer_qualifier, cart_qualifier, buy_item_selector, get_item_selector, discount, buy_x, get_x, max_sets) super(condition, customer_qualifier, cart_qualifier) @line_item_selector = buy_item_selector @get_item_selector = get_item_selector @discount = discount @buy_x = buy_x @get_x = get_x @max_sets = max_sets == 0 ? nil : max_sets end def run(cart) raise "Campaign requires a discount" unless @discount return unless qualifies?(cart) return unless cart.line_items.reduce(0) {|total, item| total += item.quantity } >= @buy_x applicable_buy_items = nil eligible_get_items = nil discountable_sets = 0 # Find the items that qualify for buy_x if @line_item_selector.nil? applicable_buy_items = cart.line_items else applicable_buy_items = cart.line_items.select { |item| @line_item_selector.match?(item) } end # Find the items that qualify for get_x if @get_item_selector.nil? eligible_get_items = cart.line_items else eligible_get_items = cart.line_items.select {|item| @get_item_selector.match?(item) } end # Check if cart qualifies for discounts and limit the discount sets purchased_quantity = applicable_buy_items.reduce(0) { |total, item| total += item.quantity } discountable_sets = (@max_sets ? [purchased_quantity / @buy_x, @max_sets].min : purchased_quantity / @buy_x).to_i return if discountable_sets < 1 discountable_quantity = (discountable_sets * @get_x).to_i # Apply the discounts (sort to discount lower priced items first) eligible_get_items = eligible_get_items.sort_by { |item| item.variant.price } eligible_get_items.each do |item| break if discountable_quantity == 0 if item.quantity <= discountable_quantity @discount.apply(item) discountable_quantity -= item.quantity else new_item = item.split({ take: discountable_quantity }) @discount.apply(new_item) cart.line_items << new_item discountable_quantity = 0 end end end end class ConditionalDiscount < Campaign def initialize(condition, customer_qualifier, cart_qualifier, line_item_selector, discount, max_discounts) super(condition, customer_qualifier, cart_qualifier) @line_item_selector = line_item_selector @discount = discount @items_to_discount = max_discounts == 0 ? nil : max_discounts end def run(cart) raise "Campaign requires a discount" unless @discount return unless qualifies?(cart) applicable_items = cart.line_items.select { |item| @line_item_selector.nil? || @line_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) @discount.apply(discounted_items) cart.line_items << discounted_items @items_to_discount = 0 else @discount.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 CartHasItemQualifier < Qualifier def initialize(quantity_or_subtotal, comparison_type, amount, item_selector) @quantity_or_subtotal = quantity_or_subtotal @comparison_type = comparison_type @amount = quantity_or_subtotal == :subtotal ? Money.new(cents: amount * 100) : amount @item_selector = item_selector end def match?(cart, selector = nil) raise "Must supply an item selector for the #{self.class}" if @item_selector.nil? case @quantity_or_subtotal when :quantity total = cart.line_items.reduce(0) do |total, item| total + (@item_selector&.match?(item) ? item.quantity : 0) end when :subtotal total = cart.line_items.reduce(Money.zero) do |total, item| total + (@item_selector&.match?(item) ? item.line_price : Money.zero) end end compare_amounts(total, @comparison_type, @amount) 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 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 ProductIdSelector < Selector def initialize(match_type, product_ids) @invert = match_type == :not_one @product_ids = product_ids.map { |id| id.to_i } end def match?(line_item) @invert ^ @product_ids.include?(line_item.variant.product.id) end end class ProductVendorSelector < Selector def initialize(match_type, vendors) @invert = match_type != :is_one @vendors = vendors.map(&:downcase) end def match?(line_item) @invert ^ @vendors.include?(line_item.variant.product.vendor.downcase) 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 CAMPAIGNS = [ BuyXGetX.new( :all, nil, CartHasItemQualifier.new( :quantity, :greater_than_or_equal, 1, ProductIdSelector.new( :is_one, ["2423846273082", "2423850860602", "2424427282490", "4379124105274", "4379125776442"] ) ), ProductIdSelector.new( :is_one, ["2423846273082", "2423850860602", "2424427282490", "4379124105274", "4379125776442"] ), ProductIdSelector.new( :is_one, ["610077245498"] ), PercentageDiscount.new( 100, "" ), 1, 1, 0 ), ConditionalDiscount.new( :all, CustomerTagQualifier.new( :does, :start_with, ["pro"] ), CartHasItemQualifier.new( :quantity, :greater_than, 0, ProductVendorSelector.new( :is_one, ["Aprés Nail", "Apres Nail"] ) ), ProductVendorSelector.new( :is_one, ["Apres Nail", "Aprés Nail"] ), PercentageDiscount.new( 20, "Pro Member Discount!" ), 0 ) ].freeze CAMPAIGNS.each do |campaign| campaign.run_with_hooks(Input.cart) end Output.cart = Input.cart
Hi, did you resolve this? I have recently posted a message about "InstructionQuotaExceeded" where customers are created very large carts (entering 1million in the qty etc) & I am interested in ways to detect & prevent such errors. I wondered if you were seeing a similar problem if you're code worked in testing?
Seeing a similar issue myslef. Did you ever get resoltion to this?
unfortunately no solution yet.
I am also seeing that issue on our site.
Hi everyone,
I worked around this problem by placing some boundary conditions in my code after experimenting with cart sizes & working out where the limits get exceeded.
I assume these attempts are trying to get free stuff or trying to break the checkout, so if the numbers are too high my code simply ignores the cart & doesn't even attempt to apply a discount...
Here's my main class that handles this before applying the discount code (which I have left out for simplicity).
class PromotionCampaign
def initialize(cart)
@cart = cart
@Max_items = 20
@Max_qty = 80
end
def run!
if(@cart.line_items.count <= @Max_items)
if(@cart.line_items.map {|cart_line| cart_line.quantity }.inject(0, :+)<=@max_qty)
discounted_products = ProductBuyXForX.new(@cart).run!
end
end
@cart
end
end
Hope that is useful to someone!
Modified your code to accomplish a similar solution, but the result is that if the product max quantity is exceeded, the line item is just removed. We were seeing the all too familiar InstructionQuotaExceeded issue with carts having qty's of 1 million per line item - so this is what we've done. It's a short lived solution since scripts will be going away soon enough; however, some peace in the interim:
# Prevent Insane Cart Quantities
class CartCleanupCampaign
def initialize(cart)
@cart = cart
@Max_qty = 200 # Maximum allowed quantity per line item
end
def run!
@cart.line_items.each do |line_item|
# Check if the line item's quantity exceeds the maximum allowed quantity
if line_item.quantity > @Max_qty
# Remove the line item from the cart
@cart.line_items.delete(line_item)
end
end
@cart
end
end
CartCleanupCampaign.new(Input.cart).run!
check_cart(Input.cart)
Output.cart = Input.cart
Starting a B2B store is a big undertaking that requires careful planning and execution. W...
By JasonH Sep 23, 2024By investing 30 minutes of your time, you can unlock the potential for increased sales,...
By Jacqui Sep 11, 2024We appreciate the diverse ways you participate in and engage with the Shopify Communi...
By JasonH Sep 9, 2024