Merge pull request #10587 from rioug/10432-vouchers-bare-minimum-checkout

10432 vouchers bare minimum checkout
This commit is contained in:
Maikel
2023-05-16 09:19:06 +10:00
committed by GitHub
27 changed files with 939 additions and 12 deletions

View File

@@ -30,6 +30,8 @@ module CheckoutCallbacks
@order.manual_shipping_selection = true
@order.checkout_processing = true
@voucher_adjustment = @order.voucher_adjustments.first
redirect_to(main_app.shop_path) && return if redirect_to_shop?
redirect_to_cart_path && return unless valid_order_line_items?
end

View File

@@ -30,6 +30,8 @@ class SplitCheckoutController < ::BaseController
end
def update
return process_voucher if params[:apply_voucher].present?
if confirm_order || update_order
return if performed?
@@ -59,6 +61,27 @@ class SplitCheckoutController < ::BaseController
replace("#flashes", partial("shared/flashes", locals: { flashes: flash }))
end
def render_voucher_section_or_redirect
respond_to do |format|
format.cable_ready { render_voucher_section }
format.html { redirect_to checkout_step_path(:payment) }
end
end
# Using the power of cable_car we replace only the #voucher_section instead of reloading the page
def render_voucher_section
render(
status: :ok,
cable_ready: cable_car.replace(
"#voucher-section",
partial(
"split_checkout/voucher_section",
locals: { order: @order, voucher_adjustment: @order.voucher_adjustments.first }
)
)
)
end
def order_error_messages
# Remove ship_address.* errors if no shipping method is not selected
remove_ship_address_errors if no_ship_address_needed?
@@ -179,10 +202,47 @@ class SplitCheckoutController < ::BaseController
selected_shipping_method.first.require_ship_address == false
end
def process_voucher
if add_voucher
render_voucher_section_or_redirect
elsif @order.errors.present?
render_error
end
end
def add_voucher
if params.dig(:order, :voucher_code).blank?
@order.errors.add(:voucher, I18n.t('split_checkout.errors.voucher_not_found'))
return false
end
# Fetch Voucher
voucher = Voucher.find_by(code: params[:order][:voucher_code], enterprise: @order.distributor)
if voucher.nil?
@order.errors.add(:voucher, I18n.t('split_checkout.errors.voucher_not_found'))
return false
end
adjustment = voucher.create_adjustment(voucher.code, @order)
if !adjustment.valid?
@order.errors.add(:voucher, I18n.t('split_checkout.errors.add_voucher_error'))
adjustment.errors.each { |error| @order.errors.import(error) }
return false
end
true
end
def summary_step?
params[:step] == "summary"
end
def payment_step?
params[:step] == "payment"
end
def advance_order_state
return if @order.complete?
@@ -252,5 +312,14 @@ class SplitCheckoutController < ::BaseController
def recalculate_tax
@order.create_tax_charge!
@order.update_order!
apply_voucher if @order.voucher_adjustments.present?
end
def apply_voucher
VoucherAdjustmentsService.calculate(@order)
# update order to take into account the voucher we applied
@order.update_order!
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
class VoucherAdjustmentsController < BaseController
include CablecarResponses
def destroy
@order = current_order
@order.voucher_adjustments.find_by(id: params[:id])&.destroy
respond_to do |format|
format.cable_ready { render_voucher_section }
format.html { redirect_to checkout_step_path(:payment) }
end
end
private
# Using the power of cable_car we replace only the #voucher_section instead of reloading the page
def render_voucher_section
render(
status: :ok,
cable_ready: cable_car.replace(
"#voucher-section",
partial(
"split_checkout/voucher_section",
locals: { order: @order, voucher_adjustment: @order.voucher_adjustments.first }
)
)
)
end
end

View File

@@ -42,9 +42,6 @@ module Spree
belongs_to :order, class_name: "Spree::Order"
belongs_to :tax_category, class_name: 'Spree::TaxCategory'
belongs_to :tax_rate, -> { where spree_adjustments: { originator_type: 'Spree::TaxRate' } },
foreign_key: 'originator_id'
validates :label, presence: true
validates :amount, numericality: true

View File

@@ -62,6 +62,13 @@ module Spree
has_many :line_item_adjustments, through: :line_items, source: :adjustments
has_many :shipment_adjustments, through: :shipments, source: :adjustments
has_many :all_adjustments, class_name: 'Spree::Adjustment', dependent: :destroy
has_many :voucher_adjustments,
-> {
where(originator_type: 'Voucher')
.order("#{Spree::Adjustment.table_name}.created_at ASC")
},
class_name: 'Spree::Adjustment',
dependent: :destroy
belongs_to :order_cycle
belongs_to :distributor, class_name: 'Enterprise'

View File

@@ -1,8 +1,15 @@
# frozen_string_literal: false
class Voucher < ApplicationRecord
acts_as_paranoid
belongs_to :enterprise
has_many :adjustments,
as: :originator,
class_name: 'Spree::Adjustment',
dependent: :nullify
validates :code, presence: true, uniqueness: { scope: :enterprise_id }
def value
@@ -12,4 +19,31 @@ class Voucher < ApplicationRecord
def display_value
Spree::Money.new(value)
end
# Ideally we would use `include CalculatedAdjustments` to be consistent with other adjustments,
# but vouchers have complicated calculation so we can't easily use Spree::Calculator. We keep
# the same method to stay as consistent as possible.
#
# Creates a new voucher adjustment for the given order
def create_adjustment(label, order)
amount = compute_amount(order)
adjustment_attributes = {
amount: amount,
originator: self,
order: order,
label: label,
mandatory: false,
state: "open",
tax_category: nil
}
order.adjustments.create(adjustment_attributes)
end
# 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 an adjustment covering the order.total
def compute_amount(order)
-value.clamp(0, order.total)
end
end

View File

@@ -0,0 +1,75 @@
# frozen_string_literal: true
class VoucherAdjustmentsService
def self.calculate(order)
return if order.nil?
# Find open Voucher Adjustment
return if order.voucher_adjustments.empty?
# We only support one voucher per order right now, we could just loop on voucher_adjustments
adjustment = order.voucher_adjustments.first
# Recalculate value
amount = adjustment.originator.compute_amount(order)
# It is quite possible to have an order with both tax included in and tax excluded from price.
# We should be able to caculate the relevant amount apply the current calculation.
#
# For now we just assume it is either all tax included in price or all tax excluded from price.
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
# Adding the voucher tax part
tax_amount = voucher_rate * order.additional_tax_total
adjustment = order.voucher_adjustments.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.voucher_adjustments.first
return unless amount != adjustment.amount || included_tax != 0
adjustment.update_columns(
amount: amount,
included_tax: included_tax,
updated_at: Time.zone.now
)
end
private_class_method :handle_tax_included_in_price, :handle_tax_excluded_from_price
end

View File

@@ -1,5 +1,7 @@
.medium-6
%div.checkout-substep{"data-controller": "paymentmethod"}
= render partial: "split_checkout/voucher_section", formats: [:cable_ready], locals: { order: @order, voucher_adjustment: @voucher_adjustment }
%div.checkout-title
= t("split_checkout.step2.payment_method.title")

View File

@@ -85,8 +85,12 @@
- checkout_adjustments_for(@order, exclude: [:line_item]).reverse_each do |adjustment|
.summary-right-line
.summary-right-line-label= adjustment.label
.summary-right-line-value= adjustment.display_amount.to_html
-if adjustment.originator_type == 'Voucher'
.summary-right-line-label.voucher= adjustment.label
.summary-right-line-value.voucher= adjustment.display_amount.to_html
-else
.summary-right-line-label= adjustment.label
.summary-right-line-value= adjustment.display_amount.to_html
- if @order.total_tax > 0
.summary-right-line

View File

@@ -0,0 +1,19 @@
%div#voucher-section
- if order.distributor.vouchers.present?
.checkout-title
= t("split_checkout.step2.voucher.apply_voucher")
.checkout-input
.two-columns-inputs.voucher{"data-controller": "toggle-button-disabled"}
- if voucher_adjustment.present?
%span.button.voucher-added
%i.ofn-i_051-check-big
= t("split_checkout.step2.voucher.voucher", voucher_amount: voucher_adjustment.originator.display_value)
= link_to t("split_checkout.step2.voucher.remove_code"), voucher_adjustment_path(id: voucher_adjustment.id), method: "delete", data: { confirm: t("split_checkout.step2.voucher.confirm_delete") }
- # This might not be true, ie payment method including a fee which wouldn't be covered by voucher or tax implication raising total to be bigger than the voucher amount ?
- if voucher_adjustment.originator.value > order.total
.checkout-input
%span.formError.standalone
= t("split_checkout.step2.voucher.warning_forfeit_remaining_amount")
- else
= text_field_tag "[order][voucher_code]", params.dig(:order, :voucher_code), data: { action: "input->toggle-button-disabled#inputIsChanged", }, placeholder: t("split_checkout.step2.voucher.placeholder") , class: "voucher"
= submit_tag t("split_checkout.step2.voucher.apply"), name: "apply_voucher", disabled: true, class: "button cancel voucher", "data-disable-with": false, data: { "toggle-button-disabled-target": "button" }

View File

@@ -0,0 +1,24 @@
import { Controller } from "stimulus";
// Since Rails 7 it adds "data-disabled-with" property to submit, you'll need to add
// 'data-disable-with="false' for this to function as expected, ie:
//
// <input id="test-submit" type="submit" data-disable-with="false" data-toggle-button-disabled-target="button"/>
//
export default class extends Controller {
static targets = ["button"];
connect() {
if (this.hasButtonTarget) {
this.buttonTarget.disabled = true;
}
}
inputIsChanged(e) {
if (e.target.value !== "") {
this.buttonTarget.disabled = false;
} else {
this.buttonTarget.disabled = true;
}
}
}

View File

@@ -332,6 +332,10 @@
padding-left: 20px;
padding-right: 20px;
border-right: 1px solid #DDD;
.voucher {
color: $teal-500
}
}
}
}
@@ -404,6 +408,48 @@
gap: 1rem;
justify-content: space-between;
&.voucher {
justify-content: normal;
align-items: center;
input {
width: 50%;
}
a {
color: inherit;
}
.button {
&.cancel {
width: 30%;
border-radius: 0.5em;
padding:0;
height: 2.5em;
background-color: $teal-400
}
}
}
.voucher-added {
padding: 10px;
background-color: $teal-300;
color: $teal-500;
margin: 0;
cursor: default;
i.ofn-i_051-check-big:before {
background-color: $teal-500;
color: $teal-300;
border-radius: 50%;
font-style: normal;
}
i.ofn-i_051-check-big {
font-style: italic;
}
}
> .checkout-input {
flex: 1;
}
@@ -418,6 +464,11 @@
&:last-child > .checkout-input {
margin-bottom: 1.5rem;
}
&.voucher {
flex-direction: row;
gap: 1rem;
}
}
}

View File

@@ -2055,6 +2055,14 @@ en:
explaination: You can review and confirm your order in the next step which includes the final costs.
submit: Next - Order summary
cancel: Back to Your details
voucher:
voucher: "%{voucher_amount} Voucher"
apply_voucher: Apply voucher
apply: Apply
placeholder: Enter voucher code
remove_code: Remove code
confirm_delete: Are you sure you want to remove the voucher?
warning_forfeit_remaining_amount: Your voucher value is more than your order. By using this voucher you are forfeiting the remaining value.
step3:
delivery_details:
title: Delivery details
@@ -2087,6 +2095,8 @@ en:
select_a_shipping_method: Select a shipping method
select_a_payment_method: Select a payment method
no_shipping_methods_available: Checkout is not possible due to absence of shipping options. Please contact the shop owner.
voucher_not_found: Not found
add_voucher_error: There was an error while adding the voucher
order_paid: PAID
order_not_paid: NOT PAID
order_total: Total order

View File

@@ -74,7 +74,7 @@ Openfoodnetwork::Application.routes.draw do
match "/checkout", via: :get, controller: "payment_gateways/stripe", action: "confirm"
match "/orders/:order_number", via: :get, controller: "payment_gateways/stripe", action: "authorize"
end
namespace :payment_gateways do
get "/paypal", to: "paypal#express", as: :paypal_express
get "/paypal/confirm", to: "paypal#confirm", as: :confirm_paypal
@@ -121,6 +121,8 @@ Openfoodnetwork::Application.routes.draw do
get '/:id/shop', to: 'enterprises#shop', as: 'enterprise_shop'
get "/enterprises/:permalink", to: redirect("/") # Legacy enterprise URL
resources :voucher_adjustments, only: [:destroy]
get 'sitemap.xml', to: 'sitemap#index', defaults: { format: 'xml' }
# Mount Spree's routes

View File

@@ -0,0 +1,6 @@
class AddDeletedAtToVouchers < ActiveRecord::Migration[6.1]
def change
add_column :vouchers, :deleted_at, :datetime
add_index :vouchers, :deleted_at
end
end

View File

@@ -1192,11 +1192,13 @@ ActiveRecord::Schema[7.0].define(version: 2023_04_24_141213) do
create_table "vouchers", force: :cascade do |t|
t.string "code", limit: 255, null: false
t.datetime "expiry_date"
t.datetime "expiry_date", precision: nil
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "enterprise_id"
t.datetime "deleted_at", precision: nil
t.index ["code", "enterprise_id"], name: "index_vouchers_on_code_and_enterprise_id", unique: true
t.index ["deleted_at"], name: "index_vouchers_on_deleted_at"
t.index ["enterprise_id"], name: "index_vouchers_on_enterprise_id"
end

View File

@@ -170,6 +170,8 @@ RSpec.configure do |config|
config.include OpenFoodNetwork::ApiHelper, type: :controller
config.include OpenFoodNetwork::ControllerHelper, type: :controller
config.include Devise::Test::IntegrationHelpers, type: :request
config.include Features::DatepickerHelper, type: :system
config.include DownloadsHelper, type: :system
end

View File

@@ -235,6 +235,73 @@ describe SplitCheckoutController, type: :controller do
expect(order.payments.first.source.id).to eq saved_card.id
end
end
describe "Vouchers" do
let(:voucher) { Voucher.create(code: 'some_code', enterprise: distributor) }
describe "adding a voucher" do
let(:checkout_params) do
{
apply_voucher: "true",
order: {
voucher_code: voucher.code
}
}
end
it "adds a voucher to the order" do
# Set the headers to simulate a cable_ready request
request.headers["accept"] = "text/vnd.cable-ready.json"
put :update, params: params
expect(response.status).to eq(200)
expect(order.reload.voucher_adjustments.length).to eq(1)
end
context "when voucher doesn't exist" do
let(:checkout_params) do
{
apply_voucher: "true",
order: {
voucher_code: "non_voucher"
}
}
end
it "returns 422 and an error message" do
put :update, params: params
expect(response.status).to eq 422
expect(flash[:error]).to match "Voucher Not found"
end
end
context "when adding fails" do
it "returns 422 and an error message" do
# Create a non valid adjustment
adjustment = build(:adjustment, label: nil)
allow(voucher).to receive(:create_adjustment).and_return(adjustment)
allow(Voucher).to receive(:find_by).and_return(voucher)
put :update, params: params
expect(response.status).to eq 422
expect(flash[:error]).to match(
"There was an error while adding the voucher and Label can't be blank"
)
end
end
context "with an html request" do
it "redirects to the payment step" do
put :update, params: params
expect(response).to redirect_to(checkout_step_path(:payment))
end
end
end
end
end
context "summary step" do

View File

@@ -0,0 +1,81 @@
/**
* @jest-environment jsdom
*/
import { Application } from "stimulus"
import toggle_button_disabled_controller from "../../../app/webpacker/controllers/toggle_button_disabled_controller"
describe("ButtonEnableToggleController", () => {
beforeAll(() => {
const application = Application.start()
application.register("toggle-button-disabled", toggle_button_disabled_controller)
})
beforeEach(() => {
document.body.innerHTML = `
<form id="test-form" data-controller="toggle-button-disabled">
<input id="test-input" type="input" data-action="input->toggle-button-disabled#inputIsChanged" />
<input
id="test-submit"
type="submit"
data-disable-with="false"
data-toggle-button-disabled-target="button"
/>
</form>
`
})
describe("#connect", () => {
it("disables the target submit button", () => {
const submit = document.getElementById("test-submit")
expect(submit.disabled).toBe(true)
})
describe("when no button present", () => {
beforeEach(() => {
document.body.innerHTML = `
<form id="test-form" data-controller="toggle-button-disabled">
<input id="test-input" type="input" data-action="input->toggle-button-disabled#inputIsChanged" />
</form>
`
})
// I am not sure if it's possible to manually trigger the loading/connect of the controller to
// try catch the error, so leaving as this. It will break if the missing target isn't handled
// properly
it("doesn't break", () => {})
})
})
describe("#formIsChanged", () => {
let input
let submit
beforeEach(() => {
input = document.getElementById("test-input")
submit = document.getElementById("test-submit")
})
describe("when the input value is not empty", () => {
it("enables the target button", () => {
input.value = "test"
input.dispatchEvent(new Event("input"));
expect(submit.disabled).toBe(false)
})
})
describe("when the input value is empty", () => {
it("disables the target button", () => {
// setting up state where target button is enabled
input.value = "test"
input.dispatchEvent(new Event("input"));
input.value = ""
input.dispatchEvent(new Event("input"));
expect(submit.disabled).toBe(true)
})
})
})
})

View File

@@ -7,6 +7,16 @@ module Spree
let(:order) { build(:order) }
let(:adjustment) { Spree::Adjustment.create(label: "Adjustment", amount: 5) }
describe "associations" do
it { is_expected.to have_one(:metadata) }
it { is_expected.to have_many(:adjustments) }
it { is_expected.to belong_to(:adjustable) }
it { is_expected.to belong_to(:originator) }
it { is_expected.to belong_to(:order) }
it { is_expected.to belong_to(:tax_category) }
end
describe "scopes" do
let!(:arbitrary_adjustment) { create(:adjustment, label: "Arbitrary") }
let!(:return_authorization_adjustment) {

View File

@@ -1430,4 +1430,21 @@ describe Spree::Order do
end
end
end
describe "#voucher_adjustments" do
let(:voucher) { Voucher.create(code: 'new_code', enterprise: order.distributor) }
context "when no voucher adjustment" do
it 'returns an empty array' do
expect(order.voucher_adjustments).to eq([])
end
end
it "returns an array of voucher adjusment" do
order.save!
expected_adjustments = Array.new(2) { voucher.create_adjustment(voucher.code, order) }
expect(order.voucher_adjustments).to eq(expected_adjustments)
end
end
end

View File

@@ -3,16 +3,55 @@
require 'spec_helper'
describe Voucher do
let(:enterprise) { build(:enterprise) }
describe 'associations' do
it { is_expected.to belong_to(:enterprise) }
it { is_expected.to have_many(:adjustments) }
end
describe 'validations' do
subject { Voucher.new(code: 'new_code', enterprise: enterprise) }
let(:enterprise) { build(:enterprise) }
it { is_expected.to validate_presence_of(:code) }
it { is_expected.to validate_uniqueness_of(:code).scoped_to(:enterprise_id) }
end
describe '#compute_amount' do
subject { Voucher.create(code: 'new_code', enterprise: enterprise) }
let(:order) { create(:order_with_totals) }
it 'returns -10' do
expect(subject.compute_amount(order).to_f).to eq(-10)
end
context 'when order total is smaller than 10' do
it 'returns minus the order total' do
order.total = 6
order.save!
expect(subject.compute_amount(order).to_f).to eq(-6)
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

@@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'spec_helper'
describe VoucherAdjustmentsController, type: :request do
let(:user) { order.user }
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
let(:order) { create( :order_with_line_items, line_items_count: 1, distributor: distributor) }
let(:voucher) { Voucher.create(code: 'some_code', enterprise: distributor) }
let!(:adjustment) { voucher.create_adjustment(voucher.code, order) }
before do
Flipper.enable(:split_checkout)
# Make sure the order is created by the order user, the factory doesn't set ip properly
order.created_by = user
order.save!
sign_in user
end
describe "DELETE voucher_adjustments/:id" do
let(:cable_ready_header) { { accept: "text/vnd.cable-ready.json" } }
context "with a cable ready request" do
it "deletes the voucher adjustment" do
delete("/voucher_adjustments/#{adjustment.id}", headers: cable_ready_header)
expect(order.voucher_adjustments.length).to eq(0)
end
it "render a succesful response" do
delete("/voucher_adjustments/#{adjustment.id}", headers: cable_ready_header)
expect(response).to be_successful
end
context "when adjustment doesn't exits" do
it "does nothing" do
delete "/voucher_adjustments/-1", headers: cable_ready_header
expect(order.voucher_adjustments.length).to eq(1)
end
it "render a succesful response" do
delete "/voucher_adjustments/-1", headers: cable_ready_header
expect(response).to be_successful
end
end
end
context "with an html request" do
it "redirect to checkout payment step" do
delete "/voucher_adjustments/#{adjustment.id}"
expect(response).to redirect_to(checkout_step_path(:payment))
end
end
end
end

View File

@@ -0,0 +1,131 @@
# frozen_string_literal: true
require 'spec_helper'
describe VoucherAdjustmentsService do
describe '.calculate' do
let(:enterprise) { build(:enterprise) }
let(:voucher) { Voucher.create(code: 'new_code', enterprise: enterprise) }
context 'when voucher covers the order total' do
subject { order.voucher_adjustments.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!
VoucherAdjustmentsService.calculate(order)
expect(subject.amount.to_f).to eq(-6.0)
end
end
context 'with price included in order price' do
subject { order.voucher_adjustments.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!
VoucherAdjustmentsService.calculate(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!
VoucherAdjustmentsService.calculate(order)
end
it 'includes amount withou tax' do
adjustment = order.voucher_adjustments.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.voucher_adjustments.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.voucher_adjustments.first
expect(adjustment.state).to eq('closed')
end
end
context 'when no order given' do
it "doesn't blow up" do
expect { VoucherAdjustmentsService.calculate(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 { VoucherAdjustmentsService.calculate(order) }.to_not raise_error
end
end
end
end

View File

@@ -33,7 +33,7 @@ describe "As a consumer, I want to checkout my order" do
let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23, tax_category: fee_tax_category) }
let(:free_shipping_with_required_address) {
create(:shipping_method, require_ship_address: true,
create(:shipping_method, require_ship_address: true,
name: "A Free Shipping with required address")
}
let(:free_shipping) {
@@ -708,6 +708,93 @@ describe "As a consumer, I want to checkout my order" do
end
end
describe "vouchers" do
context "with no voucher available" do
before do
visit checkout_step_path(:payment)
end
it "doesn't show voucher input" do
expect(page).not_to have_content "Apply voucher"
end
end
context "with voucher available" do
let!(:voucher) { Voucher.create(code: 'some_code', enterprise: distributor) }
before do
visit checkout_step_path(:payment)
end
it "shows voucher input" do
expect(page).to have_content "Apply voucher"
end
describe "adding voucher to the order" do
shared_examples "adding voucher to the order" do
before do
fill_in "Enter voucher code", with: voucher.code
click_button("Apply")
end
it "adds a voucher to the order" do
expect(page).to have_content("$10.00 Voucher")
expect(order.reload.voucher_adjustments.length).to eq(1)
end
end
it_behaves_like "adding voucher to the order"
context "when voucher covers more then the order total" do
before do
order.total = 6
order.save!
end
it_behaves_like "adding voucher to the order"
it "shows a warning message" do
fill_in "Enter voucher code", with: voucher.code
click_button("Apply")
expect(page).to have_content(
"Your voucher value is more than your order. " \
"By using this voucher you are forfeiting the remaining value."
)
end
end
context "voucher doesn't exist" do
it "show an error" do
fill_in "Enter voucher code", with: "non_code"
click_button("Apply")
expect(page).to have_content("Voucher Not found")
end
end
end
describe "removing voucher from order" do
before do
voucher.create_adjustment(voucher.code, order)
# Reload the page so we pickup the voucher
visit checkout_step_path(:payment)
end
it "removes voucher" do
accept_confirm "Are you sure you want to remove the voucher?" do
click_on "Remove code"
end
within '.voucher' do
expect(page).to have_button("Apply", disabled: true)
end
expect(order.voucher_adjustments.length).to eq(0)
end
end
end
end
describe "choosing" do
shared_examples "different payment methods" do |pay_method|
context "checking out with #{pay_method}", if: pay_method.eql?("Stripe SCA") == false do
@@ -937,8 +1024,6 @@ describe "As a consumer, I want to checkout my order" do
end
context "when the terms have been accepted in the past" do
context "with a dedicated ToS file" do
before do
TermsOfServiceFile.create!(
@@ -1024,6 +1109,25 @@ describe "As a consumer, I want to checkout my order" do
}.to change { order.reload.state }.from("confirmation").to("address")
end
end
describe "vouchers" do
let(:voucher) { Voucher.create(code: 'some_code', enterprise: distributor) }
before do
# Add voucher to the order
voucher.create_adjustment(voucher.code, order)
# Update order so voucher adjustment is properly taken into account
order.update_order!
visit checkout_step_path(:summary)
end
it "shows the applied voucher" do
within ".summary-right" do
expect(page).to have_content "some_code"
end
end
end
end
context "with previous open orders" do

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.voucher_adjustments.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.voucher_adjustments.first
voucher_tax_adjustment = order_within_zone.voucher_adjustments.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