diff --git a/core/app/models/spree/promotion/actions/create_adjustment.rb b/core/app/models/spree/promotion/actions/create_adjustment.rb index 00d14e38ede..2f095347e50 100644 --- a/core/app/models/spree/promotion/actions/create_adjustment.rb +++ b/core/app/models/spree/promotion/actions/create_adjustment.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Actions class CreateAdjustment < PromotionAction include Spree::CalculatedAdjustments diff --git a/core/app/models/spree/promotion/actions/create_item_adjustments.rb b/core/app/models/spree/promotion/actions/create_item_adjustments.rb index 387ad9a7d75..f1d730adee1 100644 --- a/core/app/models/spree/promotion/actions/create_item_adjustments.rb +++ b/core/app/models/spree/promotion/actions/create_item_adjustments.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Actions class CreateItemAdjustments < PromotionAction include Spree::CalculatedAdjustments diff --git a/core/app/models/spree/promotion/actions/create_quantity_adjustments.rb b/core/app/models/spree/promotion/actions/create_quantity_adjustments.rb index e5a7005f33b..cad494da060 100644 --- a/core/app/models/spree/promotion/actions/create_quantity_adjustments.rb +++ b/core/app/models/spree/promotion/actions/create_quantity_adjustments.rb @@ -1,135 +1,139 @@ -module Spree::Promotion::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)) - if !adjustment_amount.is_a?(BigDecimal) - Spree::Deprecation.warn "#{calculator.class.name}#compute returned #{adjustment_amount.inspect}, it should return a BigDecimal" - end - 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 { |a| a.source == self && a.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 +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)) + if !adjustment_amount.is_a?(BigDecimal) + Spree::Deprecation.warn "#{calculator.class.name}#compute returned #{adjustment_amount.inspect}, it should return a BigDecimal" + end + 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 { |a| a.source == self && a.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 diff --git a/core/app/models/spree/promotion/actions/free_shipping.rb b/core/app/models/spree/promotion/actions/free_shipping.rb index dedee69c6e3..2b8c0e1b64a 100644 --- a/core/app/models/spree/promotion/actions/free_shipping.rb +++ b/core/app/models/spree/promotion/actions/free_shipping.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Actions class FreeShipping < Spree::PromotionAction def perform(payload = {}) diff --git a/core/app/models/spree/promotion/rules/first_order.rb b/core/app/models/spree/promotion/rules/first_order.rb index 6537f1c4dff..70e6b6946ff 100644 --- a/core/app/models/spree/promotion/rules/first_order.rb +++ b/core/app/models/spree/promotion/rules/first_order.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules class FirstOrder < PromotionRule attr_reader :user, :email diff --git a/core/app/models/spree/promotion/rules/first_repeat_purchase_since.rb b/core/app/models/spree/promotion/rules/first_repeat_purchase_since.rb index 0a5c58ffe78..e6cd6cd465c 100644 --- a/core/app/models/spree/promotion/rules/first_repeat_purchase_since.rb +++ b/core/app/models/spree/promotion/rules/first_repeat_purchase_since.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules class FirstRepeatPurchaseSince < PromotionRule preference :days_ago, :integer, default: 365 diff --git a/core/app/models/spree/promotion/rules/item_total.rb b/core/app/models/spree/promotion/rules/item_total.rb index 84d0821ace0..3ade555556c 100644 --- a/core/app/models/spree/promotion/rules/item_total.rb +++ b/core/app/models/spree/promotion/rules/item_total.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules # A rule to apply to an order greater than (or greater than or equal to) # a specific amount diff --git a/core/app/models/spree/promotion/rules/one_use_per_user.rb b/core/app/models/spree/promotion/rules/one_use_per_user.rb index db9100d88ce..df826f99f92 100644 --- a/core/app/models/spree/promotion/rules/one_use_per_user.rb +++ b/core/app/models/spree/promotion/rules/one_use_per_user.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules class OneUsePerUser < PromotionRule def applicable?(promotable) diff --git a/core/app/models/spree/promotion/rules/option_value.rb b/core/app/models/spree/promotion/rules/option_value.rb index 4a780363ec8..55b27f3cf02 100644 --- a/core/app/models/spree/promotion/rules/option_value.rb +++ b/core/app/models/spree/promotion/rules/option_value.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules class OptionValue < PromotionRule MATCH_POLICIES = %w(any) diff --git a/core/app/models/spree/promotion/rules/product.rb b/core/app/models/spree/promotion/rules/product.rb index 2a699186ab2..173993ce92d 100644 --- a/core/app/models/spree/promotion/rules/product.rb +++ b/core/app/models/spree/promotion/rules/product.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules # A rule to limit a promotion based on products in the order. Can # require all or any of the products to be present. Valid products diff --git a/core/app/models/spree/promotion/rules/taxon.rb b/core/app/models/spree/promotion/rules/taxon.rb index 4ff7cf919fc..144660254e0 100644 --- a/core/app/models/spree/promotion/rules/taxon.rb +++ b/core/app/models/spree/promotion/rules/taxon.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules class Taxon < PromotionRule has_many :promotion_rule_taxons, class_name: 'Spree::PromotionRuleTaxon', foreign_key: :promotion_rule_id, diff --git a/core/app/models/spree/promotion/rules/user.rb b/core/app/models/spree/promotion/rules/user.rb index 86d9425c006..c6afc3454b1 100644 --- a/core/app/models/spree/promotion/rules/user.rb +++ b/core/app/models/spree/promotion/rules/user.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules class User < PromotionRule has_many :promotion_rule_users, class_name: 'Spree::PromotionRuleUser', diff --git a/core/app/models/spree/promotion/rules/user_logged_in.rb b/core/app/models/spree/promotion/rules/user_logged_in.rb index cd5d21025ff..9a1743293cc 100644 --- a/core/app/models/spree/promotion/rules/user_logged_in.rb +++ b/core/app/models/spree/promotion/rules/user_logged_in.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules class UserLoggedIn < PromotionRule def applicable?(promotable) diff --git a/core/app/models/spree/promotion/rules/user_role.rb b/core/app/models/spree/promotion/rules/user_role.rb index 7c2eb878704..a8cb6ea67dd 100644 --- a/core/app/models/spree/promotion/rules/user_role.rb +++ b/core/app/models/spree/promotion/rules/user_role.rb @@ -1,5 +1,5 @@ module Spree - class Promotion + class Promotion < Spree::Base module Rules class UserRole < PromotionRule preference :role_ids, :array, default: []