Buy one get one within collection

Highlighted
New Member
28 0 0

Hi there, I've found several BOGO scripts on the forum but have been unable to find one that will provide this functionality: buy one product in a collection, get 1 free product within that collection. 

I found this script on github and it does seem to work - discounts items with a "bogo" tag, however i need to modify it to discount only once. Any ideas?

 

class TagSelector

  def initialize(tag)
    @tag = tag
  end

  # Returns whether a line item matches this selector or not.
  #
  # Arguments
  # ---------
  #
  # * line_item
  #   The item to check for matching.
  #
  # Example
  # -------
  # Given `TagSelector.new("sale")` and
  # a line_item with a variant with tags = ["sale", "boat", "hat"]
  #
  #    selector.match?(line_item) # returns true
  #
  def match?(line_item)
    line_item.variant.product.tags.include?(@tag)
  end
end

# PercentageDiscount
# ==================
#
# The `PercentageDiscount` gives percentage discounts to item prices.
#
# Example
# -------
#   * 15% off
#
class PercentageDiscount

  # Initializes the discount.
  #
  # Arguments
  # ---------
  #
  # * percent
  #   The percentage by which the item will be discounted.
  #
  # * message
  #   The message to show for the discount.
  #
  def initialize(percent, message)
    # Calculate the percentage, while ensuring that Decimal values are used in
    # order to maintain precision.
    @percent = Decimal.new(percent) / 100.0
    @message = message
  end

  # Applies the discount on a line item.
  #
  # Arguments
  # ---------
  #
  # * line_item
  #   The item on which the discount will be applied.
  #
  # Example
  # -------
  # Given `PercentageDiscount.new(10, "Great discount")` and the following line item:
  #
  #   * Quantity = 2, Price = 10
  #
  # The discount will give $1 off per quantity, for a total of $2 off.
  #
  def apply(line_item)
    # Calculate the discount for this line item
    line_discount = line_item.line_price * @percent

    # Calculated the discounted line price
    new_line_price = line_item.line_price - line_discount

    # Apply the new line price to this line item with a given message
    # describing the discount, which may be displayed in cart pages and
    # confirmation emails to describe the applied discount.
    line_item.change_line_price(new_line_price, message: @message)

    # Print a debugging line to the console
    puts "Discounted line item with variant #{line_item.variant.id} by #{line_discount}."
  end
end

# LowToHighPartitioner
# ====================
#
# The `LowToHighPartitioner` is used by campaigns for which not all items
# are discounted, such as `BogoCampaign`. It tries to discount items so that
# the cheaper items are prioritized for discounting.
#
# Example
# -------
# Given `LowToHighPartitioner.new(2,1)` and a cart containing the following
# line items:
#
#   * (A) Quantity = 2, Price = 5
#   * (B) Quantity = 3, Price = 10
#
# The partitioner will:
#
#   * Sort them by ascending price (A, B)
#   * Count the total items to be discounted (1)
#   * Take 1 of A to be discounted
#
# The items to be discounted will be (before discount):
#
#   * (A) Quantity = 1, Price = 5
#
class LowToHighPartitioner

  # Initializes the partitioner.
  #
  # Arguments
  # ---------
  #
  # * paid_item_count
  #   The number of items to skip before selecting items to discount.
  #
  # * discounted_item_count
  #   The number of items to return for discounting.
  #
  # Example
  # -------
  # To create a campaign such as "Buy two, the 3rd item is discounted"
  #
  #    LowToHighPartitioner.new(2,1)
  #
  def initialize(paid_item_count, discounted_item_count)
    @paid_item_count = paid_item_count
    @discounted_item_count = discounted_item_count
  end

  # Partitions the items and returns the items that are to be discounted.
  #
  # Arguments
  # ---------
  #
  # * cart
  #   The cart to which split items will be added (typically Input.cart).
  #
  # * line_items
  #   The selected items that are applicable for the campaign.
  #
  # Example
  # -------
  #
  # To create a campaign such that for all items under $5, the 3rd one is discounted:
  #
  #    selected_items = Input.cart.line_items.select{|item| item.variant.price < Money.new(cents: 5_00)}
  #    partitioner = LowToHighPartitioner.new(2,1)
  #    items_to_discount = partitioner.partition(Input.cart, selected_items)
  #
  # After this, the campaign has to apply discounts to `items_to_discount`.
  #
  def partition(cart, applicable_line_items)
    # Sort the items by price from low to high
    sorted_items = applicable_line_items.sort_by{|line_item| line_item.variant.price}
    # Find the total quantity of items
    total_applicable_quantity = sorted_items.map(&:quantity).reduce(0, :+)
    # Find the quantity of items that must be discounted
    discounted_items_remaining = Integer(total_applicable_quantity / (@paid_item_count + @discounted_item_count) * @discounted_item_count)
    # Create an array of items to return
    discounted_items = []

    # Loop over all the items and find those to be discounted
    sorted_items.each do |line_item|
      # Exit the loop if all discounted items have been found
      break if discounted_items_remaining == 0
      # The item will be discounted
      discounted_item = line_item
      if line_item.quantity > discounted_items_remaining
        # If the item has more quantity than what must be discounted, split it
        discounted_item = line_item.split(take: discounted_items_remaining)

        # Insert the newly-created item in the cart, right after the original item
        position = cart.line_items.find_index(line_item)
        cart.line_items.insert(position + 1, discounted_item)
      end

      # Decrement the items left to be discounted
      discounted_items_remaining -= discounted_item.quantity
      # Add the item to be returned
      discounted_items.push(discounted_item)
    end
  # Example
  # ------- Check to see if additional discount code is used. If used, add customer discount code and remove BOGO. Same applies if discount code is removed. Apply BOGO instead.
        cart_discounted_subtotal =
      case cart.discount_code
      when CartDiscount::Percentage
        if cart.subtotal_price >= cart.discount_code.minimum_order_amount
          cart.subtotal_price * ((Decimal.new(100) - cart.discount_code.percentage) / 100)
        else
          cart.subtotal_price
        end
      when CartDiscount::FixedAmount
        if cart.subtotal_price >= cart.discount_code.minimum_order_amount
          [cart.subtotal_price - cart.discount_code.amount, Money.new(0)].max
        else
          cart.subtotal_price
        end
      else
        cart.subtotal_price
      end
    #--- 
    # Return the items to be discounted
    discounted_items
  end
end

# BogoCampaign
# ============
#
# Example campaigns
# -----------------
#
#   * Buy one, get one free
#   * Buy one, get one 50% off
#   * Buy two items and get a third for $5 off
#
class BogoCampaign

  # Initializes the campaign.
  #
  # Arguments
  # ---------
  #
  # * selector
  #   The selector finds eligible items for this campaign.
  #
  # * discount
  #   The discount changes the prices of the items returned by the partitioner.
  #
  # * partitioner
  #   The partitioner takes all applicable items, and returns only those that
  #   are to be discounted. In a "Buy two items, get the third for free"
  #   campaign, the partitioner would skip two items and return the third item.
  #
  def initialize(selector, discount, partitioner)
    @selector = selector
    @discount = discount
    @partitioner = partitioner
  end

  # Runs the campaign on the given cart.
  #
  # Arguments
  # ---------
  #
  # * cart
  #   The cart to which the campaign is applied.
  #
  # Example
  # -------
  # To run the campaign on the input cart:
  #
  #    campaign.run(Input.cart)
  #
  def run(cart)
    applicable_items = cart.line_items.select do |line_item|
      @selector.match?(line_item)
    end
    discounted_items = @partitioner.partition(cart, applicable_items)

    discounted_items.each do |line_item|
      @discount.apply(line_item)
    end
  end
end

# Use an array to keep track of the discount campaigns desired.
CAMPAIGNS = [
  # Give every 5th item with the tag "BOGO" for free.
  BogoCampaign.new(
    TagSelector.new("Bogo"),
    PercentageDiscount.new(100, "5TH ITEM IS FREE!"),
    LowToHighPartitioner.new(1,1),
  )
]

# Iterate through each of the discount campaigns.
CAMPAIGNS.each do |campaign|
  # Apply the campaign onto the cart.
  campaign.run(Input.cart)
end

# In order to have the changes to the line items be reflected, the output of
# the script needs to be specified.
Output.cart = Input.cart

 

0 Likes
Highlighted
Shopify Partner
132 1 18

Hi Nikolas,

Should the discount only be applied once per cart, or once per collection per cart?

Cheers,

Elliott

Feeling a bit lost? Contact elliot@mandelbrotian.com for help with theme setup, alterations, custom functionality, and app development.
0 Likes
Highlighted
New Member
28 0 0

once per cart

0 Likes
Highlighted
Shopify Partner
132 1 18

No worries, if you change the run function in the BogoCampaign class to 

  def run(cart)
    applicable_items = cart.line_items.select do |line_item|
      @selector.match?(line_item)
    end
    discounted_items = @partitioner.partition(cart, applicable_items)

    discounted_items.first(1).each do |line_item|
      @discount.apply(line_item)
    end
  end

the bogo will only apply to the first applicable item per campaign.

Cheers,

Elliott

Feeling a bit lost? Contact elliot@mandelbrotian.com for help with theme setup, alterations, custom functionality, and app development.
1 Like
Highlighted
New Member
28 0 0

good thinking! that works!

0 Likes