Re: InstructionQuotaExceeded Help!!

Why am I getting production errors in my script editor?

Rooster_Admin
Tourist
4 0 0

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
Replies 6 (6)

rancet
Tourist
5 0 4

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?

 

williammadden
Visitor
2 0 0

Seeing a similar issue myslef. Did you ever get resoltion to this?

Rooster_Admin
Tourist
4 0 0

unfortunately no solution yet. 

cmyers
Tourist
4 0 0

I am also seeing that issue on our site.

InstructionQuotaExceeded.jpg

rancet
Tourist
5 0 4

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!

 

Kneeko
Shopify Partner
7 0 7

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