Skip to content

Commit

Permalink
Rescue from InsufficientStock error on frontend checkout flow
Browse files Browse the repository at this point in the history
When two users try to purchase the last item remaining from a non-backordeable
stock location at the same time then the last one will experience an unhandled
error `Spree::Order::InsufficientStock`.

This happens only if there is a second backorderable stock location for the
product.

When there is no backorderable stock location the controller `before_action`
`ensure_sufficient_stock_lines` is enough to catch the issue in advance.

The error is generated by this line in Spree::Order model:

`before_transition to: :complete, do: :validate_line_item_availability`

Generally, `ensure_sufficient_stock_lines` prevent customers to complete the
checkout process when there is not enough stock availability, but the case
above is not caught here.

So, by using `rescue_from` the customer is now redirected to the checkout
`address` page and shown an error message that suggests to repeat the
checkout process.

The order's shipments will be rebuilt using the backorderable stock location
in the delivery step, allowing them to (hopefully!) complete the purchase.

`rescue_from` is already used on the `api` and `backend` section in order to
manage `InsufficientStock` errors, so this was a natural choice also on the
`frontend`.
  • Loading branch information
spaghetticode committed Jan 12, 2019
1 parent 942ec5c commit fb9a631
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 0 deletions.
1 change: 1 addition & 0 deletions core/config/locales/en.yml
Expand Up @@ -1472,6 +1472,7 @@ en:
inventory_adjustment: Inventory Adjustment
inventory_canceled: Inventory canceled
inventory_error_flash_for_insufficient_quantity: "%{names} became unavailable."
inventory_error_flash_for_insufficient_shipment_quantity: "Quantity selected of %{unavailable_items} is not available. Still, items may be available from another stock location, please try again."
inventory_not_available: Inventory not available for %{item}.
inventory_state: Inventory State
inventory_states:
Expand Down
18 changes: 18 additions & 0 deletions frontend/app/controllers/spree/checkout_controller.rb
Expand Up @@ -24,6 +24,7 @@ class CheckoutController < Spree::StoreController
helper 'spree/orders'

rescue_from Spree::Core::GatewayError, with: :rescue_from_spree_gateway_error
rescue_from Spree::Order::InsufficientStock, with: :insufficient_stock_error

# Updates the order and advances to the next state (when possible.)
def update
Expand Down Expand Up @@ -233,5 +234,22 @@ def rescue_from_spree_gateway_error(exception)
def check_authorization
authorize!(:edit, current_order, cookies.signed[:guest_token])
end

def insufficient_stock_error
packages = @order.shipments.map(&:to_package)
if packages.empty?
flash[:error] = I18n.t('spree.insufficient_stock_for_order')
redirect_to cart_path
else
availability_validator = Spree::Stock::AvailabilityValidator.new
unavailable_items = @order.line_items.reject { |line_item| availability_validator.validate(line_item) }
if unavailable_items.any?
item_names = unavailable_items.map(&:name).to_sentence
flash[:error] = t('spree.inventory_error_flash_for_insufficient_shipment_quantity', unavailable_items: item_names)
@order.restart_checkout_flow
redirect_to spree.checkout_state_path(@order.state)
end
end
end
end
end
39 changes: 39 additions & 0 deletions frontend/spec/controllers/spree/checkout_controller_spec.rb
Expand Up @@ -420,6 +420,45 @@ def post_address
expect(flash[:error]).to eq(I18n.t('spree.payment_processing_failed'))
end
end

context "when InsufficientStock error is raised" do
before do
allow(controller).to receive_messages current_order: order
allow(controller).to receive_messages check_authorization: true
allow(controller).to receive_messages ensure_sufficient_stock_lines: true
end

context "when the order has no shipments" do
let(:order) { Spree::TestingSupport::OrderWalkthrough.up_to(:address) }

before do
allow(order).to receive_messages shipments: []
# Order#next is the tipical failure point here:
allow(order).to receive(:next).and_raise(Spree::Order::InsufficientStock)
end

it "redirects the customer to the cart page with an error message" do
put :update, params: { state: order.state, order: {} }
expect(flash[:error]).to eq(I18n.t('spree.insufficient_stock_for_order'))
expect(response).to redirect_to(spree.cart_path)
end
end

context "when the order has shipments" do
let(:order) { Spree::TestingSupport::OrderWalkthrough.up_to(:payment) }

context "when items become somehow not available anymore" do
before { Spree::StockItem.update_all backorderable: false }

it "redirects the customer to the address checkout page with an error message" do
put :update, params: { state: order.state, order: {} }
error = I18n.t('spree.inventory_error_flash_for_insufficient_shipment_quantity', unavailable_items: order.products.first.name)
expect(flash[:error]).to eq(error)
expect(response).to redirect_to(spree.checkout_state_path(state: :address))
end
end
end
end
end

context "When last inventory item has been purchased" do
Expand Down
71 changes: 71 additions & 0 deletions frontend/spec/features/checkout_confirm_insufficient_stock_spec.rb
@@ -0,0 +1,71 @@
# frozen_string_literal: true

require 'spec_helper'

describe "Checkout confirm page submission", type: :feature do
include_context 'checkout setup'

context "when the product from the order is not backorderable but has enough stock quantity" do
let(:user) { create(:user) }

let(:order) { Spree::TestingSupport::OrderWalkthrough.up_to(:payment) }
let(:order_product) { order.products.first }
let(:order_stock_item) { order_product.stock_items.first }

before do
order_stock_item.update! backorderable: false
order_stock_item.set_count_on_hand(1)
allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order)
allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user)
allow_any_instance_of(Spree::OrdersController).to receive_messages(try_spree_current_user: user)
end

context 'when there are not other backorderable stock locations' do
context 'when the customer is on the confirm page and the availabilty drops to zero' do
before do
visit spree.checkout_state_path(:confirm)
order_stock_item.set_count_on_hand(0)
end

it 'redirects to cart page and shows an unavailable product message' do
click_button "Place Order"
expect(page).to have_content "#{order_product.name} became unavailable"
expect(page).to have_current_path spree.cart_path
end
end
end

context 'when there is another backorderable stock location' do
before do
create :stock_location, backorderable_default: true, default: false
end

context 'when the customer is on the confirm page and the availabilty drops to zero' do
before do
visit spree.checkout_state_path(:confirm)
order_stock_item.set_count_on_hand(0)
end

it "redirects to the address checkout page and shows an availability changed message" do
click_button "Place Order"
error_message = "Quantity selected of #{order_product.name} is not available. Still, items may be available from another stock location, please try again."
expect(page).to have_content error_message
expect(page).to have_current_path spree.checkout_state_path(:address)
end

it "can still complete the order using the backorderable stock location by restarting the checkout" do
click_button "Place Order"
expect(page).to have_current_path spree.checkout_state_path(:address)
click_button "Save and Continue"
expect(page).to have_current_path spree.checkout_state_path(:delivery)
click_button "Save and Continue"
expect(page).to have_current_path spree.checkout_state_path(:payment)
click_button "Save and Continue"
expect(page).to have_current_path spree.checkout_state_path(:confirm)
click_button "Place Order"
expect(page).to have_content 'Your order has been processed successfully'
end
end
end
end
end

0 comments on commit fb9a631

Please sign in to comment.