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.
@@ -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