mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-29 21:17:17 +00:00
It was possible that several people bought the same variant even though there wasn't enough stock for everybody. That resulted in negative stock.
269 lines
8.9 KiB
Ruby
269 lines
8.9 KiB
Ruby
require 'open_food_network/address_finder'
|
|
|
|
class CheckoutController < Spree::CheckoutController
|
|
layout 'darkswarm'
|
|
|
|
prepend_around_filter :lock_variants, only: :update
|
|
|
|
prepend_before_filter :check_hub_ready_for_checkout
|
|
prepend_before_filter :check_order_cycle_expiry
|
|
prepend_before_filter :require_order_cycle
|
|
prepend_before_filter :require_distributor_chosen
|
|
|
|
before_filter :enable_embedded_shopfront
|
|
|
|
include OrderCyclesHelper
|
|
include EnterprisesHelper
|
|
|
|
def edit
|
|
# This is only required because of spree_paypal_express. If we implement
|
|
# a version of paypal that uses this controller, and more specifically
|
|
# the #update_failed method, then we can remove this call
|
|
RestartCheckout.new(@order).call
|
|
end
|
|
|
|
def update
|
|
shipping_method_id = object_params.delete(:shipping_method_id)
|
|
|
|
return update_failed unless @order.update_attributes(object_params)
|
|
|
|
check_order_for_phantom_fees
|
|
fire_event('spree.checkout.update')
|
|
|
|
while @order.state != "complete"
|
|
if @order.state == "payment"
|
|
return if redirect_to_paypal_express_form_if_needed
|
|
end
|
|
|
|
if @order.state == "delivery"
|
|
@order.select_shipping_method(shipping_method_id)
|
|
end
|
|
|
|
next if advance_order_state(@order)
|
|
|
|
flash[:error] = if @order.errors.present?
|
|
@order.errors.full_messages.to_sentence
|
|
else
|
|
t(:payment_processing_failed)
|
|
end
|
|
update_failed
|
|
return
|
|
end
|
|
return update_failed unless @order.state == "complete" || @order.completed?
|
|
|
|
set_default_bill_address
|
|
set_default_ship_address
|
|
|
|
ResetOrderService.new(self, current_order).call
|
|
session[:access_token] = current_order.token
|
|
|
|
flash[:notice] = t(:order_processed_successfully)
|
|
respond_to do |format|
|
|
format.html do
|
|
respond_with(@order, location: order_path(@order))
|
|
end
|
|
format.json do
|
|
render json: { path: order_path(@order) }, status: :ok
|
|
end
|
|
end
|
|
end
|
|
|
|
# Clears the cached order. Required for #current_order to return a new order
|
|
# to serve as cart. See https://github.com/spree/spree/blob/1-3-stable/core/lib/spree/core/controller_helpers/order.rb#L14
|
|
# for details.
|
|
def expire_current_order
|
|
session[:order_id] = nil
|
|
@current_order = nil
|
|
end
|
|
|
|
private
|
|
|
|
# We need locks to avoid a race condition on stock checking. Otherwise we end
|
|
# up with negative stock when many people check out at the same time. This
|
|
# implementation makes a checkout wait for all other checkouts containing one
|
|
# of the order`s variants.
|
|
#
|
|
# There are many places in which stock is stored in the database. Row locking
|
|
# on variant level ensures that there are no conflicts even when an item is
|
|
# sold through different shops.
|
|
#
|
|
# Ordering the variants by id prevents deadlocks. Plucking the id sends the
|
|
# locking query without building Spree::Variant objects.
|
|
def lock_variants
|
|
# The before action `load_order` handles this error state:
|
|
return yield if current_order.nil?
|
|
|
|
ActiveRecord::Base.transaction do
|
|
# Prevent another checkout from completing the order at the same time:
|
|
current_order.lock!
|
|
|
|
# Prevent any other checkouts of the same line items at the same time:
|
|
variant_ids = current_order.line_items.select(:variant_id)
|
|
Spree::Variant.where(id: variant_ids).order(:id).lock.pluck(:id)
|
|
|
|
yield
|
|
end
|
|
end
|
|
|
|
def set_default_bill_address
|
|
if params[:order][:default_bill_address]
|
|
new_bill_address = @order.bill_address.clone.attributes
|
|
|
|
user_bill_address_id = spree_current_user.bill_address.andand.id
|
|
spree_current_user.update_attributes(
|
|
bill_address_attributes: new_bill_address.merge('id' => user_bill_address_id)
|
|
)
|
|
|
|
customer_bill_address_id = @order.customer.bill_address.andand.id
|
|
@order.customer.update_attributes(
|
|
bill_address_attributes: new_bill_address.merge('id' => customer_bill_address_id)
|
|
)
|
|
end
|
|
end
|
|
|
|
def set_default_ship_address
|
|
if params[:order][:default_ship_address]
|
|
new_ship_address = @order.ship_address.clone.attributes
|
|
|
|
user_ship_address_id = spree_current_user.ship_address.andand.id
|
|
spree_current_user.update_attributes(
|
|
ship_address_attributes: new_ship_address.merge('id' => user_ship_address_id)
|
|
)
|
|
|
|
customer_ship_address_id = @order.customer.ship_address.andand.id
|
|
@order.customer.update_attributes(
|
|
ship_address_attributes: new_ship_address.merge('id' => customer_ship_address_id)
|
|
)
|
|
end
|
|
end
|
|
|
|
def check_order_for_phantom_fees
|
|
phantom_fees = @order.adjustments.
|
|
joins("LEFT OUTER JOIN spree_line_items"\
|
|
" ON spree_line_items.id = spree_adjustments.source_id").
|
|
where("originator_type = 'EnterpriseFee'"\
|
|
" AND source_type = 'Spree::LineItem' AND spree_line_items.id IS NULL")
|
|
|
|
if phantom_fees.any?
|
|
Bugsnag.notify(RuntimeError.new("Phantom Fees"),
|
|
phantom_fees: {
|
|
phantom_total: phantom_fees.sum(&:amount).to_s,
|
|
phantom_fees: phantom_fees.as_json
|
|
})
|
|
end
|
|
end
|
|
|
|
# Copied and modified from spree. Remove check for order state, since the state machine is
|
|
# progressed all the way in one go with the one page checkout.
|
|
def object_params
|
|
# For payment step, filter order parameters to produce the expected
|
|
# nested attributes for a single payment and its source,
|
|
# discarding attributes for payment methods other than the one selected
|
|
if params[:payment_source].present? && source_params = params.delete(:payment_source)[params[:order][:payments_attributes].first[:payment_method_id].underscore]
|
|
params[:order][:payments_attributes].first[:source_attributes] = source_params
|
|
end
|
|
if params[:order][:payments_attributes]
|
|
params[:order][:payments_attributes].first[:amount] = @order.total
|
|
end
|
|
if params[:order][:existing_card_id]
|
|
construct_saved_card_attributes
|
|
end
|
|
params[:order]
|
|
end
|
|
|
|
# Perform order.next, guarding against StaleObjectErrors
|
|
def advance_order_state(order)
|
|
tries ||= 3
|
|
order.next
|
|
rescue ActiveRecord::StaleObjectError
|
|
retry unless (tries -= 1).zero?
|
|
false
|
|
end
|
|
|
|
def update_failed
|
|
current_order.updater.shipping_address_from_distributor
|
|
RestartCheckout.new(@order).call
|
|
|
|
respond_to do |format|
|
|
format.html do
|
|
render :edit
|
|
end
|
|
format.json do
|
|
render json: { errors: @order.errors, flash: flash.to_hash }.to_json, status: :bad_request
|
|
end
|
|
end
|
|
end
|
|
|
|
def load_order
|
|
@order = current_order
|
|
redirect_to(main_app.shop_path) && return unless @order && @order.checkout_allowed?
|
|
redirect_to_cart_path && return unless valid_order_line_items?
|
|
redirect_to(main_app.shop_path) && return if @order.completed?
|
|
before_address
|
|
setup_for_current_state
|
|
end
|
|
|
|
def before_address
|
|
associate_user
|
|
|
|
finder = OpenFoodNetwork::AddressFinder.new(@order.email, @order.customer, spree_current_user)
|
|
|
|
@order.bill_address = finder.bill_address
|
|
@order.ship_address = finder.ship_address
|
|
end
|
|
|
|
def valid_order_line_items?
|
|
@order.insufficient_stock_lines.empty? &&
|
|
OrderCycleDistributedVariants.new(@order.order_cycle, @order.distributor).
|
|
distributes_order_variants?(@order)
|
|
end
|
|
|
|
def redirect_to_cart_path
|
|
respond_to do |format|
|
|
format.html do
|
|
redirect_to main_app.cart_path
|
|
end
|
|
|
|
format.json do
|
|
render json: { path: main_app.cart_path }, status: :bad_request
|
|
end
|
|
end
|
|
end
|
|
|
|
def redirect_to_paypal_express_form_if_needed
|
|
return unless params[:order][:payments_attributes]
|
|
|
|
payment_method_id = params[:order][:payments_attributes].first[:payment_method_id]
|
|
payment_method = Spree::PaymentMethod.find(payment_method_id)
|
|
return unless payment_method.is_a?(Spree::Gateway::PayPalExpress)
|
|
|
|
render json: { path: spree.paypal_express_path(payment_method_id: payment_method.id) },
|
|
status: :ok
|
|
true
|
|
end
|
|
|
|
def construct_saved_card_attributes
|
|
existing_card_id = params[:order].delete(:existing_card_id)
|
|
return if existing_card_id.blank?
|
|
|
|
credit_card = Spree::CreditCard.find(existing_card_id)
|
|
if credit_card.try(:user_id).blank? || credit_card.user_id != spree_current_user.try(:id)
|
|
raise Spree::Core::GatewayError, I18n.t(:invalid_credit_card)
|
|
end
|
|
|
|
# Not currently supported but maybe we should add it...?
|
|
credit_card.verification_value = params[:cvc_confirm] if params[:cvc_confirm].present?
|
|
|
|
params[:order][:payments_attributes].first[:source] = credit_card
|
|
params[:order][:payments_attributes].first.delete :source_attributes
|
|
end
|
|
|
|
def rescue_from_spree_gateway_error(error)
|
|
flash[:error] = t(:spree_gateway_error_flash_for_checkout, error: error.message)
|
|
respond_to do |format|
|
|
format.html { render :edit }
|
|
format.json { render json: { flash: flash.to_hash }, status: :bad_request }
|
|
end
|
|
end
|
|
end
|