Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ def current_discounts=(args)
@current_discounts = args
end

def current_lane_discounts
@current_lane_discounts ||= []
end

def current_lane_discounts=(args)
@current_lane_discounts = args
end

def reset_current_discounts
@current_discounts = []
end
Expand Down
19 changes: 18 additions & 1 deletion promotions/app/models/solidus_promotions/benefit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def discount(adjustable, ...)
# @return [BigDecimal] a negative amount suitable for creating an adjustment
def compute_amount(adjustable, ...)
promotion_amount = calculator.compute(adjustable, ...) || Spree::ZERO
[adjustable.discountable_amount, promotion_amount.abs].min * -1
[remaining_discountable_amount_for_item(adjustable), promotion_amount.abs].min * -1
end

# Builds the localized label for adjustments created by this benefit.
Expand Down Expand Up @@ -231,6 +231,23 @@ def possible_conditions

private

def remaining_discountable_amount_for_item(item)
return item.discountable_amount unless promotion.amount_limit
remaining_amount_limit = promotion.amount_limit + promotion_total_for_order(item.order)
return Spree::ZERO if remaining_amount_limit <= Spree::ZERO
[item.discountable_amount, remaining_amount_limit].min
end

def promotion_total_for_order(order)
current_lane_discounts_for_order(order).sum(&:amount)
end

def current_lane_discounts_for_order(order)
(order.line_items + order.shipments).flat_map(&:current_lane_discounts).select do |item_discount|
item_discount.source.in?(promotion.benefits)
end
end

# Prevents destroying a benefit when it has adjustments on completed orders.
#
# Adds an error and aborts the destroy callback chain when such adjustments exist.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ def call
lane_promotions = promotions.select { |promotion| promotion.lane == lane }
lane_benefits = eligible_benefits_for_promotable(lane_promotions.flat_map(&:benefits), order)
perform_order_benefits(lane_benefits, lane) unless dry_run
line_item_discounts = adjust_line_items(lane_benefits)
shipment_discounts = adjust_shipments(lane_benefits)
shipping_rate_discounts = adjust_shipping_rates(lane_benefits)
(line_item_discounts + shipment_discounts + shipping_rate_discounts).each do |item, chosen_discounts|
item.current_discounts.concat(chosen_discounts)
discounted_line_items = adjust_line_items(lane_benefits)
discounted_shipments = adjust_shipments(lane_benefits)
discounted_shipping_rates = adjust_shipping_rates(lane_benefits)
(discounted_line_items + discounted_shipments + discounted_shipping_rates).each do |discountable|
discountable.current_discounts.concat(discountable.current_lane_discounts)
discountable.current_lane_discounts.clear
end
end

Expand Down Expand Up @@ -50,15 +51,17 @@ def adjust_line_items(benefits)

discounts = generate_discounts(benefits, line_item)
chosen_item_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
[line_item, chosen_item_discounts]
line_item.current_lane_discounts += chosen_item_discounts
line_item
end
end

def adjust_shipments(benefits)
order.shipments.map do |shipment|
discounts = generate_discounts(benefits, shipment)
chosen_item_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
[shipment, chosen_item_discounts]
shipment.current_lane_discounts += chosen_item_discounts
shipment
end
end

Expand All @@ -68,7 +71,8 @@ def adjust_shipping_rates(benefits)

discounts = generate_discounts(benefits, rate)
chosen_item_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
[rate, chosen_item_discounts]
rate.current_lane_discounts += chosen_item_discounts
rate
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddAmountLimitToPromotions < ActiveRecord::Migration[7.0]
def change
add_column :solidus_promotions_promotions, :amount_limit, :decimal, precision: 10, scale: 2, null: true
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
<% end %>
<% end %>

<%= f.field_container :overall_amount_limit do %>
<%= f.label :amount_limit %>
<%= f.number_field :amount_limit, class: 'fullwidth' %>
<% end %>

<div id="starts_at_field" class="form-group">
<%= f.label :starts_at %>
<%= f.field_hint :starts_at %>
Expand Down
195 changes: 195 additions & 0 deletions promotions/spec/models/promotion/promotion_with_amount_limit_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe SolidusPromotions::Calculators::Percent, type: :model do
describe "#compute_line_item" do
let(:promotion) { create(:solidus_promotion, amount_limit:, apply_automatically: true) }
let(:order) { create(:order) }

let!(:taxon_electronics) { create(:taxon, name: "Electronics") }
let!(:taxon_clothing) { create(:taxon, name: "Clothing") }

let!(:product_laptop) { create(:product, price: 1000, taxons: [taxon_electronics]) }
let!(:product_phone) { create(:product, price: 800, taxons: [taxon_electronics]) }
let!(:product_shirt) { create(:product, price: 50, taxons: [taxon_clothing]) }
let!(:product_jacket) { create(:product, price: 200, taxons: [taxon_clothing]) }

let(:calculator_electronics) { described_class.new(preferred_percent: 20) }
let(:calculator_clothing) { described_class.new(preferred_percent: 30) }

let(:amount_limit) { 250 }
let!(:benefit_electronics) do
SolidusPromotions::Benefits::AdjustLineItem.create!(
calculator: calculator_electronics,
promotion: promotion,
conditions: [SolidusPromotions::Conditions::LineItemTaxon.new(taxons: [taxon_electronics])]
)
end

let!(:benefit_clothing) do
SolidusPromotions::Benefits::AdjustLineItem.create!(
calculator: calculator_clothing,
promotion: promotion,
conditions: [SolidusPromotions::Conditions::LineItemTaxon.new(taxons: [taxon_clothing])]
)
end

context "when both benefits stay within cap" do
before do
order.contents.add(product_laptop.master, 1)
order.contents.add(product_shirt.master, 1)
end

it "applies full discounts" do
expect(order.promo_total).to eq(-215)
expect(order.line_items.find_by(variant: product_laptop.master).adjustment_total).to eq(-200)
expect(order.line_items.find_by(variant: product_shirt.master).adjustment_total).to eq(-15)
end
end

context "when electronics benefit exhausts the cap" do
before do
order.contents.add(product_laptop.master, 1)
order.contents.add(product_phone.master, 1)
order.contents.add(product_shirt.master, 1)
end

it "caps electronics at $250 and gives clothing $0" do
expect(order.promo_total).to eq(-250)
laptop_discount = order.line_items.find_by(variant: product_laptop.master).adjustment_total
phone_discount = order.line_items.find_by(variant: product_phone.master).adjustment_total
expect(laptop_discount + phone_discount).to eq(-250)
expect(order.line_items.find_by(variant: product_shirt.master).adjustment_total).to eq(0)
end
end

context "when clothing benefit is applied first" do
before do
order.contents.add(product_shirt.master, 1)
order.contents.add(product_jacket.master, 1)
order.contents.add(product_phone.master, 1)
end

it "applies clothing discounts then remaining cap to electronics" do
shirt_discount = order.line_items.find_by(variant: product_shirt.master).adjustment_total
jacket_discount = order.line_items.find_by(variant: product_jacket.master).adjustment_total
phone_discount = order.line_items.find_by(variant: product_phone.master).adjustment_total

expect(shirt_discount).to eq(-15)
expect(jacket_discount).to eq(-60)
expect(phone_discount).to eq(-160)
expect(order.promo_total).to eq(-235)
end
end

context "when cap is exactly reached" do
before do
order.contents.add(product_laptop.master, 1)
order.contents.add(product_shirt.master, 1)
order.contents.add(product_jacket.master, 1)
end

it "stops at exactly $250" do
expect(order.promo_total).to eq(-250)
end
end

context "when cap is exceeded by both benefits combined" do
before do
order.contents.add(product_laptop.master, 2)
order.contents.add(product_jacket.master, 2)
end

it "distributes cap proportionally" do
expect(order.promo_total).to eq(-250)
end
end

context "when one line item alone would exceed cap" do
let!(:expensive_laptop) { create(:product, price: 2000, taxons: [taxon_electronics]) }

before do
order.contents.add(expensive_laptop.master, 1)
end

it "caps single item discount at $250" do
expect(order.promo_total).to eq(-250)
end
end

context "with small purchases" do
before do
order.contents.add(product_shirt.master, 2)
end

it "applies full discount when well below cap" do
expect(order.promo_total).to eq(-30)
end
end

context "when cap is zero" do
let(:amount_limit) { 0 }

before do
order.contents.add(product_laptop.master, 1)
end

it "applies no discount" do
expect(order.promo_total).to eq(0)
end
end

context "when cap is negative" do
let(:amount_limit) { -100 }

before do
order.contents.add(product_laptop.master, 1)
end

it "applies no discount" do
expect(order.promo_total).to eq(0)
end
end

context "when order has no applicable items" do
let!(:product_other) { create(:product, price: 100) }

before do
order.contents.add(product_other.master, 1)
end

it "applies no discount" do
expect(order.promo_total).to eq(0)
end
end

context "with multiple quantities of same product" do
before do
order.contents.add(product_shirt.master, 5)
order.contents.add(product_jacket.master, 2)
end

it "calculates correctly across quantities" do
total_clothing = (50 * 5) + (200 * 2)
expected_discount = total_clothing * 0.30
expect(order.promo_total).to eq(-expected_discount)
end
end

context "when benefits have different percentages approaching cap" do
before do
order.contents.add(product_phone.master, 1)
order.contents.add(product_jacket.master, 3)
end

it "applies both benefits up to shared cap" do
phone_full = 800 * 0.20
jacket_full = 200 * 3 * 0.30
total_full = phone_full + jacket_full

expect(order.promo_total).to eq(-[total_full, 250].min)
end
end
end
end
2 changes: 2 additions & 0 deletions promotions/spec/models/solidus_promotions/promotion_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
it { is_expected.to have_many(:order_promotions).dependent(:destroy) }
it { is_expected.to have_many(:code_batches).dependent(:destroy) }

it { is_expected.to respond_to(:amount_limit) }

describe "lane" do
it { is_expected.to respond_to(:lane) }

Expand Down
Loading