Basic support for Paypal Express in Spree

This commit contains enough to do an express checkout authorization (not purchase) from the cart
page, to save all relevant info, and to be able to view some of it in the admin pages. It is very
much work in progress!

Some admin features won't work (eg captures, viewing transactions), since handling of payments
and gateways needs to be generalised first. Also need to get the orders handled like those for
guest checkouts.

It's also tied to GB/UK details at the moment (this will change), and it needs a modified version
of active merchant which will be released/circulated soon.
This commit is contained in:
paulcc 2009-05-14 11:11:59 +01:00
commit 11f88be5c5
13 changed files with 425 additions and 0 deletions

3
README.markdown Normal file
View File

@ -0,0 +1,3 @@
= Paypal Express
Description goes here

120
Rakefile Normal file
View File

@ -0,0 +1,120 @@
# I think this is the one that should be moved to the extension Rakefile template
# In rails 1.2, plugins aren't available in the path until they're loaded.
# Check to see if the rspec plugin is installed first and require
# it if it is. If not, use the gem version.
# Determine where the RSpec plugin is by loading the boot
unless defined? SPREE_ROOT
ENV["RAILS_ENV"] = "test"
case
when ENV["SPREE_ENV_FILE"]
require File.dirname(ENV["SPREE_ENV_FILE"]) + "/boot"
when File.dirname(__FILE__) =~ %r{vendor/SPREE/vendor/extensions}
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../")}/config/boot"
else
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../")}/config/boot"
end
end
require 'rake'
require 'rake/rdoctask'
require 'rake/testtask'
rspec_base = File.expand_path(SPREE_ROOT + '/vendor/plugins/rspec/lib')
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
require 'spec/rake/spectask'
# require 'spec/translator'
# Cleanup the SPREE_ROOT constant so specs will load the environment
Object.send(:remove_const, :SPREE_ROOT)
extension_root = File.expand_path(File.dirname(__FILE__))
task :default => :spec
task :stats => "spec:statsetup"
desc "Run all specs in spec directory"
Spec::Rake::SpecTask.new(:spec) do |t|
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
t.spec_files = FileList["#{extension_root}/spec/**/*_spec.rb"]
end
namespace :spec do
desc "Run all specs in spec directory with RCov"
Spec::Rake::SpecTask.new(:rcov) do |t|
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
t.spec_files = FileList['spec/**/*_spec.rb']
t.rcov = true
t.rcov_opts = ['--exclude', 'spec', '--rails']
end
desc "Print Specdoc for all specs"
Spec::Rake::SpecTask.new(:doc) do |t|
t.spec_opts = ["--format", "specdoc", "--dry-run"]
t.spec_files = FileList['spec/**/*_spec.rb']
end
[:models, :controllers, :views, :helpers].each do |sub|
desc "Run the specs under spec/#{sub}"
Spec::Rake::SpecTask.new(sub) do |t|
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
end
end
# Hopefully no one has written their extensions in pre-0.9 style
# desc "Translate specs from pre-0.9 to 0.9 style"
# task :translate do
# translator = ::Spec::Translator.new
# dir = RAILS_ROOT + '/spec'
# translator.translate(dir, dir)
# end
# Setup specs for stats
task :statsetup do
require 'code_statistics'
::STATS_DIRECTORIES << %w(Model\ specs spec/models)
::STATS_DIRECTORIES << %w(View\ specs spec/views)
::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers)
::STATS_DIRECTORIES << %w(Helper\ specs spec/views)
::CodeStatistics::TEST_TYPES << "Model specs"
::CodeStatistics::TEST_TYPES << "View specs"
::CodeStatistics::TEST_TYPES << "Controller specs"
::CodeStatistics::TEST_TYPES << "Helper specs"
::STATS_DIRECTORIES.delete_if {|a| a[0] =~ /test/}
end
namespace :db do
namespace :fixtures do
desc "Load fixtures (from spec/fixtures) into the current environment's database. Load specific fixtures using FIXTURES=x,y"
task :load => :environment do
require 'active_record/fixtures'
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'spec', 'fixtures', '*.{yml,csv}'))).each do |fixture_file|
Fixtures.create_fixtures('spec/fixtures', File.basename(fixture_file, '.*'))
end
end
end
end
end
desc 'Generate documentation for the paypal_express extension.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'PaypalExpressExtension'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end
# For extensions that are in transition
desc 'Test the paypal_express extension.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
# Load any custom rakefiles for extension
Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }

View File

@ -0,0 +1,18 @@
class PaypalPayment < Payment
has_many :creditcard_txns, :foreign_key => 'creditcard_payment_id' # reused and faked
belongs_to :creditcard # allow for saving of fake details
# accepts_nested_attributes_for :creditcard
alias :txns :creditcard_txns # should PUSH to parent/interface
def find_authorization
#find the transaction associated with the original authorization/capture
txns.find(:first,
:conditions => ["txn_type = ? AND response_code IS NOT NULL", CreditcardTxn::TxnType::AUTHORIZE],
:order => 'created_at DESC')
end
def can_capture? # push to parent? perhaps not
txns.last == find_authorization
end
end

View File

@ -0,0 +1,5 @@
<div style="width: 100%; height: 4px;"> &nbsp; </div>
<a href="<%= paypal_checkout_order_url order %>" style="float: right;">
<img src="https://www.paypal.com/en_GB/GB/i/btn/btn_xpressCheckout.gif" align="left" style="margin-right:7px;"/>
</a>
<strong style="font-size: 90%; text-align: center; padding: 5px;">OR&nbsp;<br/>CHOOSE</strong>

View File

@ -0,0 +1,2 @@
!-- PayPal Logo --><table border="0" cellpadding="10" cellspacing="0" align="center"><tr><td align="center"></td></tr><tr><td align="center"><a href="#" onclick="javascript:window.open('https://www.paypal.com/uk/cgi-bin/webscr?cmd=xpt/Marketing/popup/OLCWhatIsPayPal-outside','olcwhatispaypal','toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=400, height=350');"><img src="https://www.paypal.com/en_GB/GB/i/logo/PayPal_mark_50x34.gif" border="0" alt="Acceptance Mark"></a></td></tr></table><!-- PayPal Logo -->

4
config/routes.rb Normal file
View File

@ -0,0 +1,4 @@
# Put your extension routes here.
map.resources :orders, :member => {:paypal_checkout => :any, :paypal_finish => :any}

View File

@ -0,0 +1,15 @@
class CreatePaypalExpressGateway < ActiveRecord::Migration
def self.up
login = GatewayOption.create(:name => "login", :description => "Your login email.")
password = GatewayOption.create(:name => "password", :description => "Your Paypal API Credentials Password.")
signature = GatewayOption.create(:name => "signature", :textarea => true, :description => "Your Paypal API Credentials signature string.")
gateway = Gateway.create(:name => "Paypal Express UK",
:clazz => "ActiveMerchant::Billing::PaypalExpressUkGateway",
:description => "Active Merchant's Paypal Express (UK) Gateway.",
:gateway_options => [login, password, signature])
end
def self.down
end
end

163
lib/spree/paypal_express.rb Normal file
View File

@ -0,0 +1,163 @@
# Adapted for protx3ds
module Spree::PaypalExpress
include ERB::Util
include Spree::PaymentGateway
def fixed_opts
{ :description => "Parasols or related outdoor items", # site details...
#:page_style => "foobar", # merchant account can set default
:header_image => "https://" + Spree::Config[:site_url] + "/images/logo.png",
:background_color => "e1e1e1", # must be hex only, six chars
:header_background_color => "ffffff",
:header_border_color => "00735a",
:allow_note => true,
:locale => Spree::Config[:default_locale],
:notify_url => 'to be done',
:req_confirm_shipping => false, # for security, might make an option later
}
end
def order_opts(order)
items = order.line_items.map do |item|
{ :name => item.variant.product.name,
:description => item.variant.product.description[0..120],
:sku => item.variant.sku,
:qty => item.quantity,
:amount => item.price - 0.15 * item.price, # avoid some rounding err, more needed
:tax => 0.15 * item.price,
:weight => item.variant.weight,
:height => item.variant.height,
:width => item.variant.width,
:depth => item.variant.weight }
end
site = Spree::Config[:site_url]
site = "localhost:3000"
opts = { :return_url => "https://" + site + "/orders/#{order.number}/paypal_finish",
:cancel_return_url => "http://" + site + "/orders/#{order.number}/edit",
:order_id => order.number,
:custom => order.number + '--' + order.number,
# :no_shipping => false,
# :address_override => false,
:items => items,
:subtotal => items.map {|i| i[:amount] * i[:qty] }.sum,
:shipping => NetstoresShipping::Calculator.calculate_order_shipping(order), # NEED HIDE
:handling => 0,
:tax => items.map {|i| i[:tax] * i[:qty]}.sum
# WARNING -- don't use :ship_discount, => :insurance_offered, :insurance since
# they've not been tested and may trigger some paypal bugs, eg not showing order
# see http://www.pdncommunity.com/t5/PayPal-Developer-Blog/Displaying-Order-Details-in-Express-Checkout/bc-p/92902#C851
}
# WARNING: paypal expects this sum to work (TODO: shift to AM code? and throw wobbly?)
# however: might be rounding issues when it comes to tax, though you can capture slightly extra
opts[:money] = opts.slice(:subtotal, :shipping, :handling, :tax).values.sum
if opts[:money] != order.total
raise "Ouch - precision problems: #{opts[:money]} vs #{order.total}"
end
[:money, :subtotal, :shipping, :handling, :tax].each {|amt| opts[amt] *= 100}
opts[:items].each {|item| [:amount,:tax].each {|amt| item[amt] *= 100} }
opts[:email] = current_user.email if current_user
opts
end
def all_opts(order)
fixed_opts.merge(order_opts order)
end
def paypal_checkout
# need build etc? at least to finalise the total?
gateway = paypal_gateway
opts = all_opts(@order)
out2 = gateway.setup_authorization(opts[:money], opts)
redirect_to (gateway.redirect_url_for out2.token)
end
def paypal_finish
gateway = paypal_gateway
opts = { :token => params[:token],
:payer_id => params[:PayerID] }.merge all_opts(@order)
info = gateway.details_for params[:token]
response = gateway.authorize(opts[:money], opts)
#render :text => "<pre>" + params.to_yaml + "\n\n\n" + out1.to_yaml + "\n\n\n" + info.to_yaml + "</pre>"
# unless gateway.successful? response
unless [ 'Success', 'SuccessWithWarning' ].include?(response.params["ack"]) ## HACKY
# TMP render :text => "<pre>" + response.params.inspect + "\n\n\n" + params.to_yaml + "\n\n\n" + response.to_yaml + "\n\n\n" + info.to_yaml + "</pre>" and return
# gateway_error(response)
end
# now save info
order = Order.find_by_number(params[:id])
order.email = info.email
order.special_instructions = info.params["note"]
ship_address = info.address
order.ship_address = Address.create :firstname => info.params["first_name"],
:lastname => info.params["last_name"],
:address1 => ship_address["address1"],
:address2 => ship_address["address2"],
:city => ship_address["city"],
:state => State.find_by_name(ship_address["state"]),
:country => Country.find_by_iso(ship_address["country"]),
:zipcode => ship_address["zip"],
:phone => ship_address["phone"] || "(not given)"
shipment = Shipment.create :address => order.ship_address,
:ship_method => ShippingMethod.first # TODO: refine/choose
order.shipments << shipment
fake_card = Creditcard.new :order => order,
:cc_type => "visa", # hands are tied
:month => Time.now.month,
:year => Time.now.year,
:first_name => info.params["first_name"],
:last_name => info.params["last_name"],
:display_number => "paypal:" + info.payer_id
payment = order.paypal_payments.create(:amount => response.params["gross_amount"].to_i || 999,
:creditcard => fake_card)
# query - need 0 in amount for an auth? see main code
transaction = CreditcardTxn.new( :amount => response.params["gross_amount"].to_i || 999,
:response_code => response.authorization,
:txn_type => CreditcardTxn::TxnType::AUTHORIZE)
payment.creditcard_txns << transaction
order.save!
order.complete # get return of status? throw of problems??? else weak go-ahead
session[:order_id] = nil if order.checkout_complete
redirect_to order_url(order, :checkout_complete => true, :order_token => session[:order_token])
end
private
# temporary until options are sorted out
def paypal_gateway
gw_opts = { :login => "paul_1240924284_biz_api1.rocket-works.co.uk",
:password => "HPDLRQ5DGPAWDAUB",
:signature => "A2VYNHC1wYRx0ZwMX6dXwoFDGTMnAYt4SmzCH6LS3nVKLszXCtL-rp9o" } # or by cls
gateway = ActiveMerchant::Billing::PaypalExpressUkGateway.new gw_opts
return gateway
#? return Spree::BogusGateway.new if ENV['RAILS_ENV'] == "development" and Spree::Gateway::Config[:use_bogus]
gateway_config = GatewayConfiguration.find_by_name "Paypal Express (UK)"
config_options = {}
gateway_config.gateway_option_values.each do |option_value|
key = option_value.gateway_option.name.to_sym
config_options[key] = option_value.value
end
gateway = gateway_config.gateway.clazz.constantize.new(config_options)
#
# => [#<GatewayOptionValue id: 11, gateway_configuration_id: 1, gateway_option_id: 12, value: "simplybenches", created_at: "2009-04-05 09:46:52", updated_at: "2009-04-05 09:46:52">] >> GatewayConfiguration => GatewayConfiguration(id: integer, gateway_id: integer, created_at: datetime, updated_at: datetime) >> GatewayConfiguration.all => [#<GatewayConfiguration id: 1, gateway_id: 5, created_at: "2009-02-19 23:17:43", updated_at: "2009-03-17 11:52:09">]
end
end

View File

@ -0,0 +1,28 @@
namespace :db do
desc "Bootstrap your database for Spree."
task :bootstrap => :environment do
# load initial database fixtures (in db/sample/*.yml) into the current environment's database
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
Dir.glob(File.join(PaypalExpressExtension.root, "db", 'sample', '*.{yml,csv}')).each do |fixture_file|
Fixtures.create_fixtures("#{PaypalExpressExtension.root}/db/sample", File.basename(fixture_file, '.*'))
end
end
end
namespace :spree do
namespace :extensions do
namespace :paypal_express do
desc "Copies public assets of the Paypal Express to the instance public/ directory."
task :update => :environment do
is_svn_git_or_dir = proc {|path| path =~ /\.svn/ || path =~ /\.git/ || File.directory?(path) }
Dir[PaypalExpressExtension.root + "/public/**/*"].reject(&is_svn_git_or_dir).each do |file|
path = file.sub(PaypalExpressExtension.root, '')
directory = File.dirname(path)
puts "Copying #{path}..."
mkdir_p RAILS_ROOT + directory
cp file, RAILS_ROOT + path
end
end
end
end
end

View File

@ -0,0 +1,24 @@
# Uncomment this if you reference any of your controllers in activate
# require_dependency 'application'
class PaypalExpressExtension < Spree::Extension
version "1.0"
description "Describe your extension here"
url "http://yourwebsite.com/paypal_express"
# Please use paypal_express/config/routes.rb instead for extension routes.
# def self.require_gems(config)
# config.gem "gemname-goes-here", :version => '1.2.3'
# end
def activate
# admin.tabs.add "Paypal Express", "/admin/paypal_express", :after => "Layouts", :visibility => [:all]
OrdersController.class_eval do
ssl_required :paypal_checkout, :paypal_finish
end
Order.class_eval do
has_many :paypal_payments
end
end
end

BIN
public/images/paypal111.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

6
spec/spec.opts Normal file
View File

@ -0,0 +1,6 @@
--colour
--format
progress
--loadby
mtime
--reverse

37
spec/spec_helper.rb Normal file
View File

@ -0,0 +1,37 @@
unless defined? SPREE_ROOT
ENV["RAILS_ENV"] = "test"
case
when ENV["SPREE_ENV_FILE"]
require ENV["SPREE_ENV_FILE"]
when File.dirname(__FILE__) =~ %r{vendor/SPREE/vendor/extensions}
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../../")}/config/environment"
else
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../")}/config/environment"
end
end
require "#{SPREE_ROOT}/spec/spec_helper"
if File.directory?(File.dirname(__FILE__) + "/scenarios")
Scenario.load_paths.unshift File.dirname(__FILE__) + "/scenarios"
end
if File.directory?(File.dirname(__FILE__) + "/matchers")
Dir[File.dirname(__FILE__) + "/matchers/*.rb"].each {|file| require file }
end
Spec::Runner.configure do |config|
# config.use_transactional_fixtures = true
# config.use_instantiated_fixtures = false
# config.fixture_path = RAILS_ROOT + '/spec/fixtures'
# You can declare fixtures for each behaviour like this:
# describe "...." do
# fixtures :table_a, :table_b
#
# Alternatively, if you prefer to declare them only once, you can
# do so here, like so ...
#
# config.global_fixtures = :table_a, :table_b
#
# If you declare global fixtures, be aware that they will be declared
# for all of your examples, even those that don't use them.
end