mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Merge pull request #6574 from Matt-Yorkley/bye-bye-spree-paypal
Bye Bye Spree Paypal
This commit is contained in:
7
Gemfile
7
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'
|
||||
|
||||
10
Gemfile.lock
10
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
|
||||
|
||||
17
app/assets/javascripts/admin/spree_paypal_express.js
Normal file
17
app/assets/javascripts/admin/spree_paypal_express.js
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
20
app/controllers/spree/admin/paypal_payments_controller.rb
Normal file
20
app/controllers/spree/admin/paypal_payments_controller.rb
Normal file
@@ -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
|
||||
@@ -1,6 +1,8 @@
|
||||
module Spree
|
||||
class OrdersController < Spree::StoreController
|
||||
include OrderCyclesHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
layout 'darkswarm'
|
||||
|
||||
ssl_required :show
|
||||
|
||||
244
app/controllers/spree/paypal_controller.rb
Normal file
244
app/controllers/spree/paypal_controller.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
114
app/models/spree/gateway/pay_pal_express.rb
Normal file
114
app/models/spree/gateway/pay_pal_express.rb
Normal file
@@ -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
|
||||
6
app/models/spree/paypal_express_checkout.rb
Normal file
6
app/models/spree/paypal_express_checkout.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class PaypalExpressCheckout < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
18
app/views/spree/admin/payments/_paypal_complete.html.haml
Normal file
18
app/views/spree/admin/payments/_paypal_complete.html.haml
Normal file
@@ -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'
|
||||
28
app/views/spree/admin/payments/paypal_refund.html.haml
Normal file
28
app/views/spree/admin/payments/paypal_refund.html.haml
Normal file
@@ -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'
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
15
config/initializers/paypal.rb
Normal file
15
config/initializers/paypal.rb
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spree/core/controller_helpers/auth'
|
||||
|
||||
module Spree
|
||||
module Api
|
||||
module ControllerSetup
|
||||
|
||||
Reference in New Issue
Block a user