Multiple discounts on products with script editor

Solved

Multiple discounts on products with script editor

Codesto1
Shopify Partner
41 3 5

Using script editor we can set a discount for product with the following (just partial)

 

PRODUCT_DISCOUNT_TIERS = [
  {
    product_selector_match_type: :include,
    product_selector_type: :type,
    product_selectors: ["mytype"],
    tiers: [
       {
        quantity: 1,
        discount_type: :dollar,
        discount_amount: 0.01,
        discount_message: 'you just saved a penny!',
      },
.....

I would like to replicate this on multiple product types.  However I can only publish on script at a time.  My original idea was to do a script for each product discount I'd like.  Sometimes I'm going to use a type other times a tag.  How would I be able to accomplish this?

Accepted Solution (1)

Codesto1
Shopify Partner
41 3 5

This is an accepted solution.

After reviewing this I noticed that PRODUCT_DISCOUNT_TIERS is an array.  I was able to add more discount products with a different type by simply creating a different object in the array.

 

PRODUCT_DISCOUNT_TIERS = [
  {
    product_selector_match_type: :include,
    product_selector_type: :type,
    product_selectors: ["mytype"],
    tiers: [
       {
        quantity: 1,
        discount_type: :dollar,
        discount_amount: 0.01,
        discount_message: 'you just saved a penny!',
      }....
},
{ // new object with product_selector etc here...
}

View solution in original post

Replies 3 (3)

playwright-mike
Shopify Partner
72 18 33

We support this kind of thing using the Playwright app. I used it to quickly generate an example below. Hopefully that's a helpful starting point for you. If for some reason copying and pasting the script doesn't work, I've attached it as well.

 

Let me know if that works for you!

Best,

Matthew

 

########################################################################
##  Create Shopify scripts without writing code at playwrightapp.com  ##
########################################################################
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 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,
    nil,
    nil,
    ProductTypeSelector.new(:is_one, ["MyTypeA"]),
    :percent,
    :line_quantity,
    [
      {
        :tier => "1", 
        :discount => "0.01", 
        :message => "You saved a penny"
      },
      {
        :tier => "2", 
        :discount => "0.02", 
        :message => "You saved two pennies"
      }
    ]
  ),
  TieredDiscount.new(
    :all,
    nil,
    nil,
    ProductTypeSelector.new(:is_one, ["MyTypeB"]),
    :percent,
    :line_quantity,
    [
      {
        :tier => "1",
        :discount => "1",
        :message => "You saved a dollar"
      },
      {
        :tier => "2",
        :discount => "2",
        :message => "You saved two dollars"
      }
    ]
  )
].freeze

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

Output.cart = Input.cart

 

Playwright | Create Shopify Scripts without writing code | https://playwrightapp.com
- Was my reply helpful? Please Like and Accept Solution.

Codesto1
Shopify Partner
41 3 5

Thanks for the great help.  I was looking for something more along the lines of any documentation on how to achieve it or some direction on how I can do it.  As a developer I'd like to learn how to get it done and not pay the $49 a month your service charges.  

Codesto1
Shopify Partner
41 3 5

This is an accepted solution.

After reviewing this I noticed that PRODUCT_DISCOUNT_TIERS is an array.  I was able to add more discount products with a different type by simply creating a different object in the array.

 

PRODUCT_DISCOUNT_TIERS = [
  {
    product_selector_match_type: :include,
    product_selector_type: :type,
    product_selectors: ["mytype"],
    tiers: [
       {
        quantity: 1,
        discount_type: :dollar,
        discount_amount: 0.01,
        discount_message: 'you just saved a penny!',
      }....
},
{ // new object with product_selector etc here...
}