killbill 4.0.0 → 4.1.0

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