killbill-paypal-express 4.0.0 → 4.1.0

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.
@@ -16,6 +16,7 @@ module Killbill #:nodoc:
16
16
  ::Killbill::PaypalExpress::PaypalExpressResponse)
17
17
 
18
18
  @ip = ::Killbill::Plugin::ActiveMerchant::Utils.ip
19
+ @private_api = ::Killbill::PaypalExpress::PrivatePaymentPlugin.new
19
20
  end
20
21
 
21
22
  def on_event(event)
@@ -56,20 +57,73 @@ module Killbill #:nodoc:
56
57
  end
57
58
 
58
59
  def purchase_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
59
- # Pass extra parameters for the gateway here
60
- options = {}
61
-
62
- add_required_options(kb_payment_transaction_id, kb_payment_method_id, context, options)
63
-
64
- properties = merge_properties(properties, options)
65
-
66
- # Can't use default implementation: the purchase signature is for one-off payments only
67
- #super(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
68
- gateway_call_proc = Proc.new do |gateway, linked_transaction, payment_source, amount_in_cents, options|
69
- gateway.reference_transaction(amount_in_cents, options)
60
+ payment_processor_account_id = find_value_from_properties(properties, 'payment_processor_account_id')
61
+
62
+ # Callback from the plugin itself (HPP flow)
63
+ if find_value_from_properties(properties, 'from_hpp') == 'true'
64
+ token = find_value_from_properties(properties, 'token')
65
+
66
+ response = @response_model.create(:api_call => :build_form_descriptor,
67
+ :kb_account_id => kb_account_id,
68
+ :kb_payment_id => kb_payment_id,
69
+ :kb_payment_transaction_id => kb_payment_transaction_id,
70
+ :transaction_type => :PURCHASE,
71
+ :authorization => token,
72
+ :payment_processor_account_id => payment_processor_account_id,
73
+ :kb_tenant_id => context.tenant_id,
74
+ :success => true,
75
+ :created_at => Time.now.utc,
76
+ :updated_at => Time.now.utc,
77
+ :message => { :payment_plugin_status => :PENDING }.to_json)
78
+ transaction = response.to_transaction_info_plugin(nil)
79
+ transaction.amount = amount
80
+ transaction.currency = currency
81
+ transaction
82
+ else
83
+ options = {}
84
+ add_required_options(kb_payment_transaction_id, kb_payment_method_id, context, options)
85
+
86
+ # We have a baid on file
87
+ if options[:token]
88
+ gateway_call_proc = Proc.new do |gateway, linked_transaction, payment_source, amount_in_cents, options|
89
+ # Can't use default implementation: the purchase signature is for one-off payments only
90
+ gateway.reference_transaction(amount_in_cents, options)
91
+ end
92
+ else
93
+ # One-off payment
94
+ options[:token] = find_value_from_properties(properties, 'token') || find_last_token(kb_account_id, context.tenant_id)
95
+ gateway_call_proc = Proc.new do |gateway, linked_transaction, payment_source, amount_in_cents, options|
96
+ gateway.purchase(amount_in_cents, options)
97
+ end
98
+ end
99
+
100
+ # Populate the Payer id if missing
101
+ options[:payer_id] = find_value_from_properties(properties, 'payer_id')
102
+ begin
103
+ options[:payer_id] ||= find_payer_id(options[:token],
104
+ kb_account_id,
105
+ context.tenant_id,
106
+ properties_to_hash(properties))
107
+ rescue => e
108
+ # Maybe invalid token?
109
+ response = @response_model.create(:api_call => :purchase,
110
+ :kb_account_id => kb_account_id,
111
+ :kb_payment_id => kb_payment_id,
112
+ :kb_payment_transaction_id => kb_payment_transaction_id,
113
+ :transaction_type => :PURCHASE,
114
+ :authorization => nil,
115
+ :payment_processor_account_id => payment_processor_account_id,
116
+ :kb_tenant_id => context.tenant_id,
117
+ :success => false,
118
+ :created_at => Time.now.utc,
119
+ :updated_at => Time.now.utc,
120
+ :message => { :payment_plugin_status => :CANCELED, :exception_class => e.class.to_s, :exception_message => e.message }.to_json)
121
+ return response.to_transaction_info_plugin(nil)
122
+ end
123
+
124
+ properties = merge_properties(properties, options)
125
+ dispatch_to_gateways(:purchase, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context, gateway_call_proc)
70
126
  end
71
-
72
- dispatch_to_gateways(:purchase, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context, gateway_call_proc)
73
127
  end
74
128
 
75
129
  def void_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, properties, context)
@@ -105,11 +159,14 @@ module Killbill #:nodoc:
105
159
  end
106
160
 
107
161
  def get_payment_info(kb_account_id, kb_payment_id, properties, context)
108
- # Pass extra parameters for the gateway here
109
- options = {}
162
+ t_info_plugins = super(kb_account_id, kb_payment_id, properties, context)
110
163
 
111
- properties = merge_properties(properties, options)
112
- super(kb_account_id, kb_payment_id, properties, context)
164
+ # Completed purchases will have two rows in the responses table (one for api_call 'build_form_descriptor', one for api_call 'purchase')
165
+ # Other transaction types don't support the :PENDING state
166
+ is_purchase_pending = t_info_plugins.find { |t_info_plugin| t_info_plugin.transaction_type == :PURCHASE && t_info_plugin.status != :PENDING }.nil?
167
+ t_info_plugins_without_purchase_pending = t_info_plugins.reject { |t_info_plugin| t_info_plugin.transaction_type == :PURCHASE && t_info_plugin.status == :PENDING }
168
+
169
+ is_purchase_pending ? t_info_plugins: t_info_plugins_without_purchase_pending
113
170
  end
114
171
 
115
172
  def search_payments(search_key, offset, limit, properties, context)
@@ -121,25 +178,22 @@ module Killbill #:nodoc:
121
178
  end
122
179
 
123
180
  def add_payment_method(kb_account_id, kb_payment_method_id, payment_method_props, set_default, properties, context)
124
- # token is passed via properties
125
- token = find_value_from_properties(properties, 'token')
126
- # token is passed from the json body
127
- token = find_value_from_properties(payment_method_props.properties, 'token') if token.nil?
128
- raise 'No token specified!' if token.nil?
129
-
130
- # Go to Paypal to get the Payer id (GetExpressCheckoutDetails call)
131
- options = properties_to_hash(properties)
132
- payment_processor_account_id = options[:payment_processor_account_id] || :default
133
- gateway = lookup_gateway(payment_processor_account_id, context.tenant_id)
134
- gw_response = gateway.details_for(token)
135
- response, transaction = save_response_and_transaction(gw_response, :details_for, kb_account_id, context.tenant_id, payment_processor_account_id)
136
- raise response.message unless response.success? and !response.payer_id.blank?
137
-
138
- # Pass extra parameters for the gateway here
139
- options = {
140
- :paypal_express_token => token,
141
- :paypal_express_payer_id => response.payer_id
142
- }
181
+ all_properties = (payment_method_props.nil? || payment_method_props.properties.nil? ? [] : payment_method_props.properties) + properties
182
+ token = find_value_from_properties(all_properties, 'token')
183
+
184
+ if token.nil?
185
+ # HPP flow
186
+ options = {
187
+ :skip_gw => true
188
+ }
189
+ else
190
+ # Go to Paypal to get the Payer id (GetExpressCheckoutDetails call)
191
+ payer_id = find_payer_id(token, kb_account_id, context.tenant_id, properties_to_hash(properties))
192
+ options = {
193
+ :paypal_express_token => token,
194
+ :paypal_express_payer_id => payer_id
195
+ }
196
+ end
143
197
 
144
198
  properties = merge_properties(properties, options)
145
199
  super(kb_account_id, kb_payment_method_id, payment_method_props, set_default, properties, context)
@@ -186,17 +240,49 @@ module Killbill #:nodoc:
186
240
  end
187
241
 
188
242
  def build_form_descriptor(kb_account_id, descriptor_fields, properties, context)
189
- # Pass extra parameters for the gateway here
190
- options = {}
191
- properties = merge_properties(properties, options)
192
-
193
- # Add your custom static hidden tags here
194
- options = {
195
- #:token => config[:paypal-express][:token]
196
- }
197
- descriptor_fields = merge_properties(descriptor_fields, options)
243
+ jcontext = @kb_apis.create_context(context.tenant_id)
244
+
245
+ all_properties = descriptor_fields + properties
246
+ options = properties_to_hash(all_properties)
247
+
248
+ kb_account = ::Killbill::Plugin::ActiveMerchant::Utils::LazyEvaluator.new { @kb_apis.account_user_api.get_account_by_id(kb_account_id, jcontext) }
249
+ amount = (options[:amount] || '0').to_f
250
+ currency = options[:currency] || kb_account.currency
251
+
252
+ response = initiate_express_checkout(kb_account_id, amount, currency, all_properties, context)
253
+
254
+ descriptor = super(kb_account_id, descriptor_fields, properties, context)
255
+ descriptor.form_url = @private_api.to_express_checkout_url(response, context.tenant_id, options)
256
+ descriptor.form_method = 'GET'
257
+ descriptor.properties << build_property('token', response.token)
258
+
259
+ # By default, pending payments are not created for HPP
260
+ create_pending_payment = ::Killbill::Plugin::ActiveMerchant::Utils.normalized(options, :create_pending_payment)
261
+ if create_pending_payment
262
+ custom_props = hash_to_properties(:from_hpp => true,
263
+ :token => response.token)
264
+ payment_external_key = ::Killbill::Plugin::ActiveMerchant::Utils.normalized(options, :payment_external_key)
265
+ transaction_external_key = ::Killbill::Plugin::ActiveMerchant::Utils.normalized(options, :transaction_external_key)
266
+
267
+ kb_payment_method = (@kb_apis.payment_api.get_account_payment_methods(kb_account_id, false, [], jcontext).find { |pm| pm.plugin_name == 'killbill-paypal-express' })
268
+ payment = @kb_apis.payment_api
269
+ .create_purchase(kb_account.send(:__instance_object__),
270
+ kb_payment_method.id,
271
+ nil,
272
+ amount,
273
+ currency,
274
+ payment_external_key,
275
+ transaction_external_key,
276
+ custom_props,
277
+ jcontext)
278
+
279
+ descriptor.properties << build_property('kb_payment_id', payment.id)
280
+ descriptor.properties << build_property('kb_payment_external_key', payment.external_key)
281
+ descriptor.properties << build_property('kb_transaction_id', payment.transactions.first.id)
282
+ descriptor.properties << build_property('kb_transaction_external_key', payment.transactions.first.external_key)
283
+ end
198
284
 
199
- super(kb_account_id, descriptor_fields, properties, context)
285
+ descriptor
200
286
  end
201
287
 
202
288
  def process_notification(notification, properties, context)
@@ -227,17 +313,59 @@ module Killbill #:nodoc:
227
313
 
228
314
  private
229
315
 
316
+ def find_last_token(kb_account_id, kb_tenant_id)
317
+ @response_model.last_token(kb_account_id, kb_tenant_id)
318
+ end
319
+
320
+ def find_payer_id(token, kb_account_id, kb_tenant_id, options = {})
321
+ raise 'Could not find the payer_id: the token is missing' if token.blank?
322
+
323
+ # Go to Paypal to get the Payer id (GetExpressCheckoutDetails call)
324
+ payment_processor_account_id = options[:payment_processor_account_id] || :default
325
+ gateway = lookup_gateway(payment_processor_account_id, kb_tenant_id)
326
+ gw_response = gateway.details_for(token)
327
+ response, transaction = save_response_and_transaction(gw_response, :details_for, kb_account_id, kb_tenant_id, payment_processor_account_id)
328
+
329
+ raise response.message unless response.success?
330
+ raise "Could not find the payer_id for token #{token}" if response.payer_id.blank?
331
+
332
+ response.payer_id
333
+ end
334
+
230
335
  def add_required_options(kb_payment_transaction_id, kb_payment_method_id, context, options)
231
336
  payment_method = @payment_method_model.from_kb_payment_method_id(kb_payment_method_id, context.tenant_id)
232
337
 
233
- options[:payer_id] ||= payment_method.paypal_express_payer_id
234
- options[:token] ||= payment_method.paypal_express_token
235
- options[:reference_id] ||= payment_method.token # baid
338
+ options[:payer_id] ||= payment_method.paypal_express_payer_id.presence
339
+ options[:token] ||= payment_method.paypal_express_token.presence
340
+ options[:reference_id] ||= payment_method.token.presence # baid
236
341
 
237
342
  options[:payment_type] ||= 'Any'
238
343
  options[:invoice_id] ||= kb_payment_transaction_id
239
344
  options[:ip] ||= @ip
240
345
  end
346
+
347
+ def initiate_express_checkout(kb_account_id, amount, currency, properties, context)
348
+ properties_hash = properties_to_hash(properties)
349
+
350
+ with_baid = ::Killbill::Plugin::ActiveMerchant::Utils.normalized(properties_hash, :with_baid)
351
+
352
+ options = {}
353
+ options[:return_url] = ::Killbill::Plugin::ActiveMerchant::Utils.normalized(properties_hash, :return_url)
354
+ options[:cancel_return_url] = ::Killbill::Plugin::ActiveMerchant::Utils.normalized(properties_hash, :cancel_return_url)
355
+
356
+ amount_in_cents = amount.nil? ? nil : to_cents(amount, currency)
357
+ response = @private_api.initiate_express_checkout(kb_account_id,
358
+ context.tenant_id.to_s,
359
+ amount_in_cents,
360
+ currency,
361
+ with_baid,
362
+ options)
363
+ unless response.success?
364
+ raise "Unable to initiate paypal express checkout: #{response.message}"
365
+ end
366
+
367
+ response
368
+ end
241
369
  end
242
370
  end
243
371
  end
@@ -67,6 +67,7 @@ post '/plugins/killbill-paypal-express/1.0/setup-checkout', :provides => 'json'
67
67
  kb_tenant_id,
68
68
  data['amount_in_cents'] || 0,
69
69
  data['currency'] || 'USD',
70
+ true,
70
71
  options
71
72
  unless response.success?
72
73
  status 500
@@ -0,0 +1,18 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ module PaypalCommonAPI
4
+
5
+ DUPLICATE_REQUEST_CODE = '11607'
6
+
7
+ alias_method :original_successful?, :successful?
8
+
9
+ # Note: this may need more thoughts when/if we want to support MsgSubID
10
+ # See https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECRelatedAPIOps/#idempotency
11
+ # For now, we just want to correctly handle a subsequent payment using a one-time token
12
+ # (error "A successful transaction has already been completed for this token.")
13
+ def successful?(response)
14
+ response[:error_codes] == DUPLICATE_REQUEST_CODE ? false : original_successful?(response)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -59,6 +59,14 @@ module Killbill #:nodoc:
59
59
  model)
60
60
  end
61
61
 
62
+ def self.last_token(kb_account_id, kb_tenant_id)
63
+ response = where(:api_call => 'initiate_express_checkout',
64
+ :success => true,
65
+ :kb_account_id => kb_account_id,
66
+ :kb_tenant_id => kb_tenant_id).last
67
+ response.nil? ? nil : response.token
68
+ end
69
+
62
70
  def to_transaction_info_plugin(transaction=nil)
63
71
  t_info_plugin = super(transaction)
64
72
 
@@ -10,23 +10,23 @@ module Killbill #:nodoc:
10
10
  end
11
11
 
12
12
  # See https://cms.paypal.com/uk/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_ECReferenceTxns
13
- def initiate_express_checkout(kb_account_id, kb_tenant_id, amount_in_cents=0, currency='USD', options = {})
14
- payment_processor_account_id = (options[:payment_processor_account_id] || :default)
15
-
13
+ def initiate_express_checkout(kb_account_id, kb_tenant_id, amount_in_cents=0, currency='USD', with_baid=true, options = {})
16
14
  options[:currency] ||= currency
17
15
 
18
16
  # Required arguments
19
17
  options[:return_url] ||= 'http://www.example.com/success'
20
18
  options[:cancel_return_url] ||= 'http://www.example.com/sad_panda'
21
19
 
22
- options[:billing_agreement] ||= {}
23
- options[:billing_agreement][:type] ||= 'MerchantInitiatedBilling'
24
- options[:billing_agreement][:description] ||= 'Kill Bill billing agreement'
20
+ if with_baid
21
+ options[:billing_agreement] ||= {}
22
+ options[:billing_agreement][:type] ||= 'MerchantInitiatedBilling'
23
+ options[:billing_agreement][:description] ||= 'Kill Bill billing agreement'
24
+ end
25
25
 
26
26
  # Go to Paypal (SetExpressCheckout call)
27
- payment_processor_account_id = options[:payment_processor_account_id] || :default
28
- paypal_express_response = gateway(payment_processor_account_id, kb_tenant_id).setup_authorization(amount_in_cents, options)
29
- response, transaction = save_response_and_transaction(paypal_express_response, :initiate_express_checkout, kb_account_id, kb_tenant_id, payment_processor_account_id)
27
+ payment_processor_account_id = options[:payment_processor_account_id] || :default
28
+ paypal_express_response = gateway(payment_processor_account_id, kb_tenant_id).setup_authorization(amount_in_cents, options)
29
+ response, transaction = save_response_and_transaction(paypal_express_response, :initiate_express_checkout, kb_account_id, kb_tenant_id, payment_processor_account_id)
30
30
 
31
31
  response
32
32
  end
data/pom.xml CHANGED
@@ -26,7 +26,7 @@
26
26
  <groupId>org.kill-bill.billing.plugin.ruby</groupId>
27
27
  <artifactId>paypal-express-plugin</artifactId>
28
28
  <packaging>pom</packaging>
29
- <version>4.0.0</version>
29
+ <version>4.1.0</version>
30
30
  <name>paypal-express-plugin</name>
31
31
  <url>http://github.com/killbill/killbill-paypal-express-plugin</url>
32
32
  <description>Plugin for accessing Paypal Express Checkout as a payment gateway</description>
data/release.sh CHANGED
File without changes
@@ -31,23 +31,6 @@ describe Killbill::PaypalExpress::PaymentPlugin do
31
31
  @plugin.stop_plugin
32
32
  end
33
33
 
34
- it 'should generate forms correctly' do
35
- kb_account_id = SecureRandom.uuid
36
- kb_tenant_id = SecureRandom.uuid
37
- context = @plugin.kb_apis.create_context(kb_tenant_id)
38
- fields = @plugin.hash_to_properties({
39
- :order_id => '1234',
40
- :amount => 10
41
- })
42
- form = @plugin.build_form_descriptor kb_account_id, fields, [], context
43
-
44
- form.kb_account_id.should == kb_account_id
45
- form.form_method.should == 'POST'
46
- form.form_url.should == 'https://www.paypal.com/cgi-bin/webscr'
47
-
48
- form_fields = @plugin.properties_to_hash(form.form_fields)
49
- end
50
-
51
34
  it 'should receive notifications correctly' do
52
35
  description = 'description'
53
36
 
@@ -0,0 +1,287 @@
1
+ require 'spec_helper'
2
+
3
+ ActiveMerchant::Billing::Base.mode = :test
4
+
5
+ describe Killbill::PaypalExpress::PaymentPlugin do
6
+
7
+ include ::Killbill::Plugin::ActiveMerchant::RSpec
8
+
9
+ # Share the BAID
10
+ before(:all) do
11
+ @plugin = build_plugin(::Killbill::PaypalExpress::PaymentPlugin, 'paypal_express')
12
+ svcs = @plugin.kb_apis.proxied_services
13
+ svcs[:payment_api] = PaypalExpressJavaPaymentApi.new(@plugin)
14
+ @plugin.kb_apis = ::Killbill::Plugin::KillbillApi.new('paypal_express', svcs)
15
+ @plugin.start_plugin
16
+
17
+ @call_context = build_call_context
18
+
19
+ @properties = []
20
+ @amount = BigDecimal.new('100')
21
+ @currency = 'USD'
22
+
23
+ kb_account_id = SecureRandom.uuid
24
+ external_key, kb_account_id = create_kb_account(kb_account_id, @plugin.kb_apis.proxied_services[:account_user_api])
25
+
26
+ # Initiate the setup process
27
+ response = create_token(kb_account_id, @call_context.tenant_id)
28
+ token = response.token
29
+ print "\nPlease go to #{@plugin.to_express_checkout_url(response, @call_context.tenant_id)} to proceed and press any key to continue...
30
+ Note: you need to log-in with a paypal sandbox account (create one here: https://developer.paypal.com/webapps/developer/applications/accounts)\n"
31
+ $stdin.gets
32
+
33
+ # Complete the setup process
34
+ @properties << build_property('token', token)
35
+ @pm = create_payment_method(::Killbill::PaypalExpress::PaypalExpressPaymentMethod, kb_account_id, @call_context.tenant_id, @properties)
36
+
37
+ # Verify our table directly. Note that @pm.token is the baid
38
+ payment_methods = ::Killbill::PaypalExpress::PaypalExpressPaymentMethod.from_kb_account_id_and_token(@pm.token, kb_account_id, @call_context.tenant_id)
39
+ payment_methods.size.should == 1
40
+ payment_method = payment_methods.first
41
+ payment_method.should_not be_nil
42
+ payment_method.paypal_express_payer_id.should_not be_nil
43
+ payment_method.token.should == @pm.token
44
+ payment_method.kb_payment_method_id.should == @pm.kb_payment_method_id
45
+ end
46
+
47
+ before(:each) do
48
+ ::Killbill::PaypalExpress::PaypalExpressTransaction.delete_all
49
+ ::Killbill::PaypalExpress::PaypalExpressResponse.delete_all
50
+
51
+ kb_payment_id = SecureRandom.uuid
52
+ 1.upto(6) do
53
+ @kb_payment = @plugin.kb_apis.proxied_services[:payment_api].add_payment(kb_payment_id)
54
+ end
55
+ end
56
+
57
+ it 'should be able to charge and refund' do
58
+ payment_response = @plugin.purchase_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
59
+ payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
60
+ payment_response.amount.should == @amount
61
+ payment_response.transaction_type.should == :PURCHASE
62
+
63
+ # Verify GET API
64
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
65
+ payment_infos.size.should == 1
66
+ payment_infos[0].kb_payment_id.should == @kb_payment.id
67
+ payment_infos[0].transaction_type.should == :PURCHASE
68
+ payment_infos[0].amount.should == @amount
69
+ payment_infos[0].currency.should == @currency
70
+ payment_infos[0].status.should == :PROCESSED
71
+ payment_infos[0].gateway_error.should == 'Success'
72
+ payment_infos[0].gateway_error_code.should be_nil
73
+
74
+ # Try a full refund
75
+ refund_response = @plugin.refund_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[1].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
76
+ refund_response.status.should eq(:PROCESSED), refund_response.gateway_error
77
+ refund_response.amount.should == @amount
78
+ refund_response.transaction_type.should == :REFUND
79
+
80
+ # Verify GET API
81
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
82
+ payment_infos.size.should == 2
83
+ payment_infos[0].kb_payment_id.should == @kb_payment.id
84
+ payment_infos[0].transaction_type.should == :PURCHASE
85
+ payment_infos[0].amount.should == @amount
86
+ payment_infos[0].currency.should == @currency
87
+ payment_infos[0].status.should == :PROCESSED
88
+ payment_infos[0].gateway_error.should == 'Success'
89
+ payment_infos[0].gateway_error_code.should be_nil
90
+ payment_infos[1].kb_payment_id.should == @kb_payment.id
91
+ payment_infos[1].transaction_type.should == :REFUND
92
+ payment_infos[1].amount.should == @amount
93
+ payment_infos[1].currency.should == @currency
94
+ payment_infos[1].status.should == :PROCESSED
95
+ payment_infos[1].gateway_error.should == 'Success'
96
+ payment_infos[1].gateway_error_code.should be_nil
97
+ end
98
+
99
+ it 'should be able to auth, capture and refund' do
100
+ payment_response = @plugin.authorize_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
101
+ payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
102
+ payment_response.amount.should == @amount
103
+ payment_response.transaction_type.should == :AUTHORIZE
104
+
105
+ # Verify GET API
106
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
107
+ payment_infos.size.should == 1
108
+ payment_infos[0].kb_payment_id.should == @kb_payment.id
109
+ payment_infos[0].transaction_type.should == :AUTHORIZE
110
+ payment_infos[0].amount.should == @amount
111
+ payment_infos[0].currency.should == @currency
112
+ payment_infos[0].status.should == :PROCESSED
113
+ payment_infos[0].gateway_error.should == 'Success'
114
+ payment_infos[0].gateway_error_code.should be_nil
115
+
116
+ # Try multiple partial captures
117
+ partial_capture_amount = BigDecimal.new('10')
118
+ 1.upto(3) do |i|
119
+ payment_response = @plugin.capture_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[i].id, @pm.kb_payment_method_id, partial_capture_amount, @currency, @properties, @call_context)
120
+ payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
121
+ payment_response.amount.should == partial_capture_amount
122
+ payment_response.transaction_type.should == :CAPTURE
123
+
124
+ # Verify GET API
125
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
126
+ payment_infos.size.should == 1 + i
127
+ payment_infos[i].kb_payment_id.should == @kb_payment.id
128
+ payment_infos[i].transaction_type.should == :CAPTURE
129
+ payment_infos[i].amount.should == partial_capture_amount
130
+ payment_infos[i].currency.should == @currency
131
+ payment_infos[i].status.should == :PROCESSED
132
+ payment_infos[i].gateway_error.should == 'Success'
133
+ payment_infos[i].gateway_error_code.should be_nil
134
+ end
135
+
136
+ # Try a partial refund
137
+ refund_response = @plugin.refund_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[4].id, @pm.kb_payment_method_id, partial_capture_amount, @currency, @properties, @call_context)
138
+ refund_response.status.should eq(:PROCESSED), refund_response.gateway_error
139
+ refund_response.amount.should == partial_capture_amount
140
+ refund_response.transaction_type.should == :REFUND
141
+
142
+ # Verify GET API
143
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
144
+ payment_infos.size.should == 5
145
+ payment_infos[4].kb_payment_id.should == @kb_payment.id
146
+ payment_infos[4].transaction_type.should == :REFUND
147
+ payment_infos[4].amount.should == partial_capture_amount
148
+ payment_infos[4].currency.should == @currency
149
+ payment_infos[4].status.should == :PROCESSED
150
+ payment_infos[4].gateway_error.should == 'Success'
151
+ payment_infos[4].gateway_error_code.should be_nil
152
+
153
+ # Try to capture again
154
+ payment_response = @plugin.capture_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[5].id, @pm.kb_payment_method_id, partial_capture_amount, @currency, @properties, @call_context)
155
+ payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
156
+ payment_response.amount.should == partial_capture_amount
157
+ payment_response.transaction_type.should == :CAPTURE
158
+
159
+ # Verify GET API
160
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
161
+ payment_infos.size.should == 6
162
+ payment_infos[5].kb_payment_id.should == @kb_payment.id
163
+ payment_infos[5].transaction_type.should == :CAPTURE
164
+ payment_infos[5].amount.should == partial_capture_amount
165
+ payment_infos[5].currency.should == @currency
166
+ payment_infos[5].status.should == :PROCESSED
167
+ payment_infos[5].gateway_error.should == 'Success'
168
+ payment_infos[5].gateway_error_code.should be_nil
169
+ end
170
+
171
+ it 'should be able to auth and void' do
172
+ payment_response = @plugin.authorize_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
173
+ payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
174
+ payment_response.amount.should == @amount
175
+ payment_response.transaction_type.should == :AUTHORIZE
176
+
177
+ # Verify GET API
178
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
179
+ payment_infos.size.should == 1
180
+ payment_infos[0].kb_payment_id.should == @kb_payment.id
181
+ payment_infos[0].transaction_type.should == :AUTHORIZE
182
+ payment_infos[0].amount.should == @amount
183
+ payment_infos[0].currency.should == @currency
184
+ payment_infos[0].status.should == :PROCESSED
185
+ payment_infos[0].gateway_error.should == 'Success'
186
+ payment_infos[0].gateway_error_code.should be_nil
187
+
188
+ payment_response = @plugin.void_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[1].id, @pm.kb_payment_method_id, @properties, @call_context)
189
+ payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
190
+ payment_response.transaction_type.should == :VOID
191
+
192
+ # Verify GET API
193
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
194
+ payment_infos.size.should == 2
195
+ payment_infos[0].kb_payment_id.should == @kb_payment.id
196
+ payment_infos[0].transaction_type.should == :AUTHORIZE
197
+ payment_infos[0].amount.should == @amount
198
+ payment_infos[0].currency.should == @currency
199
+ payment_infos[0].status.should == :PROCESSED
200
+ payment_infos[0].gateway_error.should == 'Success'
201
+ payment_infos[0].gateway_error_code.should be_nil
202
+ payment_infos[1].kb_payment_id.should == @kb_payment.id
203
+ payment_infos[1].transaction_type.should == :VOID
204
+ payment_infos[1].amount.should be_nil
205
+ payment_infos[1].currency.should be_nil
206
+ payment_infos[1].status.should == :PROCESSED
207
+ payment_infos[1].gateway_error.should == 'Success'
208
+ payment_infos[1].gateway_error_code.should be_nil
209
+ end
210
+
211
+ it 'should be able to auth, partial capture and void' do
212
+ payment_response = @plugin.authorize_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[0].id, @pm.kb_payment_method_id, @amount, @currency, @properties, @call_context)
213
+ payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
214
+ payment_response.amount.should == @amount
215
+ payment_response.transaction_type.should == :AUTHORIZE
216
+
217
+ # Verify GET API
218
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
219
+ payment_infos.size.should == 1
220
+ payment_infos[0].kb_payment_id.should == @kb_payment.id
221
+ payment_infos[0].transaction_type.should == :AUTHORIZE
222
+ payment_infos[0].amount.should == @amount
223
+ payment_infos[0].currency.should == @currency
224
+ payment_infos[0].status.should == :PROCESSED
225
+ payment_infos[0].gateway_error.should == 'Success'
226
+ payment_infos[0].gateway_error_code.should be_nil
227
+
228
+ partial_capture_amount = BigDecimal.new('10')
229
+ payment_response = @plugin.capture_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[1].id, @pm.kb_payment_method_id, partial_capture_amount, @currency, @properties, @call_context)
230
+ payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
231
+ payment_response.amount.should == partial_capture_amount
232
+ payment_response.transaction_type.should == :CAPTURE
233
+
234
+ # Verify GET API
235
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
236
+ payment_infos.size.should == 2
237
+ payment_infos[1].kb_payment_id.should == @kb_payment.id
238
+ payment_infos[1].transaction_type.should == :CAPTURE
239
+ payment_infos[1].amount.should == partial_capture_amount
240
+ payment_infos[1].currency.should == @currency
241
+ payment_infos[1].status.should == :PROCESSED
242
+ payment_infos[1].gateway_error.should == 'Success'
243
+ payment_infos[1].gateway_error_code.should be_nil
244
+
245
+ payment_response = @plugin.void_payment(@pm.kb_account_id, @kb_payment.id, @kb_payment.transactions[2].id, @pm.kb_payment_method_id, @properties, @call_context)
246
+ payment_response.status.should eq(:PROCESSED), payment_response.gateway_error
247
+ payment_response.transaction_type.should == :VOID
248
+
249
+ # Verify GET API
250
+ payment_infos = @plugin.get_payment_info(@pm.kb_account_id, @kb_payment.id, [], @call_context)
251
+ payment_infos.size.should == 3
252
+ payment_infos[2].kb_payment_id.should == @kb_payment.id
253
+ payment_infos[2].transaction_type.should == :VOID
254
+ payment_infos[2].amount.should be_nil
255
+ payment_infos[2].currency.should be_nil
256
+ payment_infos[2].status.should == :PROCESSED
257
+ payment_infos[2].gateway_error.should == 'Success'
258
+ payment_infos[2].gateway_error_code.should be_nil
259
+ end
260
+
261
+ it 'should generate forms correctly' do
262
+ context = @plugin.kb_apis.create_context(@call_context.tenant_id)
263
+ fields = @plugin.hash_to_properties(
264
+ :order_id => '1234',
265
+ :amount => 12,
266
+ )
267
+
268
+ properties = @plugin.hash_to_properties(
269
+ :create_pending_payment => false
270
+ )
271
+
272
+ form = @plugin.build_form_descriptor(@pm.kb_account_id, fields, properties, context)
273
+
274
+ form.kb_account_id.should == @pm.kb_account_id
275
+ form.form_method.should == 'GET'
276
+ form.form_url.should start_with('https://www.sandbox.paypal.com/cgi-bin/webscr')
277
+ end
278
+
279
+ private
280
+
281
+ def create_token(kb_account_id, kb_tenant_id)
282
+ private_plugin = ::Killbill::PaypalExpress::PrivatePaymentPlugin.new
283
+ response = private_plugin.initiate_express_checkout(kb_account_id, kb_tenant_id)
284
+ response.success.should be_true
285
+ response
286
+ end
287
+ end