diff --git a/Gemfile b/Gemfile index 1a2b18d571..39c0140964 100644 --- a/Gemfile +++ b/Gemfile @@ -61,12 +61,7 @@ gem 'paranoia', '~> 2.4' gem 'state_machines-activerecord' gem 'stringex', '~> 2.8.5' -# Our branch contains the following changes: -# - Pass customer email and phone number to PayPal (merged to upstream master) -# - Change type of password from string to password to hide it in the form -# - Skip CA cert file and use the ones provided by the OS -gem 'spree_paypal_express', github: 'openfoodfoundation/better_spree_paypal_express', branch: '2-1-0-stable' - +gem 'paypal-sdk-merchant', '1.106.1' gem 'stripe' gem 'devise' diff --git a/Gemfile.lock b/Gemfile.lock index 86a11240b3..de66fc091a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,14 +4,6 @@ GIT specs: custom_error_message (1.1.1) -GIT - remote: https://github.com/openfoodfoundation/better_spree_paypal_express.git - revision: 1a477e9f7763297944cc99b6f4dd3d962aa963e9 - branch: 2-1-0-stable - specs: - spree_paypal_express (2.0.3) - paypal-sdk-merchant (= 1.106.1) - GIT remote: https://github.com/openfoodfoundation/ofn-qz.git revision: 467f6ea1c44529c7c91cac4c8211bbd863588c0b @@ -797,6 +789,7 @@ DEPENDENCIES paper_trail (~> 10.3.1) paperclip (~> 3.4.1) paranoia (~> 2.4) + paypal-sdk-merchant (= 1.106.1) pg (~> 0.21.0) pry pry-byebug @@ -822,7 +815,6 @@ DEPENDENCIES selenium-webdriver shoulda-matchers simplecov - spree_paypal_express! spring spring-commands-rspec state_machines-activerecord diff --git a/app/assets/javascripts/admin/spree_paypal_express.js b/app/assets/javascripts/admin/spree_paypal_express.js new file mode 100644 index 0000000000..81d6a899af --- /dev/null +++ b/app/assets/javascripts/admin/spree_paypal_express.js @@ -0,0 +1,17 @@ +//= require admin/spree_backend + +SpreePaypalExpress = { + hideSettings: function(paymentMethod) { + if (SpreePaypalExpress.paymentMethodID && paymentMethod.val() == SpreePaypalExpress.paymentMethodID) { + $('.payment-method-settings').children().hide(); + $('#payment_amount').prop('disabled', 'disabled'); + $('button[type="submit"]').prop('disabled', 'disabled'); + $('#paypal-warning').show(); + } else if (SpreePaypalExpress.paymentMethodID) { + $('.payment-method-settings').children().show(); + $('button[type=submit]').prop('disabled', ''); + $('#payment_amount').prop('disabled', ''); + $('#paypal-warning').hide(); + } + } +} diff --git a/app/controllers/spree/admin/payments_controller.rb b/app/controllers/spree/admin/payments_controller.rb index 8b5f42c472..e0a530a823 100644 --- a/app/controllers/spree/admin/payments_controller.rb +++ b/app/controllers/spree/admin/payments_controller.rb @@ -67,6 +67,25 @@ module Spree redirect_to request.referer end + def paypal_refund + if request.get? + if @payment.source.state == 'refunded' + flash[:error] = Spree.t(:already_refunded, scope: 'paypal') + redirect_to admin_order_payment_path(@order, @payment) + end + elsif request.post? + response = @payment.payment_method.refund(@payment, params[:refund_amount]) + if response.success? + flash[:success] = Spree.t(:refund_successful, scope: 'paypal') + redirect_to admin_order_payments_path(@order) + else + flash.now[:error] = Spree.t(:refund_unsuccessful, scope: 'paypal') + + " (#{response.errors.first.long_message})" + render + end + end + end + private def load_payment_source diff --git a/app/controllers/spree/admin/paypal_payments_controller.rb b/app/controllers/spree/admin/paypal_payments_controller.rb new file mode 100644 index 0000000000..872a23d6d7 --- /dev/null +++ b/app/controllers/spree/admin/paypal_payments_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Spree + module Admin + class PaypalPaymentsController < Spree::Admin::BaseController + before_action :load_order + + def index + @payments = @order.payments.includes(:payment_method). + where(spree_payment_methods: { type: "Spree::Gateway::PayPalExpress" }) + end + + private + + def load_order + @order = Spree::Order.where(number: params[:order_id]).first + end + end + end +end diff --git a/app/controllers/spree/orders_controller.rb b/app/controllers/spree/orders_controller.rb index 740f563bf5..616300c2b1 100644 --- a/app/controllers/spree/orders_controller.rb +++ b/app/controllers/spree/orders_controller.rb @@ -1,6 +1,8 @@ module Spree class OrdersController < Spree::StoreController include OrderCyclesHelper + include Rails.application.routes.url_helpers + layout 'darkswarm' ssl_required :show diff --git a/app/controllers/spree/paypal_controller.rb b/app/controllers/spree/paypal_controller.rb new file mode 100644 index 0000000000..ca44867165 --- /dev/null +++ b/app/controllers/spree/paypal_controller.rb @@ -0,0 +1,244 @@ +# frozen_string_literal: true + +module Spree + class PaypalController < StoreController + 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 + # TODO: Remove in Spree 2.2 + tax_adjustments = tax_adjustments.additional if tax_adjustments.respond_to?(: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] } + # Would use tax_total here, but it can include "included" taxes as well. + # For instance, tax_total would include the 10% GST in Australian stores. + # A quick sum will get us around that little problem. + # TODO: Remove additional check in 2.2 + tax_adjustments = current_order.adjustments.tax + tax_adjustments = tax_adjustments.additional if tax_adjustments.respond_to?(: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) + spree.order_path(order, token: order.token) + end + + def address_required? + payment_method.preferred_solution.eql?('Sole') + end + end +end diff --git a/app/controllers/spree/paypal_controller_decorator.rb b/app/controllers/spree/paypal_controller_decorator.rb deleted file mode 100644 index 97fedf74af..0000000000 --- a/app/controllers/spree/paypal_controller_decorator.rb +++ /dev/null @@ -1,157 +0,0 @@ -# frozen_string_literal: true - -Spree::PaypalController.class_eval do - 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 - # TODO: Remove in Spree 2.2 - tax_adjustments = tax_adjustments.additional if tax_adjustments.respond_to?(: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 #10 - # https://cms.paypal.com/uk/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_ECCustomizing - # "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. See https://github.com/spree/spree/blob/1-3-stable/core/lib/spree/core/controller_helpers/order.rb#L14 - # for details. - def expire_current_order - session[:order_id] = nil - @current_order = nil - end - - private - - 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 - if current_order.complete? - flash[:notice] = t(:order_processed_successfully) - - OrderCompletionReset.new(self, current_order).call - session[:access_token] = current_order.token - end - 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 completion_route(order) - spree.order_path(order, token: order.token) - 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 -end diff --git a/app/models/spree/gateway/pay_pal_express.rb b/app/models/spree/gateway/pay_pal_express.rb new file mode 100644 index 0000000000..357d563264 --- /dev/null +++ b/app/models/spree/gateway/pay_pal_express.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'paypal-sdk-merchant' + +module Spree + class Gateway + class PayPalExpress < Gateway + preference :login, :string + preference :password, :password + preference :signature, :string + preference :server, :string, default: 'sandbox' + preference :solution, :string, default: 'Mark' + preference :landing_page, :string, default: 'Billing' + preference :logourl, :string, default: '' + + def supports?(_source) + true + end + + def provider_class + ::PayPal::SDK::Merchant::API + end + + def provider + ::PayPal::SDK.configure( + mode: preferred_server.presence || "sandbox", + username: preferred_login, + password: preferred_password, + signature: preferred_signature + ) + provider_class.new + end + + def auto_capture? + true + end + + def method_type + 'paypal' + end + + def purchase(_amount, express_checkout, _gateway_options = {}) + pp_details_request = provider.build_get_express_checkout_details( + Token: express_checkout.token + ) + pp_details_response = provider.get_express_checkout_details(pp_details_request) + + pp_request = provider.build_do_express_checkout_payment( + DoExpressCheckoutPaymentRequestDetails: { + PaymentAction: "Sale", + Token: express_checkout.token, + PayerID: express_checkout.payer_id, + PaymentDetails: pp_details_response. + get_express_checkout_details_response_details.PaymentDetails + } + ) + + pp_response = provider.do_express_checkout_payment(pp_request) + if pp_response.success? + # We need to store the transaction id for the future. + # This is mainly so we can use it later on to refund the payment if the user wishes. + transaction_id = pp_response.do_express_checkout_payment_response_details. + payment_info.first.transaction_id + express_checkout.update_column(:transaction_id, transaction_id) + # This is rather hackish, required for payment/processing handle_response code. + Class.new do + def success?; true; end + + def authorization; nil; end + end.new + else + class << pp_response + def to_s + errors.map(&:long_message).join(" ") + end + end + pp_response + end + end + + def refund(payment, amount) + refund_type = payment.amount == amount.to_f ? "Full" : "Partial" + refund_transaction = provider.build_refund_transaction( + TransactionID: payment.source.transaction_id, + RefundType: refund_type, + Amount: { + currencyID: payment.currency, + value: amount + }, + RefundSource: "any" + ) + refund_transaction_response = provider.refund_transaction(refund_transaction) + if refund_transaction_response.success? + payment.source.update_attributes( + refunded_at: Time.now, + refund_transaction_id: refund_transaction_response.RefundTransactionID, + state: "refunded", + refund_type: refund_type + ) + + payment.class.create!( + order: payment.order, + source: payment, + payment_method: payment.payment_method, + amount: amount.to_f.abs * -1, + response_code: refund_transaction_response.RefundTransactionID, + state: 'completed' + ) + end + refund_transaction_response + end + end + end +end diff --git a/app/models/spree/paypal_express_checkout.rb b/app/models/spree/paypal_express_checkout.rb new file mode 100644 index 0000000000..93e0b0e6d0 --- /dev/null +++ b/app/models/spree/paypal_express_checkout.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Spree + class PaypalExpressCheckout < ActiveRecord::Base + end +end diff --git a/app/views/spree/admin/payments/_paypal_complete.html.haml b/app/views/spree/admin/payments/_paypal_complete.html.haml new file mode 100644 index 0000000000..d2ea6405f0 --- /dev/null +++ b/app/views/spree/admin/payments/_paypal_complete.html.haml @@ -0,0 +1,18 @@ += form_tag paypal_refund_admin_order_payment_path(@order, @payment) do + .label-block.left.five.columns.alpha + %div + %fieldset + %legend= Spree.t('refund', :scope => :paypal) + .field + = label_tag 'refund_amount', Spree.t(:refund_amount, :scope => 'paypal') + %small + %em= Spree.t(:original_amount, :scope => 'paypal', :amount => @payment.display_amount) + %br/ + - symbol = ::Money.new(1, Spree::Config[:currency]).symbol + - if Spree::Config[:currency_symbol_position] == "before" + = symbol + = text_field_tag 'refund_amount', @payment.amount + - else + = text_field_tag 'refund_amount', @payment.amount + = symbol + = button Spree.t(:refund, :scope => 'paypal'), 'icon-dollar' diff --git a/app/views/spree/admin/payments/paypal_refund.html.haml b/app/views/spree/admin/payments/paypal_refund.html.haml new file mode 100644 index 0000000000..98641d05a2 --- /dev/null +++ b/app/views/spree/admin/payments/paypal_refund.html.haml @@ -0,0 +1,28 @@ += render partial: 'spree/admin/shared/order_tabs', locals: { current: 'Payments' } + +- content_for :page_title do + %i.icon-arrow-right + = link_to Spree.t(:payments), admin_order_payments_path(@order) + %i.icon-arrow-right + = payment_method_name(@payment) + %i.icon-arrow-right + = Spree.t('refund', scope: :paypal) + += form_tag paypal_refund_admin_order_payment_path(@order, @payment) do + .label-block.left.five.columns.alpha + %div + %fieldset + %legend= Spree.t('refund', scope: :paypal) + .field + = label_tag 'refund_amount', Spree.t(:refund_amount, scope: 'paypal') + %small + %em= Spree.t(:original_amount, scope: 'paypal', amount: @payment.display_amount) + %br/ + - symbol = ::Money.new(1, Spree::Config[:currency]).symbol + - if Spree::Config[:currency_symbol_position] == "before" + = symbol + = text_field_tag 'refund_amount', @payment.amount + - else + = text_field_tag 'refund_amount', @payment.amount + = symbol + = button Spree.t(:refund, scope: 'paypal'), 'icon-dollar' diff --git a/app/views/spree/admin/payments/source_forms/_paypal.html.haml b/app/views/spree/admin/payments/source_forms/_paypal.html.haml index 124139dae0..dbdfe44d7a 100644 --- a/app/views/spree/admin/payments/source_forms/_paypal.html.haml +++ b/app/views/spree/admin/payments/source_forms/_paypal.html.haml @@ -1,8 +1,2 @@ --# We can remove this file as soon as we have a version of better_spree_paypal_express that includes: --# https://github.com/spree-contrib/better_spree_paypal_express/commit/4360a1fb82d30d7601bc6a98e7b74819f0b377f0 - --# The selectors in app/assets/javascripts/spree/backend/paypal_express.js don't work with the version --# of the views we are using, so the warning below wasn't displaying without this override. - #paypal-warning %strong= t('.no_payment_via_admin_backend', :scope => 'paypal') diff --git a/app/views/spree/admin/payments/source_views/_paypal.html.haml b/app/views/spree/admin/payments/source_views/_paypal.html.haml new file mode 100644 index 0000000000..d1094fa000 --- /dev/null +++ b/app/views/spree/admin/payments/source_views/_paypal.html.haml @@ -0,0 +1,36 @@ +%fieldset + %legend{align: "center"}= Spree.t(:transaction, scope: :paypal) + .row + .alpha.six.columns + %dl + %dt + = Spree.t(:payer_id, scope: :paypal) + \: + %dd= payment.source.payer_id + %dt + = Spree.t(:token, scope: :paypal) + \: + %dd= payment.source.token + %dt + = Spree.t(:transaction_id) + \: + %dd= payment.source.transaction_id + - if payment.source.state != 'refunded' + = button_link_to Spree.t('actions.refund', scope: :paypal), + spree.paypal_refund_admin_order_payment_path(@order, payment), + icon: 'icon-dollar' + - else + .alpha.six.columns + %dl + %dt + = Spree.t(:state, scope: :paypal) + \: + %dd= payment.source.state.titleize + %dt + = Spree.t(:refunded_at, scope: :paypal) + \: + %dd= pretty_time(payment.source.refunded_at) + %dt + = Spree.t(:refund_transaction_id, scope: :paypal) + \: + %dd= payment.source.refund_transaction_id diff --git a/config/application.rb b/config/application.rb index d69b4cdf34..e48ce0556b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -129,6 +129,7 @@ module Openfoodnetwork app.config.spree.payment_methods << Spree::Gateway::Pin app.config.spree.payment_methods << Spree::Gateway::StripeConnect app.config.spree.payment_methods << Spree::Gateway::StripeSCA + app.config.spree.payment_methods << Spree::Gateway::PayPalExpress end # Settings in config/environments/* take precedence over those specified here. diff --git a/config/initializers/paypal.rb b/config/initializers/paypal.rb new file mode 100644 index 0000000000..31ec5f826a --- /dev/null +++ b/config/initializers/paypal.rb @@ -0,0 +1,15 @@ +# Fixes the issue about some PayPal requests failing with +# OpenSSL::SSL::SSLError (SSL_connect returned=1 errno=0 state=error: certificate verify failed) +module CAFileHack + # This overrides paypal-sdk-core default so we don't pass the cert the gem provides to the + # NET::HTTP instance. This way we rely on the default behavior of validating the server's cert + # against the CA certs of the OS (we assume), which tend to be up to date. + # + # See https://github.com/openfoodfoundation/openfoodnetwork/issues/5855 for details. + def default_ca_file + nil + end +end + +require 'paypal-sdk-merchant' +PayPal::SDK::Core::Util::HTTPHelper.prepend(CAFileHack) diff --git a/config/locales/en.yml b/config/locales/en.yml index 613f2adb45..3a4700a9f5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3679,6 +3679,24 @@ See the %{link} to find out more about %{sitename}'s features and to start using ended: ended paused: paused canceled: cancelled + paypal: + already_refunded: "This payment has been refunded and no further action can be taken on it." + no_payment_via_admin_backend: "You cannot charge PayPal accounts through the admin backend at this time." + transaction: "PayPal Transaction" + payer_id: "Payer ID" + transaction_id: "Transaction ID" + token: "Token" + refund: "Refund" + refund_amount: "Amount" + original_amount: "Original amount: %{amount}" + refund_successful: "PayPal refund successful" + refund_unsuccessful: "PayPal refund unsuccessful" + actions: + refund: "Refund" + flash: + cancel: "Don't want to use PayPal? No problems." + connection_failed: "Could not connect to PayPal." + generic_error: "PayPal failed. %{reasons}" users: form: account_settings: Account Settings diff --git a/lib/spree/api/controller_setup.rb b/lib/spree/api/controller_setup.rb index 7d9215a332..010ce77b1e 100644 --- a/lib/spree/api/controller_setup.rb +++ b/lib/spree/api/controller_setup.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require 'spree/core/controller_helpers/auth' + module Spree module Api module ControllerSetup