killbill-paypal-express 4.0.0 → 4.1.0

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