Files
openfoodnetwork/app/controllers/spree/paypal_controller.rb
Maikel 173cf9e536 Merge pull request #6565 from Matt-Yorkley/adjustments-inclusive
[Adjustments] Improve inclusive/additional tax recording
2021-02-08 09:38:31 +11:00

239 lines
7.9 KiB
Ruby

# frozen_string_literal: true
module Spree
class PaypalController < ::BaseController
ssl_allowed
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)
items = order.line_items.map(&method(:line_item))
tax_adjustments = order.adjustments.tax.additional
shipping_adjustments = order.adjustments.shipping
order.adjustments.eligible.each do |adjustment|
next if (tax_adjustments + shipping_adjustments).include?(adjustment)
items << {
Name: adjustment.label,
Quantity: 1,
Amount: {
currencyID: order.currency,
value: adjustment.amount
}
}
end
# Because PayPal doesn't accept $0 items at all.
# See https://github.com/spree-contrib/better_spree_paypal_express/issues/10
# "It can be a positive or negative value but not zero."
items.reject! do |item|
item[:Amount][:value].zero?
end
pp_request = provider.build_set_express_checkout(
express_checkout_request_details(order, items)
)
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 spree.checkout_state_path(:payment)
end
rescue SocketError
flash[:error] = Spree.t('flash.connection_failed', scope: 'paypal')
redirect_to spree.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.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 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 line_item(item)
{
Name: item.product.name,
Number: item.variant.sku,
Quantity: item.quantity,
Amount: {
currencyID: item.order.currency,
value: item.price
},
ItemCategory: "Physical"
}
end
def express_checkout_request_details(order, items)
{
SetExpressCheckoutRequestDetails: {
InvoiceID: order.number,
BuyerEmail: order.email,
ReturnURL: spree.confirm_paypal_url(
payment_method_id: params[:payment_method_id], utm_nooverride: 1
),
CancelURL: spree.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(items)]
}
}
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(items)
item_sum = items.sum { |i| i[:Quantity] * i[:Amount][:value] }
tax_adjustments = current_order.adjustments.tax.additional
tax_adjustments_total = tax_adjustments.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, token: order.token)
end
def address_required?
payment_method.preferred_solution.eql?('Sole')
end
end
end