Files
openfoodnetwork/app/controllers/payment_gateways/paypal_controller.rb
Matt-Yorkley 9f49a84e7f Clarify use of access tokens used for viewing order details as a guest user
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!
2021-12-16 13:35:55 +00:00

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