Rework how voucher are applied to an order and handle tax calculation

At the time when we can apply a voucher to an order (payment step) we
don't have any data on possible taxes and payment fees. These are
calculated when we move to the summary step. So voucher are applied in
two step:
- create an "open" voucher adjustment on the order when a customer enters
a voucher code.
- when we move to the summary step, recalulate the amount and
possible tax for the voucher adjustment, once taxes and payment fees
have been included in the order. And then close the original and update
order to reflect the changes
This commit is contained in:
Gaetan Craig-Riou
2023-03-28 11:41:26 +11:00
committed by Maikel Linke
parent d157d91054
commit 9789911523
5 changed files with 321 additions and 1 deletions

View File

@@ -290,5 +290,14 @@ class SplitCheckoutController < ::BaseController
def recalculate_tax
@order.create_tax_charge!
@order.update_order!
apply_voucher if @order.vouchers.present?
end
def apply_voucher
Voucher.adjust!(@order)
# update order to take into account the voucher we applied
@order.update_order!
end
end

View File

@@ -13,6 +13,73 @@ class Voucher < ApplicationRecord
before_validation :add_calculator
def self.adjust!(order)
return if order.nil?
# Find open Voucher Adjustment
return if order.vouchers.empty?
# We only support one voucher per order right now, we could just loop on vouchers
adjustment = order.vouchers.first
# Recalculate value
amount = adjustment.originator.compute_amount(order)
if order.additional_tax_total.positive?
handle_tax_excluded_from_price(order, amount)
else
handle_tax_included_in_price(order, amount)
end
# Move to closed state
adjustment.close
end
def self.handle_tax_excluded_from_price(order, amount)
voucher_rate = amount / order.total
# TODO: might need to use VoucherTax has originator (sub class of Voucher)
# Adding the voucher tax part
tax_amount = voucher_rate * order.additional_tax_total
adjustment = order.vouchers.first
adjustment_attributes = {
amount: tax_amount,
originator: adjustment.originator,
order: order,
label: "Tax #{adjustment.label}",
mandatory: false,
state: 'closed',
tax_category: nil,
included_tax: 0
}
order.adjustments.create(adjustment_attributes)
# Update the adjustment amount
amount = voucher_rate * (order.total - order.additional_tax_total)
adjustment.update_columns(
amount: amount,
updated_at: Time.zone.now
)
end
def self.handle_tax_included_in_price(order, amount)
voucher_rate = amount / order.total
included_tax = voucher_rate * order.included_tax_total
# Update Adjustment
adjustment = order.vouchers.first
return unless amount != adjustment.amount || included_tax != 0
adjustment.update_columns(
amount: amount,
included_tax: included_tax,
updated_at: Time.zone.now
)
end
def value
10
end
@@ -21,9 +88,30 @@ class Voucher < ApplicationRecord
Spree::Money.new(value)
end
# override the one from CalculatedAdjustments
# Create an "open" adjustment which will be updated later once tax and other fees have
# been applied to the order
def create_adjustment(label, order, mandatory = false, _state = "open", tax_category = nil)
amount = compute_amount(order)
return if amount.zero? && !mandatory
adjustment_attributes = {
amount: amount,
originator: self,
order: order,
label: label,
mandatory: mandatory,
state: "open",
tax_category: tax_category
}
order.adjustments.create(adjustment_attributes)
end
# override the one from CalculatedAdjustments so we limit adjustment to the maximum amount
# needed to cover the order, ie if the voucher covers more than the order.total we only need
# to create a adjustment covering the order.total
# to create an adjustment covering the order.total
# Doesn't work with taxes for now
def compute_amount(order)
amount = calculator.compute(order)

View File

@@ -29,6 +29,131 @@ describe Voucher do
end
end
describe '.adjust!' do
let(:voucher) { Voucher.create(code: 'new_code', enterprise: enterprise) }
context 'when voucher covers the order total' do
subject { order.vouchers.first }
let(:order) { create(:order_with_totals) }
it 'updates the adjustment amount to -order.total' do
voucher.create_adjustment(voucher.code, order)
order.total = 6
order.save!
Voucher.adjust!(order)
expect(subject.amount.to_f).to eq(-6.0)
end
end
context 'with price included in order price' do
subject { order.vouchers.first }
let(:order) do
create(
:order_with_taxes,
distributor: enterprise,
ship_address: create(:address),
product_price: 110,
tax_rate_amount: 0.10,
included_in_price: true,
tax_rate_name: "Tax 1"
)
end
before do
# create adjustment before tax are set
voucher.create_adjustment(voucher.code, order)
# Update taxes
order.create_tax_charge!
order.update_shipping_fees!
order.update_order!
Voucher.adjust!(order)
end
it 'updates the adjustment included_tax' do
# voucher_rate = amount / order.total
# -10 / 150 = -0.066666667
# included_tax = voucher_rate * order.included_tax_total
# -0.66666666 * 10 = -0.67
expect(subject.included_tax.to_f).to eq(-0.67)
end
it 'moves the adjustment state to closed' do
expect(subject.state).to eq('closed')
end
end
context 'with price not included in order price' do
let(:order) do
create(
:order_with_taxes,
distributor: enterprise,
ship_address: create(:address),
product_price: 110,
tax_rate_amount: 0.10,
included_in_price: false,
tax_rate_name: "Tax 1"
)
end
before do
# create adjustment before tax are set
voucher.create_adjustment(voucher.code, order)
# Update taxes
order.create_tax_charge!
order.update_shipping_fees!
order.update_order!
Voucher.adjust!(order)
end
it 'includes amount withou tax' do
adjustment = order.vouchers.first
# voucher_rate = amount / order.total
# -10 / 161 = -0.062111801
# amount = voucher_rate * (order.total - order.additional_tax_total)
# -0.062111801 * (161 -11) = -9.32
expect(adjustment.amount.to_f).to eq(-9.32)
end
it 'creates a tax adjustment' do
# voucher_rate = amount / order.total
# -10 / 161 = -0.062111801
# amount = voucher_rate * order.additional_tax_total
# -0.0585 * 11 = -0.68
tax_adjustment = order.vouchers.second
expect(tax_adjustment.amount.to_f).to eq(-0.68)
expect(tax_adjustment.label).to match("Tax")
end
it 'moves the adjustment state to closed' do
adjustment = order.vouchers.first
expect(adjustment.state).to eq('closed')
end
end
context 'when no order given' do
it "doesn't blow up" do
expect { Voucher.adjust!(nil) }.to_not raise_error
end
end
context 'when no voucher used on the given order' do
let(:order) { create(:order_with_line_items, line_items_count: 1, distributor: enterprise) }
it "doesn't blow up" do
expect { Voucher.adjust!(order) }.to_not raise_error
end
end
end
describe '#compute_amount' do
subject { Voucher.create(code: 'new_code', enterprise: enterprise) }
@@ -47,4 +172,23 @@ describe Voucher do
end
end
end
describe '#create_adjustment' do
subject(:adjustment) { voucher.create_adjustment(voucher.code, order) }
let(:voucher) { Voucher.create(code: 'new_code', enterprise: enterprise) }
let(:order) { create(:order_with_line_items, line_items_count: 1, distributor: enterprise) }
it 'includes the full voucher amount' do
expect(adjustment.amount.to_f).to eq(-10.0)
end
it 'has no included_tax' do
expect(adjustment.included_tax.to_f).to eq(0.0)
end
it 'sets the adjustment as open' do
expect(adjustment.state).to eq("open")
end
end
end

View File

@@ -134,6 +134,44 @@ describe "As a consumer, I want to see adjustment breakdown" do
# DB checks
assert_db_tax_incl
end
context "when using a voucher" do
let!(:voucher) { Voucher.create(code: 'some_code', enterprise: distributor) }
it "will include a tax included amount on the voucher adjustment" do
visit checkout_step_path(:details)
choose "Delivery"
click_button "Next - Payment method"
# add Voucher
fill_in "Enter voucher code", with: voucher.code
click_button("Apply")
# Choose payment ??
click_on "Next - Order summary"
click_on "Complete order"
# UI checks
expect(page).to have_content("Confirmed")
expect(page).to have_selector('#order_total', text: with_currency(0.00))
expect(page).to have_selector('#tax-row', text: with_currency(1.15))
# Voucher
within "#line-items" do
expect(page).to have_content(voucher.code)
expect(page).to have_content(with_currency(-10.00))
end
# DB check
order_within_zone.reload
voucher_adjustment = order_within_zone.vouchers.first
expect(voucher_adjustment.amount.to_f).to eq(-10)
expect(voucher_adjustment.included_tax.to_f).to eq(-1.15)
end
end
end
end

View File

@@ -142,6 +142,47 @@ describe "As a consumer, I want to see adjustment breakdown" do
expect(page).to have_selector('#order_total', text: with_currency(11.30))
expect(page).to have_selector('#tax-row', text: with_currency(1.30))
end
context "when using a voucher" do
let!(:voucher) { Voucher.create(code: 'some_code', enterprise: distributor) }
it "will include a tax included amount on the voucher adjustment" do
visit checkout_step_path(:details)
choose "Delivery"
click_button "Next - Payment method"
# add Voucher
fill_in "Enter voucher code", with: voucher.code
click_button("Apply")
# Choose payment ??
click_on "Next - Order summary"
click_on "Complete order"
# UI checks
expect(page).to have_content("Confirmed")
expect(page).to have_selector('#order_total', text: with_currency(1.30))
expect(page).to have_selector('#tax-row', text: with_currency(1.30))
# Voucher
within "#line-items" do
expect(page).to have_content(voucher.code)
expect(page).to have_content(with_currency(-8.85))
expect(page).to have_content("Tax #{voucher.code}")
expect(page).to have_content(with_currency(-1.15))
end
# DB check
order_within_zone.reload
voucher_adjustment = order_within_zone.vouchers.first
voucher_tax_adjustment = order_within_zone.vouchers.second
expect(voucher_adjustment.amount.to_f).to eq(-8.85)
expect(voucher_tax_adjustment.amount.to_f).to eq(-1.15)
end
end
end
end