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