killbill 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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +12 -3
  3. data/Jarfile +8 -7
  4. data/Jarfile.lock +9 -8
  5. data/README.md +131 -108
  6. data/gen_config/api.conf +1 -1
  7. data/gen_config/plugin_api.conf +7 -6
  8. data/generators/active_merchant/templates/Jarfile.rb +7 -7
  9. data/generators/active_merchant/templates/plugin.gemspec.rb +2 -3
  10. data/killbill.gemspec +5 -2
  11. data/lib/killbill.rb +26 -22
  12. data/lib/killbill/gen/api/admin_payment_api.rb +76 -0
  13. data/lib/killbill/gen/api/payment_api.rb +82 -0
  14. data/lib/killbill/gen/api/payment_gateway_api.rb +75 -0
  15. data/lib/killbill/gen/api/require_gen.rb +1 -0
  16. data/lib/killbill/gen/plugin-api/catalog_plugin_api.rb +76 -0
  17. data/lib/killbill/gen/plugin-api/on_failure_payment_routing_result.rb +65 -0
  18. data/lib/killbill/gen/plugin-api/on_success_payment_routing_result.rb +53 -0
  19. data/lib/killbill/gen/plugin-api/payment_routing_api_exception.rb +51 -0
  20. data/lib/killbill/gen/plugin-api/payment_routing_context.rb +246 -0
  21. data/lib/killbill/gen/plugin-api/payment_routing_plugin_api.rb +138 -0
  22. data/lib/killbill/gen/plugin-api/prior_payment_routing_result.rb +107 -0
  23. data/lib/killbill/gen/plugin-api/require_gen.rb +9 -0
  24. data/lib/killbill/gen/plugin-api/standalone_plugin_catalog.rb +174 -0
  25. data/lib/killbill/gen/plugin-api/versioned_plugin_catalog.rb +83 -0
  26. data/lib/killbill/helpers/active_merchant.rb +6 -4
  27. data/lib/killbill/helpers/active_merchant/active_record/models/response.rb +16 -4
  28. data/lib/killbill/helpers/active_merchant/configuration.rb +7 -2
  29. data/lib/killbill/helpers/active_merchant/gateway.rb +76 -5
  30. data/lib/killbill/helpers/active_merchant/payment_plugin.rb +12 -8
  31. data/lib/killbill/helpers/catalog.rb +15 -0
  32. data/lib/killbill/payment_control.rb +23 -0
  33. data/lib/killbill/version.rb +3 -0
  34. data/spec/killbill/helpers/configuration_spec.rb +18 -4
  35. data/spec/killbill/helpers/payment_plugin_spec.rb +140 -36
  36. data/spec/killbill/helpers/response_spec.rb +1 -1
  37. metadata +49 -5
  38. data/VERSION +0 -1
@@ -4,15 +4,16 @@ module Killbill
4
4
  require 'active_merchant'
5
5
 
6
6
  class Gateway
7
- def self.wrap(gateway_builder, config)
8
- Gateway.new(config, gateway_builder.call(config))
7
+ def self.wrap(gateway_builder, config, logger)
8
+ Gateway.new(config, gateway_builder.call(config), logger)
9
9
  end
10
10
 
11
11
  attr_reader :config
12
12
 
13
- def initialize(config, am_gateway)
13
+ def initialize(config, am_gateway, logger)
14
14
  @config = config
15
15
  @gateway = am_gateway
16
+ @logger = logger
16
17
 
17
18
  # Override urls if needed (there is no easy way to do it earlier, because AM uses class_attribute)
18
19
  @gateway.class.test_url = @config[:test_url] unless @config[:test_url].nil?
@@ -57,19 +58,89 @@ module Killbill
57
58
  end
58
59
 
59
60
  def method_missing(m, *args, &block)
61
+ options = {}
62
+
60
63
  # The options hash should be the last argument, iterate through all to be safe
61
64
  args.reverse.each do |arg|
62
- if arg.respond_to?(:has_key?) && Utils.normalized(arg, :skip_gw)
63
- return ::ActiveMerchant::Billing::Response.new(true, 'Skipped Gateway call')
65
+ if arg.respond_to?(:has_key?)
66
+ options = arg
67
+ return ::ActiveMerchant::Billing::Response.new(true, 'Skipped Gateway call') if Utils.normalized(arg, :skip_gw)
64
68
  end
65
69
  end
66
70
 
67
71
  @gateway.send(m, *args, &block)
72
+ rescue ::ActiveMerchant::ConnectionError => e
73
+ # Need to unwrap it
74
+ until e.triggering_exception.nil?
75
+ e = e.triggering_exception
76
+ break unless e.is_a?(::ActiveMerchant::ConnectionError)
77
+ end
78
+ handle_exception(e, options)
79
+ rescue => e
80
+ handle_exception(e, options)
68
81
  end
69
82
 
70
83
  def respond_to?(method, include_private=false)
71
84
  @gateway.respond_to?(method, include_private) || super
72
85
  end
86
+
87
+ private
88
+
89
+ UNKNOWN_CONNECTION_ERRORS = [
90
+ # Corrupted stream (e.g. Zlib::BufError)
91
+ ::ActiveMerchant::InvalidResponseError,
92
+ # We attempted a payment, but the gateway replied >= 300. This is gateway specific, hopefully the individual
93
+ # gateway implementation knows how to rescue from it and this is not risen
94
+ ::ActiveMerchant::ResponseError,
95
+ # Should not be risen directly
96
+ ::ActiveMerchant::RetriableConnectionError,
97
+ ::ActiveMerchant::ActiveMerchantError
98
+ ]
99
+
100
+ PROBABLY_UNKNOWN_CONNECTION_ERRORS = [
101
+ EOFError,
102
+ Errno::ECONNRESET,
103
+ Timeout::Error,
104
+ Errno::ETIMEDOUT
105
+ ]
106
+
107
+ SAFE_CONNECTION_ERRORS = [
108
+ SocketError,
109
+ Errno::EHOSTUNREACH,
110
+ Errno::ECONNREFUSED,
111
+ ::OpenSSL::SSL::SSLError,
112
+ # Invalid certificate (e.g. OpenSSL::X509::CertificateError)
113
+ ::ActiveMerchant::ClientCertificateError
114
+ ]
115
+
116
+ # See https://github.com/killbill/killbill-plugin-framework-ruby/issues/44
117
+ def handle_exception(e, options = {})
118
+ message = "#{e.class} #{e.message}"
119
+
120
+ if SAFE_CONNECTION_ERRORS.include?(e.class)
121
+ # Easy case: we didn't attempt the payment
122
+ @logger.warn("Connection error with the gateway: #{message}")
123
+ payment_plugin_status = :CANCELED
124
+ else
125
+ # For anything else, tell Kill Bill we don't know. If the gateway supports retrieving a payment status,
126
+ # the plugin should implement get_payment_info accordingly for the Janitor.
127
+ # Otherwise, the transaction will need to be fixed manually using the admin APIs.
128
+
129
+ # Note that PROBABLY_UNKNOWN_CONNECTION_ERRORS/UNKNOWN_CONNECTION_ERRORS are a bit _better_, as they can be expected and we don't have any control over them.
130
+ # Any other exception might be caused by a bug in our code!
131
+ if PROBABLY_UNKNOWN_CONNECTION_ERRORS.include?(e.class) || UNKNOWN_CONNECTION_ERRORS.include?(e.class)
132
+ @logger.warn("Unstable connection with the gateway: #{message}")
133
+ else
134
+ @logger.warn("Unexpected exception: #{message}")
135
+ end
136
+
137
+ # Allow clients to force a PLUGIN_FAILURE instead of UNKNOWN (the default is a conservative behavior)
138
+ payment_plugin_status = Utils.normalized(options, :connection_errors_safe) ? :CANCELED : :UNDEFINED
139
+ end
140
+
141
+ response_message = { :exception_class => e.class.to_s, :exception_message => e.message, :payment_plugin_status => payment_plugin_status }.to_json
142
+ ::ActiveMerchant::Billing::Response.new(false, response_message)
143
+ end
73
144
  end
74
145
  end
75
146
  end
@@ -406,7 +406,7 @@ module Killbill
406
406
  end
407
407
 
408
408
  # Filter before all gateways call
409
- before_gateways(kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options)
409
+ before_gateways(kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options, context)
410
410
 
411
411
  # Dispatch to the gateways. In most cases (non split settlements), we only dispatch to a single gateway account
412
412
  gw_responses = []
@@ -424,14 +424,14 @@ module Killbill
424
424
  gateway = lookup_gateway(payment_processor_account_id, context.tenant_id)
425
425
 
426
426
  # Filter before each gateway call
427
- before_gateway(gateway, kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options)
427
+ before_gateway(gateway, kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options, context)
428
428
 
429
429
  # Perform the operation in the gateway
430
430
  gw_response = gateway_call_proc.call(gateway, linked_transaction, payment_source, amount_in_cents, options)
431
431
  response, transaction = save_response_and_transaction(gw_response, operation, kb_account_id, context.tenant_id, payment_processor_account_id, kb_payment_id, kb_payment_transaction_id, operation.upcase, amount_in_cents, currency)
432
432
 
433
433
  # Filter after each gateway call
434
- after_gateway(response, transaction, gw_response)
434
+ after_gateway(response, transaction, gw_response, context)
435
435
 
436
436
  gw_responses << gw_response
437
437
  responses << response
@@ -439,7 +439,7 @@ module Killbill
439
439
  end
440
440
 
441
441
  # Filter after all gateways call
442
- after_gateways(responses, transactions, gw_responses)
442
+ after_gateways(responses, transactions, gw_responses, context)
443
443
 
444
444
  # Merge data
445
445
  merge_transaction_info_plugins(payment_processor_account_ids, responses, transactions)
@@ -453,18 +453,22 @@ module Killbill
453
453
  kb_transaction
454
454
  end
455
455
 
456
- def before_gateways(kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options)
456
+ # Default nil value for context only for backward compatibility (Kill Bill 0.14.0)
457
+ def before_gateways(kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options, context = nil)
457
458
  end
458
459
 
459
- def after_gateways(response, transaction, gw_response)
460
+ # Default nil value for context only for backward compatibility (Kill Bill 0.14.0)
461
+ def after_gateways(response, transaction, gw_response, context = nil)
460
462
  end
461
463
 
462
- def before_gateway(gateway, kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options)
464
+ # Default nil value for context only for backward compatibility (Kill Bill 0.14.0)
465
+ def before_gateway(gateway, kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options, context = nil)
463
466
  # Can be used to implement idempotency for example: lookup the payment in the gateway
464
467
  # and pass options[:skip_gw] if the payment has already been through
465
468
  end
466
469
 
467
- def after_gateway(response, transaction, gw_response)
470
+ # Default nil value for context only for backward compatibility (Kill Bill 0.14.0)
471
+ def after_gateway(response, transaction, gw_response, context = nil)
468
472
  end
469
473
 
470
474
  def to_cents(amount, currency)
@@ -0,0 +1,15 @@
1
+ require 'killbill/plugin'
2
+
3
+ module Killbill
4
+ module Plugin
5
+ class CatalogPluginApi < Notification
6
+
7
+ class OperationUnsupported < NotImplementedError
8
+ end
9
+
10
+ def get_versioned_plugin_catalog(properties, context)
11
+ raise OperationUnsupported
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ require 'killbill/plugin'
2
+
3
+ module Killbill
4
+ module Plugin
5
+ class PaymentRoutingPluginApi < Notification
6
+
7
+ class OperationUnsupportedByGatewayError < NotImplementedError
8
+ end
9
+
10
+ def prior_call(routing_context, properties)
11
+ raise OperationUnsupportedByGatewayError
12
+ end
13
+
14
+ def on_success_call(routing_context, properties)
15
+ raise OperationUnsupportedByGatewayError
16
+ end
17
+
18
+ def on_failure_call(routing_context, properties)
19
+ raise OperationUnsupportedByGatewayError
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Killbill
2
+ VERSION = '4.1.0'
3
+ end
@@ -19,6 +19,16 @@ describe Killbill::Plugin::ActiveMerchant do
19
19
  do_initialize_with_config_path!(nil)
20
20
  end
21
21
 
22
+ # See https://github.com/killbill/killbill-plugin-framework-ruby/issues/46
23
+ it 'handles gracefully mis-configurations' do
24
+ do_initialize_without_config(Proc.new { |config| config[:login] })
25
+
26
+ do_common_checks
27
+
28
+ gw = ::Killbill::Plugin::ActiveMerchant.gateways(call_context.tenant_id)
29
+ gw.should be_empty
30
+ end
31
+
22
32
  it 'should support multi-tenancy configurations' do
23
33
  do_initialize!(<<-eos)
24
34
  :login: admin
@@ -211,17 +221,21 @@ describe Killbill::Plugin::ActiveMerchant do
211
221
  end
212
222
  end
213
223
 
214
- def do_initialize_with_config_path!(path = nil, gw_builder = Proc.new { |config| config })
224
+ def do_initialize_without_config(gw_builder = Proc.new { |config| config })
225
+ do_initialize_with_config_path!(nil, gw_builder, svcs_with_per_tenant_config(':nothing:'))
226
+ end
227
+
228
+ def do_initialize_with_config_path!(path = nil, gw_builder = Proc.new { |config| config }, per_tenant_config = svcs_with_per_tenant_config)
215
229
  ::Killbill::Plugin::ActiveMerchant.initialize!(gw_builder,
216
230
  :test,
217
231
  logger,
218
232
  :KEY,
219
233
  path,
220
- ::Killbill::Plugin::KillbillApi.new('test', svcs_with_per_tenant_config))
234
+ ::Killbill::Plugin::KillbillApi.new('test', per_tenant_config))
221
235
  end
222
236
 
223
- def svcs_with_per_tenant_config
224
- per_tenant_config =<<-oes
237
+ def svcs_with_per_tenant_config(per_tenant_config = nil)
238
+ per_tenant_config ||=<<-oes
225
239
  :test:
226
240
  :login: admin2
227
241
  :password: password2
@@ -311,16 +311,14 @@ describe Killbill::Plugin::ActiveMerchant::PaymentPlugin do
311
311
  end
312
312
 
313
313
  context 'with a dummy gateway' do
314
- let(:gateway) { DummyRecordingGateway.new }
314
+ let(:gateway) { plugin.lookup_gateway(:default, @call_context.tenant_id) }
315
315
 
316
316
  let(:plugin) do
317
- plugin = ::Killbill::Plugin::ActiveMerchant::PaymentPlugin.new(Proc.new { |config| gateway },
318
- :test,
319
- ::Killbill::Test::TestPaymentMethod,
320
- ::Killbill::Test::TestTransaction,
321
- ::Killbill::Test::TestResponse)
322
- plugin.kb_apis = kb_apis
323
- plugin.logger = logger
317
+ jplugin.delegate_plugin
318
+ end
319
+
320
+ let(:jplugin) do
321
+ ze_jplugin = nil
324
322
 
325
323
  plugin_config = {
326
324
  :test => [
@@ -328,14 +326,20 @@ describe Killbill::Plugin::ActiveMerchant::PaymentPlugin do
328
326
  ]
329
327
  }
330
328
  with_plugin_yaml_config('test.yml', plugin_config) do |file|
331
- plugin.conf_dir = File.dirname(file)
332
- plugin.root = File.dirname(file)
329
+ ze_jplugin = ::Killbill::Plugin::Api::PaymentPluginApi.new('DummyRecordingGatewayPlugin',
330
+ {
331
+ 'payment_api' => payment_api,
332
+ 'tenant_user_api' => tenant_api,
333
+ 'logger' => logger,
334
+ 'conf_dir' => File.dirname(file),
335
+ 'root' => File.dirname(file)
336
+ })
333
337
 
334
338
  # Start the plugin here - since the config file will be deleted
335
- plugin.start_plugin
339
+ ze_jplugin.start_plugin
336
340
  end
337
341
 
338
- plugin
342
+ ze_jplugin
339
343
  end
340
344
 
341
345
  after(:each) do
@@ -347,51 +351,46 @@ describe Killbill::Plugin::ActiveMerchant::PaymentPlugin do
347
351
  end
348
352
 
349
353
  it 'sets the kb_payment_transaction_id as order_id by default' do
350
- plugin.add_payment_method(@kb_account_id, @kb_payment_method_id, @payment_method_props, true, [], @call_context)
351
-
352
- kb_payment_transaction_id = SecureRandom.uuid
353
- plugin.purchase_payment(@kb_account_id, @kb_payment_id, kb_payment_transaction_id, @kb_payment_method_id, @amount_in_cents, @currency, [], @call_context)
354
+ ptip = trigger_purchase
354
355
 
355
356
  sent_options = gateway.call_stack[-1][:options]
356
357
  sent_options.size.should == 11
357
358
  sent_options[:currency].should == @currency
358
- sent_options[:description].should == "Kill Bill purchase for #{kb_payment_transaction_id}"
359
- sent_options[:order_id].should == kb_payment_transaction_id
359
+ sent_options[:description].should == "Kill Bill purchase for #{ptip.kb_transaction_payment_id}"
360
+ sent_options[:order_id].should == ptip.kb_transaction_payment_id
360
361
  end
361
362
 
362
363
  it 'sets the kb_payment_transaction_id as order_id if specified' do
363
- plugin.add_payment_method(@kb_account_id, @kb_payment_method_id, @payment_method_props, true, [], @call_context)
364
-
365
364
  property = ::Killbill::Plugin::Model::PluginProperty.new
366
365
  property.key = 'external_key_as_order_id'
367
366
  property.value = 'false'
367
+ properties = [property]
368
368
 
369
- kb_payment_transaction_id = SecureRandom.uuid
370
- plugin.purchase_payment(@kb_account_id, @kb_payment_id, kb_payment_transaction_id, @kb_payment_method_id, @amount_in_cents, @currency, [property], @call_context)
369
+ ptip = trigger_purchase(properties)
371
370
 
372
371
  sent_options = gateway.call_stack[-1][:options]
373
372
  sent_options.size.should == 12
374
373
  sent_options[:currency].should == @currency
375
- sent_options[:description].should == "Kill Bill purchase for #{kb_payment_transaction_id}"
376
- sent_options[:order_id].should == kb_payment_transaction_id
374
+ sent_options[:description].should == "Kill Bill purchase for #{ptip.kb_transaction_payment_id}"
375
+ sent_options[:order_id].should == ptip.kb_transaction_payment_id
377
376
  end
378
377
 
379
378
  it 'sets the payment transaction external key as order_id if specified' do
380
- plugin.add_payment_method(@kb_account_id, @kb_payment_method_id, @payment_method_props, true, [], @call_context)
381
-
382
379
  property = ::Killbill::Plugin::Model::PluginProperty.new
383
380
  property.key = 'external_key_as_order_id'
384
381
  property.value = 'true'
382
+ properties = [property]
385
383
 
386
384
  kb_payment_transaction_id = SecureRandom.uuid
387
385
  kb_payment_transaction_external_key = SecureRandom.uuid
388
- payment_api.add_payment(@kb_payment_id, kb_payment_transaction_id, kb_payment_transaction_external_key, :PURCHASE)
389
- plugin.purchase_payment(@kb_account_id, @kb_payment_id, kb_payment_transaction_id, @kb_payment_method_id, @amount_in_cents, @currency, [property], @call_context)
386
+ payment_api.add_payment(@kb_payment_id, kb_payment_transaction_id, kb_payment_transaction_external_key, :PURCHASE)
387
+
388
+ ptip = trigger_purchase(properties, kb_payment_transaction_id)
390
389
 
391
390
  sent_options = gateway.call_stack[-1][:options]
392
391
  sent_options.size.should == 12
393
392
  sent_options[:currency].should == @currency
394
- sent_options[:description].should == "Kill Bill purchase for #{kb_payment_transaction_id}"
393
+ sent_options[:description].should == "Kill Bill purchase for #{ptip.kb_transaction_payment_id}"
395
394
  sent_options[:order_id].should == kb_payment_transaction_external_key
396
395
  end
397
396
 
@@ -403,47 +402,152 @@ describe Killbill::Plugin::ActiveMerchant::PaymentPlugin do
403
402
 
404
403
  ::ActiveRecord::Base.connection_pool.active_connection?.should == false
405
404
  end
405
+
406
+ # Regression tests for the Kill Bill API conventions
407
+ # TODO Go through Java generated code
408
+
409
+ it 'returns ERROR if the payment transaction went through but failed' do
410
+ gateway.next_success = false
411
+
412
+ # Verify the purchase call for the Kill Bill payment state machine and the get_payment_info call for the Janitor
413
+ ptip = trigger_purchase
414
+ verify_purchase_status(ptip, :ERROR)
415
+
416
+ # Check debugging fields
417
+ ptip.gateway_error.should == 'false'
418
+ end
419
+
420
+ it 'returns UNDEFINED for plugin bugs' do
421
+ gateway.next_exception = NoMethodError.new("undefined method `split' for 12:Fixnum")
422
+
423
+ # Verify the purchase call for the Kill Bill payment state machine and the get_payment_info call for the Janitor
424
+ ptip = trigger_purchase
425
+ verify_purchase_status(ptip, :UNDEFINED)
426
+
427
+ # Check debugging fields
428
+ ptip.gateway_error.should == "undefined method `split' for 12:Fixnum"
429
+ end
430
+
431
+ # Specific ActiveMerchant errors handling
432
+ # See https://github.com/Shopify/active_merchant/blob/2e7eebe38020db4d262b91778797910ede2f31be/lib/active_merchant/network_connection_retries.rb#L21-L34
433
+
434
+ it 'returns CANCELED if the payment was not attempted' do
435
+ {
436
+ Errno::ECONNREFUSED => 'The remote server refused the connection',
437
+ SocketError => 'The connection to the remote server could not be established',
438
+ Errno::EHOSTUNREACH => 'The connection to the remote server could not be established',
439
+ OpenSSL::SSL::SSLError => 'The SSL connection to the remote server could not be established',
440
+ ::ActiveMerchant::ClientCertificateError => 'The remote server did not accept the provided SSL certificate'
441
+ }.each do |ek, msg|
442
+ gateway.next_exception = ::ActiveMerchant::ConnectionError.new(msg, ek.new(msg))
443
+
444
+ # Verify the purchase call for the Kill Bill payment state machine and the get_payment_info call for the Janitor
445
+ ptip = trigger_purchase
446
+ verify_purchase_status(ptip, :CANCELED)
447
+
448
+ # Check debugging fields
449
+ ptip.gateway_error.ends_with?(msg).should be_true
450
+ ptip.gateway_error_code.should == ek.to_s
451
+ end
452
+ end
453
+
454
+ it 'returns UNDEFINED if we are not sure' do
455
+ {
456
+ EOFError => 'The remote server dropped the connection',
457
+ Errno::ECONNRESET => 'The remote server reset the connection',
458
+ Timeout::Error => 'The connection to the remote server timed out',
459
+ Errno::ETIMEDOUT => 'The connection to the remote server timed out',
460
+ ::ActiveMerchant::InvalidResponseError => 'The remote server replied with an invalid response'
461
+ }.each do |ek, msg|
462
+ gateway.next_exception = ::ActiveMerchant::ConnectionError.new(msg, ek.new(msg))
463
+
464
+ # Verify the purchase call for the Kill Bill payment state machine and the get_payment_info call for the Janitor
465
+ ptip = trigger_purchase
466
+ verify_purchase_status(ptip, :UNDEFINED)
467
+
468
+ # Check debugging fields
469
+ ptip.gateway_error.ends_with?(msg).should be_true
470
+ ptip.gateway_error_code.should == ek.to_s
471
+ end
472
+ end
406
473
  end
407
474
 
408
475
  private
409
476
 
410
- def verify_transaction_info_plugin(t_info_plugin, kb_transaction_id, type, transaction_nb, payment_processor_account_id='default')
477
+ def trigger_purchase(purchase_properties=[], kb_payment_transaction_id=SecureRandom.uuid)
478
+ plugin.get_payment_method_detail(@kb_account_id, @kb_payment_method_id, [], @call_context) rescue plugin.add_payment_method(@kb_account_id, @kb_payment_method_id, @payment_method_props, true, [], @call_context)
479
+ plugin.purchase_payment(@kb_account_id, @kb_payment_id, kb_payment_transaction_id, @kb_payment_method_id, @amount_in_cents, @currency, purchase_properties, @call_context)
480
+ end
481
+
482
+ def verify_purchase_status(t_info_plugin, status)
483
+ verify_transaction_info_plugin(t_info_plugin, t_info_plugin.kb_transaction_payment_id, :PURCHASE, nil, 'default', status)
484
+ end
485
+
486
+ def verify_transaction_info_plugin(t_info_plugin, kb_transaction_id, type, transaction_nb, payment_processor_account_id='default', status=:PROCESSED)
411
487
  t_info_plugin.kb_payment_id.should == @kb_payment_id
412
488
  t_info_plugin.kb_transaction_payment_id.should == kb_transaction_id
413
489
  t_info_plugin.transaction_type.should == type
414
- if type == :VOID
490
+ if type == :VOID || status != :PROCESSED
415
491
  t_info_plugin.amount.should be_nil
416
492
  t_info_plugin.currency.should be_nil
417
493
  else
418
494
  t_info_plugin.amount.should == @amount_in_cents
419
495
  t_info_plugin.currency.should == @currency
420
496
  end
421
- t_info_plugin.status.should == :PROCESSED
497
+ t_info_plugin.status.should == status
422
498
 
423
499
  # Verify we routed to the right gateway
424
500
  (t_info_plugin.properties.find { |kv| kv.key.to_s == 'payment_processor_account_id' }).value.to_s.should == payment_processor_account_id
425
501
 
426
502
  transactions = plugin.get_payment_info(@kb_account_id, @kb_payment_id, [], @call_context)
427
- transactions.size.should == transaction_nb
428
- transactions[transaction_nb - 1].to_json.should == t_info_plugin.to_json
503
+ transactions.size.should == transaction_nb unless transaction_nb.nil?
504
+ transactions[-1].to_json.should == t_info_plugin.to_json
505
+ end
506
+
507
+ class DummyRecordingGatewayPlugin < ::Killbill::Plugin::ActiveMerchant::PaymentPlugin
508
+
509
+ def initialize
510
+ super(Proc.new { |config| DummyRecordingGateway.new },
511
+ :test,
512
+ ::Killbill::Test::TestPaymentMethod,
513
+ ::Killbill::Test::TestTransaction,
514
+ ::Killbill::Test::TestResponse)
515
+ end
429
516
  end
430
517
 
431
518
  class DummyRecordingGateway < ::ActiveMerchant::Billing::Gateway
432
519
 
433
520
  attr_reader :call_stack
521
+ attr_writer :next_success, :next_exception
434
522
 
435
523
  def initialize
436
524
  @call_stack = []
525
+ @next_success = true
526
+ @next_exception = nil
437
527
  end
438
528
 
439
529
  def purchase(money, paysource, options = {})
530
+ success = before_purchase
440
531
  @call_stack << {:money => money, :source => paysource, :options => options}
441
- ::ActiveMerchant::Billing::Response.new(true, 'Success!', {:authorized_amount => money}, :test => true, :authorization => 12345)
532
+ ::ActiveMerchant::Billing::Response.new(success, success.to_s, {:authorized_amount => money}, :test => true, :authorization => '12345')
442
533
  end
443
534
 
444
535
  def store(paysource, options = {})
445
536
  @call_stack << {:source => paysource, :options => options}
446
- ::ActiveMerchant::Billing::Response.new(true, 'Success!', {:billingid => '1'}, :test => true, :authorization => 12345)
537
+ ::ActiveMerchant::Billing::Response.new(true, 'Success!', {:billingid => '1'}, :test => true, :authorization => '12345')
538
+ end
539
+
540
+ # Testing helpers
541
+
542
+ def before_purchase
543
+ unless @next_exception.nil?
544
+ e = @next_exception
545
+ @next_exception = nil
546
+ raise e
547
+ end
548
+ s = @next_success
549
+ @next_success = true
550
+ s
447
551
  end
448
552
  end
449
553
  end