mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-25 20:46:48 +00:00
When using paypal, we need to redeem the voucher before redirecting to the payment gateway url, otherwise the voucher will never get redeemed.
718 lines
23 KiB
Ruby
718 lines
23 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe CheckoutController do
|
|
let(:user) { order.user }
|
|
let(:address) { create(:address) }
|
|
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
|
|
let(:order_cycle) { create(:order_cycle, distributors: [distributor]) }
|
|
let(:exchange) { order_cycle.exchanges.outgoing.first }
|
|
let(:order) { create(:order_with_line_items, line_items_count: 1, distributor:, order_cycle:) }
|
|
let(:payment_method) { distributor.payment_methods.first }
|
|
let(:shipping_method) { distributor.shipping_methods.first }
|
|
|
|
before do
|
|
exchange.variants << order.line_items.first.variant
|
|
allow(controller).to receive(:current_order) { order }
|
|
allow(controller).to receive(:spree_current_user) { user }
|
|
end
|
|
|
|
describe "#edit" do
|
|
it "renders the checkout" do
|
|
get :edit, params: { step: "details" }
|
|
expect(response).to have_http_status :ok
|
|
end
|
|
|
|
it "redirects to current step if no step is given" do
|
|
get :edit
|
|
expect(response).to redirect_to checkout_step_path(:details)
|
|
end
|
|
|
|
context "when line items in the cart are not valid" do
|
|
before { allow(controller).to receive(:valid_order_line_items?) { false } }
|
|
|
|
it "redirects to cart" do
|
|
get :edit
|
|
expect(response).to redirect_to cart_path
|
|
end
|
|
end
|
|
|
|
context "when the given `step` params is inconsistent with the current order state" do
|
|
context "when order state is `cart`" do
|
|
before do
|
|
order.update!(state: "cart")
|
|
end
|
|
|
|
it "redirects to the valid step if params is `payment`" do
|
|
get :edit, params: { step: "payment" }
|
|
expect(response).to redirect_to checkout_step_path(:details)
|
|
end
|
|
it "redirects to the valid step if params is `summary`" do
|
|
get :edit, params: { step: "summary" }
|
|
expect(response).to redirect_to checkout_step_path(:details)
|
|
end
|
|
end
|
|
|
|
context "when order state is `payment`" do
|
|
before do
|
|
order.update!(state: "payment")
|
|
end
|
|
|
|
it "redirects to the valid step if params is `summary`" do
|
|
get :edit, params: { step: "summary" }
|
|
expect(response).to redirect_to checkout_step_path(:payment)
|
|
end
|
|
end
|
|
|
|
context "when order state is 'confirmation'" do
|
|
before do
|
|
order.update!(state: "confirmation")
|
|
end
|
|
|
|
context "when loading payment step" do
|
|
it "updates the order state to payment" do
|
|
get :edit, params: { step: "payment" }
|
|
|
|
expect(response).to have_http_status :ok
|
|
expect(order.reload.state).to eq("payment")
|
|
end
|
|
end
|
|
|
|
context "when loading address step" do
|
|
it "updates the order state to address" do
|
|
get :edit, params: { step: "details" }
|
|
|
|
expect(response).to have_http_status :ok
|
|
expect(order.reload.state).to eq("address")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when order state is 'payment'" do
|
|
context "when loading address step" do
|
|
before do
|
|
order.update!(state: "payment")
|
|
end
|
|
|
|
it "updates the order state to address" do
|
|
get :edit, params: { step: "details" }
|
|
|
|
expect(response).to have_http_status :ok
|
|
expect(order.reload.state).to eq("address")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#update" do
|
|
let(:checkout_params) { {} }
|
|
let(:params) { { step: }.merge(checkout_params) }
|
|
|
|
context "details step" do
|
|
let(:step) { "details" }
|
|
|
|
context "with incomplete data" do
|
|
let(:checkout_params) { { order: { email: user.email } } }
|
|
|
|
it "returns 422 and some feedback" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to have_http_status :unprocessable_entity
|
|
expect(flash[:error]).to match "Saving failed, please update the highlighted fields."
|
|
expect(order.reload.state).to eq "cart"
|
|
end
|
|
end
|
|
|
|
context "with complete data" do
|
|
let(:checkout_params) do
|
|
{
|
|
order: {
|
|
email: user.email,
|
|
bill_address_attributes: address.to_param,
|
|
ship_address_attributes: address.to_param
|
|
},
|
|
shipping_method_id: shipping_method.id
|
|
}
|
|
end
|
|
|
|
it "updates and redirects to payment step" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to checkout_step_path(:payment)
|
|
expect(order.reload.state).to eq "payment"
|
|
end
|
|
|
|
context "with insufficient stock" do
|
|
it "redirects to details page" do
|
|
allow(order).to receive_message_chain(:insufficient_stock_lines,
|
|
:empty?).and_return false
|
|
|
|
put(:update, params:)
|
|
|
|
expect_cable_ready_redirect(response)
|
|
end
|
|
end
|
|
|
|
describe "saving default addresses" do
|
|
it "doesn't update default bill address on user" do
|
|
expect {
|
|
put :update, params: params.merge(order: { save_bill_address: "0" })
|
|
}.not_to change {
|
|
order.user.reload.bill_address
|
|
}
|
|
end
|
|
|
|
it "updates default bill address on user and customer" do
|
|
put :update, params: params.merge(order: { save_bill_address: "1" })
|
|
|
|
expect(order.customer.bill_address).to eq(order.bill_address)
|
|
expect(order.user.bill_address).to eq(order.bill_address)
|
|
end
|
|
|
|
it "doesn't update default ship address on user" do
|
|
expect {
|
|
put :update, params: params.merge(order: { save_ship_address: "0" })
|
|
}.not_to change {
|
|
order.user.reload.ship_address
|
|
}
|
|
end
|
|
|
|
it "updates default ship address on user and customer" do
|
|
put :update, params: params.merge(order: { save_ship_address: "1" })
|
|
|
|
expect(order.customer.ship_address).to eq(order.ship_address)
|
|
expect(order.user.ship_address).to eq(order.ship_address)
|
|
end
|
|
end
|
|
|
|
describe "with a voucher" do
|
|
let(:checkout_params) do
|
|
{
|
|
order: {
|
|
email: user.email,
|
|
bill_address_attributes: address.to_param,
|
|
ship_address_attributes: address.to_param
|
|
},
|
|
shipping_method_id: order.shipment.shipping_method.id.to_s
|
|
}
|
|
end
|
|
|
|
let(:voucher) { create(:voucher_flat_rate, enterprise: distributor) }
|
|
let(:service) { mock_voucher_adjustment_service }
|
|
|
|
before do
|
|
voucher.create_adjustment(voucher.code, order)
|
|
end
|
|
|
|
it "doesn't recalculate the voucher adjustment" do
|
|
expect(service).not_to receive(:update)
|
|
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to checkout_step_path(:payment)
|
|
end
|
|
|
|
context "when updating shipping method" do
|
|
let(:checkout_params) do
|
|
{
|
|
order: {
|
|
email: user.email,
|
|
bill_address_attributes: address.to_param,
|
|
ship_address_attributes: address.to_param
|
|
},
|
|
shipping_method_id: new_shipping_method.id.to_s
|
|
}
|
|
end
|
|
let(:new_shipping_method) { create(:shipping_method, distributors: [distributor]) }
|
|
|
|
before do
|
|
# Add a shipping rates for the new shipping method to prevent
|
|
# order.select_shipping_method from failing
|
|
order.shipment.shipping_rates <<
|
|
Spree::ShippingRate.create(shipping_method: new_shipping_method, selected: true)
|
|
end
|
|
|
|
it "recalculates the voucher adjustment" do
|
|
expect(service).to receive(:update)
|
|
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to checkout_step_path(:payment)
|
|
end
|
|
|
|
context "when no shipments available" do
|
|
before do
|
|
order.shipments.destroy_all
|
|
end
|
|
|
|
it "recalculates the voucher adjustment" do
|
|
expect(service).to receive(:update)
|
|
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to checkout_step_path(:payment)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "payment step" do
|
|
let(:step) { "payment" }
|
|
|
|
before do
|
|
order.bill_address = address
|
|
order.ship_address = address
|
|
order.select_shipping_method shipping_method.id
|
|
Orders::WorkflowService.new(order).advance_to_payment
|
|
end
|
|
|
|
context "with incomplete data" do
|
|
let(:checkout_params) { { order: { email: user.email } } }
|
|
|
|
it "returns 422 and some feedback" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to have_http_status :unprocessable_entity
|
|
expect(flash[:error]).to match "Saving failed, please update the highlighted fields."
|
|
expect(order.reload.state).to eq "payment"
|
|
end
|
|
end
|
|
|
|
context "with complete data" do
|
|
let(:checkout_params) do
|
|
{
|
|
order: {
|
|
payments_attributes: [
|
|
{ payment_method_id: payment_method.id }
|
|
]
|
|
}
|
|
}
|
|
end
|
|
|
|
it "updates and redirects to summary step" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to checkout_step_path(:summary)
|
|
expect(order.reload.state).to eq "confirmation"
|
|
end
|
|
|
|
describe "with a voucher" do
|
|
let(:voucher) { create(:voucher_flat_rate, enterprise: distributor) }
|
|
|
|
before do
|
|
voucher.create_adjustment(voucher.code, order)
|
|
end
|
|
|
|
# so we need to recalculate voucher to account for payment fees
|
|
it "recalculates the voucher adjustment" do
|
|
service = mock_voucher_adjustment_service
|
|
expect(service).to receive(:update)
|
|
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to checkout_step_path(:summary)
|
|
end
|
|
end
|
|
|
|
context "with insufficient stock" do
|
|
it "redirects to details page" do
|
|
allow(order).to receive_message_chain(:insufficient_stock_lines,
|
|
:empty?).and_return false
|
|
|
|
put(:update, params:)
|
|
|
|
expect_cable_ready_redirect(response)
|
|
end
|
|
end
|
|
|
|
context "with existing invalid payments" do
|
|
let(:invalid_payments) {
|
|
[
|
|
create(:payment, state: :failed),
|
|
create(:payment, state: :void),
|
|
]
|
|
}
|
|
|
|
before do
|
|
order.payments = invalid_payments
|
|
end
|
|
|
|
it "deletes invalid payments" do
|
|
expect{
|
|
put(:update, params:)
|
|
}.to change { order.payments.to_a }.from(invalid_payments)
|
|
end
|
|
end
|
|
|
|
context "with different payment method previously chosen" do
|
|
let(:other_payment_method) { build(:payment_method, distributors: [distributor]) }
|
|
let(:other_payment) {
|
|
build(:payment, amount: order.total, payment_method: other_payment_method)
|
|
}
|
|
|
|
before do
|
|
order.payments = [other_payment]
|
|
end
|
|
|
|
context "and order is in an earlier state" do
|
|
# This revealed obscure bug #12693. If you progress to order summary, go back to payment
|
|
# method, then open delivery details in a new tab (or hover over the link with Turbo
|
|
# enabled), then submit new payment details, this happens.
|
|
|
|
before do
|
|
order.back_to_address
|
|
end
|
|
|
|
it "deletes invalid (old) payments" do
|
|
put(:update, params:)
|
|
order.payments.reload
|
|
expect(order.payments).not_to include other_payment
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with no payment source" do
|
|
let(:checkout_params) do
|
|
{
|
|
order: {
|
|
payments_attributes: [
|
|
{
|
|
payment_method_id:,
|
|
source_attributes: {
|
|
first_name: "Jane",
|
|
last_name: "Doe",
|
|
month: "",
|
|
year: "",
|
|
cc_type: "",
|
|
last_digits: "",
|
|
gateway_payment_profile_id: ""
|
|
}
|
|
}
|
|
]
|
|
},
|
|
commit: "Next - Order Summary"
|
|
}
|
|
end
|
|
|
|
context "with a cash/check payment method" do
|
|
let!(:payment_method_id) { payment_method.id }
|
|
|
|
it "updates and redirects to summary step" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to have_http_status :found
|
|
expect(response).to redirect_to checkout_step_path(:summary)
|
|
expect(order.reload.state).to eq "confirmation"
|
|
end
|
|
end
|
|
|
|
context "with a StripeSCA payment method" do
|
|
let(:stripe_payment_method) {
|
|
create(:stripe_sca_payment_method, distributor_ids: [distributor.id],
|
|
environment: Rails.env)
|
|
}
|
|
let!(:payment_method_id) { stripe_payment_method.id }
|
|
|
|
it "updates and redirects to summary step" do
|
|
put(:update, params:)
|
|
expect(response).to have_http_status :unprocessable_entity
|
|
expect(flash[:error]).to match "Saving failed, please update the highlighted fields."
|
|
expect(order.reload.state).to eq "payment"
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with payment fees" do
|
|
let(:payment_method_with_fee) do
|
|
create(:payment_method, :flat_rate, amount: "1.23", distributors: [distributor])
|
|
end
|
|
let(:checkout_params) do
|
|
{
|
|
order: {
|
|
payments_attributes: [
|
|
{ payment_method_id: payment_method_with_fee.id }
|
|
]
|
|
}
|
|
}
|
|
end
|
|
|
|
it "applies the fee and updates the order total" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to checkout_step_path(:summary)
|
|
|
|
order.reload
|
|
|
|
expect(order.state).to eq "confirmation"
|
|
expect(order.payments.first.adjustment.amount).to eq 1.23
|
|
expect(order.payments.first.amount).to eq order.item_total + order.adjustment_total
|
|
expect(order.adjustment_total).to eq 1.23
|
|
expect(order.total).to eq order.item_total + order.adjustment_total
|
|
end
|
|
end
|
|
|
|
context "with a zero-priced order" do
|
|
let(:params) do
|
|
{ step: "payment", order: { payments_attributes: [{ amount: 0 }] } }
|
|
end
|
|
|
|
before do
|
|
order.line_items.first.update(price: 0)
|
|
order.update_totals_and_states
|
|
end
|
|
|
|
it "allows proceeding to confirmation" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to checkout_step_path(:summary)
|
|
expect(order.reload.state).to eq "confirmation"
|
|
expect(order.payments.count).to eq 1
|
|
expect(order.payments.first.amount).to eq 0
|
|
end
|
|
end
|
|
|
|
context "with a saved credit card" do
|
|
let!(:saved_card) { create(:stored_credit_card, user:) }
|
|
let(:checkout_params) do
|
|
{
|
|
order: {
|
|
payments_attributes: [
|
|
{ payment_method_id: payment_method.id }
|
|
]
|
|
},
|
|
existing_card_id: saved_card.id
|
|
}
|
|
end
|
|
|
|
it "updates and redirects to payment step" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to checkout_step_path(:summary)
|
|
expect(order.reload.state).to eq "confirmation"
|
|
expect(order.payments.first.source.id).to eq saved_card.id
|
|
end
|
|
end
|
|
end
|
|
|
|
context "summary step" do
|
|
let(:step) { "summary" }
|
|
let(:checkout_params) { { confirm_order: "Complete order" } }
|
|
|
|
before do
|
|
order.bill_address = address
|
|
order.ship_address = address
|
|
order.select_shipping_method shipping_method.id
|
|
Orders::WorkflowService.new(order).advance_to_payment
|
|
|
|
order.payments << build(:payment, amount: order.total, payment_method:)
|
|
order.next
|
|
end
|
|
|
|
describe "confirming the order" do
|
|
it "completes the order and redirects to order confirmation" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to order_path(order, order_token: order.token)
|
|
expect(order.reload.state).to eq "complete"
|
|
end
|
|
|
|
it "syncs stock before locking the order" do
|
|
actions = []
|
|
expect(StockSyncJob).to receive(:sync_linked_catalogs_now) do
|
|
actions << "sync stock"
|
|
end
|
|
expect(CurrentOrderLocker).to receive(:around) do
|
|
actions << "lock order"
|
|
end
|
|
|
|
put(:update, params:)
|
|
|
|
expect(actions).to eq ["sync stock", "lock order"]
|
|
end
|
|
end
|
|
|
|
context "with insufficient stock" do
|
|
it "redirects to details page" do
|
|
allow(order).to receive_message_chain(:insufficient_stock_lines, :empty?).and_return false
|
|
|
|
put(:update, params:)
|
|
|
|
expect_cable_ready_redirect(response)
|
|
end
|
|
end
|
|
|
|
context "when accepting T&Cs is required" do
|
|
before do
|
|
allow(TermsOfService).to receive(:platform_terms_required?) { true }
|
|
end
|
|
|
|
describe "submitting without accepting the T&Cs" do
|
|
let(:checkout_params) { {} }
|
|
|
|
it "returns 422 and some feedback" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to have_http_status :unprocessable_entity
|
|
expect(flash[:error]).to match "Saving failed, please update the highlighted fields."
|
|
expect(order.reload.state).to eq "confirmation"
|
|
end
|
|
end
|
|
|
|
describe "submitting and accepting the T&Cs" do
|
|
let(:checkout_params) { { accept_terms: true } }
|
|
|
|
it "completes the order and redirects to order confirmation" do
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to order_path(order, order_token: order.token)
|
|
expect(order.reload.state).to eq "complete"
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with a VINE voucher", feature: :connected_apps do
|
|
let(:vine_voucher) {
|
|
create(:vine_voucher, code: 'some_code', enterprise: distributor, amount: 6)
|
|
}
|
|
let(:vine_voucher_redeemer) { instance_double(Vine::VoucherRedeemerService) }
|
|
|
|
before do
|
|
# Adding voucher to the order
|
|
vine_voucher.create_adjustment(vine_voucher.code, order)
|
|
OrderManagement::Order::Updater.new(order).update_voucher
|
|
|
|
allow(Vine::VoucherRedeemerService).to receive(:new).and_return(vine_voucher_redeemer)
|
|
end
|
|
|
|
it "completes the order and redirects to order confirmation" do
|
|
expect(vine_voucher_redeemer).to receive(:redeem).and_return(true)
|
|
|
|
put(:update, params:)
|
|
|
|
expect(response).to redirect_to order_path(order, order_token: order.token)
|
|
expect(order.reload.state).to eq "complete"
|
|
end
|
|
|
|
context "when redeeming the voucher fails" do
|
|
it "returns 422 and some error" do
|
|
allow(vine_voucher_redeemer).to receive(:redeem).and_return(false)
|
|
allow(vine_voucher_redeemer).to receive(:errors).and_return(
|
|
{ redeeming_failed: "Redeeming the voucher failed" }
|
|
)
|
|
|
|
put(:update, params:)
|
|
|
|
expect(response).to have_http_status :unprocessable_entity
|
|
expect(flash[:error]).to match "Redeeming the voucher failed"
|
|
end
|
|
end
|
|
|
|
context "when an other error happens" do
|
|
it "returns 422 and some error" do
|
|
allow(vine_voucher_redeemer).to receive(:redeem).and_return(false)
|
|
allow(vine_voucher_redeemer).to receive(:errors).and_return(
|
|
{ vine_api: "There was an error communicating with the API" }
|
|
)
|
|
|
|
put(:update, params:)
|
|
|
|
expect(response).to have_http_status :unprocessable_entity
|
|
expect(flash[:error]).to match "There was an error while trying to redeem your voucher"
|
|
end
|
|
end
|
|
|
|
context "when an external payment gateway is used" do
|
|
before do
|
|
expect(payment_method).to receive(:external_gateway?) { true }
|
|
expect(payment_method).to receive(:external_payment_url) { "https://example.com/pay" }
|
|
mock_payment_method_fetcher(payment_method)
|
|
end
|
|
|
|
it "redeems the voucher and redirect to the payment gateway's URL" do
|
|
expect(vine_voucher_redeemer).to receive(:redeem).and_return(true)
|
|
|
|
put(:update, params:)
|
|
|
|
expect(response.body).to match("https://example.com/pay").and match("redirect")
|
|
expect(order.reload.state).to eq "confirmation"
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when an external payment gateway is used" do
|
|
before do
|
|
expect(payment_method).to receive(:external_gateway?) { true }
|
|
expect(payment_method).to receive(:external_payment_url) { "https://example.com/pay" }
|
|
mock_payment_method_fetcher(payment_method)
|
|
end
|
|
|
|
describe "confirming the order" do
|
|
it "redirects to the payment gateway's URL" do
|
|
put(:update, params:)
|
|
|
|
expect(response.body).to match("https://example.com/pay").and match("redirect")
|
|
expect(order.reload.state).to eq "confirmation"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "running out of stock" do
|
|
let(:order_cycle_distributed_variants) { double(:order_cycle_distributed_variants) }
|
|
|
|
before do
|
|
allow(controller).to receive(:current_order).and_return(order)
|
|
allow(order).to receive(:distributor).and_return(distributor)
|
|
order.update(order_cycle:)
|
|
|
|
allow(OrderCycles::DistributedVariantsService).to receive(:new).and_return(
|
|
order_cycle_distributed_variants
|
|
)
|
|
end
|
|
|
|
shared_examples "handling not available items" do |step|
|
|
context "#{step} step" do
|
|
let(:step) { step.to_s }
|
|
|
|
it "redirects when some items are not available" do
|
|
allow(order).to receive_message_chain(:insufficient_stock_lines, :empty?).and_return true
|
|
expect(order_cycle_distributed_variants).to receive(
|
|
:distributes_order_variants?
|
|
).with(order).and_return(false)
|
|
|
|
get :edit
|
|
expect(response).to redirect_to cart_path
|
|
end
|
|
end
|
|
end
|
|
|
|
it_behaves_like "handling not available items", "details"
|
|
it_behaves_like "handling not available items", "payment"
|
|
it_behaves_like "handling not available items", "summary"
|
|
end
|
|
|
|
def mock_voucher_adjustment_service
|
|
voucher_adjustment_service = instance_double(VoucherAdjustmentsService)
|
|
allow(VoucherAdjustmentsService).to receive(:new).and_return(voucher_adjustment_service)
|
|
|
|
voucher_adjustment_service
|
|
end
|
|
|
|
def expect_cable_ready_redirect(response)
|
|
expect(response.parsed_body).to eq(
|
|
[{ "url" => "/checkout/details", "operation" => "redirectTo" }].to_json
|
|
)
|
|
end
|
|
|
|
def mock_payment_method_fetcher(payment_method)
|
|
payment_method_fetcher = instance_double(Checkout::PaymentMethodFetcher, call: payment_method)
|
|
expect(Checkout::PaymentMethodFetcher).to receive(:new).and_return(payment_method_fetcher)
|
|
end
|
|
end
|