From 9797e3a0955f1ee7d234be01d04d1921bebbeda4 Mon Sep 17 00:00:00 2001 From: Kimsrung Lov Date: Fri, 8 Dec 2023 10:59:30 +0700 Subject: [PATCH] Close #769 discount maximum cap --- .../flat_percent_item_total_decorator.rb | 16 +++ .../percent_on_line_item_decorator.rb | 16 +++ .../order_updater_decorator.rb | 17 +++ .../create_date_specific_item_adjustments.rb | 7 +- .../_adjustments/adjustments.html.erb.deface | 27 +++++ .../_form/cap_field.html.erb.deface | 13 ++ .../_create_adjustment/cap.html.erb.deface | 11 ++ .../cap.html.erb.deface | 11 ++ ...te_date_specific_item_adjustments.html.erb | 9 ++ ...231114015730_add_cap_to_spree_promotion.rb | 5 + ...31117032031_add_cap_to_spree_calculator.rb | 5 + .../flat_percent_item_total_spec.rb | 47 ++++++++ .../calculator/percent_on_line_item_spec.rb | 27 +++++ spec/models/spree/order_spec.rb | 114 ++++++++++++++++++ ...ate_date_specific_item_adjustments_spec.rb | 78 ++++++++++-- 15 files changed, 390 insertions(+), 13 deletions(-) create mode 100644 app/models/spree_cm_commissioner/calculator/flat_percent_item_total_decorator.rb create mode 100644 app/models/spree_cm_commissioner/calculator/percent_on_line_item_decorator.rb create mode 100644 app/overrides/spree/admin/orders/_adjustments/adjustments.html.erb.deface create mode 100644 app/overrides/spree/admin/promotions/_form/cap_field.html.erb.deface create mode 100644 app/overrides/spree/admin/promotions/actions/_create_adjustment/cap.html.erb.deface create mode 100644 app/overrides/spree/admin/promotions/actions/_create_item_adjustments/cap.html.erb.deface create mode 100644 db/migrate/20231114015730_add_cap_to_spree_promotion.rb create mode 100644 db/migrate/20231117032031_add_cap_to_spree_calculator.rb create mode 100644 spec/models/spree/calculator/flat_percent_item_total_spec.rb create mode 100644 spec/models/spree/calculator/percent_on_line_item_spec.rb diff --git a/app/models/spree_cm_commissioner/calculator/flat_percent_item_total_decorator.rb b/app/models/spree_cm_commissioner/calculator/flat_percent_item_total_decorator.rb new file mode 100644 index 000000000..123d51375 --- /dev/null +++ b/app/models/spree_cm_commissioner/calculator/flat_percent_item_total_decorator.rb @@ -0,0 +1,16 @@ +module SpreeCmCommissioner + module Calculator + module FlatPercentItemTotalDecorator + def compute(object) + computed_amount = (object.amount * preferred_flat_percent / 100).round(2) + computed_amount = cap if cap && computed_amount > cap + + [computed_amount, object.amount].min + end + end + end +end + +unless Spree::Calculator::FlatPercentItemTotal.included_modules.include?(SpreeCmCommissioner::Calculator::FlatPercentItemTotalDecorator) + Spree::Calculator::FlatPercentItemTotal.prepend(SpreeCmCommissioner::Calculator::FlatPercentItemTotalDecorator) +end diff --git a/app/models/spree_cm_commissioner/calculator/percent_on_line_item_decorator.rb b/app/models/spree_cm_commissioner/calculator/percent_on_line_item_decorator.rb new file mode 100644 index 000000000..eb8ea78fd --- /dev/null +++ b/app/models/spree_cm_commissioner/calculator/percent_on_line_item_decorator.rb @@ -0,0 +1,16 @@ +module SpreeCmCommissioner + module Calculator + module PercentOnLineItemDecorator + def compute(object) + computed_amount = (object.amount * preferred_percent / 100).round(2) + computed_amount = cap if cap && computed_amount > cap + + [computed_amount, object.amount].min + end + end + end +end + +unless Spree::Calculator::PercentOnLineItem.included_modules.include?(SpreeCmCommissioner::Calculator::PercentOnLineItemDecorator) + Spree::Calculator::PercentOnLineItem.prepend(SpreeCmCommissioner::Calculator::PercentOnLineItemDecorator) +end diff --git a/app/models/spree_cm_commissioner/order_updater_decorator.rb b/app/models/spree_cm_commissioner/order_updater_decorator.rb index 9e902e0fe..48280583e 100644 --- a/app/models/spree_cm_commissioner/order_updater_decorator.rb +++ b/app/models/spree_cm_commissioner/order_updater_decorator.rb @@ -5,6 +5,23 @@ def update_item_total order.item_total = line_items.sum(&:amount) update_order_total end + + # override + def update_adjustment_total + super + + promotion_adjustment = adjustments.promotion.eligible.first + + return if promotion_adjustment.blank? # Check if the order has a promotion adjustment + + promotion = promotion_adjustment.source.promotion + + return unless promotion&.cap && order.adjustment_total.abs > promotion&.cap # Check if cap exist + + order.adjustment_total = order.promo_total = promotion.cap * -1 + + update_order_total + end end end diff --git a/app/models/spree_cm_commissioner/promotion/actions/create_date_specific_item_adjustments.rb b/app/models/spree_cm_commissioner/promotion/actions/create_date_specific_item_adjustments.rb index 3aa27b319..01ac2370a 100644 --- a/app/models/spree_cm_commissioner/promotion/actions/create_date_specific_item_adjustments.rb +++ b/app/models/spree_cm_commissioner/promotion/actions/create_date_specific_item_adjustments.rb @@ -36,8 +36,11 @@ def compute_amount(line_item) def compute_line_item_amount(line_item) line_item.date_range.filter_map do |date| if promotion.date_eligible?(date) - line_item_per_unit = Struct.new(:amount, :currency, :quantity) - .new(line_item.amount_per_date_unit, line_item.currency, line_item.quantity) + line_item_per_unit = Struct.new(:amount, :currency, :quantity, :price) + .new(line_item.amount_per_date_unit, line_item.currency, + line_item.quantity, line_item.price + ) + compute(line_item_per_unit) end end.sum diff --git a/app/overrides/spree/admin/orders/_adjustments/adjustments.html.erb.deface b/app/overrides/spree/admin/orders/_adjustments/adjustments.html.erb.deface new file mode 100644 index 000000000..e26639e07 --- /dev/null +++ b/app/overrides/spree/admin/orders/_adjustments/adjustments.html.erb.deface @@ -0,0 +1,27 @@ + +<% eligible_adjustments = adjustments.eligible%> + + <% eligible_adjustments.group_by(&:label).each do |label, adjustments| %> + + + <%= label %>: + + + + <%= Spree::Money.new( + adjustments.sum(&:amount), + currency: adjustments.first.order.try(:currency) + ) %> + + <% if eligible_adjustments.promotion.present? %> + ~ (Cap + <%= Spree::Money.new( + eligible_adjustments.promotion.first.source.calculator&.cap, + currency: adjustments.first.order.try(:currency) + ) %>) + <%end%> + + + + <% end %> + \ No newline at end of file diff --git a/app/overrides/spree/admin/promotions/_form/cap_field.html.erb.deface b/app/overrides/spree/admin/promotions/_form/cap_field.html.erb.deface new file mode 100644 index 000000000..b36edba9a --- /dev/null +++ b/app/overrides/spree/admin/promotions/_form/cap_field.html.erb.deface @@ -0,0 +1,13 @@ + + +
+ <%= f.field_container :cap do %> + <%= f.label :cap %> +
+
+ <%= currency_symbol(current_currency) %> +
+ <%= f.text_field :cap, value: number_to_currency(@promotion.cap, unit: ''), class: 'form-control' %> +
+ <% end %> +
\ No newline at end of file diff --git a/app/overrides/spree/admin/promotions/actions/_create_adjustment/cap.html.erb.deface b/app/overrides/spree/admin/promotions/actions/_create_adjustment/cap.html.erb.deface new file mode 100644 index 000000000..fca86004f --- /dev/null +++ b/app/overrides/spree/admin/promotions/actions/_create_adjustment/cap.html.erb.deface @@ -0,0 +1,11 @@ + + +
+ <%= label_tag "#{param_prefix}[calculator_attributes][cap]", "Cap" %> +
+
+ <%= currency_symbol(current_currency) %> +
+ <%= text_field_tag "#{param_prefix}[calculator_attributes][cap]", number_to_currency(promotion_action.calculator.cap, unit: ''), class: 'form-control' %> +
+
\ No newline at end of file diff --git a/app/overrides/spree/admin/promotions/actions/_create_item_adjustments/cap.html.erb.deface b/app/overrides/spree/admin/promotions/actions/_create_item_adjustments/cap.html.erb.deface new file mode 100644 index 000000000..fca86004f --- /dev/null +++ b/app/overrides/spree/admin/promotions/actions/_create_item_adjustments/cap.html.erb.deface @@ -0,0 +1,11 @@ + + +
+ <%= label_tag "#{param_prefix}[calculator_attributes][cap]", "Cap" %> +
+
+ <%= currency_symbol(current_currency) %> +
+ <%= text_field_tag "#{param_prefix}[calculator_attributes][cap]", number_to_currency(promotion_action.calculator.cap, unit: ''), class: 'form-control' %> +
+
\ No newline at end of file diff --git a/app/views/spree/admin/promotions/actions/_create_date_specific_item_adjustments.html.erb b/app/views/spree/admin/promotions/actions/_create_date_specific_item_adjustments.html.erb index 7d0bbc720..b77241f87 100644 --- a/app/views/spree/admin/promotions/actions/_create_date_specific_item_adjustments.html.erb +++ b/app/views/spree/admin/promotions/actions/_create_date_specific_item_adjustments.html.erb @@ -20,6 +20,15 @@ <%= hidden_field_tag "#{param_prefix}[calculator_attributes][id]", promotion_action.calculator.id %> <% end %> +
+ <%= label_tag "#{param_prefix}[calculator_attributes][cap]", "Cap" %> +
+
+ <%= currency_symbol(current_currency) %> +
+ <%= text_field_tag "#{param_prefix}[calculator_attributes][cap]", number_to_currency(promotion_action.calculator.cap, unit: ''), class: 'form-control' %> +
+
<% if promotion_action.calculator.respond_to?(:preferences) %>
diff --git a/db/migrate/20231114015730_add_cap_to_spree_promotion.rb b/db/migrate/20231114015730_add_cap_to_spree_promotion.rb new file mode 100644 index 000000000..d6b77539e --- /dev/null +++ b/db/migrate/20231114015730_add_cap_to_spree_promotion.rb @@ -0,0 +1,5 @@ +class AddCapToSpreePromotion < ActiveRecord::Migration[7.0] + def change + add_column :spree_promotions, :cap, :float, if_not_exists: true + end +end diff --git a/db/migrate/20231117032031_add_cap_to_spree_calculator.rb b/db/migrate/20231117032031_add_cap_to_spree_calculator.rb new file mode 100644 index 000000000..7b1445751 --- /dev/null +++ b/db/migrate/20231117032031_add_cap_to_spree_calculator.rb @@ -0,0 +1,5 @@ +class AddCapToSpreeCalculator < ActiveRecord::Migration[7.0] + def change + add_column :spree_calculators, :cap, :float, if_not_exists: true + end +end diff --git a/spec/models/spree/calculator/flat_percent_item_total_spec.rb b/spec/models/spree/calculator/flat_percent_item_total_spec.rb new file mode 100644 index 000000000..46625ffcb --- /dev/null +++ b/spec/models/spree/calculator/flat_percent_item_total_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +RSpec.describe Spree::Calculator::FlatPercentItemTotal, type: :model do + let(:calculator) { Spree::Calculator::FlatPercentItemTotal.new } + let(:order) { create(:order) } + + before { allow(calculator).to receive_messages preferred_flat_percent: 10 } + + describe '#compute' do + context 'without cap' do + it 'rounds result correctly' do + allow(order).to receive_messages amount: 31.08 + expect(calculator.compute(order)).to eq 3.11 + + allow(order).to receive_messages amount: 31.00 + expect(calculator.compute(order)).to eq 3.10 + end + + it 'returns object.amount if computed amount is greater' do + allow(order).to receive_messages amount: 30.00 + allow(calculator).to receive_messages preferred_flat_percent: 110 + + expect(calculator.compute(order)).to eq 30.0 + end + end + + context 'with cap' do + before do + calculator.update(cap: 10) + end + + it 'returns cap if computed amount is greater' do + allow(order).to receive_messages amount: 50.00 + allow(calculator).to receive_messages preferred_flat_percent: 50 + + expect(calculator.compute(order)).to eq 10.00 + end + + it 'returns computed amount if cap is greater' do + allow(order).to receive_messages amount: 15.00 + allow(calculator).to receive_messages preferred_flat_percent: 50 + + expect(calculator.compute(order)).to eq 7.50 + end + end + end +end \ No newline at end of file diff --git a/spec/models/spree/calculator/percent_on_line_item_spec.rb b/spec/models/spree/calculator/percent_on_line_item_spec.rb new file mode 100644 index 000000000..523fc88b1 --- /dev/null +++ b/spec/models/spree/calculator/percent_on_line_item_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +RSpec.describe Spree::Calculator::PercentOnLineItem, type: :model do + let(:calculator) { Spree::Calculator::PercentOnLineItem.new } + let(:line_item) { create(:line_item) } + + before { allow(calculator).to receive_messages preferred_percent: 10 } + + describe '#compute' do + context 'without cap' do + it 'rounds result correctly' do + allow(line_item).to receive_messages amount: 31.08 + expect(calculator.compute(line_item)).to eq 3.11 + + allow(line_item).to receive_messages amount: 31.00 + expect(calculator.compute(line_item)).to eq 3.10 + end + + it 'returns object.amount if computed amount is greater' do + allow(line_item).to receive_messages amount: 30.00 + allow(calculator).to receive_messages preferred_percent: 110 + + expect(calculator.compute(line_item)).to eq 30.0 + end + end + end +end \ No newline at end of file diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 9f67744b2..6ca3b063e 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -295,6 +295,120 @@ end end + describe '#FlatPercentItemTotal' do + let(:calculator) { Spree::Calculator::FlatPercentItemTotal.create(preferred_flat_percent: 50) } + let!(:promotion) { create(:promotion_with_order_adjustment, starts_at: Date.today - 1.day, expires_at: Date.today + 5.day, match_policy: "any")} + let!(:order) { create(:order_with_totals, line_items_price: 30, total: 30, item_total: 30) } + let!(:promotion_action) { Spree::Promotion::Actions::CreateAdjustment.create!(promotion: promotion, calculator: calculator) } + let!(:order_promotion) { create(:order_promotion, order: order, promotion: promotion)} + let!(:order_adjustment) { create(:adjustment, order: order, source: promotion_action, adjustable: order)} + + context 'when order has a promotion with a cap' do + before do + calculator.update(cap: 20) + end + + it 'does not adjust promotion order adjustment if within the cap' do + order.update_with_updater! + order.reload + + expect(order.item_total.to_f).to eq(30.0) + expect(order.total.to_f).to eq(15.0) + expect(order.adjustment_total.to_f).to eq(-15.0) + end + + it 'adjusts promotion order adjustment to cap if exceeded' do + create(:line_item, order: order, price: 100) + order.reload.update_with_updater! + order.reload + + expect(order.item_total.to_f).to eq(130.0) + expect(order.total.to_f).to eq(110.0) + expect(order.adjustment_total.to_f).to eq(-20.0) + end + end + + context 'when order has a promotion without a cap' do + it 'adjust promotion order adjustment' do + order.reload.update_with_updater! + + expect(order.item_total.to_f).to eq(30.0) + expect(order.total.to_f).to eq(15.0) + expect(order.adjustment_total.to_f).to eq(-15.0) + end + + it 'adjusts promotion order adjustment 50%' do + create(:line_item, order: order, price: 100) + + order.reload.update_with_updater! + order.update_totals + + expect(order.item_total.to_f).to eq(130.0) + expect(order.total.to_f).to eq(65.0) + expect(order.adjustment_total.to_f).to eq(-65.0) + end + end + end + + describe '#PercentOnLineItem' do + let(:calculator) { Spree::Calculator::PercentOnLineItem.create(preferred_percent: 20) } + let!(:promotion) { create(:promotion_with_order_adjustment, starts_at: Date.today - 1.day, expires_at: Date.today + 5.day, match_policy: "any")} + let!(:order) { create(:order) } + let!(:line_item) { create(:line_item, order: order, quantity: 2, price: 30.0)} + let!(:promotion_action) { Spree::Promotion::Actions::CreateItemAdjustments.create!(promotion: promotion, calculator: calculator) } + let!(:order_promotion) { create(:order_promotion, order: order, promotion: promotion)} + let!(:order_adjustment) { create(:adjustment, order: order, source: promotion_action, adjustable: line_item)} + + context 'when order has a promotion with a cap' do + before do + calculator.update(cap: 20) + end + + it 'does not adjust promotion line item if within the cap' do + order.update_with_updater! + order.reload + + expect(order.item_total.to_f).to eq(60.0) + expect(order.total.to_f).to eq(48.0) + expect(order.adjustment_total.to_f).to eq(-12.0) + end + + it 'adjusts promotion line item to cap if exceeded' do + line_item2 = create(:line_item, order: order, quantity: 2, price: 60) + create(:adjustment, order: order, source: promotion_action, adjustable: line_item2) + order.reload.update_with_updater! + order.reload + + # line_item1 discount 60x0.2 = 12 < cap 20, line_item2 discount 120x0.2 = 24 > cap 20 + expect(order.item_total.to_f).to eq(180.0) + expect(order.total.to_f).to eq(148.0) + expect(order.adjustment_total.to_f).to eq(-32.0) + end + end + + context 'when order has a promotion without a cap' do + it 'adjust promotion line item adjustment' do + order.reload.update_with_updater! + + expect(order.item_total.to_f).to eq(60.0) + expect(order.total.to_f).to eq(48.0) + expect(order.adjustment_total.to_f).to eq(-12.0) + end + + it 'adjusts promotion line item adjustment 20% ' do + line_item2 = create(:line_item, order: order, quantity: 2, price: 60) + create(:adjustment, order: order, source: promotion_action, adjustable: line_item2) + + order.reload.update_with_updater! + order.update_totals + + # line_item1 discount 60x0.2 = 12 , line_item2 discount 120x0.2 = 24 + expect(order.item_total.to_f).to eq(180.0) + expect(order.total.to_f).to eq(144.0) + expect(order.adjustment_total.to_f).to eq(-36.0) + end + end + end describe '#valid_promotion_ids' do let!(:order) { create(:order, total: 100) } let!(:promotion) { create(:promotion, :with_order_adjustment) } diff --git a/spec/models/spree_cm_commissioner/promotion/actions/create_date_specific_item_adjustments_spec.rb b/spec/models/spree_cm_commissioner/promotion/actions/create_date_specific_item_adjustments_spec.rb index 1b9b8c7ca..c5e138649 100644 --- a/spec/models/spree_cm_commissioner/promotion/actions/create_date_specific_item_adjustments_spec.rb +++ b/spec/models/spree_cm_commissioner/promotion/actions/create_date_specific_item_adjustments_spec.rb @@ -38,20 +38,76 @@ let!(:promotion) { create(:promotion, promotion_rules: [rule]) } describe '#compute_line_item_amount' do - it 'return computed amount of -10% = 1$ for a matched date: 2023-01-12' do - subject = described_class.new(calculator: ten_percent_off_calculator, promotion: promotion) - amount = subject.compute_line_item_amount(line_item_10_to_12) + context 'when calculator has no cap' do + let(:subject) { described_class.new(calculator: ten_percent_off_calculator, promotion: promotion) } - expect(amount).to eq (1.0) - expect(-amount).to eq (subject.compute_amount(line_item_10_to_12)) - end + it 'return computed amount of -10% = 1$ for a matched date: 2023-01-12' do + amount = subject.compute_line_item_amount(line_item_10_to_12) - it 'return computed amount of -10% = 2$ for 2 matched dates: 2023-01-13, 2023-01-14' do - subject = described_class.new(calculator: ten_percent_off_calculator, promotion: promotion) - amount = subject.compute_line_item_amount(line_item_13_to_14) + expect(amount).to eq (1.0) + expect(-amount).to eq (subject.compute_amount(line_item_10_to_12)) + end + + it 'return computed amount of -10% = 2$ for 2 matched dates: 2023-01-13, 2023-01-14' do + amount = subject.compute_line_item_amount(line_item_13_to_14) + + expect(amount).to eq (2.0) + expect(-amount).to eq (subject.compute_amount(line_item_13_to_14)) + end + end - expect(amount).to eq (2.0) - expect(-amount).to eq (subject.compute_amount(line_item_13_to_14)) + context 'when calculator has 50% discount with a cap' do + let(:product1) { create(:cm_accommodation_product, price: BigDecimal('30.0'), permanent_stock: 4) } + let(:line_item_product1_10_to_12) { + create(:line_item, + order: order, + quantity: 1, + price: BigDecimal('30.0'), + product: product, + from_date: '2023-01-10'.to_date, + to_date: '2023-01-13'.to_date + ) + } + let(:line_item_product1_13_to_14) { + create(:line_item, + order: order, + quantity: 1, + price: BigDecimal('30.0'), + product: product1, + from_date: '2023-01-13'.to_date, + to_date: '2023-01-15'.to_date + ) + } + let(:fifty_percent_off_with_cap_calculator) { Spree::Calculator::PercentOnLineItem.new(preferred_percent: 50, cap: 5) } + let(:subject) { described_class.new(calculator: fifty_percent_off_with_cap_calculator, promotion: promotion) } + + it 'return computed amount of cap(5$ x 1) = 5$ for a matched date: 2023-01-12 of product 30$' do + amount = subject.compute_line_item_amount(line_item_product1_10_to_12) + + expect(amount).to eq (0.5e1) + expect(-amount).to eq (subject.compute_amount(line_item_product1_10_to_12)) + end + + it 'return computed amount of cap(5$ x 2) = 10$ for a matched dates: 2023-01-13, 2023-01-14 of product 30$' do + amount = subject.compute_line_item_amount(line_item_product1_13_to_14) + + expect(amount).to eq (10.0) + expect(-amount).to eq (subject.compute_amount(line_item_product1_13_to_14)) + end + + it 'return computed amount of 50% = 5$ for a matched date: 2023-01-12' do + amount = subject.compute_line_item_amount(line_item_10_to_12) + + expect(amount).to eq (0.5e1) + expect(-amount).to eq (subject.compute_amount(line_item_10_to_12)) + end + + it 'return computed amount of 50% = 10$ for 2 matched dates: 2023-01-13, 2023-01-14' do + amount = subject.compute_line_item_amount(line_item_13_to_14) + + expect(amount).to eq (10.0) + expect(-amount).to eq (subject.compute_amount(line_item_13_to_14)) + end end end