Browse Source

Refactored and improved support for 0.40.3 and later

1-2-stable-sysmocom
Brian Quinn 11 years ago
parent
commit
9086741f52
  1. 1
      .gitignore
  2. 34
      Gemfile
  3. 17
      README.markdown
  4. 80
      Rakefile
  5. 91
      app/controllers/checkout_controller_decorator.rb
  6. 31
      app/controllers/paypal_express_callbacks_controller.rb
  7. 99
      app/views/admin/payments/source_views/_paypalexpress.html.erb
  8. 2
      config/locales/en.yml
  9. 7
      config/routes.rb
  10. 22
      db/migrate/20100122120551_create_paypal_txns.rb
  11. 11
      db/migrate/20100127083342_rename_avs_code.rb
  12. 9
      db/migrate/20100128115525_add_transaction_id_to_ppx_txn.rb
  13. 361
      lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb
  14. 128
      lib/active_merchant/billing/gateways/paypal_express.rb
  15. 14
      lib/active_merchant/billing/gateways/paypal_express_uk.rb
  16. 7
      lib/spree/checkouts_controller_with_paypal_express.rb
  17. 15
      lib/spree_paypal_express.rb
  18. 278
      spec/controllers/checkout_controller_spec.rb
  19. 13
      spec/factories/address_factory.rb
  20. 11
      spec/factories/order_factory.rb
  21. 5
      spec/factories/state_factory.rb
  22. 21
      spec/spec_helper.rb

1
.gitignore

@ -1 +1,2 @@
*.swp
spec/test_app

34
Gemfile

@ -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

17
README.markdown

@ -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).
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
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.
@ -23,17 +24,7 @@ USAGE (Checkout Payment)
========================
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
You'll need to have a Paypal developer account (developer.paypal.com) and both buyer and seller test accounts.
@ -124,10 +115,10 @@ USAGE (Checkout Payment)
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
Spree::Config.set(:auto_capture => true)
end
[1] If you check the review checkbox in the admin section for Payment Methods/Paypal Express, the flow is slightly different. Instead of Pay Now on Paypal's order details page, it now says Continue. And the user is directed back to the spree app's Confirmation page showing a place order button. Use whichever suits your needs best. Personally, I leave review unchecked to cut down on the steps in the checkout flow.
[1] If you check the review checkbox in the admin section for Payment Methods/Paypal Express, the flow is slightly different. Instead of Pay Now on Paypal's order details page, it now says Continue. And the user is directed back to the spree app's Confirmation page showing a place order button. Use whichever suits your needs best. Personally, I leave review unchecked to cut down on the steps in the checkout flow.

80
Rakefile

@ -1,31 +1,69 @@
require File.expand_path('../../config/application', __FILE__)
# encoding: utf-8
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rake/packagetask'
require 'rake/gempackagetask'
spec = eval(File.read('spree_paypal_express.gemspec'))
desc "Default Task"
task :default => [ :spec ]
Rake::GemPackageTask.new(spec) do |p|
p.gem_spec = spec
end
gemfile = File.expand_path('../spec/test_app/Gemfile', __FILE__)
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
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"
task :default => [ :spec ]
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
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new
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

91
lib/spree/paypal_express.rb → app/controllers/checkout_controller_decorator.rb

@ -1,31 +1,25 @@
# aim to unpick this later
module Spree::PaypalExpress
include ERB::Util
include ActiveMerchant::RequiresParameters
def self.included(target)
target.before_filter :redirect_to_paypal_express_form_if_needed, :only => [:update]
end
CheckoutController.class_eval do
before_filter :redirect_to_paypal_express_form_if_needed, :only => [:update]
def paypal_checkout
load_order
opts = all_opts(@order, params[:payment_method_id], 'checkout')
opts.merge!(address_options(@order))
gateway = paypal_gateway
@gateway = paypal_gateway
if Spree::Config[:auto_capture]
response = gateway.setup_purchase(opts[:money], opts)
@ppx_response = @gateway.setup_purchase(opts[:money], opts)
else
response = gateway.setup_authorization(opts[:money], opts)
@ppx_response = @gateway.setup_authorization(opts[:money], opts)
end
unless response.success?
gateway_error(response)
unless @ppx_response.success?
gateway_error(@ppx_response)
redirect_to edit_order_url(@order)
return
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
gateway_error I18n.t(:unable_to_connect_to_gateway)
redirect_to :back
@ -35,21 +29,21 @@ module Spree::PaypalExpress
load_order
opts = all_opts(@order,params[:payment_method_id], 'payment')
opts.merge!(address_options(@order))
gateway = paypal_gateway
@gateway = paypal_gateway
if Spree::Config[:auto_capture]
response = gateway.setup_purchase(opts[:money], opts)
@ppx_response = @gateway.setup_purchase(opts[:money], opts)
else
response = gateway.setup_authorization(opts[:money], opts)
@ppx_response = @gateway.setup_authorization(opts[:money], opts)
end
unless response.success?
gateway_error(response)
unless @ppx_response.success?
gateway_error(@ppx_response)
redirect_to edit_order_checkout_url(@order, :state => "payment")
return
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
gateway_error I18n.t(:unable_to_connect_to_gateway)
redirect_to :back
@ -126,10 +120,9 @@ module Spree::PaypalExpress
ppx_auth_response = gateway.authorize((@order.total*100).to_i, opts)
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,
:source => paypal_account,
:source_type => 'PaypalAccount',
@ -137,24 +130,23 @@ module Spree::PaypalExpress
:response_code => ppx_auth_response.params["ack"],
:avs_response => ppx_auth_response.avs_result["code"])
# transition through the state machine. This way any callbacks can be made
payment.started_processing!
payment.pend!
payment.started_processing!
record_log payment, ppx_auth_response
if ppx_auth_response.success?
#confirm status
case ppx_auth_response.params["payment_status"]
when "Completed"
payment.complete!
when "Pending"
payment.pend!
else
payment.pend!
Rails.logger.error "Unexpected response from PayPal Express"
Rails.logger.error ppx_auth_response.to_yaml
end
record_log payment, ppx_auth_response
@order.save!
#need to force checkout to complete state
until @order.state == "complete"
if @order.next!
@ -166,6 +158,7 @@ module Spree::PaypalExpress
redirect_to completion_route
else
payment.fail!
order_params = {}
gateway_error(ppx_auth_response)
@ -207,7 +200,7 @@ module Spree::PaypalExpress
{ :description => "Goods from #{Spree::Config[:site_name]}", # site details...
#: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
:header_background_color => "ffffff",
:header_border_color => "ffffff",
@ -252,43 +245,33 @@ module Spree::PaypalExpress
end
credits_total = 0
credits.compact!
if !credits.empty?
items.concat credits
credits_total = credits.map {|i| i[:amount] * i[:qty] }.sum
end
credits.compact!
if credits.present?
items.concat credits
credits_total = credits.map {|i| i[:amount] * i[:qty] }.sum
end
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",
:order_id => 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"
# 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[: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_timeout] = 3
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
elsif stage == "payment"
#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
#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[:money] = (order.total*100).to_i
end
opts
@ -374,4 +357,6 @@ module Spree::PaypalExpress
def paypal_gateway
payment_method.provider
end
end

31
app/controllers/paypal_express_callbacks_controller.rb

@ -5,16 +5,20 @@ class PaypalExpressCallbacksController < Spree::BaseController
def notify
retrieve_details #need to retreive details first to ensure ActiveMerchant gets configured correctly.
@notification = Paypal::Notification.new(request.raw_post)
# we only care about eChecks (for now?)
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"]
when "Denied"
create_txn PaypalTxn::TxnType::DENIED
@payment.fail!
when "Completed"
create_txn PaypalTxn::TxnType::CAPTURE
@payment.complete!
end
end
@ -27,31 +31,10 @@ class PaypalExpressCallbacksController < Spree::BaseController
@order = Order.find_by_number(params["invoice"])
if @order
@payment = @order.checkout.payments.find(:first,
:conditions => {"transactions.txn_type" => PaypalTxn::TxnType::AUTHORIZE,
"transactions.payment_type" => params["payment_type"]},
:joins => :transactions)
@payment = @order.payments.where(:state => "pending", :source_type => "PaypalAccount").try(:first)
@payment.try(:payment_method).try(:provider) #configures ActiveMerchant
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

99
app/views/admin/payments/source_views/_paypalexpress.html.erb

@ -32,44 +32,79 @@
<legend><%= t('transactions') %></legend>
<% payment.log_entries.reverse.each do |log| %>
<% details = YAML.load(log.details) %>
<% details = YAML.load(log.details) rescue "" %>
<table class="index">
<tr>
<th colspan="6"><%= t('transaction') %> <%= details.params["transaction_id"] %> - <%= log.created_at.to_s(:date_time24) %></th>
</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" %>
<% if details.is_a? ActiveMerchant::Billing::PaypalExpressResponse %>
<tr>
<th colspan="6"><%= t('transaction') %> <%= details.params["transaction_id"] %> - <%= log.created_at.to_s(:date_time24) %></th>
</tr>
<tr>
<td><label><%= t("pending_reason") %>:</label></td>
<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>
<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["pending_reason"] %>
<%= 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 %>
</table>
<% end %>
</fieldset>
</fieldset>

2
config/locales/en.yml

@ -1,5 +1,7 @@
---
en:
ipn_transaction: IPN Transaction
unknown_transaction: Unknown Transaction
edit_paypal_info: Edit Paypal Express Payment
paypal_payment: Paypal Express Payment
paypal_txn_id: Transaction Code

7
config/routes.rb

@ -11,16 +11,15 @@ Rails.application.routes.draw do
end
match '/paypal_notify' => 'paypal_express_callbacks#notify', :via => [:get, :post]
resources :paypal_express_callbacks
namespace :admin do
resources :orders do
resources :paypal_payments do
member do
get :refund
get :capture
end
end
end
end
end
end
end

22
db/migrate/20100122120551_create_paypal_txns.rb

@ -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

11
db/migrate/20100127083342_rename_avs_code.rb

@ -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

9
db/migrate/20100128115525_add_transaction_id_to_ppx_txn.rb

@ -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

361
lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb

@ -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

128
lib/active_merchant/billing/gateways/paypal_express.rb

@ -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

14
lib/active_merchant/billing/gateways/paypal_express_uk.rb

@ -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

7
lib/spree/checkouts_controller_with_paypal_express.rb

@ -1,7 +0,0 @@
module Spree::CheckoutControllerWithPaypalExpress
def self.included(target)
target.before_filter :redirect_to_paypal_express_form, :only => [:update]
end
private
end

15
lib/spree_paypal_express.rb

@ -7,21 +7,16 @@ module SpreePaypalExpress
config.autoload_paths += %W(#{config.root}/lib)
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|
Rails.env.production? ? require(c) : load(c)
end
BillingIntegration::PaypalExpress.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
config.to_prepare &method(:activate).to_proc

278
spec/controllers/checkout_controller_spec.rb

@ -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

13
spec/factories/address_factory.rb

@ -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

11
spec/factories/order_factory.rb

@ -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

5
spec/factories/state_factory.rb

@ -0,0 +1,5 @@
Factory.define :md_state do |f|
f.name 'Maryland'
f.abbr 'MD'
f.country { |country| country.association(:country) }
end

21
spec/spec_helper.rb

@ -1,13 +1,17 @@
# This file is copied to ~/spec when you run 'ruby script/generate rspec'
# from the project root directory.
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../../config/environment", __FILE__)
require File.expand_path("../test_app/config/environment", __FILE__)
require 'rspec/rails'
require 'fabrication'
# Requires supporting files with custom matchers and macros, etc,