# 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