# frozen_string_literal: true module PaymentGateways class PaypalController < ::BaseController include OrderStockCheck include OrderCompletion before_action :destroy_orphaned_paypal_payments, only: :confirm before_action :load_checkout_order, only: [:express, :confirm] before_action :handle_insufficient_stock, only: [:express, :confirm] before_action -> { check_order_cycle_expiry(should_empty_order: false) }, only: [ :express, :confirm ] before_action :permit_parameters! after_action :reset_order_when_complete, only: :confirm def express return redirect_to order_failed_route if @any_out_of_stock == true 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_step_path(:payment) end rescue SocketError flash[:error] = Spree.t('flash.connection_failed', scope: 'paypal') redirect_to main_app.checkout_step_path(:payment) end end def confirm return redirect_to order_failed_route if @any_out_of_stock == true # 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... @order.payments.create!( source: Spree::PaypalExpressCheckout.create( token: params[:token], payer_id: params[:PayerID] ), amount: @order.total, payment_method: ) process_payment_completion! end def cancel flash[:notice] = Spree.t('flash.cancel', scope: 'paypal') redirect_to main_app.checkout_path 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? order_completion_reset(current_order) 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 address_required? payment_method.preferred_solution.eql?('Sole') end end end