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 +36 -0
- data/.travis.yml +22 -0
- data/Gemfile +3 -0
- data/Jarfile +3 -0
- data/README.md +7 -0
- data/Rakefile +30 -0
- data/VERSION +1 -0
- data/config.ru +4 -0
- data/db/schema.rb +81 -0
- data/killbill-paypal-express.gemspec +41 -0
- data/killbill.properties +3 -0
- data/lib/paypal_express/api.rb +124 -0
- data/lib/paypal_express/config/application.rb +50 -0
- data/lib/paypal_express/config/configuration.rb +35 -0
- data/lib/paypal_express/config/properties.rb +27 -0
- data/lib/paypal_express/models/paypal_express_payment_method.rb +44 -0
- data/lib/paypal_express/models/paypal_express_response.rb +159 -0
- data/lib/paypal_express/models/paypal_express_transaction.rb +16 -0
- data/lib/paypal_express/paypal/gateway.rb +26 -0
- data/lib/paypal_express/paypal_express_utils.rb +17 -0
- data/lib/paypal_express/private_api.rb +53 -0
- data/lib/paypal_express.rb +129 -0
- data/pom.xml +35 -0
- data/release.sh +11 -0
- data/spec/paypal_express/base_plugin_spec.rb +31 -0
- data/spec/paypal_express/remote/integration_spec.rb +114 -0
- data/spec/spec_helper.rb +33 -0
- metadata +225 -0
@@ -0,0 +1,159 @@
|
|
1
|
+
module Killbill::PaypalExpress
|
2
|
+
class PaypalExpressResponse < ActiveRecord::Base
|
3
|
+
has_one :paypal_express_transaction
|
4
|
+
attr_accessible :api_call,
|
5
|
+
:kb_payment_id,
|
6
|
+
:message,
|
7
|
+
# transaction_id, authorization_id (reauthorization) or refund_transaction_id
|
8
|
+
:authorization,
|
9
|
+
:fraud_review,
|
10
|
+
:test,
|
11
|
+
:token,
|
12
|
+
:payer_id,
|
13
|
+
:billing_agreement_id,
|
14
|
+
:payer_name,
|
15
|
+
:payer_email,
|
16
|
+
:payer_country,
|
17
|
+
:contact_phone,
|
18
|
+
:ship_to_address_name,
|
19
|
+
:ship_to_address_company,
|
20
|
+
:ship_to_address_address1,
|
21
|
+
:ship_to_address_address2,
|
22
|
+
:ship_to_address_city,
|
23
|
+
:ship_to_address_state,
|
24
|
+
:ship_to_address_country,
|
25
|
+
:ship_to_address_zip,
|
26
|
+
:ship_to_address_phone,
|
27
|
+
:receiver_info_business,
|
28
|
+
:receiver_info_receiver,
|
29
|
+
:receiver_info_receiverid,
|
30
|
+
:payment_info_transactionid,
|
31
|
+
:payment_info_parenttransactionid,
|
32
|
+
:payment_info_receiptid,
|
33
|
+
:payment_info_transactiontype,
|
34
|
+
:payment_info_paymenttype,
|
35
|
+
:payment_info_paymentdate,
|
36
|
+
:payment_info_grossamount,
|
37
|
+
:payment_info_feeamount,
|
38
|
+
:payment_info_taxamount,
|
39
|
+
:payment_info_exchangerate,
|
40
|
+
:payment_info_paymentstatus,
|
41
|
+
:payment_info_pendingreason,
|
42
|
+
:payment_info_reasoncode,
|
43
|
+
:payment_info_protectioneligibility,
|
44
|
+
:payment_info_protectioneligibilitytype,
|
45
|
+
:payment_info_shipamount,
|
46
|
+
:payment_info_shiphandleamount,
|
47
|
+
:payment_info_shipdiscount,
|
48
|
+
:payment_info_insuranceamount,
|
49
|
+
:payment_info_subject,
|
50
|
+
:avs_result_code,
|
51
|
+
:avs_result_message,
|
52
|
+
:avs_result_street_match,
|
53
|
+
:avs_result_postal_match,
|
54
|
+
:cvv_result_code,
|
55
|
+
:cvv_result_message,
|
56
|
+
:success
|
57
|
+
|
58
|
+
def self.from_response(api_call, kb_payment_id, response)
|
59
|
+
PaypalExpressResponse.new({
|
60
|
+
:api_call => api_call,
|
61
|
+
:kb_payment_id => kb_payment_id,
|
62
|
+
:message => response.message,
|
63
|
+
:authorization => response.authorization,
|
64
|
+
:fraud_review => response.fraud_review?,
|
65
|
+
:test => response.test?,
|
66
|
+
:token => response.token,
|
67
|
+
:payer_id => response.payer_id,
|
68
|
+
:billing_agreement_id => response.params['billing_agreement_id'],
|
69
|
+
:payer_name => response.name,
|
70
|
+
:payer_email => response.email,
|
71
|
+
:payer_country => response.payer_country,
|
72
|
+
:contact_phone => response.contact_phone,
|
73
|
+
:ship_to_address_name => response.address['name'],
|
74
|
+
:ship_to_address_company => response.address['company'],
|
75
|
+
:ship_to_address_address1 => response.address['address1'],
|
76
|
+
:ship_to_address_address2 => response.address['address2'],
|
77
|
+
:ship_to_address_city => response.address['city'],
|
78
|
+
:ship_to_address_state => response.address['state'],
|
79
|
+
:ship_to_address_country => response.address['country'],
|
80
|
+
:ship_to_address_zip => response.address['zip'],
|
81
|
+
:ship_to_address_phone => response.address['phone'],
|
82
|
+
:receiver_info_business => receiver_info(response)['Business'],
|
83
|
+
:receiver_info_receiver => receiver_info(response)['Receiver'],
|
84
|
+
:receiver_info_receiverid => receiver_info(response)['ReceiverID'],
|
85
|
+
:payment_info_transactionid => payment_info(response)['TransactionID'],
|
86
|
+
:payment_info_parenttransactionid => payment_info(response)['ParentTransactionID'],
|
87
|
+
:payment_info_receiptid => payment_info(response)['ReceiptID'],
|
88
|
+
:payment_info_transactiontype => payment_info(response)['TransactionType'],
|
89
|
+
:payment_info_paymenttype => payment_info(response)['PaymentType'],
|
90
|
+
:payment_info_paymentdate => payment_info(response)['PaymentDate'],
|
91
|
+
:payment_info_grossamount => payment_info(response)['GrossAmount'],
|
92
|
+
:payment_info_feeamount => payment_info(response)['FeeAmount'],
|
93
|
+
:payment_info_taxamount => payment_info(response)['TaxAmount'],
|
94
|
+
:payment_info_exchangerate => payment_info(response)['ExchangeRate'],
|
95
|
+
:payment_info_paymentstatus => payment_info(response)['PaymentStatus'],
|
96
|
+
:payment_info_pendingreason => payment_info(response)['PendingReason'],
|
97
|
+
:payment_info_reasoncode => payment_info(response)['ReasonCode'],
|
98
|
+
:payment_info_protectioneligibility => payment_info(response)['ProtectionEligibility'],
|
99
|
+
:payment_info_protectioneligibilitytype => payment_info(response)['ProtectionEligibilityType'],
|
100
|
+
:payment_info_shipamount => payment_info(response)['ShipAmount'],
|
101
|
+
:payment_info_shiphandleamount => payment_info(response)['ShipHandleAmount'],
|
102
|
+
:payment_info_shipdiscount => payment_info(response)['ShipDiscount'],
|
103
|
+
:payment_info_insuranceamount => payment_info(response)['InsuranceAmount'],
|
104
|
+
:payment_info_subject => payment_info(response)['Subject'],
|
105
|
+
:avs_result_code => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.code : response.avs_result['code'],
|
106
|
+
:avs_result_message => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.message : response.avs_result['message'],
|
107
|
+
:avs_result_street_match => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.street_match : response.avs_result['street_match'],
|
108
|
+
:avs_result_postal_match => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.postal_match : response.avs_result['postal_match'],
|
109
|
+
:cvv_result_code => response.cvv_result.kind_of?(ActiveMerchant::Billing::CVVResult) ? response.cvv_result.code : response.cvv_result['code'],
|
110
|
+
:cvv_result_message => response.cvv_result.kind_of?(ActiveMerchant::Billing::CVVResult) ? response.cvv_result.message : response.cvv_result['message'],
|
111
|
+
:success => response.success?
|
112
|
+
})
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_express_checkout_url
|
116
|
+
url = Killbill::PaypalExpress.test ? Killbill::PaypalExpress.paypal_sandbox_url : Killbill::PaypalExpress.paypal_production_url
|
117
|
+
"#{url}?cmd=_express-checkout&token=#{token}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_payment_response
|
121
|
+
to_killbill_response Killbill::Plugin::PaymentResponse
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_refund_response
|
125
|
+
to_killbill_response Killbill::Plugin::RefundResponse
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def to_killbill_response(klass)
|
131
|
+
if paypal_express_transaction.nil?
|
132
|
+
# payment_info_grossamount is e.g. "100.00" - we need to convert it in cents
|
133
|
+
amount_in_cents = payment_info_grossamount ? (payment_info_grossamount.to_f * 100).to_i : nil
|
134
|
+
created_date = created_at
|
135
|
+
else
|
136
|
+
amount_in_cents = paypal_express_transaction.amount_in_cents
|
137
|
+
created_date = paypal_express_transaction.created_at
|
138
|
+
end
|
139
|
+
|
140
|
+
effective_date = created_date
|
141
|
+
status = message
|
142
|
+
gateway_error = nil
|
143
|
+
gateway_error_code = nil
|
144
|
+
|
145
|
+
klass.new(amount_in_cents, created_date, effective_date, status, gateway_error, gateway_error_code)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Paypal has various response formats depending on the API call and the ActiveMerchant Paypal plugin doesn't try to
|
149
|
+
# unify them, hence the gymnastic here
|
150
|
+
|
151
|
+
def self.receiver_info(response)
|
152
|
+
response.params['ReceiverInfo'] || (response.params['PaymentTransactionDetails'] || {})['ReceiverInfo'] || {}
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.payment_info(response)
|
156
|
+
response.params['PaymentInfo'] || (response.params['PaymentTransactionDetails'] || {})['PaymentInfo'] || {}
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Killbill::PaypalExpress
|
2
|
+
class PaypalExpressTransaction < ActiveRecord::Base
|
3
|
+
belongs_to :paypal_express_response
|
4
|
+
attr_accessible :amount_in_cents,
|
5
|
+
:api_call,
|
6
|
+
:kb_payment_id,
|
7
|
+
:paypal_express_txn_id
|
8
|
+
|
9
|
+
def self.from_kb_payment_id(kb_payment_id)
|
10
|
+
paypal_express_transactions = find_all_by_api_call_and_kb_payment_id(:charge, kb_payment_id)
|
11
|
+
raise "Unable to find Paypal Express transaction id for payment #{kb_payment_id}" if paypal_express_transactions.empty?
|
12
|
+
raise "Killbill payment mapping to multiple Paypal Express transactions for payment #{kb_payment_id}" if paypal_express_transactions.size > 1
|
13
|
+
paypal_express_transactions[0]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Killbill::PaypalExpress
|
2
|
+
class Gateway
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
def configure(config)
|
6
|
+
if config[:test]
|
7
|
+
ActiveMerchant::Billing::Base.mode = :test
|
8
|
+
end
|
9
|
+
|
10
|
+
if config[:log_file]
|
11
|
+
ActiveMerchant::Billing::PaypalExpressGateway.wiredump_device = File.open(config[:log_file], 'w')
|
12
|
+
ActiveMerchant::Billing::PaypalExpressGateway.wiredump_device.sync = true
|
13
|
+
end
|
14
|
+
|
15
|
+
@gateway = ActiveMerchant::Billing::PaypalExpressGateway.new({
|
16
|
+
:signature => config[:signature],
|
17
|
+
:login => config[:login],
|
18
|
+
:password => config[:password]
|
19
|
+
})
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(m, *args, &block)
|
23
|
+
@gateway.send(m, *args, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Killbill::PaypalExpress
|
4
|
+
class Utils
|
5
|
+
def self.ip
|
6
|
+
first_public_ipv4 ? first_public_ipv4.ip_address : first_private_ipv4.ip_address
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.first_private_ipv4
|
10
|
+
@@first_private_ipv4 ||= Socket.ip_address_list.detect{ |intf| intf.ipv4_private? }
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.first_public_ipv4
|
14
|
+
@@first_public_ipv4 ||= Socket.ip_address_list.detect{ |intf| intf.ipv4? and !intf.ipv4_loopback? and !intf.ipv4_multicast? and !intf.ipv4_private? }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Killbill::PaypalExpress
|
2
|
+
class PrivatePaymentPlugin
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
# See https://cms.paypal.com/uk/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_ECReferenceTxns
|
6
|
+
def initiate_express_checkout(kb_account_id, amount_in_cents=0, currency='USD', options = {})
|
7
|
+
options[:currency] ||= currency
|
8
|
+
|
9
|
+
# Required arguments
|
10
|
+
options[:return_url] ||= 'http://www.example.com/success'
|
11
|
+
options[:cancel_return_url] ||= 'http://www.example.com/sad_panda'
|
12
|
+
|
13
|
+
options[:billing_agreement] ||= {}
|
14
|
+
options[:billing_agreement][:type] ||= "MerchantInitiatedBilling"
|
15
|
+
options[:billing_agreement][:description] ||= "Kill Bill billing agreement"
|
16
|
+
|
17
|
+
# Go to Paypal (SetExpressCheckout call)
|
18
|
+
paypal_express_response = gateway.setup_authorization amount_in_cents, options
|
19
|
+
response = save_response paypal_express_response, :initiate_express_checkout
|
20
|
+
|
21
|
+
if response.success?
|
22
|
+
# Create the payment method (not associated to a Killbill payment method yet)
|
23
|
+
Killbill::PaypalExpress::PaypalExpressPaymentMethod.create :kb_account_id => kb_account_id,
|
24
|
+
:kb_payment_method_id => nil,
|
25
|
+
:paypal_express_payer_id => nil,
|
26
|
+
:paypal_express_token => response.token
|
27
|
+
end
|
28
|
+
|
29
|
+
response
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def save_response(paypal_express_response, api_call)
|
35
|
+
logger.warn "Unsuccessful #{api_call}: #{paypal_express_response.message}" unless paypal_express_response.success?
|
36
|
+
|
37
|
+
# Save the response to our logs
|
38
|
+
response = PaypalExpressResponse.from_response(api_call, nil, paypal_express_response)
|
39
|
+
response.save!
|
40
|
+
response
|
41
|
+
end
|
42
|
+
|
43
|
+
def gateway
|
44
|
+
# The gateway should have been configured when the plugin started
|
45
|
+
Killbill::PaypalExpress::Gateway.instance
|
46
|
+
end
|
47
|
+
|
48
|
+
def logger
|
49
|
+
# The logger should have been configured when the plugin started
|
50
|
+
Killbill::PaypalExpress.logger
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'activemerchant'
|
3
|
+
require 'pathname'
|
4
|
+
require 'sinatra'
|
5
|
+
require 'singleton'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
require 'killbill'
|
9
|
+
require 'killbill/response/payment_method_response'
|
10
|
+
require 'killbill/response/payment_response'
|
11
|
+
require 'killbill/response/refund_response'
|
12
|
+
|
13
|
+
require 'paypal_express/config/configuration'
|
14
|
+
require 'paypal_express/config/properties'
|
15
|
+
|
16
|
+
require 'paypal_express/paypal/gateway'
|
17
|
+
|
18
|
+
require 'paypal_express/models/paypal_express_payment_method'
|
19
|
+
require 'paypal_express/models/paypal_express_response'
|
20
|
+
require 'paypal_express/models/paypal_express_transaction'
|
21
|
+
|
22
|
+
require 'paypal_express/paypal_express_utils'
|
23
|
+
|
24
|
+
require 'paypal_express/api'
|
25
|
+
require 'paypal_express/private_api'
|
26
|
+
|
27
|
+
|
28
|
+
# Thank you Rails for the following!
|
29
|
+
|
30
|
+
class Object
|
31
|
+
def blank?
|
32
|
+
respond_to?(:empty?) ? empty? : !self
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Hash
|
37
|
+
# By default, only instances of Hash itself are extractable.
|
38
|
+
# Subclasses of Hash may implement this method and return
|
39
|
+
# true to declare themselves as extractable. If a Hash
|
40
|
+
# is extractable, Array#extract_options! pops it from
|
41
|
+
# the Array when it is the last element of the Array.
|
42
|
+
def extractable_options?
|
43
|
+
instance_of?(Hash)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Array
|
48
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
49
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
50
|
+
#
|
51
|
+
# def options(*args)
|
52
|
+
# args.extract_options!
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# options(1, 2) # => {}
|
56
|
+
# options(1, 2, a: :b) # => {:a=>:b}
|
57
|
+
def extract_options!
|
58
|
+
if last.is_a?(Hash) && last.extractable_options?
|
59
|
+
pop
|
60
|
+
else
|
61
|
+
{}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Module
|
67
|
+
def mattr_reader(*syms)
|
68
|
+
options = syms.extract_options!
|
69
|
+
syms.each do |sym|
|
70
|
+
raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
|
71
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
72
|
+
@@#{sym} = nil unless defined? @@#{sym}
|
73
|
+
|
74
|
+
def self.#{sym}
|
75
|
+
@@#{sym}
|
76
|
+
end
|
77
|
+
EOS
|
78
|
+
|
79
|
+
unless options[:instance_reader] == false || options[:instance_accessor] == false
|
80
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
81
|
+
def #{sym}
|
82
|
+
@@#{sym}
|
83
|
+
end
|
84
|
+
EOS
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def mattr_writer(*syms)
|
90
|
+
options = syms.extract_options!
|
91
|
+
syms.each do |sym|
|
92
|
+
raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
|
93
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
94
|
+
def self.#{sym}=(obj)
|
95
|
+
@@#{sym} = obj
|
96
|
+
end
|
97
|
+
EOS
|
98
|
+
|
99
|
+
unless options[:instance_writer] == false || options[:instance_accessor] == false
|
100
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
101
|
+
def #{sym}=(obj)
|
102
|
+
@@#{sym} = obj
|
103
|
+
end
|
104
|
+
EOS
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Extends the module object with module and instance accessors for class attributes,
|
110
|
+
# just like the native attr* accessors for instance attributes.
|
111
|
+
#
|
112
|
+
# module AppConfiguration
|
113
|
+
# mattr_accessor :google_api_key
|
114
|
+
#
|
115
|
+
# self.google_api_key = "123456789"
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# AppConfiguration.google_api_key # => "123456789"
|
119
|
+
# AppConfiguration.google_api_key = "overriding the api key!"
|
120
|
+
# AppConfiguration.google_api_key # => "overriding the api key!"
|
121
|
+
#
|
122
|
+
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
|
123
|
+
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
|
124
|
+
# To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
|
125
|
+
def mattr_accessor(*syms)
|
126
|
+
mattr_reader(*syms)
|
127
|
+
mattr_writer(*syms)
|
128
|
+
end
|
129
|
+
end
|
data/pom.xml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!--
|
3
|
+
~ Copyright 2010-2013 Ning, Inc.
|
4
|
+
~
|
5
|
+
~ Ning licenses this file to you under the Apache License, version 2.0
|
6
|
+
~ (the "License"); you may not use this file except in compliance with the
|
7
|
+
~ License. You may obtain a copy of the License at:
|
8
|
+
~
|
9
|
+
~ http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
~
|
11
|
+
~ Unless required by applicable law or agreed to in writing, software
|
12
|
+
~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
13
|
+
~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
14
|
+
~ License for the specific language governing permissions and limitations
|
15
|
+
~ under the License.
|
16
|
+
-->
|
17
|
+
|
18
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
19
|
+
<parent>
|
20
|
+
<groupId>org.sonatype.oss</groupId>
|
21
|
+
<artifactId>oss-parent</artifactId>
|
22
|
+
<version>5</version>
|
23
|
+
</parent>
|
24
|
+
<modelVersion>4.0.0</modelVersion>
|
25
|
+
<groupId>com.ning.killbill.ruby</groupId>
|
26
|
+
<artifactId>paypal-express-plugin</artifactId>
|
27
|
+
<packaging>pom</packaging>
|
28
|
+
<version>1.0.0</version>
|
29
|
+
<name>paypal-express-plugin</name>
|
30
|
+
<scm>
|
31
|
+
<connection>scm:git:git://github.com/killbill/killbill-paypal-express-plugin.git</connection>
|
32
|
+
<url>https://github.com/killbill/killbill-paypal-express-plugin/</url>
|
33
|
+
<developerConnection>scm:git:git@github.com:killbill/killbill-paypal-express-plugin.git</developerConnection>
|
34
|
+
</scm>
|
35
|
+
</project>
|
data/release.sh
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
VERSION=`grep -E '<version>([0-9]+\.[0-9]+\.[0-9]+)</version>' pom.xml | sed 's/[\t \n]*<version>\(.*\)<\/version>[\t \n]*/\1/'`
|
2
|
+
ARTIFACT="$PWD/pkg/killbill-paypal-express-$VERSION.tar.gz"
|
3
|
+
echo "Pushing $ARTIFACT to Maven Central"
|
4
|
+
mvn gpg:sign-and-deploy-file \
|
5
|
+
-DgroupId=com.ning.killbill.ruby \
|
6
|
+
-DartifactId=paypal-express-plugin \
|
7
|
+
-Dversion=$VERSION \
|
8
|
+
-Dpackaging=tar.gz \
|
9
|
+
-DrepositoryId=ossrh-releases \
|
10
|
+
-Durl=https://oss.sonatype.org/service/local/staging/deploy/maven2/ \
|
11
|
+
-Dfile=$ARTIFACT
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
describe Killbill::PaypalExpress::PaymentPlugin do
|
5
|
+
before(:each) do
|
6
|
+
Dir.mktmpdir do |dir|
|
7
|
+
file = File.new(File.join(dir, 'paypal_express.yml'), "w+")
|
8
|
+
file.write(<<-eos)
|
9
|
+
:paypal:
|
10
|
+
:signature: 'signature'
|
11
|
+
:login: 'login'
|
12
|
+
:password: 'password'
|
13
|
+
:database:
|
14
|
+
:adapter: 'sqlite3'
|
15
|
+
:database: 'shouldntmatter.db'
|
16
|
+
eos
|
17
|
+
file.close
|
18
|
+
|
19
|
+
@plugin = Killbill::PaypalExpress::PaymentPlugin.new
|
20
|
+
@plugin.root = File.dirname(file)
|
21
|
+
@plugin.logger = Logger.new(STDOUT)
|
22
|
+
|
23
|
+
# Start the plugin here - since the config file will be deleted
|
24
|
+
@plugin.start_plugin
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should start and stop correctly" do
|
29
|
+
@plugin.stop_plugin
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveMerchant::Billing::Base.mode = :test
|
5
|
+
|
6
|
+
describe Killbill::PaypalExpress::PaymentPlugin do
|
7
|
+
before(:each) do
|
8
|
+
@plugin = Killbill::PaypalExpress::PaymentPlugin.new
|
9
|
+
@plugin.root = File.expand_path(File.dirname(__FILE__) + '../../../../')
|
10
|
+
|
11
|
+
logger = Logger.new(STDOUT)
|
12
|
+
logger.level = Logger::DEBUG
|
13
|
+
@plugin.logger = logger
|
14
|
+
|
15
|
+
@plugin.start_plugin
|
16
|
+
|
17
|
+
@pm = create_payment_method
|
18
|
+
end
|
19
|
+
|
20
|
+
after(:each) do
|
21
|
+
@plugin.stop_plugin
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should be able to charge and refund" do
|
25
|
+
amount_in_cents = 10000
|
26
|
+
currency = 'USD'
|
27
|
+
kb_payment_id = SecureRandom.uuid
|
28
|
+
|
29
|
+
payment_response = @plugin.process_payment @pm.kb_account_id, kb_payment_id, @pm.kb_payment_method_id, amount_in_cents, currency
|
30
|
+
payment_response.amount_in_cents.should == amount_in_cents
|
31
|
+
payment_response.status.should == "Success"
|
32
|
+
|
33
|
+
# Verify our table directly
|
34
|
+
response = Killbill::PaypalExpress::PaypalExpressResponse.find_by_api_call_and_kb_payment_id :charge, kb_payment_id
|
35
|
+
response.test.should be_true
|
36
|
+
response.success.should be_true
|
37
|
+
response.message.should == "Success"
|
38
|
+
|
39
|
+
# Check we can retrieve the payment
|
40
|
+
payment_response = @plugin.get_payment_info @pm.kb_account_id, kb_payment_id
|
41
|
+
payment_response.amount_in_cents.should == amount_in_cents
|
42
|
+
payment_response.status.should == "Success"
|
43
|
+
|
44
|
+
# Check we cannot refund an amount greater than the original charge
|
45
|
+
lambda { @plugin.process_refund @pm.kb_account_id, kb_payment_id, amount_in_cents + 1, currency }.should raise_error RuntimeError
|
46
|
+
|
47
|
+
refund_response = @plugin.process_refund @pm.kb_account_id, kb_payment_id, amount_in_cents, currency
|
48
|
+
refund_response.amount_in_cents.should == amount_in_cents
|
49
|
+
refund_response.status.should == "Success"
|
50
|
+
|
51
|
+
# Verify our table directly
|
52
|
+
response = Killbill::PaypalExpress::PaypalExpressResponse.find_by_api_call_and_kb_payment_id :refund, kb_payment_id
|
53
|
+
response.test.should be_true
|
54
|
+
response.success.should be_true
|
55
|
+
|
56
|
+
# Try another payment to verify the BAID
|
57
|
+
second_amount_in_cents = 9423
|
58
|
+
second_kb_payment_id = SecureRandom.uuid
|
59
|
+
payment_response = @plugin.process_payment @pm.kb_account_id, second_kb_payment_id, @pm.kb_payment_method_id, second_amount_in_cents, currency
|
60
|
+
payment_response.amount_in_cents.should == second_amount_in_cents
|
61
|
+
payment_response.status.should == "Success"
|
62
|
+
|
63
|
+
# Check we can refund it as well
|
64
|
+
refund_response = @plugin.process_refund @pm.kb_account_id, second_kb_payment_id, second_amount_in_cents, currency
|
65
|
+
refund_response.amount_in_cents.should == second_amount_in_cents
|
66
|
+
refund_response.status.should == "Success"
|
67
|
+
|
68
|
+
# it "should be able to create and retrieve payment methods"
|
69
|
+
# This should be in a separate scenario but since it's so hard to create a payment method (need manual intervention),
|
70
|
+
# we can't easily delete it
|
71
|
+
pms = @plugin.get_payment_methods @pm.kb_account_id
|
72
|
+
pms.size.should == 1
|
73
|
+
pms[0].external_payment_method_id.should == @pm.paypal_express_baid
|
74
|
+
|
75
|
+
pm_details = @plugin.get_payment_method_detail(@pm.kb_account_id, @pm.kb_payment_method_id)
|
76
|
+
pm_details.external_payment_method_id.should == @pm.paypal_express_baid
|
77
|
+
|
78
|
+
@plugin.delete_payment_method @pm.kb_account_id, @pm.kb_payment_method_id
|
79
|
+
|
80
|
+
@plugin.get_payment_methods(@pm.kb_account_id).size.should == 0
|
81
|
+
lambda { @plugin.get_payment_method_detail(@pm.kb_account_id, @pm.kb_payment_method_id) }.should raise_error RuntimeError
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def create_payment_method
|
87
|
+
kb_account_id = SecureRandom.uuid
|
88
|
+
private_plugin = Killbill::PaypalExpress::PrivatePaymentPlugin.instance
|
89
|
+
|
90
|
+
# Initiate the setup process
|
91
|
+
response = private_plugin.initiate_express_checkout kb_account_id
|
92
|
+
response.success.should be_true
|
93
|
+
token = response.token
|
94
|
+
|
95
|
+
print "\nPlease go to #{response.to_express_checkout_url} to proceed and press any key to continue...
|
96
|
+
Note: you need to log-in with a paypal sandbox account (create one here: https://developer.paypal.com/webapps/developer/applications/accounts)\n"
|
97
|
+
$stdin.gets
|
98
|
+
|
99
|
+
# Complete the setup process
|
100
|
+
kb_payment_method_id = SecureRandom.uuid
|
101
|
+
info = Killbill::Plugin::PaymentMethodResponse.new nil, nil, [Killbill::Plugin::PaymentMethodProperty.new("token", token, false)]
|
102
|
+
response = @plugin.add_payment_method kb_account_id, kb_payment_method_id, info
|
103
|
+
response.should be_true
|
104
|
+
|
105
|
+
# Verify our table directly
|
106
|
+
payment_method = Killbill::PaypalExpress::PaypalExpressPaymentMethod.from_kb_account_id_and_token(kb_account_id, token)
|
107
|
+
payment_method.should_not be_nil
|
108
|
+
payment_method.paypal_express_payer_id.should_not be_nil
|
109
|
+
payment_method.paypal_express_baid.should_not be_nil
|
110
|
+
payment_method.kb_payment_method_id.should == kb_payment_method_id
|
111
|
+
|
112
|
+
payment_method
|
113
|
+
end
|
114
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'paypal_express'
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.color_enabled = true
|
8
|
+
config.tty = true
|
9
|
+
config.formatter = 'documentation'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'active_record'
|
13
|
+
ActiveRecord::Base.establish_connection(
|
14
|
+
:adapter => 'sqlite3',
|
15
|
+
:database => 'test.db'
|
16
|
+
)
|
17
|
+
# Create the schema
|
18
|
+
require File.expand_path(File.dirname(__FILE__) + '../../db/schema.rb')
|
19
|
+
|
20
|
+
begin
|
21
|
+
require 'securerandom'
|
22
|
+
SecureRandom.uuid
|
23
|
+
rescue LoadError, NoMethodError
|
24
|
+
# See http://jira.codehaus.org/browse/JRUBY-6176
|
25
|
+
module SecureRandom
|
26
|
+
def self.uuid
|
27
|
+
ary = self.random_bytes(16).unpack("NnnnnN")
|
28
|
+
ary[2] = (ary[2] & 0x0fff) | 0x4000
|
29
|
+
ary[3] = (ary[3] & 0x3fff) | 0x8000
|
30
|
+
"%08x-%04x-%04x-%04x-%04x%08x" % ary
|
31
|
+
end unless respond_to?(:uuid)
|
32
|
+
end
|
33
|
+
end
|