Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions core/app/models/spree/calculator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ class Calculator < Spree::Base
# This method calls a compute_<computable> method. must be overriden in concrete calculator.
#
# It should return amount computed based on #calculable and the computable parameter
def compute(computable)
def compute(computable, ...)
# Spree::LineItem -> :compute_line_item
computable_name = computable.class.name.demodulize.underscore
method_name = "compute_#{computable_name}".to_sym
method_name = :"compute_#{computable_name}"
calculator_class = self.class
if respond_to?(method_name)
send(method_name, computable)
send(method_name, computable, ...)
else
raise NotImplementedError, "Please implement '#{method_name}(#{computable_name})' in your calculator: #{calculator_class.name}"
end
Expand Down
12 changes: 11 additions & 1 deletion core/spec/models/spree/calculator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def self.name
"SimpleCalculator"
end

def compute_simple_computable(_)
def compute_simple_computable(_, _options = {})
"computed"
end
end
Expand Down Expand Up @@ -53,5 +53,15 @@ def self.name
expect { subject }.to raise_error NotImplementedError, /Please implement \'compute_line_item\(line_item\)\' in your calculator/
end
end

context "with options" do
let(:order) { double(Spree::Order) }
subject { calculator.compute(computable, order: order) }

it "passes the options to compute_simple_computable" do
expect(calculator).to receive(:compute_simple_computable).with(computable, order: order)
subject
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module SolidusPromotions
module AdjustedAmountByLane
def adjusted_amount_by_lanes(lanes)
amount + adjustment_amount_by_lanes(lanes)
end

def adjustment_amount_by_lanes(lanes)
adjustments.select do |adjustment|
adjustment.promotion? && adjustment.source.promotion.lane.in?(lanes)
end.sum(&:amount)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ def round_to_currency(number, currency)
currency_exponent = ::Money::Currency.find(currency).exponent
number.round(currency_exponent)
end

def adjusted_amount_before_current_lane(item)
item.adjusted_amount_by_lanes(promotion.previous_lanes)
end

delegate :promotion, to: :calculable
end
end
end
8 changes: 4 additions & 4 deletions promotions/app/models/solidus_promotions/benefit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ def can_discount?(object)
"`SolidusPromotions::Benefits::LineItemBenefit` or `SolidusPromotions::Benefits::ShipmentBenefit` modules"
end

def discount(adjustable)
amount = compute_amount(adjustable)
def discount(adjustable, ...)
amount = compute_amount(adjustable, ...)
return if amount.zero?
ItemDiscount.new(
item: adjustable,
Expand All @@ -41,8 +41,8 @@ def discount(adjustable)
end

# Ensure a negative amount which does not exceed the object's amount
def compute_amount(adjustable)
promotion_amount = calculator.compute(adjustable) || Spree::ZERO
def compute_amount(adjustable, ...)
promotion_amount = calculator.compute(adjustable, ...) || Spree::ZERO
[adjustable.discountable_amount, promotion_amount.abs].min * -1
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def initialize(line_item)
end

def discountable_amount
return Spree::ZERO if @line_item.quantity.zero?
@line_item.discountable_amount / @line_item.quantity.to_d
end
alias_method :amount, :discountable_amount
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module SolidusPromotions
module Benefits
class AdjustPrice < Benefit
def can_discount?(object)
object.is_a?(Spree::LineItem) || object.is_a?(Spree::Price)
end

def possible_conditions
super + SolidusPromotions.config.price_conditions
end

def level
:line_item
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,41 @@ module Calculators
class FlatRate < Spree::Calculator
include PromotionCalculator

preference :amount, :decimal, default: 0
preference :amount, :decimal, default: Spree::ZERO
preference :currency, :string, default: -> { Spree::Config[:currency] }

def compute(object = nil)
currency = object.order.currency
if object && preferred_currency.casecmp(currency).zero?
preferred_amount
def compute_item(item)
currency = item.order.currency
if item && preferred_currency.casecmp(currency).zero?
compute_for_amount(item.discountable_amount)
else
0
Spree::ZERO
end
end
alias_method :compute_line_item, :compute_item
alias_method :compute_shipment, :compute_item
alias_method :compute_shipping_rate, :compute_item

def compute_price(price, options = {})
order = options[:order]
quantity = options[:quantity]
return preferred_amount unless order
return Spree::ZERO if order.currency != preferred_currency
line_item_with_variant = order.line_items.detect { _1.variant == price.variant }
desired_extra_amount = quantity * price.discountable_amount
current_discounted_amount = line_item_with_variant ? adjusted_amount_before_current_lane(line_item_with_variant) : Spree::ZERO
round_to_currency(
(compute_for_amount(current_discounted_amount + desired_extra_amount.to_f) -
compute_for_amount(current_discounted_amount)) / quantity,
preferred_currency
)
end

private

def compute_for_amount(amount)
[amount, preferred_amount].min
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,34 @@ class FlexiRate < Spree::Calculator
preference :max_items, :integer, default: 0
preference :currency, :string, default: -> { Spree::Config[:currency] }

def compute(object)
items_count = object.quantity
items_count = [items_count, preferred_max_items].min unless preferred_max_items.zero?
def compute_line_item(line_item)
compute_for_quantity(line_item.quantity)
end

def compute_price(price, options = {})
order = options[:order]
desired_quantity = options[:quantity] || 0
return Spree::ZERO if desired_quantity.zero?

already_ordered_quantity = if order
order.line_items.detect do |line_item|
line_item.variant == price.variant
end&.quantity || 0
else
0
end
possible_discount = compute_for_quantity(already_ordered_quantity + desired_quantity)
existing_discount = compute_for_quantity(already_ordered_quantity)
round_to_currency(
(possible_discount - existing_discount) / desired_quantity,
price.currency
)
end

private

def compute_for_quantity(quantity)
items_count = preferred_max_items.zero? ? quantity : [quantity, preferred_max_items].min

return Spree::ZERO if items_count == 0

Expand Down
17 changes: 15 additions & 2 deletions promotions/app/models/solidus_promotions/calculators/percent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,21 @@ class Percent < Spree::Calculator

preference :percent, :decimal, default: 0

def compute(object)
round_to_currency(object.discountable_amount * preferred_percent / 100, object.order.currency)
def compute_item(item)
compute_with_currency(item, item.order.currency)
end
alias_method :compute_line_item, :compute_item
alias_method :compute_shipment, :compute_item
alias_method :compute_shipping_rate, :compute_item

def compute_price(price, _options = {})
compute_with_currency(price, price.currency)
end

private

def compute_with_currency(item, currency)
round_to_currency(item.discountable_amount * preferred_percent / 100, currency)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ class LineItemOptionValue < Condition

preference :eligible_values, :hash

def eligible?(line_item, _options = {})
pid = line_item.product.id
ovids = line_item.variant.option_values.pluck(:id)
def eligible?(line_item_or_price, _options = {})
pid = line_item_or_price.variant.product_id
ovids = line_item_or_price.variant.option_values.pluck(:id)

product_ids.include?(pid) && (value_ids(pid) & ovids).present?
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ def preload_relations
[:products]
end

def eligible?(line_item, _options = {})
order_includes_product = product_ids.include?(line_item.variant.product_id)
success = inverse? ? !order_includes_product : order_includes_product
def eligible?(line_item_or_price, _options = {})
item_matches_product = product_ids.include?(line_item_or_price.variant.product_id)
success = inverse? ? !item_matches_product : item_matches_product

unless success
message_code = inverse? ? :has_excluded_product : :no_applicable_products
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ def preload_relations
[:taxons]
end

def eligible?(line_item, _options = {})
def eligible?(line_item_or_price, _options = {})
found = Spree::Classification.where(
product_id: line_item.variant.product_id,
product_id: line_item_or_price.variant.product_id,
taxon_id: condition_taxon_ids_with_children
).exists?

Expand Down
54 changes: 54 additions & 0 deletions promotions/app/models/solidus_promotions/product_discounter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

module SolidusPromotions
class ProductDiscounter
attr_reader :order, :product, :pricing_options, :promotions, :quantity

def initialize(product:, order:, pricing_options:, quantity: 1)
@product = product
@order = order
@pricing_options = pricing_options
@quantity = quantity
@promotions = SolidusPromotions::LoadPromotions.new(order:).call
end

def call
if product.has_variants?
product.variants.each { |variant| discount_variant(variant) }
else
discount_variant(product.master)
end
end

private

def discount_variant(variant)
variant.discountable_price = variant.price_for_options(pricing_options)

return unless variant.product.promotionable?

SolidusPromotions::Promotion.ordered_lanes.each_key do |lane|
lane_promotions = promotions.select { |promotion| promotion.lane == lane }
lane_benefits = eligible_benefits_for_promotable(lane_promotions.flat_map(&:benefits), order)
discounts = generate_discounts(lane_benefits, variant.discountable_price)
chosen_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
variant.discountable_price.current_discounts.concat(chosen_discounts)
end
end

def eligible_benefits_for_promotable(possible_benefits, promotable)
possible_benefits.select do |candidate|
candidate.eligible_by_applicable_conditions?(promotable)
end
end

def generate_discounts(possible_benefits, item)
eligible_benefits = eligible_benefits_for_promotable(possible_benefits, item)
eligible_benefits.select do |benefit|
benefit.can_discount?(item)
end.map do |benefit|
benefit.discount(item, order:, quantity:)
end.compact
end
end
end
7 changes: 7 additions & 0 deletions promotions/app/models/solidus_promotions/promotion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ def usage_limit_exceeded?(excluded_orders: [])
usage_count(excluded_orders: excluded_orders) >= usage_limit
end

def previous_lanes
current_lane_index = self.class.ordered_lanes.detect { |name, _index| lane == name }.last
self.class.ordered_lanes.filter_map do |name, index|
name if index < current_lane_index
end
end

def not_expired?(time = Time.current)
!expired?(time)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ def reset_quantity_setter

Spree::LineItem.prepend self
Spree::LineItem.prepend SolidusPromotions::DiscountableAmount
Spree::LineItem.prepend SolidusPromotions::AdjustedAmountByLane
end
end
14 changes: 14 additions & 0 deletions promotions/app/patches/models/solidus_promotions/price_patch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module SolidusPromotions
module PricePatch
def self.prepended(base)
base.alias_method :discounted_amount, :discountable_amount
base.alias_method :discounts, :current_discounts
base.money_methods :discounted_amount
end

Spree::Price.prepend SolidusPromotions::DiscountableAmount
Spree::Price.prepend self
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module SolidusPromotions
module ProductPatch
extend ActiveSupport::Concern

def self.prepended(base)
base.delegate :discounted_price, :undiscounted_price, :price_discounts, to: :master
end
Spree::Product.prepend self
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ def reset_current_discounts
end

Spree::Shipment.prepend self
Spree::Shipment.prepend SolidusPromotions::AdjustedAmountByLane
end
end
Loading