killbill-paypal-express 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,36 @@
1
+ *.gem
2
+ *.rbc
3
+ *.swp
4
+ .bundle
5
+ .config
6
+ coverage
7
+ InstalledFiles
8
+ lib/bundler/man
9
+ pkg
10
+ rdoc
11
+ spec/reports
12
+ test/tmp
13
+ test/version_tmp
14
+ tmp
15
+
16
+ # YARD artifacts
17
+ .yardoc
18
+ _yardoc
19
+ doc/
20
+
21
+ .jbundler
22
+ Jarfile.lock
23
+ Gemfile.lock
24
+
25
+ .DS_Store
26
+
27
+ # Build directory
28
+ killbill-paypal-express/
29
+
30
+ # Config file
31
+ paypal_express.yml
32
+
33
+ # Testing database
34
+ test.db
35
+
36
+ target
data/.travis.yml ADDED
@@ -0,0 +1,22 @@
1
+ language: ruby
2
+
3
+ notifications:
4
+ email:
5
+ - killbilling-dev@googlegroups.com
6
+
7
+ rvm:
8
+ - 1.9.2
9
+ - 1.9.3
10
+ - ruby-head
11
+ - jruby-19mode
12
+ - jruby-head
13
+
14
+ jdk:
15
+ - openjdk7
16
+ - oraclejdk7
17
+ - openjdk6
18
+
19
+ matrix:
20
+ allow_failures:
21
+ - rvm: ruby-head
22
+ - rvm: jruby-head
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/Jarfile ADDED
@@ -0,0 +1,3 @@
1
+ jar 'com.ning.billing:killbill-api', '0.1.63'
2
+ jar 'com.ning.billing:killbill-util:tests', '0.1.63'
3
+ jar 'javax.servlet:javax.servlet-api', '3.0.1'
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ [![Build Status](https://travis-ci.org/killbill/killbill-paypal-express-plugin.png)](https://travis-ci.org/killbill/killbill-paypal-express-plugin)
2
+ [![Code Climate](https://codeclimate.com/github/killbill/killbill-paypal-express-plugin.png)](https://codeclimate.com/github/killbill/killbill-paypal-express-plugin)
3
+
4
+ killbill-paypal-express-plugin
5
+ ==============================
6
+
7
+ Plugin to use Express Checkout as a gateway
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env rake
2
+
3
+ # Install tasks to build and release the plugin
4
+ require 'bundler/setup'
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ # Install test tasks
8
+ require 'rspec/core/rake_task'
9
+ namespace :test do
10
+ desc "Run RSpec tests"
11
+ RSpec::Core::RakeTask.new do |task|
12
+ task.name = 'spec'
13
+ task.pattern = './spec/*/*_spec.rb'
14
+ end
15
+
16
+ namespace :remote do
17
+ desc "Run RSpec remote tests"
18
+ RSpec::Core::RakeTask.new do |task|
19
+ task.name = 'spec'
20
+ task.pattern = './spec/*/remote/*_spec.rb'
21
+ end
22
+ end
23
+ end
24
+
25
+ # Install tasks to package the plugin for Killbill
26
+ require 'killbill/rake_task'
27
+ Killbill::PluginHelper.install_tasks
28
+
29
+ # Run tests by default
30
+ task :default => 'test:spec'
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
data/config.ru ADDED
@@ -0,0 +1,4 @@
1
+ require 'paypal_express'
2
+ require 'paypal_express/config/application'
3
+
4
+ run Sinatra::Application
data/db/schema.rb ADDED
@@ -0,0 +1,81 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Schema.define(:version => 20130311153635) do
4
+ create_table "paypal_express_payment_methods", :force => true do |t|
5
+ t.string "kb_account_id", :null => false
6
+ t.string "kb_payment_method_id" # NULL before Killbill knows about it
7
+ t.string "paypal_express_payer_id" # NULL before the express checkout is completed
8
+ t.string "paypal_express_baid" # NULL before the express checkout is completed
9
+ t.string "paypal_express_token", :null => false, :unique => true
10
+ t.boolean "is_deleted", :null => false, :default => false
11
+ t.datetime "created_at", :null => false
12
+ t.datetime "updated_at", :null => false
13
+ end
14
+
15
+ create_table "paypal_express_transactions", :force => true do |t|
16
+ t.integer "paypal_express_response_id", :null => false
17
+ t.string "api_call", :null => false
18
+ t.string "kb_payment_id", :null => false
19
+ t.string "paypal_express_txn_id", :null => false
20
+ t.integer "amount_in_cents", :null => false
21
+ t.datetime "created_at", :null => false
22
+ t.datetime "updated_at", :null => false
23
+ end
24
+
25
+ create_table "paypal_express_responses", :force => true do |t|
26
+ t.string "api_call", :null => false
27
+ t.string "kb_payment_id"
28
+ t.string "message"
29
+ t.string "authorization"
30
+ t.boolean "fraud_review"
31
+ t.boolean "test"
32
+ t.string "token"
33
+ t.string "payer_id"
34
+ t.string "billing_agreement_id"
35
+ t.string "payer_name"
36
+ t.string "payer_email"
37
+ t.string "payer_country"
38
+ t.string "contact_phone"
39
+ t.string "ship_to_address_name"
40
+ t.string "ship_to_address_company"
41
+ t.string "ship_to_address_address1"
42
+ t.string "ship_to_address_address2"
43
+ t.string "ship_to_address_city"
44
+ t.string "ship_to_address_state"
45
+ t.string "ship_to_address_country"
46
+ t.string "ship_to_address_zip"
47
+ t.string "ship_to_address_phone"
48
+ t.string "receiver_info_business"
49
+ t.string "receiver_info_receiver"
50
+ t.string "receiver_info_receiverid"
51
+ t.string "payment_info_transactionid"
52
+ t.string "payment_info_parenttransactionid"
53
+ t.string "payment_info_receiptid"
54
+ t.string "payment_info_transactiontype"
55
+ t.string "payment_info_paymenttype"
56
+ t.string "payment_info_paymentdate"
57
+ t.string "payment_info_grossamount"
58
+ t.string "payment_info_feeamount"
59
+ t.string "payment_info_taxamount"
60
+ t.string "payment_info_exchangerate"
61
+ t.string "payment_info_paymentstatus"
62
+ t.string "payment_info_pendingreason"
63
+ t.string "payment_info_reasoncode"
64
+ t.string "payment_info_protectioneligibility"
65
+ t.string "payment_info_protectioneligibilitytype"
66
+ t.string "payment_info_shipamount"
67
+ t.string "payment_info_shiphandleamount"
68
+ t.string "payment_info_shipdiscount"
69
+ t.string "payment_info_insuranceamount"
70
+ t.string "payment_info_subject"
71
+ t.string "avs_result_code"
72
+ t.string "avs_result_message"
73
+ t.string "avs_result_street_match"
74
+ t.string "avs_result_postal_match"
75
+ t.string "cvv_result_code"
76
+ t.string "cvv_result_message"
77
+ t.boolean "success"
78
+ t.datetime "created_at", :null => false
79
+ t.datetime "updated_at", :null => false
80
+ end
81
+ end
@@ -0,0 +1,41 @@
1
+ version = File.read(File.expand_path('../VERSION', __FILE__)).strip
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'killbill-paypal-express'
5
+ s.version = version
6
+ s.summary = 'Plugin to use Express Checkout as a gateway.'
7
+ s.description = 'Killbill payment plugin for Paypal Express Checkout.'
8
+
9
+ s.required_ruby_version = '>= 1.9.3'
10
+
11
+ s.license = 'Apache License (2.0)'
12
+
13
+ s.author = 'Killbill core team'
14
+ s.email = 'killbilling-users@googlegroups.com'
15
+ s.homepage = 'http://www.killbilling.org'
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.bindir = 'bin'
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.rdoc_options << '--exclude' << '.'
24
+
25
+ s.add_dependency 'killbill', '~> 1.0.12'
26
+ s.add_dependency 'activemerchant', '~> 1.31.1'
27
+ s.add_dependency 'activerecord', '~> 3.2.1'
28
+ s.add_dependency 'sinatra', '~> 1.3.4'
29
+ if defined?(JRUBY_VERSION)
30
+ s.add_dependency 'activerecord-jdbcmysql-adapter', '~> 1.2.9'
31
+ end
32
+
33
+ s.add_development_dependency 'jbundler', '~> 0.4.1'
34
+ s.add_development_dependency 'rake', '>= 10.0.0'
35
+ s.add_development_dependency 'rspec', '~> 2.12.0'
36
+ if defined?(JRUBY_VERSION)
37
+ s.add_development_dependency 'activerecord-jdbcsqlite3-adapter', '~> 1.2.6'
38
+ else
39
+ s.add_development_dependency 'sqlite3', '~> 1.3.7'
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ mainClass=Killbill::PaypalExpress::PaymentPlugin
2
+ require=paypal_express
3
+ pluginType=PAYMENT
@@ -0,0 +1,124 @@
1
+ module Killbill::PaypalExpress
2
+ class PaymentPlugin < Killbill::Plugin::Payment
3
+ def start_plugin
4
+ Killbill::PaypalExpress.initialize! "#{@root}/paypal_express.yml", @logger
5
+ @gateway = Killbill::PaypalExpress.gateway
6
+
7
+ @ip = Utils.ip
8
+
9
+ super
10
+
11
+ @logger.info "Killbill::PaypalExpress::PaymentPlugin started"
12
+ end
13
+
14
+ def process_payment(kb_account_id, kb_payment_id, kb_payment_method_id, amount_in_cents, currency, options = {})
15
+ options[:currency] ||= currency
16
+ options[:payment_type] ||= 'Any'
17
+ options[:invoice_id] ||= kb_payment_id
18
+ options[:description] ||= "Kill Bill payment for #{kb_payment_id}"
19
+ options[:ip] ||= @ip
20
+
21
+ if options[:reference_id].blank?
22
+ payment_method = PaypalExpressPaymentMethod.from_kb_payment_method_id(kb_payment_method_id)
23
+ options[:reference_id] = payment_method.paypal_express_baid
24
+ end
25
+
26
+ # Go to Paypal (DoReferenceTransaction call)
27
+ paypal_response = @gateway.reference_transaction amount_in_cents, options
28
+ response = save_response_and_transaction paypal_response, :charge, kb_payment_id, amount_in_cents
29
+
30
+ response.to_payment_response
31
+ end
32
+
33
+ def process_refund(kb_account_id, kb_payment_id, amount_in_cents, currency, options = {})
34
+ # Find one successful charge which amount is at least the amount we are trying to refund
35
+ paypal_express_transaction = PaypalExpressTransaction.where("paypal_express_transactions.amount_in_cents >= ?", amount_in_cents).find_last_by_api_call_and_kb_payment_id(:charge, kb_payment_id)
36
+ raise "Unable to find Paypal Express transaction id for payment #{kb_payment_id}" if paypal_express_transaction.nil?
37
+
38
+ options[:currency] ||= currency
39
+ options[:refund_type] ||= paypal_express_transaction.amount_in_cents != amount_in_cents ? 'Partial' : 'Full'
40
+
41
+ identification = paypal_express_transaction.paypal_express_txn_id
42
+
43
+ # Go to Paypal
44
+ paypal_response = @gateway.refund amount_in_cents, identification, options
45
+ response = save_response_and_transaction paypal_response, :refund, kb_payment_id, amount_in_cents
46
+
47
+ response.to_refund_response
48
+ end
49
+
50
+ def get_payment_info(kb_account_id, kb_payment_id, options = {})
51
+ paypal_express_transaction = PaypalExpressTransaction.from_kb_payment_id(kb_payment_id)
52
+
53
+ begin
54
+ transaction_id = paypal_express_transaction.paypal_express_txn_id
55
+ response = @gateway.transaction_details transaction_id
56
+ PaypalExpressResponse.from_response(:transaction_details, kb_payment_id, response).to_payment_response
57
+ rescue => e
58
+ @logger.warn("Exception while retrieving Paypal Express transaction detail for payment #{kb_payment_id}, defaulting to cached response: #{e}")
59
+ paypal_express_transaction.paypal_express_response.to_payment_response
60
+ end
61
+ end
62
+
63
+ def add_payment_method(kb_account_id, kb_payment_method_id, payment_method_props, set_default=false, options = {})
64
+ token = payment_method_props.value('token')
65
+ return false if token.nil?
66
+
67
+ # The payment method should have been created during the setup step (see private api)
68
+ payment_method = PaypalExpressPaymentMethod.from_kb_account_id_and_token(kb_account_id, token)
69
+
70
+ # Go to Paypal to get the Payer id (GetExpressCheckoutDetails call)
71
+ paypal_express_details_response = @gateway.details_for token
72
+ response = save_response_and_transaction paypal_express_details_response, :details_for
73
+ return false unless response.success?
74
+
75
+ payer_id = response.payer_id
76
+ unless payer_id.nil?
77
+ # Go to Paypal to create the BAID for recurring payments (CreateBillingAgreement call)
78
+ paypal_express_baid_response = @gateway.create_billing_agreement :token => token
79
+ response = save_response_and_transaction paypal_express_baid_response, :create_billing_agreement
80
+ return false unless response.success?
81
+
82
+ payment_method.kb_payment_method_id = kb_payment_method_id
83
+ payment_method.paypal_express_payer_id = payer_id
84
+ payment_method.paypal_express_baid = response.billing_agreement_id
85
+ payment_method.save!
86
+
87
+ logger.info "Created BAID #{payment_method.paypal_express_baid} for payment method #{kb_payment_method_id} (account #{kb_account_id})"
88
+ true
89
+ else
90
+ logger.warn "Unable to retrieve Payer id details for token #{token} (account #{kb_account_id})"
91
+ false
92
+ end
93
+ end
94
+
95
+ def delete_payment_method(kb_account_id, kb_payment_method_id, options = {})
96
+ PaypalExpressPaymentMethod.mark_as_deleted! kb_payment_method_id
97
+ end
98
+
99
+ def get_payment_method_detail(kb_account_id, kb_payment_method_id, options = {})
100
+ PaypalExpressPaymentMethod.from_kb_payment_method_id(kb_payment_method_id).to_payment_method_response
101
+ end
102
+
103
+ def get_payment_methods(kb_account_id, refresh_from_gateway = false, options = {})
104
+ PaypalExpressPaymentMethod.from_kb_account_id(kb_account_id).collect { |pm| pm.to_payment_method_response }
105
+ end
106
+
107
+ private
108
+
109
+ def save_response_and_transaction(paypal_express_response, api_call, kb_payment_id=nil, amount_in_cents=0)
110
+ @logger.warn "Unsuccessful #{api_call}: #{paypal_express_response.message}" unless paypal_express_response.success?
111
+
112
+ # Save the response to our logs
113
+ response = PaypalExpressResponse.from_response(api_call, kb_payment_id, paypal_express_response)
114
+ response.save!
115
+
116
+ if response.success and !response.authorization.blank?
117
+ # Record the transaction
118
+ transaction = response.create_paypal_express_transaction!(:amount_in_cents => amount_in_cents, :api_call => api_call, :kb_payment_id => kb_payment_id, :paypal_express_txn_id => response.authorization)
119
+ @logger.debug "Recorded transaction: #{transaction.inspect}"
120
+ end
121
+ response
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,50 @@
1
+ configure do
2
+ # Usage: rackup -Ilib -E test
3
+ if development? or test?
4
+ Killbill::PaypalExpress.initialize! unless Killbill::PaypalExpress.initialized
5
+ end
6
+ end
7
+
8
+ helpers do
9
+ def plugin
10
+ Killbill::PaypalExpress::PrivatePaymentPlugin.instance
11
+ end
12
+ end
13
+
14
+ # curl -v -XPOST http://127.0.0.1:9292/plugins/killbill-paypal-express/1.0/setup-checkout --data-binary '{"kb_account_id":"a6b33ba1"}'
15
+ post '/plugins/killbill-paypal-express/1.0/setup-checkout', :provides => 'json' do
16
+ begin
17
+ data = JSON.parse request.body.read
18
+ rescue JSON::ParserError => e
19
+ halt 400, {'Content-Type' => 'text/plain'}, "Invalid payload: #{e}"
20
+ end
21
+
22
+ response = plugin.initiate_express_checkout data['kb_account_id'],
23
+ data['amount_in_cents'] || 0,
24
+ data['currency'] || 'USD',
25
+ data['options'] || {}
26
+ unless response.success?
27
+ status 500
28
+ response.message
29
+ else
30
+ redirect response.to_express_checkout_url
31
+ end
32
+ end
33
+
34
+ # curl -v http://127.0.0.1:9292/plugins/killbill-paypal-express/1.0/pms/1
35
+ get '/plugins/killbill-paypal-express/1.0/pms/:id', :provides => 'json' do
36
+ if pm = Killbill::PaypalExpress::PaypalExpressPaymentMethod.find_by_id(params[:id].to_i)
37
+ pm.to_json
38
+ else
39
+ status 404
40
+ end
41
+ end
42
+
43
+ # curl -v http://127.0.0.1:9292/plugins/killbill-paypal-express/1.0/transactions/1
44
+ get '/plugins/killbill-paypal-express/1.0/transactions/:id', :provides => 'json' do
45
+ if transaction = Killbill::PaypalExpress::PaypalExpressTransaction.find_by_id(params[:id].to_i)
46
+ transaction.to_json
47
+ else
48
+ status 404
49
+ end
50
+ end
@@ -0,0 +1,35 @@
1
+ require 'logger'
2
+
3
+ module Killbill::PaypalExpress
4
+ mattr_reader :logger
5
+ mattr_reader :config
6
+ mattr_reader :gateway
7
+ mattr_reader :paypal_sandbox_url
8
+ mattr_reader :paypal_production_url
9
+ mattr_reader :initialized
10
+ mattr_reader :test
11
+
12
+ def self.initialize!(config_file='paypal_express.yml', logger=Logger.new(STDOUT))
13
+ @@logger = logger
14
+
15
+ @@config = Properties.new(config_file)
16
+ @@config.parse!
17
+
18
+ @@paypal_sandbox_url = @@config[:paypal][:sandbox_url] || 'https://www.sandbox.paypal.com/cgi-bin/webscr'
19
+ @@paypal_production_url = @@config[:paypal][:production_url] || 'https://www.paypal.com/cgi-bin/webscr'
20
+ @@test = @@config[:paypal][:test]
21
+
22
+ @@gateway = Killbill::PaypalExpress::Gateway.instance
23
+ @@gateway.configure(@@config[:paypal])
24
+
25
+ if defined?(JRUBY_VERSION)
26
+ # See https://github.com/jruby/activerecord-jdbc-adapter/issues/302
27
+ require 'jdbc/mysql'
28
+ Jdbc::MySQL.load_driver(:require) if Jdbc::MySQL.respond_to?(:load_driver)
29
+ end
30
+
31
+ ActiveRecord::Base.establish_connection(@@config[:database])
32
+
33
+ @@initialized = true
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ module Killbill::PaypalExpress
2
+ class Properties
3
+ def initialize(file = 'paypal_express.yml')
4
+ @config_file = Pathname.new(file).expand_path
5
+ end
6
+
7
+ def parse!
8
+ raise "#{@config_file} is not a valid file" unless @config_file.file?
9
+ @config = YAML.load_file(@config_file.to_s)
10
+ validate!
11
+ end
12
+
13
+ def [](key)
14
+ @config[key]
15
+ end
16
+
17
+ private
18
+
19
+ def validate!
20
+ raise "Bad configuration for PaypalExpress plugin. Config is #{@config.inspect}" if @config.blank? ||
21
+ @config[:paypal].blank? ||
22
+ @config[:paypal][:signature].blank? ||
23
+ @config[:paypal][:login].blank? ||
24
+ @config[:paypal][:password].blank?
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ module Killbill::PaypalExpress
2
+ class PaypalExpressPaymentMethod < ActiveRecord::Base
3
+ attr_accessible :kb_account_id,
4
+ :kb_payment_method_id,
5
+ :paypal_express_payer_id,
6
+ :paypal_express_baid,
7
+ :paypal_express_token
8
+
9
+ def self.from_kb_account_id(kb_account_id)
10
+ find_all_by_kb_account_id_and_is_deleted(kb_account_id, false)
11
+ end
12
+
13
+ def self.from_kb_payment_method_id(kb_payment_method_id)
14
+ payment_methods = find_all_by_kb_payment_method_id_and_is_deleted(kb_payment_method_id, false)
15
+ raise "No payment method found for payment method #{kb_payment_method_id}" if payment_methods.empty?
16
+ raise "Killbill payment method mapping to multiple active PaypalExpress tokens for payment method #{kb_payment_method_id}" if payment_methods.size > 1
17
+ payment_methods[0]
18
+ end
19
+
20
+ # Used to complete the checkout process
21
+ def self.from_kb_account_id_and_token(kb_account_id, token)
22
+ payment_methods = find_all_by_kb_account_id_and_paypal_express_token_and_is_deleted(kb_account_id, token, false)
23
+ raise "No payment method found for account #{kb_account_id}" if payment_methods.empty?
24
+ raise "Paypal token mapping to multiple active PaypalExpress payment methods #{kb_account_id}" if payment_methods.size > 1
25
+ payment_methods[0]
26
+ end
27
+
28
+ def self.mark_as_deleted!(kb_payment_method_id)
29
+ payment_method = from_kb_payment_method_id(kb_payment_method_id)
30
+ payment_method.is_deleted = true
31
+ payment_method.save!
32
+ end
33
+
34
+ def to_payment_method_response
35
+ external_payment_method_id = paypal_express_baid
36
+ # No concept of default payment method in Paypal Express
37
+ is_default = false
38
+ # We don't store extra information in Paypal Express
39
+ properties = []
40
+
41
+ Killbill::Plugin::PaymentMethodResponse.new external_payment_method_id, is_default, properties
42
+ end
43
+ end
44
+ end