Skip to content

Instantly share code, notes, and snippets.

@ECkurt
Created December 12, 2017 14:03
Show Gist options
  • Save ECkurt/ad33378c43332897025066f52c44e018 to your computer and use it in GitHub Desktop.
Save ECkurt/ad33378c43332897025066f52c44e018 to your computer and use it in GitHub Desktop.
FREEBIE_PRODUCT_ID = 6621649541
CART_TOTAL_FOR_DISCOUNT_APPLIED = Money.new(cents: 100) * 100
DISCOUNT_MESSAGE = "Get a FREE gift for ordering $100 or more"
freebie_in_cart = false
cart_price_exceeds_discounted_freebie_amount = false
cost_of_freebie = Money.zero
# Test if the freebie is in the cart, also get its cost if it is so we can deduct from the cart total
Input.cart.line_items.select do |line_item|
product = line_item.variant.product
if product.id == FREEBIE_PRODUCT_ID
freebie_in_cart = true
cost_of_freebie = line_item.line_price
end
end
# If the freebie exists in the cart, check the subtotal of the other items to see if the freebie should be discounted
if freebie_in_cart
cart_subtotal_minus_freebie_cost = Input.cart.subtotal_price - cost_of_freebie
if cart_subtotal_minus_freebie_cost >= CART_TOTAL_FOR_DISCOUNT_APPLIED
cart_price_exceeds_discounted_freebie_amount = true
end
end
# Only true if the freebie is in the cart
was_discount_applied = false
if cart_price_exceeds_discounted_freebie_amount
Input.cart.line_items.each do |item|
if item.variant.product.id == FREEBIE_PRODUCT_ID && was_discount_applied == false
if item.quantity > 1
new_line_item = item.split(take: 1)
new_line_item.change_line_price(Money.zero, message: DISCOUNT_MESSAGE)
Input.cart.line_items << new_line_item
next
else
item.change_line_price(Money.zero, message: DISCOUNT_MESSAGE)
end
end
end
end
Output.cart = Input.cart
@RHB5
Copy link

RHB5 commented Nov 14, 2018

What if you only want the discount to apply to one GWP? How would you add a limit of one product discount per cart

@RHB5
Copy link

RHB5 commented Nov 14, 2018

Solution

class Campaign
  def initialize(condition, *qualifiers)
    @condition = condition == :default ? :all? : (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 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)
    @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
    revert_changes(cart) unless @post_amount_qualifier.nil? || @post_amount_qualifier.match?(cart)
  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 CartAmountQualifier < Qualifier
  def initialize(cart_or_item, comparison_type, amount)
    @cart_or_item = cart_or_item == :default ? :cart : cart_or_item
    @comparison_type = comparison_type == :default ? :greater_than : comparison_type
    @amount = Money.new(cents: amount * 100)
  end

  def match?(cart, selector = nil)
    total = cart.subtotal_price
    if @cart_or_item == :item
      total = cart.line_items.reduce(Money.zero) do |total, item|
        total + (selector&.match?(item) ? item.original_line_price : Money.zero)
      end
    end
    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 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 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

CAMPAIGNS = [
  ConditionalDiscount.new(
    :all,
    nil,
    CartAmountQualifier.new(
      :cart,
      :greater_than,
      150
    ),
    ProductIdSelector.new(
      :is_one,
      ["EnterProductIdHere"]
    ),
    FixedItemDiscount.new(
      100,
      "Free Gift!"
    ),
    1
  ),
].freeze

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

Output.cart = Input.cart

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment