killbill-paypal-express 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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