killbill-paypal-express 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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