mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-27 21:06:49 +00:00
There are 4 or 5 different places in the app where we reference a :token and params[:token] for completely different purposes (they're not even vaguely the *same* token). This is an attempt to clarify the places in the app where we use params[:token] in relation to *orders*, for allowing guest users (who are not logged in) to view details of an order they have placed (like after checkout completion), and differentiate it from the various other places where params[:token] can actually be used for something entirely different!
203 lines
7.0 KiB
Ruby
203 lines
7.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module PaymentGateways
|
|
class PaypalController < ::BaseController
|
|
include OrderStockCheck
|
|
|
|
before_action :enable_embedded_shopfront
|
|
before_action :destroy_orphaned_paypal_payments, only: :confirm
|
|
after_action :reset_order_when_complete, only: :confirm
|
|
before_action :permit_parameters!
|
|
|
|
def express
|
|
order = current_order || raise(ActiveRecord::RecordNotFound)
|
|
|
|
pp_request = provider.build_set_express_checkout(
|
|
express_checkout_request_details(order)
|
|
)
|
|
|
|
begin
|
|
pp_response = provider.set_express_checkout(pp_request)
|
|
if pp_response.success?
|
|
# At this point Paypal has *provisionally* accepted that the payment can now be placed,
|
|
# and the user will be redirected to a Paypal payment page. On completion, the user is
|
|
# sent back and the response is handled in the #confirm action in this controller.
|
|
redirect_to provider.express_checkout_url(pp_response, useraction: 'commit')
|
|
else
|
|
flash[:error] =
|
|
Spree.t('flash.generic_error', scope: 'paypal',
|
|
reasons: pp_response.errors.map(&:long_message).join(" "))
|
|
redirect_to main_app.checkout_state_path(:payment)
|
|
end
|
|
rescue SocketError
|
|
flash[:error] = Spree.t('flash.connection_failed', scope: 'paypal')
|
|
redirect_to main_app.checkout_state_path(:payment)
|
|
end
|
|
end
|
|
|
|
def confirm
|
|
@order = current_order || raise(ActiveRecord::RecordNotFound)
|
|
|
|
# At this point the user has come back from the Paypal form, and we get one
|
|
# last chance to interact with the payment process before the money moves...
|
|
return reset_to_cart unless sufficient_stock?
|
|
|
|
@order.payments.create!(
|
|
source: Spree::PaypalExpressCheckout.create(
|
|
token: params[:token],
|
|
payer_id: params[:PayerID]
|
|
),
|
|
amount: @order.total,
|
|
payment_method: payment_method
|
|
)
|
|
@order.process_payments!
|
|
@order.next
|
|
if @order.complete?
|
|
flash.notice = Spree.t(:order_processed_successfully)
|
|
flash[:commerce_tracking] = "nothing special"
|
|
session[:order_id] = nil
|
|
redirect_to completion_route(@order)
|
|
else
|
|
redirect_to main_app.checkout_state_path(@order.state)
|
|
end
|
|
end
|
|
|
|
def cancel
|
|
flash[:notice] = Spree.t('flash.cancel', scope: 'paypal')
|
|
redirect_to main_app.checkout_path
|
|
end
|
|
|
|
# Clears the cached order. Required for #current_order to return a new order to serve as cart.
|
|
def expire_current_order
|
|
session[:order_id] = nil
|
|
@current_order = nil
|
|
end
|
|
|
|
private
|
|
|
|
def express_checkout_request_details(order)
|
|
{
|
|
SetExpressCheckoutRequestDetails: {
|
|
InvoiceID: order.number,
|
|
BuyerEmail: order.email,
|
|
ReturnURL: payment_gateways_confirm_paypal_url(
|
|
payment_method_id: params[:payment_method_id], utm_nooverride: 1
|
|
),
|
|
CancelURL: payment_gateways_cancel_paypal_url,
|
|
SolutionType: payment_method.preferred_solution.presence || "Mark",
|
|
LandingPage: payment_method.preferred_landing_page.presence || "Billing",
|
|
cppheaderimage: payment_method.preferred_logourl.presence || "",
|
|
NoShipping: 1,
|
|
PaymentDetails: [payment_details(order)]
|
|
}
|
|
}
|
|
end
|
|
|
|
def payment_method
|
|
@payment_method ||= Spree::PaymentMethod.find(params[:payment_method_id])
|
|
end
|
|
|
|
def permit_parameters!
|
|
params.permit(:token, :payment_method_id, :PayerID)
|
|
end
|
|
|
|
def reset_order_when_complete
|
|
return unless current_order.complete?
|
|
|
|
flash[:notice] = t(:order_processed_successfully)
|
|
OrderCompletionReset.new(self, current_order).call
|
|
session[:access_token] = current_order.token
|
|
end
|
|
|
|
def reset_to_cart
|
|
OrderCheckoutRestart.new(@order).call
|
|
handle_insufficient_stock
|
|
end
|
|
|
|
# See #1074 and #1837 for more detail on why we need this
|
|
# An 'orphaned' Spree::Payment is created for every call to CheckoutController#update
|
|
# for orders that are processed using a Spree::Gateway::PayPalExpress payment method
|
|
# These payments are 'orphaned' because they are never used by the spree_paypal_express gem
|
|
# which creates a brand new Spree::Payment from scratch in PayPalController#confirm
|
|
# However, the 'orphaned' payments are useful when applying a transaction fee, because the fees
|
|
# need to be calculated before the order details are sent to PayPal for confirmation
|
|
# This is our best hook for removing the orphaned payments at an appropriate time. ie. after
|
|
# the payment details have been confirmed, but before any payments have been processed
|
|
def destroy_orphaned_paypal_payments
|
|
return unless payment_method.is_a?(Spree::Gateway::PayPalExpress)
|
|
|
|
orphaned_payments = current_order.payments.
|
|
where(payment_method_id: payment_method.id, source_id: nil)
|
|
orphaned_payments.each(&:destroy)
|
|
end
|
|
|
|
def provider
|
|
payment_method.provider
|
|
end
|
|
|
|
def payment_details(order)
|
|
items = PaypalItemsBuilder.new(order).call
|
|
|
|
item_sum = items.sum { |i| i[:Quantity] * i[:Amount][:value] }
|
|
tax_adjustments_total = current_order.all_adjustments.tax.additional.sum(:amount)
|
|
|
|
if item_sum.zero?
|
|
# Paypal does not support no items or a zero dollar ItemTotal
|
|
# This results in the order summary being simply "Current purchase"
|
|
{
|
|
OrderTotal: {
|
|
currencyID: current_order.currency,
|
|
value: current_order.total
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OrderTotal: {
|
|
currencyID: current_order.currency,
|
|
value: current_order.total
|
|
},
|
|
ItemTotal: {
|
|
currencyID: current_order.currency,
|
|
value: item_sum
|
|
},
|
|
ShippingTotal: {
|
|
currencyID: current_order.currency,
|
|
value: current_order.ship_total
|
|
},
|
|
TaxTotal: {
|
|
currencyID: current_order.currency,
|
|
value: tax_adjustments_total,
|
|
},
|
|
ShipToAddress: address_options,
|
|
PaymentDetailsItem: items,
|
|
ShippingMethod: "Shipping Method Name Goes Here",
|
|
PaymentAction: "Sale"
|
|
}
|
|
end
|
|
end
|
|
|
|
def address_options
|
|
return {} unless address_required?
|
|
|
|
{
|
|
Name: current_order.bill_address.try(:full_name),
|
|
Street1: current_order.bill_address.address1,
|
|
Street2: current_order.bill_address.address2,
|
|
CityName: current_order.bill_address.city,
|
|
Phone: current_order.bill_address.phone,
|
|
StateOrProvince: current_order.bill_address.state_text,
|
|
Country: current_order.bill_address.country.iso,
|
|
PostalCode: current_order.bill_address.zipcode
|
|
}
|
|
end
|
|
|
|
def completion_route(order)
|
|
main_app.order_path(order, order_token: order.token)
|
|
end
|
|
|
|
def address_required?
|
|
payment_method.preferred_solution.eql?('Sole')
|
|
end
|
|
end
|
|
end
|