# frozen_string_literal: true module Spree class Promotion < Spree::Base module Actions class CreateQuantityAdjustments < CreateItemAdjustments preference :group_size, :integer, default: 1 has_many :line_item_actions, foreign_key: :action_id, dependent: :destroy has_many :line_items, through: :line_item_actions ## # Computes the amount for the adjustment based on the line item and any # other applicable items in the order. The rules for this specific # adjustment are as follows: # # = Setup # # We have a quantity group promotion on t-shirts. If a user orders 3 # t-shirts, they get $5 off of each. The shirts come in one size and three # colours: red, blue, and white. # # == Scenario 1 # # User has 2 red shirts, 1 white shirt, and 1 blue shirt in their # order. We want to compute the adjustment amount for the white shirt. # # *Result:* -$5 # # *Reasoning:* There are a total of 4 items that are eligible for the # promotion. Since that is greater than 3, we can discount the items. The # white shirt has a quantity of 1, therefore it will get discounted by # +adjustment_amount * 1+ or $5. # # === Scenario 1-1 # # What about the blue shirt? How much does it get discounted? # # *Result:* $0 # # *Reasoning:* We have a total quantity of 4. However, we only apply the # adjustment to groups of 3. Assuming the white and red shirts have already # had their adjustment calculated, that means 3 units have been discounted. # Leaving us with a lonely blue shirt that isn't part of a group of 3. # Therefore, it does not receive the discount. # # == Scenario 2 # # User has 4 red shirts in their order. What is the amount? # # *Result:* -$15 # # *Reasoning:* The total quantity of eligible items is 4, so we the # adjustment will be non-zero. However, we only apply it to groups of 3, # therefore there is one extra item that is not eligible for the # adjustment. +adjustment_amount * 3+ or $15. # def compute_amount(line_item) adjustment_amount = calculator.compute(PartialLineItem.new(line_item)) adjustment_amount ||= BigDecimal(0) adjustment_amount = adjustment_amount.abs order = line_item.order line_items = actionable_line_items(order) actioned_line_items = order.line_item_adjustments.reload. select { |adjustment| adjustment.source == self && adjustment.amount < 0 }. map(&:adjustable) other_line_items = actioned_line_items - [line_item] applicable_quantity = total_applicable_quantity(line_items) used_quantity = total_used_quantity(other_line_items) usable_quantity = [ applicable_quantity - used_quantity, line_item.quantity ].min persist_quantity(usable_quantity, line_item) amount = adjustment_amount * usable_quantity [line_item.amount, amount].min * -1 end private def actionable_line_items(order) order.line_items.select do |item| promotion.line_item_actionable? order, item end end def total_applicable_quantity(line_items) total_quantity = line_items.sum(&:quantity) extra_quantity = total_quantity % preferred_group_size total_quantity - extra_quantity end def total_used_quantity(line_items) line_item_actions.where( line_item_id: line_items.map(&:id) ).sum(:quantity) end def persist_quantity(quantity, line_item) line_item_action = line_item_actions.where( line_item_id: line_item.id ).first_or_initialize line_item_action.quantity = quantity line_item_action.save! end ## # Used specifically for PercentOnLineItem calculator. That calculator uses # `line_item.amount`, however we might not necessarily want to discount the # entire amount. This class allows us to determine the discount per # quantity and then calculate the adjustment amount the way we normally do # for flat rate adjustments. class PartialLineItem def initialize(line_item) @line_item = line_item end def amount @line_item.price end def order @line_item.order end def currency @line_item.currency end end end end end end