Refactored and improved support for 0.40.3 and later
This commit is contained in:
parent
b763f1ad12
commit
9086741f52
|
@ -1 +1,2 @@
|
||||||
*.swp
|
*.swp
|
||||||
|
spec/test_app
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
source 'http://rubygems.org'
|
||||||
|
|
||||||
|
gem 'sqlite3-ruby', :require => 'sqlite3'
|
||||||
|
|
||||||
|
group :test do
|
||||||
|
gem 'rspec-rails', '= 2.4.1'
|
||||||
|
gem 'factory_girl', '= 1.3.3'
|
||||||
|
gem 'factory_girl_rails', '= 1.0.1'
|
||||||
|
gem 'rcov'
|
||||||
|
gem 'shoulda'
|
||||||
|
gem 'faker'
|
||||||
|
if RUBY_VERSION < "1.9"
|
||||||
|
gem "ruby-debug"
|
||||||
|
else
|
||||||
|
gem "ruby-debug19"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
group :cucumber do
|
||||||
|
gem 'cucumber-rails'
|
||||||
|
gem 'database_cleaner', '~> 0.5.2'
|
||||||
|
gem 'nokogiri'
|
||||||
|
gem 'capybara'
|
||||||
|
gem 'factory_girl', '= 1.3.3'
|
||||||
|
gem 'factory_girl_rails', '= 1.0.1'
|
||||||
|
gem 'faker'
|
||||||
|
gem 'launchy'
|
||||||
|
|
||||||
|
if RUBY_VERSION < "1.9"
|
||||||
|
gem "ruby-debug"
|
||||||
|
else
|
||||||
|
gem "ruby-debug19"
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,6 +11,7 @@ This extension allows the store to use PayPal Express from two locations:
|
||||||
sent to the PayPal review page (along with detailed order information).
|
sent to the PayPal review page (along with detailed order information).
|
||||||
|
|
||||||
THIS FEATURE IS NOT YET COMPLETE
|
THIS FEATURE IS NOT YET COMPLETE
|
||||||
|
|
||||||
2. Cart Checkout - Presents the PayPal checkout button on the users Cart page and redirects the user to complete all shipping / addressing
|
2. Cart Checkout - Presents the PayPal checkout button on the users Cart page and redirects the user to complete all shipping / addressing
|
||||||
information on PaypPal's site. This also supports PayPal's Instant Update feature to retrieve shipping options live from Spree when the user
|
information on PaypPal's site. This also supports PayPal's Instant Update feature to retrieve shipping options live from Spree when the user
|
||||||
selects / changes their shipping address on PayPal's site.
|
selects / changes their shipping address on PayPal's site.
|
||||||
|
@ -24,16 +25,6 @@ USAGE (Checkout Payment)
|
||||||
|
|
||||||
1. Setup your application
|
1. Setup your application
|
||||||
|
|
||||||
cp config/database.yml.example config/database.yml
|
|
||||||
rake db:bootstrap
|
|
||||||
|
|
||||||
Go ahead and load sample data
|
|
||||||
|
|
||||||
Fire it up to see that it works
|
|
||||||
|
|
||||||
Shut it down
|
|
||||||
|
|
||||||
|
|
||||||
2. Configure PPE
|
2. Configure PPE
|
||||||
|
|
||||||
You'll need to have a Paypal developer account (developer.paypal.com) and both buyer and seller test accounts.
|
You'll need to have a Paypal developer account (developer.paypal.com) and both buyer and seller test accounts.
|
||||||
|
@ -124,7 +115,7 @@ USAGE (Checkout Payment)
|
||||||
NOTES
|
NOTES
|
||||||
=====
|
=====
|
||||||
|
|
||||||
To automatically capture funds, add this to you site extension's activate method:
|
To automatically capture funds or enable accepting eCheck payments, add this to you site extension's activate method:
|
||||||
|
|
||||||
if Spree::Config.instance
|
if Spree::Config.instance
|
||||||
Spree::Config.set(:auto_capture => true)
|
Spree::Config.set(:auto_capture => true)
|
||||||
|
|
84
Rakefile
84
Rakefile
|
@ -1,31 +1,69 @@
|
||||||
require File.expand_path('../../config/application', __FILE__)
|
# encoding: utf-8
|
||||||
|
|
||||||
require 'rubygems'
|
require 'rubygems'
|
||||||
require 'rake'
|
require 'rake'
|
||||||
require 'rake/testtask'
|
require 'rake/testtask'
|
||||||
require 'rake/packagetask'
|
|
||||||
require 'rake/gempackagetask'
|
|
||||||
|
|
||||||
spec = eval(File.read('spree_paypal_express.gemspec'))
|
|
||||||
|
|
||||||
Rake::GemPackageTask.new(spec) do |p|
|
|
||||||
p.gem_spec = spec
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "Release to gemcutter"
|
|
||||||
task :release => :package do
|
|
||||||
require 'rake/gemcutter'
|
|
||||||
Rake::Gemcutter::Tasks.new(spec).define
|
|
||||||
Rake::Task['gem:push'].invoke
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "Default Task"
|
desc "Default Task"
|
||||||
task :default => [ :spec ]
|
task :default => [ :spec ]
|
||||||
|
|
||||||
require 'rspec/core/rake_task'
|
gemfile = File.expand_path('../spec/test_app/Gemfile', __FILE__)
|
||||||
RSpec::Core::RakeTask.new
|
if File.exists?(gemfile) && %w(rcov spec cucumber).include?(ARGV.first.to_s)
|
||||||
|
require 'bundler'
|
||||||
|
ENV['BUNDLE_GEMFILE'] = gemfile
|
||||||
|
Bundler.setup
|
||||||
|
|
||||||
|
require 'rspec/core/rake_task'
|
||||||
|
RSpec::Core::RakeTask.new
|
||||||
|
|
||||||
|
require 'cucumber/rake/task'
|
||||||
|
Cucumber::Rake::Task.new do |t|
|
||||||
|
t.cucumber_opts = %w{--format pretty}
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Run specs with RCov"
|
||||||
|
RSpec::Core::RakeTask.new(:rcov) do |t|
|
||||||
|
t.rcov = true
|
||||||
|
t.rcov_opts = %w{ --exclude gems\/,spec\/,features\/}
|
||||||
|
t.verbose = true
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Regenerates a rails 3 app for testing"
|
||||||
|
task :test_app do
|
||||||
|
SPREE_PATH = ENV['SPREE_PATH']
|
||||||
|
raise "SPREE_PATH should be specified" unless SPREE_PATH
|
||||||
|
require File.join(SPREE_PATH, 'lib/generators/spree/test_app_generator')
|
||||||
|
class AuthTestAppGenerator < Spree::Generators::TestAppGenerator
|
||||||
|
def tweak_gemfile
|
||||||
|
append_file 'Gemfile' do
|
||||||
|
<<-gems
|
||||||
|
gem 'spree_core', :path => '#{File.join(SPREE_PATH, 'core')}'
|
||||||
|
gem 'spree_auth', :path => '#{File.join(SPREE_PATH, 'auth')}'
|
||||||
|
gem 'spree_paypal_express', :path => '#{File.dirname(__FILE__)}'
|
||||||
|
gems
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def install_gems
|
||||||
|
inside "test_app" do
|
||||||
|
run 'rake spree_core:install'
|
||||||
|
run 'rake spree_auth:install'
|
||||||
|
run 'rake spree_paypal_express:install'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def migrate_db
|
||||||
|
run_migrations
|
||||||
|
end
|
||||||
|
end
|
||||||
|
AuthTestAppGenerator.start
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace :test_app do
|
||||||
|
desc 'Rebuild test and cucumber databases'
|
||||||
|
task :rebuild_dbs do
|
||||||
|
system("cd spec/test_app && rake db:drop db:migrate RAILS_ENV=test && rake db:drop db:migrate RAILS_ENV=cucumber")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# require 'cucumber/rake/task'
|
|
||||||
# Cucumber::Rake::Task.new do |t|
|
|
||||||
# t.cucumber_opts = %w{--format pretty}
|
|
||||||
# end
|
|
|
@ -1,31 +1,25 @@
|
||||||
# aim to unpick this later
|
CheckoutController.class_eval do
|
||||||
module Spree::PaypalExpress
|
before_filter :redirect_to_paypal_express_form_if_needed, :only => [:update]
|
||||||
include ERB::Util
|
|
||||||
include ActiveMerchant::RequiresParameters
|
|
||||||
|
|
||||||
def self.included(target)
|
|
||||||
target.before_filter :redirect_to_paypal_express_form_if_needed, :only => [:update]
|
|
||||||
end
|
|
||||||
|
|
||||||
def paypal_checkout
|
def paypal_checkout
|
||||||
load_order
|
load_order
|
||||||
opts = all_opts(@order, params[:payment_method_id], 'checkout')
|
opts = all_opts(@order, params[:payment_method_id], 'checkout')
|
||||||
opts.merge!(address_options(@order))
|
opts.merge!(address_options(@order))
|
||||||
gateway = paypal_gateway
|
@gateway = paypal_gateway
|
||||||
|
|
||||||
if Spree::Config[:auto_capture]
|
if Spree::Config[:auto_capture]
|
||||||
response = gateway.setup_purchase(opts[:money], opts)
|
@ppx_response = @gateway.setup_purchase(opts[:money], opts)
|
||||||
else
|
else
|
||||||
response = gateway.setup_authorization(opts[:money], opts)
|
@ppx_response = @gateway.setup_authorization(opts[:money], opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
unless response.success?
|
unless @ppx_response.success?
|
||||||
gateway_error(response)
|
gateway_error(@ppx_response)
|
||||||
redirect_to edit_order_url(@order)
|
redirect_to edit_order_url(@order)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to (gateway.redirect_url_for response.token, :review => payment_method.preferred_review)
|
redirect_to (@gateway.redirect_url_for response.token, :review => payment_method.preferred_review)
|
||||||
rescue ActiveMerchant::ConnectionError => e
|
rescue ActiveMerchant::ConnectionError => e
|
||||||
gateway_error I18n.t(:unable_to_connect_to_gateway)
|
gateway_error I18n.t(:unable_to_connect_to_gateway)
|
||||||
redirect_to :back
|
redirect_to :back
|
||||||
|
@ -35,21 +29,21 @@ module Spree::PaypalExpress
|
||||||
load_order
|
load_order
|
||||||
opts = all_opts(@order,params[:payment_method_id], 'payment')
|
opts = all_opts(@order,params[:payment_method_id], 'payment')
|
||||||
opts.merge!(address_options(@order))
|
opts.merge!(address_options(@order))
|
||||||
gateway = paypal_gateway
|
@gateway = paypal_gateway
|
||||||
|
|
||||||
if Spree::Config[:auto_capture]
|
if Spree::Config[:auto_capture]
|
||||||
response = gateway.setup_purchase(opts[:money], opts)
|
@ppx_response = @gateway.setup_purchase(opts[:money], opts)
|
||||||
else
|
else
|
||||||
response = gateway.setup_authorization(opts[:money], opts)
|
@ppx_response = @gateway.setup_authorization(opts[:money], opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
unless response.success?
|
unless @ppx_response.success?
|
||||||
gateway_error(response)
|
gateway_error(@ppx_response)
|
||||||
redirect_to edit_order_checkout_url(@order, :state => "payment")
|
redirect_to edit_order_checkout_url(@order, :state => "payment")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to (gateway.redirect_url_for response.token, :review => payment_method.preferred_review)
|
redirect_to (@gateway.redirect_url_for @ppx_response.token, :review => payment_method.preferred_review)
|
||||||
rescue ActiveMerchant::ConnectionError => e
|
rescue ActiveMerchant::ConnectionError => e
|
||||||
gateway_error I18n.t(:unable_to_connect_to_gateway)
|
gateway_error I18n.t(:unable_to_connect_to_gateway)
|
||||||
redirect_to :back
|
redirect_to :back
|
||||||
|
@ -126,10 +120,9 @@ module Spree::PaypalExpress
|
||||||
ppx_auth_response = gateway.authorize((@order.total*100).to_i, opts)
|
ppx_auth_response = gateway.authorize((@order.total*100).to_i, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
if ppx_auth_response.success?
|
paypal_account = PaypalAccount.find_by_payer_id(params[:PayerID])
|
||||||
paypal_account = PaypalAccount.find_by_payer_id(params[:PayerID])
|
|
||||||
|
|
||||||
payment = @order.payments.create(
|
payment = @order.payments.create(
|
||||||
:amount => ppx_auth_response.params["gross_amount"].to_f,
|
:amount => ppx_auth_response.params["gross_amount"].to_f,
|
||||||
:source => paypal_account,
|
:source => paypal_account,
|
||||||
:source_type => 'PaypalAccount',
|
:source_type => 'PaypalAccount',
|
||||||
|
@ -137,24 +130,23 @@ module Spree::PaypalExpress
|
||||||
:response_code => ppx_auth_response.params["ack"],
|
:response_code => ppx_auth_response.params["ack"],
|
||||||
:avs_response => ppx_auth_response.avs_result["code"])
|
:avs_response => ppx_auth_response.avs_result["code"])
|
||||||
|
|
||||||
# transition through the state machine. This way any callbacks can be made
|
payment.started_processing!
|
||||||
payment.started_processing!
|
|
||||||
payment.pend!
|
|
||||||
|
|
||||||
|
record_log payment, ppx_auth_response
|
||||||
|
|
||||||
|
if ppx_auth_response.success?
|
||||||
#confirm status
|
#confirm status
|
||||||
case ppx_auth_response.params["payment_status"]
|
case ppx_auth_response.params["payment_status"]
|
||||||
when "Completed"
|
when "Completed"
|
||||||
payment.complete!
|
payment.complete!
|
||||||
when "Pending"
|
when "Pending"
|
||||||
|
payment.pend!
|
||||||
else
|
else
|
||||||
|
payment.pend!
|
||||||
Rails.logger.error "Unexpected response from PayPal Express"
|
Rails.logger.error "Unexpected response from PayPal Express"
|
||||||
Rails.logger.error ppx_auth_response.to_yaml
|
Rails.logger.error ppx_auth_response.to_yaml
|
||||||
end
|
end
|
||||||
|
|
||||||
record_log payment, ppx_auth_response
|
|
||||||
|
|
||||||
@order.save!
|
|
||||||
|
|
||||||
#need to force checkout to complete state
|
#need to force checkout to complete state
|
||||||
until @order.state == "complete"
|
until @order.state == "complete"
|
||||||
if @order.next!
|
if @order.next!
|
||||||
|
@ -166,6 +158,7 @@ module Spree::PaypalExpress
|
||||||
redirect_to completion_route
|
redirect_to completion_route
|
||||||
|
|
||||||
else
|
else
|
||||||
|
payment.fail!
|
||||||
order_params = {}
|
order_params = {}
|
||||||
gateway_error(ppx_auth_response)
|
gateway_error(ppx_auth_response)
|
||||||
|
|
||||||
|
@ -207,7 +200,7 @@ module Spree::PaypalExpress
|
||||||
{ :description => "Goods from #{Spree::Config[:site_name]}", # site details...
|
{ :description => "Goods from #{Spree::Config[:site_name]}", # site details...
|
||||||
|
|
||||||
#:page_style => "foobar", # merchant account can set named config
|
#:page_style => "foobar", # merchant account can set named config
|
||||||
:header_image => "https://" + Spree::Config[:site_url] + "/images/logo.png",
|
:header_image => "https://#{Spree::Config[:site_name]}/images/logo.png",
|
||||||
:background_color => "ffffff", # must be hex only, six chars
|
:background_color => "ffffff", # must be hex only, six chars
|
||||||
:header_background_color => "ffffff",
|
:header_background_color => "ffffff",
|
||||||
:header_border_color => "ffffff",
|
:header_border_color => "ffffff",
|
||||||
|
@ -252,43 +245,33 @@ module Spree::PaypalExpress
|
||||||
end
|
end
|
||||||
|
|
||||||
credits_total = 0
|
credits_total = 0
|
||||||
credits.compact!
|
credits.compact!
|
||||||
if !credits.empty?
|
if credits.present?
|
||||||
items.concat credits
|
items.concat credits
|
||||||
credits_total = credits.map {|i| i[:amount] * i[:qty] }.sum
|
credits_total = credits.map {|i| i[:amount] * i[:qty] }.sum
|
||||||
end
|
end
|
||||||
|
|
||||||
opts = { :return_url => request.protocol + request.host_with_port + "/orders/#{order.number}/checkout/paypal_confirm?payment_method_id=#{payment_method}",
|
opts = { :return_url => request.protocol + request.host_with_port + "/orders/#{order.number}/checkout/paypal_confirm?payment_method_id=#{payment_method}",
|
||||||
:cancel_return_url => "http://" + request.host_with_port + "/orders/#{order.number}/edit",
|
:cancel_return_url => "http://" + request.host_with_port + "/orders/#{order.number}/edit",
|
||||||
:order_id => order.number,
|
:order_id => order.number,
|
||||||
:custom => order.number,
|
:custom => order.number,
|
||||||
:items => items
|
:items => items,
|
||||||
}
|
:subtotal => ((order.item_total * 100) + credits_total).to_i,
|
||||||
|
:tax => ((order.adjustments.map { |a| a.amount if ( a.source_type == 'Order' && a.label == 'Tax') }.compact.sum) * 100 ).to_i,
|
||||||
|
:shipping => ((order.adjustments.map { |a| a.amount if a.source_type == 'Shipment' }.compact.sum) * 100 ).to_i,
|
||||||
|
:money => (order.total * 100 ).to_i }
|
||||||
|
|
||||||
|
|
||||||
if stage == "checkout"
|
if stage == "checkout"
|
||||||
# recalculate all totals here as we need to ignore shipping & tax because we are checking-out via paypal (spree checkout not started)
|
|
||||||
opts[:subtotal] = ((order.item_total * 100) + credits_total ).to_i
|
|
||||||
opts[:handling] = 0
|
opts[:handling] = 0
|
||||||
opts[:tax] = ((order.adjustments.map { |a| a.amount if ( a.source_type == 'Order' && a.label == 'Tax') }.compact.sum) * 100 ).to_i
|
|
||||||
opts[:shipping] = ((order.adjustments.map { |a| a.amount if a.source_type == 'Shipment' }.compact.sum) * 100 ).to_i
|
|
||||||
|
|
||||||
# overall total
|
|
||||||
opts[:money] = (order.total * 100 ).to_i
|
|
||||||
|
|
||||||
opts[:callback_url] = "http://" + request.host_with_port + "/paypal_express_callbacks/#{order.number}"
|
opts[:callback_url] = "http://" + request.host_with_port + "/paypal_express_callbacks/#{order.number}"
|
||||||
opts[:callback_timeout] = 3
|
opts[:callback_timeout] = 3
|
||||||
elsif stage == "payment"
|
elsif stage == "payment"
|
||||||
#use real totals are we are paying via paypal (spree checkout almost complete)
|
|
||||||
opts[:subtotal] = ((order.item_total * 100) + (order.credits.total * 100)).to_i
|
|
||||||
opts[:tax] = ((order.adjustments.map { |a| a.amount if ( a.source_type == 'Order' && a.label == 'Tax') }.compact.sum) * 100 ).to_i
|
|
||||||
opts[:shipping] = ((order.adjustments.map { |a| a.amount if a.source_type == 'Shipment' }.compact.sum) * 100 ).to_i
|
|
||||||
|
|
||||||
#hack to add float rounding difference in as handling fee - prevents PayPal from rejecting orders
|
#hack to add float rounding difference in as handling fee - prevents PayPal from rejecting orders
|
||||||
#because the integer totals are different from the float based total. This is temporary and will be
|
#because the integer totals are different from the float based total. This is temporary and will be
|
||||||
#removed once Spree's currency values are persisted as integers (normally only 1c)
|
#removed once Spree's currency values are persisted as integers (normally only 1c)
|
||||||
opts[:handling] = (order.total*100).to_i - opts.slice(:subtotal, :tax, :shipping).values.sum
|
opts[:handling] = (order.total*100).to_i - opts.slice(:subtotal, :tax, :shipping).values.sum
|
||||||
|
|
||||||
opts[:money] = (order.total*100).to_i
|
|
||||||
end
|
end
|
||||||
|
|
||||||
opts
|
opts
|
||||||
|
@ -374,4 +357,6 @@ module Spree::PaypalExpress
|
||||||
def paypal_gateway
|
def paypal_gateway
|
||||||
payment_method.provider
|
payment_method.provider
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,16 +5,20 @@ class PaypalExpressCallbacksController < Spree::BaseController
|
||||||
def notify
|
def notify
|
||||||
retrieve_details #need to retreive details first to ensure ActiveMerchant gets configured correctly.
|
retrieve_details #need to retreive details first to ensure ActiveMerchant gets configured correctly.
|
||||||
|
|
||||||
|
|
||||||
@notification = Paypal::Notification.new(request.raw_post)
|
@notification = Paypal::Notification.new(request.raw_post)
|
||||||
|
|
||||||
# we only care about eChecks (for now?)
|
# we only care about eChecks (for now?)
|
||||||
if @notification.params["payment_type"] == "echeck" && @notification.acknowledge && @payment && @order.total >= @payment.amount
|
if @notification.params["payment_type"] == "echeck" && @notification.acknowledge && @payment && @order.total >= @payment.amount
|
||||||
|
@payment.started_processing!
|
||||||
|
@payment.log_entries.create(:details => @notification.to_yaml)
|
||||||
|
|
||||||
case @notification.params["payment_status"]
|
case @notification.params["payment_status"]
|
||||||
when "Denied"
|
when "Denied"
|
||||||
create_txn PaypalTxn::TxnType::DENIED
|
@payment.fail!
|
||||||
|
|
||||||
when "Completed"
|
when "Completed"
|
||||||
create_txn PaypalTxn::TxnType::CAPTURE
|
@payment.complete!
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -27,31 +31,10 @@ class PaypalExpressCallbacksController < Spree::BaseController
|
||||||
@order = Order.find_by_number(params["invoice"])
|
@order = Order.find_by_number(params["invoice"])
|
||||||
|
|
||||||
if @order
|
if @order
|
||||||
@payment = @order.checkout.payments.find(:first,
|
@payment = @order.payments.where(:state => "pending", :source_type => "PaypalAccount").try(:first)
|
||||||
:conditions => {"transactions.txn_type" => PaypalTxn::TxnType::AUTHORIZE,
|
|
||||||
"transactions.payment_type" => params["payment_type"]},
|
|
||||||
:joins => :transactions)
|
|
||||||
|
|
||||||
@payment.try(:payment_method).try(:provider) #configures ActiveMerchant
|
@payment.try(:payment_method).try(:provider) #configures ActiveMerchant
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_txn(txn_type)
|
|
||||||
if txn_type == PaypalTxn::TxnType::CAPTURE
|
|
||||||
@payment.finalize! if @payment.can_finalize?
|
|
||||||
elsif txn_type == PaypalTxn::TxnType::DENIED
|
|
||||||
#maybe we should do something?
|
|
||||||
end
|
|
||||||
|
|
||||||
PaypalTxn.create(:payment => @payment,
|
|
||||||
:txn_type => txn_type,
|
|
||||||
:amount => @notification.params["payment_gross"].to_f,
|
|
||||||
:payment_status => @notification.params["payment_status"],
|
|
||||||
:transaction_id => @notification.params["txn_id"],
|
|
||||||
:transaction_type => @notification.params["txn_type"],
|
|
||||||
:payment_type => @notification.params["payment_type"])
|
|
||||||
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,42 +32,77 @@
|
||||||
<legend><%= t('transactions') %></legend>
|
<legend><%= t('transactions') %></legend>
|
||||||
|
|
||||||
<% payment.log_entries.reverse.each do |log| %>
|
<% payment.log_entries.reverse.each do |log| %>
|
||||||
<% details = YAML.load(log.details) %>
|
<% details = YAML.load(log.details) rescue "" %>
|
||||||
<table class="index">
|
<table class="index">
|
||||||
<tr>
|
|
||||||
<th colspan="6"><%= t('transaction') %> <%= details.params["transaction_id"] %> - <%= log.created_at.to_s(:date_time24) %></th>
|
<% if details.is_a? ActiveMerchant::Billing::PaypalExpressResponse %>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td width="12%;"><label><%= t('type') %>:</label></td>
|
|
||||||
<td width="20%;">
|
|
||||||
<%= details.params["transaction_type"] %>
|
|
||||||
</td>
|
|
||||||
<td width="8%;"><label><%= t("result") %>:</label></td>
|
|
||||||
<td width="20%;">
|
|
||||||
<%= details.message %>
|
|
||||||
</td>
|
|
||||||
<td width="15%;"><label><%= t("amount") %>:</label></td>
|
|
||||||
<td width="20%;">
|
|
||||||
<%= number_to_currency details.params["gross_amount"] %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><label><%= t("comment") %>:</label></td>
|
|
||||||
<td colspan="3">
|
|
||||||
<%= details.params["message"] %>
|
|
||||||
</td>
|
|
||||||
<td><label><%= t("status") %>:</label></td>
|
|
||||||
<td>
|
|
||||||
<%= details.params["payment_status"] %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<% if details.params["payment_status"] == "Pending" %>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><label><%= t("pending_reason") %>:</label></td>
|
<th colspan="6"><%= t('transaction') %> <%= details.params["transaction_id"] %> - <%= log.created_at.to_s(:date_time24) %></th>
|
||||||
<td colspan="5">
|
</tr>
|
||||||
<%= details.params["pending_reason"] %>
|
<tr>
|
||||||
|
<td width="12%;"><label><%= t('type') %>:</label></td>
|
||||||
|
<td width="20%;">
|
||||||
|
<%= details.params["transaction_type"] %>
|
||||||
|
</td>
|
||||||
|
<td width="8%;"><label><%= t("result") %>:</label></td>
|
||||||
|
<td width="20%;">
|
||||||
|
<%= details.message %>
|
||||||
|
</td>
|
||||||
|
<td width="15%;"><label><%= t("amount") %>:</label></td>
|
||||||
|
<td width="20%;">
|
||||||
|
<%= number_to_currency details.params["gross_amount"] %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label><%= t("comment") %>:</label></td>
|
||||||
|
<td colspan="3">
|
||||||
|
<%= details.params["message"] %>
|
||||||
|
</td>
|
||||||
|
<td><label><%= t("status") %>:</label></td>
|
||||||
|
<td>
|
||||||
|
<%= details.params["payment_status"] %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% if details.params["payment_status"] == "Pending" %>
|
||||||
|
<tr>
|
||||||
|
<td><label><%= t("pending_reason") %>:</label></td>
|
||||||
|
<td colspan="5">
|
||||||
|
<%= details.params["pending_reason"] %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% elsif details.is_a? ActiveMerchant::Billing::Integrations::Paypal::Notification %>
|
||||||
|
<tr>
|
||||||
|
<th colspan="6"><%= t('ipn_transaction') %> <%= details.params["txn_id"] %> - <%= log.created_at.to_s(:date_time24) %></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="12%;"><label><%= t('type') %>:</label></td>
|
||||||
|
<td width="20%;">
|
||||||
|
<%= details.params["txn_type"] %>
|
||||||
|
</td>
|
||||||
|
<td width="8%;"><label><%= t("result") %>:</label></td>
|
||||||
|
<td width="20%;">
|
||||||
|
<%= details.params["payment_status"] %>
|
||||||
|
</td>
|
||||||
|
<td width="15%;"><label><%= t("amount") %>:</label></td>
|
||||||
|
<td width="20%;">
|
||||||
|
<%= number_to_currency details.params["mc_gross"] %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label><%= t("status") %>:</label></td>
|
||||||
|
<td colspan="5">
|
||||||
|
<%= details.params["payment_status"] %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% else %>
|
||||||
|
<tr>
|
||||||
|
<th colspan="6"><%= t('unknown_transaction') %> - <%= log.created_at.to_s(:date_time24) %></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="6"><pre style="overflow: hidden; width:600px;"><%= log.details %></pre></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</table>
|
</table>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
---
|
---
|
||||||
en:
|
en:
|
||||||
|
ipn_transaction: IPN Transaction
|
||||||
|
unknown_transaction: Unknown Transaction
|
||||||
edit_paypal_info: Edit Paypal Express Payment
|
edit_paypal_info: Edit Paypal Express Payment
|
||||||
paypal_payment: Paypal Express Payment
|
paypal_payment: Paypal Express Payment
|
||||||
paypal_txn_id: Transaction Code
|
paypal_txn_id: Transaction Code
|
||||||
|
|
|
@ -12,7 +12,6 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
match '/paypal_notify' => 'paypal_express_callbacks#notify', :via => [:get, :post]
|
match '/paypal_notify' => 'paypal_express_callbacks#notify', :via => [:get, :post]
|
||||||
|
|
||||||
resources :paypal_express_callbacks
|
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
resources :orders do
|
resources :orders do
|
||||||
resources :paypal_payments do
|
resources :paypal_payments do
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
class CreatePaypalTxns < ActiveRecord::Migration
|
|
||||||
def self.up
|
|
||||||
create_table :paypal_txns do |t|
|
|
||||||
t.references :paypal_payment
|
|
||||||
t.decimal :gross_amount, :precision => 8, :scale => 2
|
|
||||||
t.string :payment_status
|
|
||||||
t.text :message
|
|
||||||
t.string :pending_reason
|
|
||||||
t.string :transaction_type
|
|
||||||
t.string :payment_type
|
|
||||||
t.string :ack
|
|
||||||
t.string :token
|
|
||||||
t.string :avs_code
|
|
||||||
t.string :cvv_code
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.down
|
|
||||||
drop_table :paypal_txns
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,11 +0,0 @@
|
||||||
class RenameAvsCode < ActiveRecord::Migration
|
|
||||||
def self.up
|
|
||||||
rename_column :paypal_txns, :avs_code, :avs_response
|
|
||||||
rename_column :paypal_txns, :cvv_code, :cvv_response
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.down
|
|
||||||
rename_column :paypal_txns, :cvv_response, :cvv_code
|
|
||||||
rename_column :paypal_txns, :avs_response, :avs_code
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,9 +0,0 @@
|
||||||
class AddTransactionIdToPpxTxn < ActiveRecord::Migration
|
|
||||||
def self.up
|
|
||||||
add_column :paypal_txns, :transaction_id, :string
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.down
|
|
||||||
remove_column :paypal_txns, :transaction_id
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,361 +0,0 @@
|
||||||
module ActiveMerchant #:nodoc:
|
|
||||||
module Billing #:nodoc:
|
|
||||||
# This module is included in both PaypalGateway and PaypalExpressGateway
|
|
||||||
module PaypalCommonAPI
|
|
||||||
def self.included(base)
|
|
||||||
base.default_currency = 'USD'
|
|
||||||
base.cattr_accessor :pem_file
|
|
||||||
base.cattr_accessor :signature
|
|
||||||
end
|
|
||||||
|
|
||||||
silence_warnings do
|
|
||||||
API_VERSION = '60.0'
|
|
||||||
end
|
|
||||||
|
|
||||||
# The gateway must be configured with either your PayPal PEM file
|
|
||||||
# or your PayPal API Signature. Only one is required.
|
|
||||||
#
|
|
||||||
# <tt>:pem</tt> The text of your PayPal PEM file. Note
|
|
||||||
# this is not the path to file, but its
|
|
||||||
# contents. If you are only using one PEM
|
|
||||||
# file on your site you can declare it
|
|
||||||
# globally and then you won't need to
|
|
||||||
# include this option
|
|
||||||
#
|
|
||||||
# <tt>:signature</tt> The text of your PayPal signature.
|
|
||||||
# If you are only using one API Signature
|
|
||||||
# on your site you can declare it
|
|
||||||
# globally and then you won't need to
|
|
||||||
# include this option
|
|
||||||
|
|
||||||
def initialize(options = {})
|
|
||||||
requires!(options, :login, :password)
|
|
||||||
|
|
||||||
@options = {
|
|
||||||
:pem => pem_file,
|
|
||||||
:signature => signature
|
|
||||||
}.update(options)
|
|
||||||
|
|
||||||
if @options[:pem].blank? && @options[:signature].blank?
|
|
||||||
raise ArgumentError, "An API Certificate or API Signature is required to make requests to PayPal"
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def test?
|
|
||||||
@options[:test] || Base.gateway_mode == :test
|
|
||||||
end
|
|
||||||
|
|
||||||
def reauthorize(money, authorization, options = {})
|
|
||||||
commit 'DoReauthorization', build_reauthorize_request(money, authorization, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def capture(money, authorization, options = {})
|
|
||||||
commit 'DoCapture', build_capture_request(money, authorization, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Transfer money to one or more recipients.
|
|
||||||
#
|
|
||||||
# gateway.transfer 1000, 'bob@example.com',
|
|
||||||
# :subject => "The money I owe you", :note => "Sorry it's so late"
|
|
||||||
#
|
|
||||||
# gateway.transfer [1000, 'fred@example.com'],
|
|
||||||
# [2450, 'wilma@example.com', :note => 'You will receive another payment on 3/24'],
|
|
||||||
# [2000, 'barney@example.com'],
|
|
||||||
# :subject => "Your Earnings", :note => "Thanks for your business."
|
|
||||||
#
|
|
||||||
def transfer(*args)
|
|
||||||
commit 'MassPay', build_mass_pay_request(*args)
|
|
||||||
end
|
|
||||||
|
|
||||||
def void(authorization, options = {})
|
|
||||||
commit 'DoVoid', build_void_request(authorization, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def credit(money, identification, options = {})
|
|
||||||
commit 'RefundTransaction', build_credit_request(money, identification, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def build_reauthorize_request(money, authorization, options)
|
|
||||||
xml = Builder::XmlMarkup.new
|
|
||||||
|
|
||||||
xml.tag! 'DoReauthorizationReq', 'xmlns' => PAYPAL_NAMESPACE do
|
|
||||||
xml.tag! 'DoReauthorizationRequest', 'xmlns:n2' => EBAY_NAMESPACE do
|
|
||||||
xml.tag! 'n2:Version', API_VERSION
|
|
||||||
xml.tag! 'AuthorizationID', authorization
|
|
||||||
xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
xml.target!
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_capture_request(money, authorization, options)
|
|
||||||
xml = Builder::XmlMarkup.new
|
|
||||||
|
|
||||||
xml.tag! 'DoCaptureReq', 'xmlns' => PAYPAL_NAMESPACE do
|
|
||||||
xml.tag! 'DoCaptureRequest', 'xmlns:n2' => EBAY_NAMESPACE do
|
|
||||||
xml.tag! 'n2:Version', API_VERSION
|
|
||||||
xml.tag! 'AuthorizationID', authorization
|
|
||||||
xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
|
|
||||||
xml.tag! 'CompleteType', 'Complete'
|
|
||||||
xml.tag! 'Note', options[:description]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
xml.target!
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_credit_request(money, identification, options)
|
|
||||||
xml = Builder::XmlMarkup.new
|
|
||||||
|
|
||||||
xml.tag! 'RefundTransactionReq', 'xmlns' => PAYPAL_NAMESPACE do
|
|
||||||
xml.tag! 'RefundTransactionRequest', 'xmlns:n2' => EBAY_NAMESPACE do
|
|
||||||
xml.tag! 'n2:Version', API_VERSION
|
|
||||||
xml.tag! 'TransactionID', identification
|
|
||||||
xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
|
|
||||||
xml.tag! 'RefundType', 'Partial'
|
|
||||||
xml.tag! 'Memo', options[:note] unless options[:note].blank?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
xml.target!
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_void_request(authorization, options)
|
|
||||||
xml = Builder::XmlMarkup.new
|
|
||||||
|
|
||||||
xml.tag! 'DoVoidReq', 'xmlns' => PAYPAL_NAMESPACE do
|
|
||||||
xml.tag! 'DoVoidRequest', 'xmlns:n2' => EBAY_NAMESPACE do
|
|
||||||
xml.tag! 'n2:Version', API_VERSION
|
|
||||||
xml.tag! 'AuthorizationID', authorization
|
|
||||||
xml.tag! 'Note', options[:description]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
xml.target!
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_mass_pay_request(*args)
|
|
||||||
default_options = args.last.is_a?(Hash) ? args.pop : {}
|
|
||||||
recipients = args.first.is_a?(Array) ? args : [args]
|
|
||||||
|
|
||||||
xml = Builder::XmlMarkup.new
|
|
||||||
|
|
||||||
xml.tag! 'MassPayReq', 'xmlns' => PAYPAL_NAMESPACE do
|
|
||||||
xml.tag! 'MassPayRequest', 'xmlns:n2' => EBAY_NAMESPACE do
|
|
||||||
xml.tag! 'n2:Version', API_VERSION
|
|
||||||
xml.tag! 'EmailSubject', default_options[:subject] if default_options[:subject]
|
|
||||||
recipients.each do |money, recipient, options|
|
|
||||||
options ||= default_options
|
|
||||||
xml.tag! 'MassPayItem' do
|
|
||||||
xml.tag! 'ReceiverEmail', recipient
|
|
||||||
xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
|
|
||||||
xml.tag! 'Note', options[:note] if options[:note]
|
|
||||||
xml.tag! 'UniqueId', options[:unique_id] if options[:unique_id]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
xml.target!
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse(action, xml)
|
|
||||||
response = {}
|
|
||||||
|
|
||||||
error_messages = []
|
|
||||||
error_codes = []
|
|
||||||
|
|
||||||
xml = REXML::Document.new(xml)
|
|
||||||
if root = REXML::XPath.first(xml, "//#{action}Response")
|
|
||||||
root.elements.each do |node|
|
|
||||||
case node.name
|
|
||||||
when 'Errors'
|
|
||||||
short_message = nil
|
|
||||||
long_message = nil
|
|
||||||
|
|
||||||
node.elements.each do |child|
|
|
||||||
case child.name
|
|
||||||
when "LongMessage"
|
|
||||||
long_message = child.text unless child.text.blank?
|
|
||||||
when "ShortMessage"
|
|
||||||
short_message = child.text unless child.text.blank?
|
|
||||||
when "ErrorCode"
|
|
||||||
error_codes << child.text unless child.text.blank?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if message = long_message || short_message
|
|
||||||
error_messages << message
|
|
||||||
end
|
|
||||||
else
|
|
||||||
parse_element(response, node)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
response[:message] = error_messages.uniq.join(". ") unless error_messages.empty?
|
|
||||||
response[:error_codes] = error_codes.uniq.join(",") unless error_codes.empty?
|
|
||||||
elsif root = REXML::XPath.first(xml, "//SOAP-ENV:Fault")
|
|
||||||
parse_element(response, root)
|
|
||||||
response[:message] = "#{response[:faultcode]}: #{response[:faultstring]} - #{response[:detail]}"
|
|
||||||
end
|
|
||||||
|
|
||||||
response
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_element(response, node)
|
|
||||||
if node.has_elements?
|
|
||||||
node.elements.each{|e| parse_element(response, e) }
|
|
||||||
else
|
|
||||||
response[node.name.underscore.to_sym] = node.text
|
|
||||||
node.attributes.each do |k, v|
|
|
||||||
response["#{node.name.underscore}_#{k.underscore}".to_sym] = v if k == 'currencyID'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_request(body)
|
|
||||||
xml = Builder::XmlMarkup.new
|
|
||||||
|
|
||||||
xml.instruct!
|
|
||||||
xml.tag! 'env:Envelope', ENVELOPE_NAMESPACES do
|
|
||||||
xml.tag! 'env:Header' do
|
|
||||||
add_credentials(xml)
|
|
||||||
end
|
|
||||||
|
|
||||||
xml.tag! 'env:Body' do
|
|
||||||
xml << body
|
|
||||||
end
|
|
||||||
end
|
|
||||||
xml.target!
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_credentials(xml)
|
|
||||||
xml.tag! 'RequesterCredentials', CREDENTIALS_NAMESPACES do
|
|
||||||
xml.tag! 'n1:Credentials' do
|
|
||||||
xml.tag! 'Username', @options[:login]
|
|
||||||
xml.tag! 'Password', @options[:password]
|
|
||||||
xml.tag! 'Subject', @options[:subject]
|
|
||||||
xml.tag! 'Signature', @options[:signature] unless @options[:signature].blank?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_address(xml, element, address)
|
|
||||||
return if address.nil?
|
|
||||||
xml.tag! element do
|
|
||||||
xml.tag! 'n2:Name', address[:name]
|
|
||||||
xml.tag! 'n2:Street1', address[:address1]
|
|
||||||
xml.tag! 'n2:Street2', address[:address2]
|
|
||||||
xml.tag! 'n2:CityName', address[:city]
|
|
||||||
xml.tag! 'n2:StateOrProvince', address[:state].blank? ? 'N/A' : address[:state]
|
|
||||||
xml.tag! 'n2:Country', address[:country]
|
|
||||||
xml.tag! 'n2:PostalCode', address[:zip]
|
|
||||||
xml.tag! 'n2:Phone', address[:phone]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_payment_detail_item(xml, item, options)
|
|
||||||
currency_code = options[:currency] || currency(item[:amount])
|
|
||||||
xml.tag! 'n2:PaymentDetailsItem' do
|
|
||||||
xml.tag! 'n2:Name', item[:name] unless item[:name].blank?
|
|
||||||
xml.tag! 'n2:Description', item[:description] unless item[:description].blank?
|
|
||||||
xml.tag! 'n2:Number', item[:sku] unless item[:sku].blank?
|
|
||||||
xml.tag! 'n2:Quantity', item[:qty] unless item[:qty].blank?
|
|
||||||
if item[:amount].to_i > 0
|
|
||||||
xml.tag! 'n2:Amount', amount(item[:amount]), 'currencyID' => currency_code unless item[:amount].blank?
|
|
||||||
else
|
|
||||||
xml.tag! 'n2:Amount', "-#{amount(item[:amount].to_i*-1)}", 'currencyID' => currency_code unless item[:amount].blank?
|
|
||||||
end
|
|
||||||
xml.tag! 'n2:Tax', amount(item[:tax]), 'currencyID' => currency_code unless item[:tax].blank?
|
|
||||||
xml.tag! 'n2:ItemWeight', item[:weight] unless item[:weight].blank?
|
|
||||||
xml.tag! 'n2:ItemHeight', item[:height] unless item[:height].blank?
|
|
||||||
xml.tag! 'n2:ItemWidth', item[:width] unless item[:width].blank?
|
|
||||||
xml.tag! 'n2:ItemLength', item[:length] unless item[:length].blank?
|
|
||||||
# not doing this yet TODO
|
|
||||||
# xml.tag! 'n2:EbayItemPaymentDetailsItem', item[:name]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_payment_details(xml, money, options)
|
|
||||||
currency_code = options[:currency] || currency(money)
|
|
||||||
|
|
||||||
xml.tag! 'n2:PaymentDetails' do
|
|
||||||
xml.tag! 'n2:OrderTotal', amount(money), 'currencyID' => currency_code
|
|
||||||
|
|
||||||
# All of the values must be included together and add up to the order total
|
|
||||||
if [:subtotal, :shipping, :handling, :tax].all?{ |o| options.has_key?(o) }
|
|
||||||
xml.tag! 'n2:ItemTotal', amount(options[:subtotal]), 'currencyID' => currency_code
|
|
||||||
xml.tag! 'n2:ShippingTotal', amount(options[:shipping]),'currencyID' => currency_code
|
|
||||||
xml.tag! 'n2:HandlingTotal', amount(options[:handling]),'currencyID' => currency_code
|
|
||||||
xml.tag! 'n2:TaxTotal', amount(options[:tax]), 'currencyID' => currency_code
|
|
||||||
end
|
|
||||||
|
|
||||||
# don't enforce inclusion yet - see how it works
|
|
||||||
xml.tag! 'n2:InsuranceOptionOffered', options[:insurance_offered] ? '1' : '0' unless options[:insurance_offered].blank?
|
|
||||||
xml.tag! 'n2:InsuranceTotal', amount(options[:insurance]), 'currencyID' => currency_code unless options[:insurance].blank?
|
|
||||||
xml.tag! 'n2:ShippingDiscount', amount(options[:ship_discount]), 'currencyID' => currency_code unless options[:ship_discount].blank?
|
|
||||||
|
|
||||||
# query - use slices too? or just risk reject? (QQ: injection risk???)
|
|
||||||
xml.tag! 'n2:OrderDescription', options[:description] unless options[:description].blank?
|
|
||||||
xml.tag! 'n2:Custom', options[:custom] unless options[:custom].blank?
|
|
||||||
xml.tag! 'n2:InvoiceID', options[:order_id] unless options[:order_id].blank?
|
|
||||||
xml.tag! 'n2:ButtonSource', application_id.to_s.slice(0,32) unless application_id.blank?
|
|
||||||
xml.tag! 'n2:NotifyURL', options[:notify_url] unless options[:notify_url].blank?
|
|
||||||
add_address(xml, 'n2:ShipToAddress', options[:shipping_address] || options[:address])
|
|
||||||
options[:items].each {|i| add_payment_detail_item xml, i, options } if options[:items]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_shipping_options(xml, shipping_options, options)
|
|
||||||
currency_code = options[:currency]
|
|
||||||
|
|
||||||
xml.tag! 'n2:FlatRateShippingOptions' do
|
|
||||||
|
|
||||||
shipping_options.each_with_index do |shipping_option, i|
|
|
||||||
xml.tag! 'n2:ShippingOptions' do
|
|
||||||
xml.tag! 'n2:ShippingOptionIsDefault', (i == 0)
|
|
||||||
xml.tag! 'n2:ShippingOptionName', shipping_option[:name]
|
|
||||||
xml.tag! 'n2:ShippingOptionLabel', shipping_option[:label]
|
|
||||||
xml.tag! 'n2:ShippingOptionAmount', amount(shipping_option[:amount] ), 'currencyID' => 'USD'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def endpoint_url
|
|
||||||
URLS[test? ? :test : :live][@options[:signature].blank? ? :certificate : :signature]
|
|
||||||
end
|
|
||||||
|
|
||||||
def commit(action, request)
|
|
||||||
response = parse(action, ssl_post(endpoint_url, build_request(request)))
|
|
||||||
|
|
||||||
build_response(successful?(response), message_from(response), response,
|
|
||||||
:test => test?,
|
|
||||||
:authorization => authorization_from(response),
|
|
||||||
:fraud_review => fraud_review?(response),
|
|
||||||
:avs_result => { :code => response[:avs_code] },
|
|
||||||
:cvv_result => response[:cvv2_code]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fraud_review?(response)
|
|
||||||
response[:error_codes] == FRAUD_REVIEW_CODE
|
|
||||||
end
|
|
||||||
|
|
||||||
def authorization_from(response)
|
|
||||||
response[:transaction_id] || response[:authorization_id] || response[:refund_transaction_id] # middle one is from reauthorization
|
|
||||||
end
|
|
||||||
|
|
||||||
def successful?(response)
|
|
||||||
SUCCESS_CODES.include?(response[:ack])
|
|
||||||
end
|
|
||||||
|
|
||||||
def message_from(response)
|
|
||||||
response[:message] || response[:ack]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,128 +0,0 @@
|
||||||
#require File.dirname(__FILE__) + '/paypal/paypal_common_api'
|
|
||||||
#require File.dirname(__FILE__) + '/paypal/paypal_express_response'
|
|
||||||
#require File.dirname(__FILE__) + '/paypal_express_common'
|
|
||||||
|
|
||||||
module ActiveMerchant #:nodoc:
|
|
||||||
module Billing #:nodoc:
|
|
||||||
class PaypalExpressGateway < Gateway
|
|
||||||
include PaypalCommonAPI
|
|
||||||
include PaypalExpressCommon
|
|
||||||
|
|
||||||
self.test_redirect_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token='
|
|
||||||
self.supported_countries = ['US']
|
|
||||||
self.homepage_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=xpt/merchant/ExpressCheckoutIntro-outside'
|
|
||||||
self.display_name = 'PayPal Express Checkout'
|
|
||||||
|
|
||||||
def setup_authorization(money, options = {})
|
|
||||||
requires!(options, :return_url, :cancel_return_url)
|
|
||||||
commit 'SetExpressCheckout', build_setup_request('Authorization', money, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def setup_purchase(money, options = {})
|
|
||||||
requires!(options, :return_url, :cancel_return_url)
|
|
||||||
|
|
||||||
commit 'SetExpressCheckout', build_setup_request('Sale', money, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def details_for(token)
|
|
||||||
commit 'GetExpressCheckoutDetails', build_get_details_request(token)
|
|
||||||
end
|
|
||||||
|
|
||||||
def authorize(money, options = {})
|
|
||||||
requires!(options, :token, :payer_id)
|
|
||||||
|
|
||||||
commit 'DoExpressCheckoutPayment', build_sale_or_authorization_request('Authorization', money, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def purchase(money, options = {})
|
|
||||||
requires!(options, :token, :payer_id)
|
|
||||||
|
|
||||||
commit 'DoExpressCheckoutPayment', build_sale_or_authorization_request('Sale', money, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def build_get_details_request(token)
|
|
||||||
xml = Builder::XmlMarkup.new :indent => 2
|
|
||||||
xml.tag! 'GetExpressCheckoutDetailsReq', 'xmlns' => PAYPAL_NAMESPACE do
|
|
||||||
xml.tag! 'GetExpressCheckoutDetailsRequest', 'xmlns:n2' => EBAY_NAMESPACE do
|
|
||||||
xml.tag! 'n2:Version', API_VERSION
|
|
||||||
xml.tag! 'Token', token
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
xml.target!
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_sale_or_authorization_request(action, money, options)
|
|
||||||
currency_code = options[:currency] || currency(money)
|
|
||||||
|
|
||||||
xml = Builder::XmlMarkup.new :indent => 2
|
|
||||||
xml.tag! 'DoExpressCheckoutPaymentReq', 'xmlns' => PAYPAL_NAMESPACE do
|
|
||||||
xml.tag! 'DoExpressCheckoutPaymentRequest', 'xmlns:n2' => EBAY_NAMESPACE do
|
|
||||||
xml.tag! 'n2:Version', API_VERSION
|
|
||||||
xml.tag! 'n2:DoExpressCheckoutPaymentRequestDetails' do
|
|
||||||
xml.tag! 'n2:PaymentAction', action
|
|
||||||
xml.tag! 'n2:Token', options[:token]
|
|
||||||
xml.tag! 'n2:PayerID', options[:payer_id]
|
|
||||||
add_payment_details(xml, money, options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
xml.target!
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_setup_request(action, money, options)
|
|
||||||
xml = Builder::XmlMarkup.new :indent => 2
|
|
||||||
xml.tag! 'SetExpressCheckoutReq', 'xmlns' => PAYPAL_NAMESPACE do
|
|
||||||
xml.tag! 'SetExpressCheckoutRequest', 'xmlns:n2' => EBAY_NAMESPACE do
|
|
||||||
xml.tag! 'n2:Version', API_VERSION
|
|
||||||
xml.tag! 'n2:SetExpressCheckoutRequestDetails' do
|
|
||||||
if options[:max_amount]
|
|
||||||
xml.tag! 'n2:MaxAmount', amount(options[:max_amount]), 'currencyID' => options[:currency] || currency(options[:max_amount])
|
|
||||||
end
|
|
||||||
xml.tag! 'n2:ReturnURL', options[:return_url]
|
|
||||||
xml.tag! 'n2:CancelURL', options[:cancel_return_url]
|
|
||||||
# xml.tag! 'n2:CallbackURL', options[:callback_url] unless options[:callback_url].blank?
|
|
||||||
# xml.tag! 'n2:CallbackTimeout', options[:callback_timeout] unless options[:callback_timeout].blank?
|
|
||||||
xml.tag! 'n2:ReqConfirmShipping', options[:req_confirm_shipping] ? '1' : '0'
|
|
||||||
xml.tag! 'n2:NoShipping', options[:no_shipping] ? '1' : '0'
|
|
||||||
## add flat rates for shipping
|
|
||||||
# add_shipping_options(xml, options[:shipping_options], options) if options[:shipping_options]
|
|
||||||
xml.tag! 'n2:AllowNote', options[:allow_note] ? '1' : '0'
|
|
||||||
xml.tag! 'n2:AddressOverride', options[:address_override] ? '1' : '0' # force yours
|
|
||||||
xml.tag! 'n2:LocaleCode', options[:locale] unless options[:locale].blank?
|
|
||||||
|
|
||||||
# Customization of the payment page
|
|
||||||
xml.tag! 'n2:PageStyle', options[:page_style] unless options[:page_style].blank?
|
|
||||||
xml.tag! 'n2:cpp-header-image', options[:header_image] unless options[:header_image].blank?
|
|
||||||
xml.tag! 'n2:cpp-header-border-color', options[:header_border_color] unless options[:header_border_color].blank?
|
|
||||||
xml.tag! 'n2:cpp-header-back-color', options[:header_background_color] unless options[:header_background_color].blank?
|
|
||||||
xml.tag! 'n2:cpp-payflow-color', options[:background_color] unless options[:background_color].blank?
|
|
||||||
|
|
||||||
xml.tag! 'n2:PaymentAction', action
|
|
||||||
xml.tag! 'n2:BuyerEmail', options[:email] unless options[:email].blank?
|
|
||||||
xml.tag! 'n2:SolutionType', options[:solution_type] unless options[:solution_type].blank?
|
|
||||||
xml.tag! 'n2:LandingPage', options[:landing_page] unless options[:landing_page].blank?
|
|
||||||
xml.tag! 'n2:ChannelType', options[:channel_type] unless options[:channel_type].blank?
|
|
||||||
|
|
||||||
# only needed for certain methods in Germany
|
|
||||||
xml.tag! 'n2:giropaySuccessURL', options[:giropay_url] unless options[:giropay_url].blank?
|
|
||||||
xml.tag! 'n2:giropayCancelURL', options[:giropay_cancel_url] unless options[:giropay_cancel_url].blank?
|
|
||||||
xml.tag! 'n2:BanktxnPendingURL', options[:banktxn_url] unless options[:banktxn_url].blank?
|
|
||||||
|
|
||||||
# for order values etc, and item info
|
|
||||||
add_payment_details(xml, money, options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
xml.target!
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_response(success, message, response, options = {})
|
|
||||||
PaypalExpressResponse.new(success, message, response, options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,14 +0,0 @@
|
||||||
require File.dirname(__FILE__) + '/paypal_express'
|
|
||||||
|
|
||||||
module ActiveMerchant #:nodoc:
|
|
||||||
module Billing #:nodoc:
|
|
||||||
class PaypalExpressUkGateway < PaypalExpressGateway
|
|
||||||
self.default_currency = 'GBP'
|
|
||||||
|
|
||||||
self.supported_countries = ['GB']
|
|
||||||
self.homepage_url = 'https://www.paypal.com/uk/cgi-bin/webscr?cmd=_additional-payment-overview-outside'
|
|
||||||
self.display_name = 'PayPal Express Checkout (UK)'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
module Spree::CheckoutControllerWithPaypalExpress
|
|
||||||
def self.included(target)
|
|
||||||
target.before_filter :redirect_to_paypal_express_form, :only => [:update]
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
end
|
|
|
@ -7,21 +7,16 @@ module SpreePaypalExpress
|
||||||
config.autoload_paths += %W(#{config.root}/lib)
|
config.autoload_paths += %W(#{config.root}/lib)
|
||||||
|
|
||||||
def self.activate
|
def self.activate
|
||||||
|
#workaround for https://github.com/Shopify/active_merchant/issuesearch?state=open&q=paypal#issue/43
|
||||||
|
require 'active_merchant'
|
||||||
|
ActiveMerchant::Billing::PaypalExpressGateway
|
||||||
|
|
||||||
Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
|
Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
|
||||||
Rails.env.production? ? require(c) : load(c)
|
Rails.env.production? ? require(c) : load(c)
|
||||||
end
|
end
|
||||||
|
|
||||||
BillingIntegration::PaypalExpress.register
|
BillingIntegration::PaypalExpress.register
|
||||||
BillingIntegration::PaypalExpressUk.register
|
BillingIntegration::PaypalExpressUk.register
|
||||||
|
|
||||||
# Load up over-rides for ActiveMerchant files
|
|
||||||
# these will be submitted to ActiveMerchant some time...
|
|
||||||
require File.join(File.dirname(__FILE__), "active_merchant", "billing", "gateways", "paypal", "paypal_common_api.rb")
|
|
||||||
require File.join(File.dirname(__FILE__), "active_merchant", "billing", "gateways", "paypal_express_uk.rb")
|
|
||||||
|
|
||||||
# inject paypal code into orders controller
|
|
||||||
CheckoutController.class_eval do
|
|
||||||
include Spree::PaypalExpress
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
config.to_prepare &method(:activate).to_proc
|
config.to_prepare &method(:activate).to_proc
|
||||||
|
|
|
@ -0,0 +1,278 @@
|
||||||
|
require File.dirname(__FILE__) + '/../spec_helper'
|
||||||
|
|
||||||
|
describe CheckoutController do
|
||||||
|
let(:token) { "EC-2OPN7UJGFWK9OYFV" }
|
||||||
|
let(:order) { Factory(:ppx_order_with_totals, :state => "payment") }
|
||||||
|
let(:order_total) { (order.total * 100).to_i }
|
||||||
|
let(:gateway_provider) { mock(ActiveMerchant::Billing::PaypalExpressGateway) }
|
||||||
|
let(:paypal_gateway) { mock(BillingIntegration::PaypalExpress, :id => 123, :preferred_review => false, :preferred_no_shipping => true, :provider => gateway_provider) }
|
||||||
|
|
||||||
|
let(:details_for_response) { mock(ActiveMerchant::Billing::PaypalExpressResponse, :success? => true,
|
||||||
|
:params => {"payer" => order.user.email, "payer_id" => "FWRVKNRRZ3WUC"}, :address => {}) }
|
||||||
|
|
||||||
|
let(:purchase_response) { mock(ActiveMerchant::Billing::PaypalExpressResponse, :success? => true,
|
||||||
|
:params => {"payer" => order.user.email, "payer_id" => "FWRVKNRRZ3WUC", "gross_amount" => order_total, "payment_status" => "Completed"},
|
||||||
|
:avs_result => "F",
|
||||||
|
:to_yaml => "fake") }
|
||||||
|
|
||||||
|
|
||||||
|
before do
|
||||||
|
Spree::Auth::Config.set(:registration_step => false)
|
||||||
|
controller.stub(:current_order => order, :check_authorization => true, :current_user => order.user)
|
||||||
|
order.stub(:checkout_allowed? => true, :completed? => false)
|
||||||
|
order.update!
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should understand paypal routes" do
|
||||||
|
assert_routing("/orders/#{order.number}/checkout/paypal_payment", {:controller => "checkout", :action => "paypal_payment", :order_id => order.number })
|
||||||
|
assert_routing("/orders/#{order.number}/checkout/paypal_confirm", {:controller => "checkout", :action => "paypal_confirm", :order_id => order.number })
|
||||||
|
end
|
||||||
|
|
||||||
|
context "paypal_checkout" do
|
||||||
|
#feature not implemented
|
||||||
|
end
|
||||||
|
|
||||||
|
context "paypal_payment without auto_capture" do
|
||||||
|
let(:redirect_url) { "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=#{token}&useraction=commit" }
|
||||||
|
|
||||||
|
before { Spree::Config.set(:auto_capture => false) }
|
||||||
|
|
||||||
|
it "should setup an authorize transaction and redirect to sandbox" do
|
||||||
|
PaymentMethod.should_receive(:find).at_least(1).with('123').and_return(paypal_gateway)
|
||||||
|
|
||||||
|
gateway_provider.should_receive(:redirect_url_for).with(token, {:review => false}).and_return redirect_url
|
||||||
|
paypal_gateway.provider.should_receive(:setup_authorization).with(order_total, anything()).and_return(mock(:success? => true, :token => token))
|
||||||
|
|
||||||
|
get :paypal_payment, {:order_id => order.number, :payment_method_id => "123" }
|
||||||
|
|
||||||
|
response.should redirect_to "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=#{assigns[:ppx_response].token}&useraction=commit"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "paypal_payment with auto_capture" do
|
||||||
|
let(:redirect_url) { "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=#{token}&useraction=commit" }
|
||||||
|
|
||||||
|
before { Spree::Config.set(:auto_capture => true) }
|
||||||
|
|
||||||
|
it "should setup a purchase transaction and redirect to sandbox" do
|
||||||
|
PaymentMethod.should_receive(:find).at_least(1).with("123").and_return(paypal_gateway)
|
||||||
|
|
||||||
|
gateway_provider.should_receive(:redirect_url_for).with(token, {:review => false}).and_return redirect_url
|
||||||
|
paypal_gateway.provider.should_receive(:setup_purchase).with(order_total, anything()).and_return(mock(:success? => true, :token => token))
|
||||||
|
|
||||||
|
get :paypal_payment, {:order_id => order.number, :payment_method_id => "123" }
|
||||||
|
|
||||||
|
response.should redirect_to "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=#{assigns[:ppx_response].token}&useraction=commit"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "paypal_confirm" do
|
||||||
|
before { PaymentMethod.should_receive(:find).at_least(1).with("123").and_return(paypal_gateway) }
|
||||||
|
|
||||||
|
context "with auto_capture and no review" do
|
||||||
|
before do
|
||||||
|
Spree::Config.set(:auto_capture => true)
|
||||||
|
paypal_gateway.stub(:preferred_review => false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should capture payment" do
|
||||||
|
paypal_gateway.provider.should_receive(:details_for).with(token).and_return(details_for_response)
|
||||||
|
|
||||||
|
paypal_gateway.provider.should_receive(:purchase).with(order_total, anything()).and_return(purchase_response)
|
||||||
|
|
||||||
|
get :paypal_confirm, {:order_id => order.number, :payment_method_id => "123", :token => token, :PayerID => "FWRVKNRRZ3WUC" }
|
||||||
|
|
||||||
|
response.should redirect_to order_url(order)
|
||||||
|
|
||||||
|
order.reload
|
||||||
|
order.state.should == "complete"
|
||||||
|
order.payments.size.should == 1
|
||||||
|
order.payment_state.should == "paid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with review" do
|
||||||
|
before { paypal_gateway.stub(:preferred_review => true) }
|
||||||
|
|
||||||
|
it "should render review" do
|
||||||
|
paypal_gateway.provider.should_receive(:details_for).with(token).and_return(details_for_response)
|
||||||
|
|
||||||
|
get :paypal_confirm, {:order_id => order.number, :payment_method_id => "123", :token => token, :PayerID => "FWRVKNRRZ3WUC" }
|
||||||
|
|
||||||
|
response.should render_template("shared/paypal_express_confirm")
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with review and shipping update" do
|
||||||
|
before do
|
||||||
|
paypal_gateway.stub(:preferred_review => true)
|
||||||
|
paypal_gateway.stub(:preferred_no_shipping => false)
|
||||||
|
|
||||||
|
details_for_response.stub(:params => details_for_response.params.merge({'first_name' => 'Dr.', 'last_name' => 'Evil'}),
|
||||||
|
:address => {'address1' => 'Apt. 187', 'address2'=> 'Some Str.', 'city' => 'Chevy Chase', 'country' => 'US', 'zip' => '20815', 'state' => 'MD' })
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should update ship_address and render review" do
|
||||||
|
paypal_gateway.provider.should_receive(:details_for).with(token).and_return(details_for_response)
|
||||||
|
|
||||||
|
get :paypal_confirm, {:order_id => order.number, :payment_method_id => "123", :token => token, :PayerID => "FWRVKNRRZ3WUC" }
|
||||||
|
|
||||||
|
order.ship_address.address1.should == "Apt. 187"
|
||||||
|
response.should render_template("shared/paypal_express_confirm")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with un-successful repsonse" do
|
||||||
|
before { details_for_response.stub(:success? => false) }
|
||||||
|
|
||||||
|
it "should log error and redirect to payment step" do
|
||||||
|
paypal_gateway.provider.should_receive(:details_for).with(token).and_return(details_for_response)
|
||||||
|
|
||||||
|
controller.should_receive(:gateway_error).with(details_for_response)
|
||||||
|
|
||||||
|
get :paypal_confirm, {:order_id => order.number, :payment_method_id => "123", :token => token, :PayerID => "FWRVKNRRZ3WUC" }
|
||||||
|
|
||||||
|
response.should redirect_to edit_order_checkout_url(order, :state => 'payment')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "paypal_finish" do
|
||||||
|
let(:paypal_account) { stub_model(PaypalAccount, :payer_id => "FWRVKNRRZ3WUC", :email => order.email ) }
|
||||||
|
let(:authorize_response) { mock(ActiveMerchant::Billing::PaypalExpressResponse, :success? => true,
|
||||||
|
:params => {"payer" => order.user.email, "payer_id" => "FWRVKNRRZ3WUC", "gross_amount" => order_total, "payment_status" => "Pending"},
|
||||||
|
:avs_result => "F",
|
||||||
|
:to_yaml => "fake") }
|
||||||
|
|
||||||
|
before do
|
||||||
|
PaymentMethod.should_receive(:find).at_least(1).with("123").and_return(paypal_gateway)
|
||||||
|
PaypalAccount.should_receive(:find_by_payer_id).with("FWRVKNRRZ3WUC").and_return(paypal_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with auto_capture" do
|
||||||
|
before { Spree::Config.set(:auto_capture => true) }
|
||||||
|
|
||||||
|
it "should capture payment" do
|
||||||
|
|
||||||
|
paypal_gateway.provider.should_receive(:purchase).with(order_total, anything()).and_return(purchase_response)
|
||||||
|
|
||||||
|
get :paypal_finish, {:order_id => order.number, :payment_method_id => "123", :token => token, :PayerID => "FWRVKNRRZ3WUC" }
|
||||||
|
|
||||||
|
response.should redirect_to order_url(order)
|
||||||
|
|
||||||
|
order.reload
|
||||||
|
order.update!
|
||||||
|
order.payments.size.should == 1
|
||||||
|
order.payment_state.should == "paid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with auto_capture and pending(echeck) response" do
|
||||||
|
before do
|
||||||
|
Spree::Config.set(:auto_capture => true)
|
||||||
|
purchase_response.params["payment_status"] = "pending"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should authorize payment" do
|
||||||
|
|
||||||
|
paypal_gateway.provider.should_receive(:purchase).with(order_total, anything()).and_return(purchase_response)
|
||||||
|
|
||||||
|
get :paypal_finish, {:order_id => order.number, :payment_method_id => "123", :token => token, :PayerID => "FWRVKNRRZ3WUC" }
|
||||||
|
|
||||||
|
response.should redirect_to order_url(order)
|
||||||
|
|
||||||
|
order.reload
|
||||||
|
order.update!
|
||||||
|
order.payments.size.should == 1
|
||||||
|
order.payment_state.should == "balance_due"
|
||||||
|
order.payment.state.should == "pending"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "without auto_capture" do
|
||||||
|
before { Spree::Config.set(:auto_capture => false) }
|
||||||
|
|
||||||
|
it "should authorize payment" do
|
||||||
|
|
||||||
|
paypal_gateway.provider.should_receive(:authorize).with(order_total, anything()).and_return(authorize_response)
|
||||||
|
|
||||||
|
get :paypal_finish, {:order_id => order.number, :payment_method_id => "123", :token => token, :PayerID => "FWRVKNRRZ3WUC" }
|
||||||
|
|
||||||
|
response.should redirect_to order_url(order)
|
||||||
|
|
||||||
|
order.reload
|
||||||
|
order.update!
|
||||||
|
order.payments.size.should == 1
|
||||||
|
order.payment_state.should == "balance_due"
|
||||||
|
order.payment.state.should == "pending"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with un-successful repsonse" do
|
||||||
|
before do
|
||||||
|
Spree::Config.set(:auto_capture => true)
|
||||||
|
purchase_response.stub(:success? => false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should log error and redirect to payment step" do
|
||||||
|
paypal_gateway.provider.should_receive(:purchase).with(order_total, anything()).and_return(purchase_response)
|
||||||
|
|
||||||
|
controller.should_receive(:gateway_error).with(purchase_response)
|
||||||
|
|
||||||
|
get :paypal_finish, {:order_id => order.number, :payment_method_id => "123", :token => token, :PayerID => "FWRVKNRRZ3WUC" }
|
||||||
|
|
||||||
|
response.should redirect_to edit_order_checkout_url(order, :state => 'payment')
|
||||||
|
|
||||||
|
order.reload
|
||||||
|
order.update!
|
||||||
|
order.payments.size.should == 1
|
||||||
|
order.payment_state.should == "failed"
|
||||||
|
order.payment.state.should == "failed"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "order_opts" do
|
||||||
|
|
||||||
|
it "should return hash containing basic order details" do
|
||||||
|
opts = controller.send(:order_opts, order, paypal_gateway.id, 'payment')
|
||||||
|
|
||||||
|
opts.class.should == Hash
|
||||||
|
opts[:money].should == order_total
|
||||||
|
opts[:subtotal].should == (order.item_total * 100).to_i
|
||||||
|
opts[:order_id].should == order.number
|
||||||
|
opts[:custom].should == order.number
|
||||||
|
opts[:handling].should == 0
|
||||||
|
opts[:shipping].should == (order.ship_total * 100).to_i
|
||||||
|
|
||||||
|
opts[:return_url].should == paypal_confirm_order_checkout_url(order, :payment_method_id => paypal_gateway.id)
|
||||||
|
opts[:cancel_return_url].should == edit_order_url(order)
|
||||||
|
|
||||||
|
opts[:items].size.should > 0
|
||||||
|
opts[:items].size.should == order.line_items.count
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should include credits in returned hash" do
|
||||||
|
order_total #need here so variable is set before credit is created.
|
||||||
|
order.adjustments.create(:label => "Credit", :amount => -1)
|
||||||
|
order.update!
|
||||||
|
|
||||||
|
opts = controller.send(:order_opts, order, paypal_gateway.id, 'payment')
|
||||||
|
|
||||||
|
opts.class.should == Hash
|
||||||
|
opts[:money].should == order_total - 100
|
||||||
|
opts[:subtotal].should == ((order.item_total * 100) + (order.adjustments.select{|c| c.amount < 0}.sum(&:amount) * 100)).to_i
|
||||||
|
|
||||||
|
opts[:items].size.should == order.line_items.count + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
Factory.define :ppx_address do |f|
|
||||||
|
f.firstname 'John'
|
||||||
|
f.lastname 'Doe'
|
||||||
|
f.address1 '10 Lovely Street'
|
||||||
|
f.address2 'Northwest'
|
||||||
|
f.city "Herndon"
|
||||||
|
f.state { |state| state.association(:ppx_state) }
|
||||||
|
f.zipcode '20170'
|
||||||
|
f.country { |country| country.association(:country) }
|
||||||
|
f.phone '123-456-7890'
|
||||||
|
f.state_name "maryland"
|
||||||
|
f.alternative_phone "123-456-7899"
|
||||||
|
end
|
|
@ -0,0 +1,11 @@
|
||||||
|
Factory.define(:ppx_order) do |record|
|
||||||
|
# associations:
|
||||||
|
record.association(:user, :factory => :user)
|
||||||
|
record.association(:bill_address, :factory => :address)
|
||||||
|
record.association(:shipping_method, :factory => :shipping_method)
|
||||||
|
record.ship_address { |ship_address| Factory(:ppx_address, :city => "Chevy Chase", :zipcode => "20815") }
|
||||||
|
end
|
||||||
|
|
||||||
|
Factory.define :ppx_order_with_totals, :parent => :order do |f|
|
||||||
|
f.after_create { |order| Factory(:line_item, :order => order) }
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
Factory.define :md_state do |f|
|
||||||
|
f.name 'Maryland'
|
||||||
|
f.abbr 'MD'
|
||||||
|
f.country { |country| country.association(:country) }
|
||||||
|
end
|
|
@ -1,13 +1,17 @@
|
||||||
# This file is copied to ~/spec when you run 'ruby script/generate rspec'
|
# This file is copied to ~/spec when you run 'ruby script/generate rspec'
|
||||||
# from the project root directory.
|
# from the project root directory.
|
||||||
ENV["RAILS_ENV"] ||= 'test'
|
ENV["RAILS_ENV"] ||= 'test'
|
||||||
require File.expand_path("../../../config/environment", __FILE__)
|
require File.expand_path("../test_app/config/environment", __FILE__)
|
||||||
require 'rspec/rails'
|
require 'rspec/rails'
|
||||||
require 'fabrication'
|
|
||||||
|
|
||||||
# Requires supporting files with custom matchers and macros, etc,
|
#include spree's factories
|
||||||
# in ./support/ and its subdirectories.
|
require File.join(ENV['SPREE_PATH'], 'core/spec/factories')
|
||||||
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
|
||||||
|
# include local factories
|
||||||
|
Dir["#{File.dirname(__FILE__)}/factories/**/*.rb"].each do |f|
|
||||||
|
fp = File.expand_path(f)
|
||||||
|
require fp
|
||||||
|
end
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
# == Mock Framework
|
# == Mock Framework
|
||||||
|
@ -21,11 +25,16 @@ RSpec.configure do |config|
|
||||||
|
|
||||||
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
||||||
|
|
||||||
#config.include Devise::TestHelpers, :type => :controller
|
|
||||||
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
||||||
# examples within a transaction, comment the following line or assign false
|
# examples within a transaction, comment the following line or assign false
|
||||||
# instead of true.
|
# instead of true.
|
||||||
config.use_transactional_fixtures = true
|
config.use_transactional_fixtures = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Zone.class_eval do
|
||||||
|
def self.global
|
||||||
|
find_by_name("GlobalZone") || Factory(:global_zone)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@configuration ||= AppConfiguration.find_or_create_by_name("Default configuration")
|
@configuration ||= AppConfiguration.find_or_create_by_name("Default configuration")
|
||||||
|
|
Loading…
Reference in New Issue