killbill 3.1.9 → 3.1.10

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fecf6eec986317ba4b930aea485670c18fbc88b2
4
- data.tar.gz: 642dbcbe75183170bfe68b07925653bf51855a29
3
+ metadata.gz: 53241b3ff5ba7293ab885ae36942071b396cdb03
4
+ data.tar.gz: 6c78b3cb59a3c28140b30fdfbcc080425858f5ce
5
5
  SHA512:
6
- metadata.gz: 900a0cb3ce17d6060cac359fe685fe42b593a492fa97b4f1a9c418255524c18415a67641c09092716a82827c17d3c905eedab1036f76f23c57a761967adda8be
7
- data.tar.gz: f03f0c261d3390cae2246a9a31464a0cc10ee5093ca414d8381eb8d1a300d4706df5aed100de725f3496a24c6871f39dcd2d774765c5c35b288cf8b5490fa700
6
+ metadata.gz: c131124735e38e0e61e7934190e36e661490df54d308d5bb8ffd74e743312b35e4561eec42f8bd9bd5df50a15e51746fd79966a72340e3ab93a680f24dfe74bf
7
+ data.tar.gz: 3e69182738dba3c38f5f012afc6846b9306454c4e97e5cb70c1ac1a6f60cfdf8fe26c4f518a4aaeaf6e416246fc40e5176442df08cddfb8c62fcf84f2f30f89a
data/NEWS CHANGED
@@ -1,6 +1,15 @@
1
+ 3.1.10
2
+ Add workarounds for ActiveRecord bugs under high load/concurrency
3
+ BoundedLRUCache performance improvements
4
+ Introduce :payment_processor_account_id option to route payment requests
5
+ Add before_gateways / after_gateways hooks
6
+
1
7
  3.1.9
2
8
  Fix memory leak in database connection handling
3
9
  Change void implementation to be more generic (and work with Cybersource)
10
+ ActiveMerchant: pass the transaction external key as the order id
11
+ Add before_gateway / after_gateway hooks
12
+ Fix API queries in multi-tenancy mode
4
13
 
5
14
  3.1.8
6
15
  Make ActiveMerchant HTTP backend configurable, add support for Typhoeus
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.1.9
1
+ 3.1.10
@@ -0,0 +1,69 @@
1
+ require 'active_record'
2
+ require 'active_record/connection_adapters/jdbc_adapter'
3
+
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ class JdbcConnection
7
+
8
+ # Sets the connection factory from the available configuration.
9
+ #
10
+ # This differs from the original implementation in the following ways:
11
+ # * We attempt to lookup the JNDI data source multiple times, to handle transient lookup issues
12
+ # * If the data source is unavailable, we don't fallback to straight JDBC (which is often not configured anyways)
13
+ # * In the failure scenario, inspect the exception instead of displaying e.message, which is often empty in our testing
14
+ def setup_connection_factory
15
+ if self.class.jndi_config?(config)
16
+ setup_done = false
17
+ jndi_retries = self.class.jndi_retries(config)
18
+
19
+ 1.upto(jndi_retries) do |i|
20
+ begin
21
+ setup_jndi_factory
22
+ setup_done = true
23
+ break
24
+ rescue => e
25
+ warn "JNDI data source unavailable: #{e.inspect} (attempt ##{i})"
26
+ end
27
+ end
28
+
29
+ raise "JNDI data source unavailable (tried #{jndi_retries} times)" unless setup_done
30
+ else
31
+ setup_jdbc_factory
32
+ end
33
+ end
34
+
35
+ def self.jndi_retries(config)
36
+ (config[:jndi_retries] || 5).to_i
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ require 'active_record/persistence'
43
+
44
+ module ActiveRecord
45
+ module Persistence
46
+
47
+ # Creates a record with values matching those of the instance attributes
48
+ # and returns its id.
49
+ def _create_record(attribute_names = @attributes.keys)
50
+ attributes_values = arel_attributes_with_values_for_create(attribute_names)
51
+
52
+ new_id = self.class.unscoped.insert attributes_values
53
+
54
+ # Under heavy load and concurrency, write_attribute_with_type_cast sometimes fail to set the id.
55
+ # Even though self.class.primary_key returns 'id' and new_id is correctly populated from the database
56
+ # (see last_insert_id in activerecord-jdbc-adapter-1.3.9/lib/arjdbc/jdbc/adapter.rb), both self.id ||= new_id
57
+ # and self.id = new_id sometimes don't set the id. I couldn't quite figure it out.
58
+ # A workaround seems to be to retry the assignment (see also activerecord-4.1.5/lib/active_record/attribute_methods/primary_key.rb).
59
+ if self.class.primary_key
60
+ self.id ||= new_id
61
+ self.id ||= new_id if id.nil?
62
+ raise "Unable to set id (new_id=#{new_id}) for #{self.inspect}" if id.nil?
63
+ end
64
+
65
+ @new_record = false
66
+ id
67
+ end
68
+ end
69
+ end
@@ -3,6 +3,8 @@ module Killbill
3
3
  module ActiveMerchant
4
4
  require 'killbill'
5
5
 
6
+ require 'killbill/ext/active_merchant/jdbc_connection'
7
+
6
8
  require 'active_support/core_ext'
7
9
  require File.dirname(__FILE__) + '/active_merchant/core_ext.rb'
8
10
  require File.dirname(__FILE__) + '/active_merchant/configuration.rb'
@@ -18,4 +20,4 @@ module Killbill
18
20
  end
19
21
  end
20
22
  end
21
- end
23
+ end
@@ -8,7 +8,7 @@ module Killbill
8
8
  # We don't want to issue raw SQL because we still want to be able to support multiple back-ends (at least SQLite and MySQL),
9
9
  # so we cache the quoted values (this should also give us SQL injection protection).
10
10
  # The gain is not 50% but more around 30% due to the Mutex overhead in the cache
11
- def build_quotes_cache(max_size=1000)
11
+ def build_quotes_cache(max_size=10000)
12
12
  # See ::ActiveRecord::Sanitization::ClassMethods.quote_bound_value
13
13
  quote_bound_value_proc = Proc.new { |value|
14
14
  c = connection
@@ -27,6 +27,11 @@ module Killbill
27
27
  # For convenience
28
28
  alias_method :authorizations_from_kb_payment_id, :authorizes_from_kb_payment_id
29
29
 
30
+ # For convenience
31
+ def voids_from_kb_payment_id(kb_payment_id, kb_tenant_id)
32
+ [void_from_kb_payment_id(kb_payment_id, kb_tenant_id)]
33
+ end
34
+
30
35
  # void is special: unique void per payment_id
31
36
  def void_from_kb_payment_id(kb_payment_id, kb_tenant_id)
32
37
  transaction_from_kb_payment_id(:VOID, kb_payment_id, kb_tenant_id, :single)
@@ -5,24 +5,38 @@ module Killbill
5
5
  module ActiveMerchant
6
6
  mattr_reader :config
7
7
  mattr_reader :currency_conversions
8
- mattr_reader :gateway
8
+ mattr_reader :gateways
9
9
  mattr_reader :initialized
10
10
  mattr_reader :kb_apis
11
11
  mattr_reader :logger
12
- mattr_reader :test
13
12
 
14
13
  def self.initialize!(gateway_builder, gateway_name, logger, config_file, kb_apis)
15
14
  @@config = Properties.new(config_file)
16
15
  @@config.parse!
17
16
 
18
- @@currency_conversions = @@config[:currency_conversions]
19
- @@kb_apis = kb_apis
20
- @@test = @@config[gateway_name][:test]
17
+ @@logger = logger
18
+ @@logger.log_level = Logger::DEBUG if (@@config[:logger] || {})[:debug]
21
19
 
22
- @@gateway = Gateway.wrap(gateway_builder, logger, @@config[gateway_name.to_sym])
20
+ @@currency_conversions = @@config[:currency_conversions]
21
+ @@kb_apis = kb_apis
23
22
 
24
- @@logger = logger
25
- @@logger.log_level = Logger::DEBUG if (@@config[:logger] || {})[:debug]
23
+ @@gateways = {}
24
+ gateway_configs = @@config[gateway_name.to_sym]
25
+ if gateway_configs.is_a?(Array)
26
+ default_gateway = nil
27
+ gateway_configs.each_with_index do |gateway_config, idx|
28
+ gateway_account_id = gateway_config[:account_id]
29
+ if gateway_account_id.nil?
30
+ @@logger.warn "Skipping config #{gateway_config} -- missing :account_id"
31
+ else
32
+ @@gateways[gateway_account_id.to_sym] = Gateway.wrap(gateway_builder, logger, gateway_config)
33
+ default_gateway = @@gateways[gateway_account_id.to_sym] if idx == 0
34
+ end
35
+ end
36
+ @@gateways[:default] = default_gateway if @@gateways[:default].nil?
37
+ else
38
+ @@gateways[:default] = Gateway.wrap(gateway_builder, logger, gateway_configs)
39
+ end
26
40
 
27
41
  if defined?(JRUBY_VERSION)
28
42
  begin
@@ -16,10 +16,13 @@ module Killbill
16
16
  ::ActiveMerchant::Billing::Gateway.wiredump_device.sync = true
17
17
  end
18
18
 
19
- Gateway.new(gateway_builder.call(config))
19
+ Gateway.new(config, gateway_builder.call(config))
20
20
  end
21
21
 
22
- def initialize(am_gateway)
22
+ attr_reader :config
23
+
24
+ def initialize(config, am_gateway)
25
+ @config = config
23
26
  @gateway = am_gateway
24
27
  end
25
28
 
@@ -42,152 +42,72 @@ module Killbill
42
42
  end
43
43
 
44
44
  def authorize_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
45
- kb_transaction = get_kb_transaction(kb_payment_id, kb_payment_transaction_id, context.tenant_id)
46
- amount_in_cents = to_cents(amount, currency)
47
-
48
- options = properties_to_hash(properties)
49
- options[:order_id] ||= kb_transaction.external_key
50
- options[:currency] ||= currency.to_s.upcase
51
- options[:description] ||= "Kill Bill authorization for #{kb_payment_transaction_id}"
52
-
53
- # Retrieve the payment method
54
- payment_source = get_payment_source(kb_payment_method_id, properties, options, context)
55
-
56
- before_gateway(kb_transaction, nil, payment_source, amount_in_cents, currency, options)
57
-
58
- # Go to the gateway
59
- gw_response = gateway.authorize(amount_in_cents, payment_source, options)
60
- response, transaction = save_response_and_transaction(gw_response, :authorize, kb_account_id, context.tenant_id, kb_payment_id, kb_payment_transaction_id, :AUTHORIZE, amount_in_cents, currency)
61
-
62
- after_gateway(response, transaction, gw_response)
45
+ gateway_call_proc = Proc.new do |gateway, payment_source, amount_in_cents, options|
46
+ gateway.authorize(amount_in_cents, payment_source, options)
47
+ end
63
48
 
64
- response.to_transaction_info_plugin(transaction)
49
+ dispatch_to_gateways(:authorize, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context, gateway_call_proc)
65
50
  end
66
51
 
67
52
  def capture_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
68
- kb_transaction = get_kb_transaction(kb_payment_id, kb_payment_transaction_id, context.tenant_id)
69
- amount_in_cents = to_cents(amount, currency)
70
-
71
- options = properties_to_hash(properties)
72
- options[:order_id] ||= kb_transaction.external_key
73
- options[:currency] ||= currency.to_s.upcase
74
- options[:description] ||= "Kill Bill capture for #{kb_payment_transaction_id}"
75
-
76
- # Retrieve the authorization
77
- # TODO We use the last AUTH transaction at the moment, is it good enough?
78
- authorization = @transaction_model.authorizations_from_kb_payment_id(kb_payment_id, context.tenant_id).last.txn_id
79
-
80
- before_gateway(kb_transaction, authorization, nil, amount_in_cents, currency, options)
81
-
82
- # Go to the gateway
83
- gw_response = gateway.capture(amount_in_cents, authorization, options)
84
- response, transaction = save_response_and_transaction(gw_response, :capture, kb_account_id, context.tenant_id, kb_payment_id, kb_payment_transaction_id, :CAPTURE, amount_in_cents, currency)
85
-
86
- after_gateway(response, transaction, gw_response)
53
+ gateway_call_proc = Proc.new do |gateway, payment_source, amount_in_cents, options|
54
+ # TODO We use the last transaction at the moment, is it good enough?
55
+ last_authorization = @transaction_model.authorizations_from_kb_payment_id(kb_payment_id, context.tenant_id).last
56
+ raise "Unable to retrieve last authorization for operation=capture, kb_payment_id=#{kb_payment_id}, kb_payment_transaction_id=#{kb_payment_transaction_id}, kb_payment_method_id=#{kb_payment_method_id}" if last_authorization.nil?
57
+ gateway.capture(amount_in_cents, last_authorization.txn_id, options)
58
+ end
87
59
 
88
- response.to_transaction_info_plugin(transaction)
60
+ dispatch_to_gateways(:capture, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context, gateway_call_proc)
89
61
  end
90
62
 
91
63
  def purchase_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
92
- kb_transaction = get_kb_transaction(kb_payment_id, kb_payment_transaction_id, context.tenant_id)
93
- amount_in_cents = to_cents(amount, currency)
94
-
95
- options = properties_to_hash(properties)
96
- options[:order_id] ||= kb_transaction.external_key
97
- options[:currency] ||= currency.to_s.upcase
98
- options[:description] ||= "Kill Bill purchase for #{kb_payment_transaction_id}"
99
-
100
- # Retrieve the payment method
101
- payment_source = get_payment_source(kb_payment_method_id, properties, options, context)
102
-
103
- before_gateway(kb_transaction, nil, payment_source, amount_in_cents, currency, options)
104
-
105
- # Go to the gateway
106
- gw_response = gateway.purchase(amount_in_cents, payment_source, options)
107
- response, transaction = save_response_and_transaction(gw_response, :purchase, kb_account_id, context.tenant_id, kb_payment_id, kb_payment_transaction_id, :PURCHASE, amount_in_cents, currency)
108
-
109
- after_gateway(response, transaction, gw_response)
64
+ gateway_call_proc = Proc.new do |gateway, payment_source, amount_in_cents, options|
65
+ gateway.purchase(amount_in_cents, payment_source, options)
66
+ end
110
67
 
111
- response.to_transaction_info_plugin(transaction)
68
+ 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)
112
69
  end
113
70
 
114
71
  def void_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, properties, context)
115
- kb_transaction = get_kb_transaction(kb_payment_id, kb_payment_transaction_id, context.tenant_id)
116
-
117
- options = properties_to_hash(properties)
118
- options[:order_id] ||= kb_transaction.external_key
119
- options[:description] ||= "Kill Bill void for #{kb_payment_transaction_id}"
120
-
121
- # If an authorization is being voided, we're performing an 'auth_reversal', otherwise,
122
- # we're voiding an unsettled capture or purchase (which often needs to happen within 24 hours).
123
- last_transaction = @transaction_model.purchases_from_kb_payment_id(kb_payment_id, context.tenant_id).last
124
- if last_transaction.nil?
125
- last_transaction = @transaction_model.captures_from_kb_payment_id(kb_payment_id, context.tenant_id).last
72
+ gateway_call_proc = Proc.new do |gateway, payment_source, amount_in_cents, options|
73
+ # If an authorization is being voided, we're performing an 'auth_reversal', otherwise,
74
+ # we're voiding an unsettled capture or purchase (which often needs to happen within 24 hours).
75
+ last_transaction = @transaction_model.purchases_from_kb_payment_id(kb_payment_id, context.tenant_id).last
126
76
  if last_transaction.nil?
127
- last_transaction = @transaction_model.authorizations_from_kb_payment_id(kb_payment_id, context.tenant_id).last
77
+ last_transaction = @transaction_model.captures_from_kb_payment_id(kb_payment_id, context.tenant_id).last
128
78
  if last_transaction.nil?
129
- raise ArgumentError.new("Kill Bill payment #{kb_payment_id} has no auth, capture or purchase, thus cannot be voided")
79
+ last_transaction = @transaction_model.authorizations_from_kb_payment_id(kb_payment_id, context.tenant_id).last
80
+ if last_transaction.nil?
81
+ raise ArgumentError.new("Kill Bill payment #{kb_payment_id} has no auth, capture or purchase, thus cannot be voided")
82
+ end
130
83
  end
131
84
  end
132
- end
133
- authorization = last_transaction.txn_id
134
-
135
- before_gateway(kb_transaction, last_transaction, nil, nil, nil, options)
136
-
137
- # Go to the gateway - while some gateways implementations are smart and have void support 'auth_reversal' and 'void' (e.g. Litle),
138
- # others (e.g. CyberSource) implement different methods
139
- gw_response = last_transaction.transaction_type == 'AUTHORIZE' && gateway.respond_to?(:auth_reversal) ? gateway.auth_reversal(last_transaction.amount_in_cents, authorization, options) : gateway.void(authorization, options)
140
- response, transaction = save_response_and_transaction(gw_response, :void, kb_account_id, context.tenant_id, kb_payment_id, kb_payment_transaction_id, :VOID)
85
+ authorization = last_transaction.txn_id
141
86
 
142
- after_gateway(response, transaction, gw_response)
87
+ # Go to the gateway - while some gateways implementations are smart and have void support 'auth_reversal' and 'void' (e.g. Litle),
88
+ # others (e.g. CyberSource) implement different methods
89
+ last_transaction.transaction_type == 'AUTHORIZE' && gateway.respond_to?(:auth_reversal) ? gateway.auth_reversal(last_transaction.amount_in_cents, authorization, options) : gateway.void(authorization, options)
90
+ end
143
91
 
144
- response.to_transaction_info_plugin(transaction)
92
+ dispatch_to_gateways(:void, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, nil, nil, properties, context, gateway_call_proc)
145
93
  end
146
94
 
147
95
  def credit_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
148
- kb_transaction = get_kb_transaction(kb_payment_id, kb_payment_transaction_id, context.tenant_id)
149
- amount_in_cents = to_cents(amount, currency)
150
-
151
- options = properties_to_hash(properties)
152
- options[:order_id] ||= kb_transaction.external_key
153
- options[:currency] ||= currency.to_s.upcase
154
- options[:description] ||= "Kill Bill credit for #{kb_payment_transaction_id}"
155
-
156
- # Retrieve the payment method
157
- payment_source = get_payment_source(kb_payment_method_id, properties, options, context)
158
-
159
- before_gateway(kb_transaction, nil, payment_source, amount_in_cents, currency, options)
160
-
161
- # Go to the gateway
162
- gw_response = gateway.credit(amount_in_cents, payment_source, options)
163
- response, transaction = save_response_and_transaction(gw_response, :credit, kb_account_id, context.tenant_id, kb_payment_id, kb_payment_transaction_id, :CREDIT, amount_in_cents, currency)
164
-
165
- after_gateway(response, transaction, gw_response)
96
+ gateway_call_proc = Proc.new do |gateway, payment_source, amount_in_cents, options|
97
+ gateway.credit(amount_in_cents, payment_source, options)
98
+ end
166
99
 
167
- response.to_transaction_info_plugin(transaction)
100
+ dispatch_to_gateways(:credit, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context, gateway_call_proc)
168
101
  end
169
102
 
170
103
  def refund_payment(kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context)
171
- kb_transaction = get_kb_transaction(kb_payment_id, kb_payment_transaction_id, context.tenant_id)
172
- amount_in_cents = to_cents(amount, currency)
173
-
174
- options = properties_to_hash(properties)
175
- options[:order_id] ||= kb_transaction.external_key
176
- options[:currency] ||= currency.to_s.upcase
177
- options[:description] ||= "Kill Bill refund for #{kb_payment_transaction_id}"
178
-
179
- # Find a transaction to refund
180
- transaction = @transaction_model.find_candidate_transaction_for_refund(kb_payment_id, context.tenant_id, amount_in_cents)
181
-
182
- before_gateway(kb_transaction, transaction, nil, amount_in_cents, currency, options)
183
-
184
- # Go to the gateway
185
- gw_response = gateway.refund(amount_in_cents, transaction.txn_id, options)
186
- response, transaction = save_response_and_transaction(gw_response, :refund, kb_account_id, context.tenant_id, kb_payment_id, kb_payment_transaction_id, :REFUND, amount_in_cents, currency)
187
-
188
- after_gateway(response, transaction, gw_response)
104
+ gateway_call_proc = Proc.new do |gateway, payment_source, amount_in_cents, options|
105
+ transaction = @transaction_model.find_candidate_transaction_for_refund(kb_payment_id, context.tenant_id, amount_in_cents)
106
+ raise "Unable to retrieve transaction to refund for operation=capture, kb_payment_id=#{kb_payment_id}, kb_payment_transaction_id=#{kb_payment_transaction_id}, kb_payment_method_id=#{kb_payment_method_id}" if transaction.nil?
107
+ gateway.refund(amount_in_cents, transaction.txn_id, options)
108
+ end
189
109
 
190
- response.to_transaction_info_plugin(transaction)
110
+ dispatch_to_gateways(:refund, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context, gateway_call_proc)
191
111
  end
192
112
 
193
113
  def get_payment_info(kb_account_id, kb_payment_id, properties, context)
@@ -212,6 +132,7 @@ module Killbill
212
132
  payment_source = get_payment_source(nil, all_properties, options, context)
213
133
 
214
134
  # Go to the gateway
135
+ gateway = lookup_gateway(options[:payment_processor_account_id] || :default)
215
136
  gw_response = gateway.store(payment_source, options)
216
137
  response, transaction = save_response_and_transaction gw_response, :add_payment_method, kb_account_id, context.tenant_id
217
138
 
@@ -235,9 +156,10 @@ module Killbill
235
156
  def delete_payment_method(kb_account_id, kb_payment_method_id, properties, context)
236
157
  options = properties_to_hash(properties)
237
158
 
238
- pm = @payment_method_model.from_kb_payment_method_id(kb_payment_method_id, context.tenant_id)
159
+ pm = @payment_method_model.from_kb_payment_method_id(kb_payment_method_id, context.tenant_id)
239
160
 
240
161
  # Delete the card
162
+ gateway = lookup_gateway(options[:payment_processor_account_id] || :default)
241
163
  if options[:customer_id]
242
164
  gw_response = gateway.unstore(options[:customer_id], pm.token, options)
243
165
  else
@@ -402,6 +324,64 @@ module Killbill
402
324
 
403
325
  # Utilities
404
326
 
327
+ # TODO Split settlements is partially implemented. Left to be done:
328
+ # * payment_source should probably be retrieved per gateway
329
+ # * amount per gateway should be retrieved from the options
330
+ def dispatch_to_gateways(operation, kb_account_id, kb_payment_id, kb_payment_transaction_id, kb_payment_method_id, amount, currency, properties, context, gateway_call_proc)
331
+ kb_transaction = get_kb_transaction(kb_payment_id, kb_payment_transaction_id, context.tenant_id)
332
+ amount_in_cents = amount.nil? ? nil : to_cents(amount, currency)
333
+
334
+ # Setup options for ActiveMerchant
335
+ options = properties_to_hash(properties)
336
+ options[:order_id] ||= kb_transaction.external_key
337
+ options[:currency] ||= currency.to_s.upcase unless currency.nil?
338
+ options[:description] ||= "Kill Bill #{operation.to_s} for #{kb_payment_transaction_id}"
339
+
340
+ # Retrieve the payment method
341
+ payment_source = get_payment_source(kb_payment_method_id, properties, options, context)
342
+
343
+ # Sanity checks
344
+ if [:authorize, :purchase, :credit].include?(operation)
345
+ raise "Unable to retrieve payment source for operation=#{operation}, kb_payment_id=#{kb_payment_id}, kb_payment_transaction_id=#{kb_payment_transaction_id}, kb_payment_method_id=#{kb_payment_method_id}" if payment_source.nil?
346
+ end
347
+
348
+ # Retrieve the previous transaction for the same operation and payment id - this is useful to detect dups for example
349
+ last_transaction = @transaction_model.send("#{operation.to_s}s_from_kb_payment_id", kb_payment_id, context.tenant_id).last
350
+
351
+ # Filter before all gateways call
352
+ before_gateways(kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options)
353
+
354
+ # Dispatch to the gateways. In most cases (non split settlements), we only dispatch to a single gateway account
355
+ gw_responses = []
356
+ responses = []
357
+ transactions = []
358
+ payment_processor_account_ids = options[:payment_processor_account_ids].nil? ? [options[:payment_processor_account_id] || :default] : options[:payment_processor_account_ids].split(',')
359
+ payment_processor_account_ids.each do |payment_processor_account_id|
360
+ # Find the gateway
361
+ gateway = lookup_gateway(payment_processor_account_id)
362
+
363
+ # Filter before each gateway call
364
+ before_gateway(gateway, kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options)
365
+
366
+ # Perform the operation in the gateway
367
+ gw_response = gateway_call_proc.call(gateway, payment_source, amount_in_cents, options)
368
+ response, transaction = save_response_and_transaction(gw_response, operation, kb_account_id, context.tenant_id, kb_payment_id, kb_payment_transaction_id, operation.upcase, amount_in_cents, currency)
369
+
370
+ # Filter after each gateway call
371
+ after_gateway(response, transaction, gw_response)
372
+
373
+ gw_responses << gw_response
374
+ responses << response
375
+ transactions << transaction
376
+ end
377
+
378
+ # Filter after all gateways call
379
+ after_gateways(responses, transactions, gw_responses)
380
+
381
+ # Merge data
382
+ merge_transaction_info_plugins(payment_processor_account_ids, responses, transactions)
383
+ end
384
+
405
385
  def get_kb_transaction(kb_payment_id, kb_payment_transaction_id, kb_tenant_id)
406
386
  kb_payment = @kb_apis.payment_api.get_payment(kb_payment_id, false, [], @kb_apis.create_context(kb_tenant_id))
407
387
  kb_transaction = kb_payment.transactions.find { |t| t.id == kb_payment_transaction_id }
@@ -410,7 +390,13 @@ module Killbill
410
390
  kb_transaction
411
391
  end
412
392
 
413
- def before_gateway(kb_transaction, transaction, payment_source, amount_in_cents, currency, options)
393
+ def before_gateways(kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options)
394
+ end
395
+
396
+ def after_gateways(response, transaction, gw_response)
397
+ end
398
+
399
+ def before_gateway(gateway, kb_transaction, last_transaction, payment_source, amount_in_cents, currency, options)
414
400
  # Can be used to implement idempotency for example: lookup the payment in the gateway
415
401
  # and pass options[:skip_gw] if the payment has already been through
416
402
  end
@@ -482,8 +468,10 @@ module Killbill
482
468
  return response, transaction
483
469
  end
484
470
 
485
- def gateway
486
- ::Killbill::Plugin::ActiveMerchant.gateway
471
+ def lookup_gateway(payment_processor_account_id=:default)
472
+ gateway = ::Killbill::Plugin::ActiveMerchant.gateways[payment_processor_account_id.to_sym]
473
+ raise "Unable to lookup gateway for payment_processor_account_id #{payment_processor_account_id}, gateways: #{::Killbill::Plugin::ActiveMerchant.gateways}" if gateway.nil?
474
+ gateway
487
475
  end
488
476
 
489
477
  def config
@@ -518,6 +506,68 @@ module Killbill
518
506
  def get_active_merchant_module
519
507
  ::ActiveMerchant::Billing::Integrations.const_get(@identifier.to_s.camelize)
520
508
  end
509
+
510
+ def merge_transaction_info_plugins(payment_processor_account_ids, responses, transactions)
511
+ result = Killbill::Plugin::Model::PaymentTransactionInfoPlugin.new
512
+ result.amount = nil
513
+ result.properties = []
514
+ result.status = :PROCESSED
515
+ # Nothing meaningful we can set here
516
+ result.first_payment_reference_id = nil
517
+ result.second_payment_reference_id = nil
518
+
519
+ responses.each_with_index do |response, idx|
520
+ t_info_plugin = response.to_transaction_info_plugin(transactions[idx])
521
+ if responses.size == 1
522
+ # We're done
523
+ return t_info_plugin
524
+ end
525
+
526
+ # Unique values
527
+ [:kb_payment_id, :kb_transaction_payment_id, :transaction_type, :currency].each do |element|
528
+ result_element = result.send(element)
529
+ t_info_plugin_element = t_info_plugin.send(element)
530
+ if result_element.nil?
531
+ result.send("#{element}=", t_info_plugin_element)
532
+ elsif result_element != t_info_plugin_element
533
+ raise "#{element.to_s} mismatch, #{result_element} != #{t_info_plugin_element}"
534
+ end
535
+ end
536
+
537
+ # Arbitrary values
538
+ [:created_date, :effective_date].each do |element|
539
+ if result.send(element).nil?
540
+ result.send("#{element}=", t_info_plugin.send(element))
541
+ end
542
+ end
543
+
544
+ t_info_plugin.properties.each do |property|
545
+ prop = Killbill::Plugin::Model::PluginProperty.new
546
+ prop.key = "#{property.key}_#{payment_processor_account_ids[idx]}"
547
+ prop.value = property.value
548
+ result.properties << prop
549
+ end
550
+
551
+ if result.amount.nil?
552
+ result.amount = t_info_plugin.amount
553
+ elsif !t_info_plugin.nil?
554
+ # TODO Adding decimals - are we losing precision?
555
+ result.amount = result.amount + t_info_plugin.amount
556
+ end
557
+
558
+ # We set an error status if we have at least one error
559
+ # TODO Does this work well with retries?
560
+ if t_info_plugin.status == :ERROR
561
+ result.status = :ERROR
562
+
563
+ # Return the first error
564
+ result.gateway_error = t_info_plugin.gateway_error if result.gateway_error.nil?
565
+ result.gateway_error_code = t_info_plugin.gateway_error_code if result.gateway_error_code.nil?
566
+ end
567
+ end
568
+
569
+ result
570
+ end
521
571
  end
522
572
  end
523
573
  end
@@ -20,16 +20,73 @@ module Killbill
20
20
  # Relies on the fact that hashes enumerate their values in the order that the corresponding keys were inserted (Ruby 1.9+)
21
21
  class BoundedLRUCache
22
22
 
23
- def initialize(proc, max_size=1000)
23
+ def initialize(proc, max_size=10000)
24
24
  @proc = proc
25
25
  @max_size = max_size
26
26
 
27
- @semaphore = Mutex.new
28
- # TODO Pre-allocate?
29
- @data = {}
27
+ if defined?(JRUBY_VERSION)
28
+ @is_jruby = true
29
+ @semaphore = nil
30
+
31
+ lru_cache = Class.new(java.util.LinkedHashMap) do
32
+ def initialize(max_size)
33
+ super(max_size, 1.0, true)
34
+ @max_size = max_size
35
+ end
36
+
37
+ # Note: renaming it to remove_eldest_entry won't work
38
+ def removeEldestEntry(eldest)
39
+ size > @max_size
40
+ end
41
+ end.new(@max_size)
42
+ @data = java.util.Collections.synchronizedMap(lru_cache)
43
+ else
44
+ @is_jruby = false
45
+ @semaphore = Mutex.new
46
+ # TODO Pre-allocate?
47
+ @data = {}
48
+ end
30
49
  end
31
50
 
32
51
  def [](key)
52
+ @is_jruby ? jruby_get(key) : ruby_get(key)
53
+ end
54
+
55
+ def []=(key, val)
56
+ @is_jruby ? jruby_set(key, val) : ruby_set(key, val)
57
+ end
58
+
59
+ # For testing
60
+
61
+ def size
62
+ @data.size
63
+ end
64
+
65
+ def keys_to_a
66
+ @is_jruby ? @data.key_set.to_a : @data.keys
67
+ end
68
+
69
+ def values_to_a
70
+ @is_jruby ? @data.values.to_a : @data.values
71
+ end
72
+
73
+ private
74
+
75
+ def jruby_get(key)
76
+ value = @data.get(key)
77
+ if value.nil?
78
+ value = @proc.call(key)
79
+ # Somebody may have beaten us to it but the mapping key -> value is constant for our purposes
80
+ jruby_set(key, value)
81
+ end
82
+ value
83
+ end
84
+
85
+ def jruby_set(key, val)
86
+ @data.put(key, val)
87
+ end
88
+
89
+ def ruby_get(key)
33
90
  @semaphore.synchronize do
34
91
  found = true
35
92
  value = @data.delete(key) { found = false }
@@ -43,7 +100,7 @@ module Killbill
43
100
  end
44
101
  end
45
102
 
46
- def []=(key, val)
103
+ def ruby_set(key, val)
47
104
  @semaphore.synchronize do
48
105
  @data.delete(key)
49
106
  @data[key] = val
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Killbill::Plugin::ActiveMerchant do
4
+
5
+ before(:all) do
6
+ @logger = Logger.new(STDOUT)
7
+ @logger.level = Logger::INFO
8
+ end
9
+
10
+ it 'should support a configuration for a single gateway' do
11
+ do_initialize!(<<-eos)
12
+ :login: admin
13
+ :password: password
14
+ :test: true
15
+ eos
16
+
17
+ do_common_checks
18
+
19
+ gw = ::Killbill::Plugin::ActiveMerchant.gateways
20
+ gw.size.should == 1
21
+ gw[:default][:login].should == 'admin'
22
+ gw[:default][:password].should == 'password'
23
+ end
24
+
25
+ it 'should support a configuration for multiple gateways with a default' do
26
+ do_initialize!(<<-eos)
27
+ - :account_id: :credentials_1
28
+ :test: true
29
+ :login: admin_1
30
+ :password: password_1
31
+ - :account_id: :credentials_2
32
+ :test: true
33
+ :login: admin_2
34
+ :password: password_2
35
+ - :account_id: :default
36
+ :test: true
37
+ :login: admin_3
38
+ :password: password_3
39
+ - :account_id: :credentials_4
40
+ :test: true
41
+ :login: admin_4
42
+ :password: password_4
43
+ eos
44
+
45
+ do_common_checks
46
+
47
+ gw = ::Killbill::Plugin::ActiveMerchant.gateways
48
+ gw.size.should == 4
49
+ [1, 2, 4].each do |i|
50
+ gw["credentials_#{i}".to_sym][:login].should == "admin_#{i}"
51
+ gw["credentials_#{i}".to_sym][:password].should == "password_#{i}"
52
+ end
53
+ gw[:default][:login].should == 'admin_3'
54
+ gw[:default][:password].should == 'password_3'
55
+ end
56
+
57
+ it 'should support a configuration for multiple gateways without a default' do
58
+ do_initialize!(<<-eos)
59
+ - :account_id: :credentials_1
60
+ :login: admin_1
61
+ :password: password_1
62
+ - :account_id: :credentials_2
63
+ :login: admin_2
64
+ :password: password_2
65
+ - :account_id: :credentials_3
66
+ :login: admin_3
67
+ :password: password_3
68
+ - :account_id: :credentials_4
69
+ :login: admin_4
70
+ :password: password_4
71
+ eos
72
+
73
+ do_common_checks
74
+
75
+ gw = ::Killbill::Plugin::ActiveMerchant.gateways
76
+ gw.size.should == 5
77
+ [1, 2, 3, 4].each do |i|
78
+ gw["credentials_#{i}".to_sym][:login].should == "admin_#{i}"
79
+ gw["credentials_#{i}".to_sym][:password].should == "password_#{i}"
80
+ end
81
+ gw[:default][:login].should == 'admin_1'
82
+ gw[:default][:password].should == 'password_1'
83
+ end
84
+
85
+ private
86
+
87
+ def do_common_checks
88
+ ::Killbill::Plugin::ActiveMerchant.config.should_not be_nil
89
+ ::Killbill::Plugin::ActiveMerchant.currency_conversions.should be_nil
90
+ ::Killbill::Plugin::ActiveMerchant.initialized.should be_true
91
+ ::Killbill::Plugin::ActiveMerchant.kb_apis.should_not be_nil
92
+ ::Killbill::Plugin::ActiveMerchant.logger.should == @logger
93
+ end
94
+
95
+ def do_initialize!(extra_config='')
96
+ Dir.mktmpdir do |dir|
97
+ file = File.new(File.join(dir, 'test.yml'), 'w+')
98
+ file.write(<<-eos)
99
+ :test:
100
+ #{extra_config}
101
+ # As defined by spec_helper.rb
102
+ :database:
103
+ :adapter: 'sqlite3'
104
+ :database: 'test.db'
105
+ eos
106
+ file.close
107
+
108
+ ::Killbill::Plugin::ActiveMerchant.initialize! Proc.new { |config| config },
109
+ :test,
110
+ @logger,
111
+ file.path,
112
+ ::Killbill::Plugin::KillbillApi.new('test', {})
113
+ end
114
+ end
115
+ end
@@ -9,7 +9,8 @@ describe Killbill::Plugin::ActiveMerchant::Utils do
9
9
  # Verify the reaper is a no-op. Management is in our hands
10
10
  pool.reaper.frequency.should be_nil
11
11
 
12
- # spec_helper created the table, so there is one active connection for that thread already
12
+ # Check-out a new connection or retrieve the one associated with the thread
13
+ ::ActiveRecord::Base.connection.should_not be_nil
13
14
  pool.active_connection?.should be_true
14
15
  pool.connections.size.should == 1
15
16
 
@@ -1,22 +1,80 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Killbill::Plugin::ActiveMerchant::Utils do
4
- it "should convert back and forth UUIDs" do
5
- uuid = SecureRandom.uuid
6
- packed = Killbill::Plugin::ActiveMerchant::Utils.compact_uuid(uuid)
4
+ it 'should convert back and forth UUIDs' do
5
+ uuid = SecureRandom.uuid
6
+ packed = Killbill::Plugin::ActiveMerchant::Utils.compact_uuid(uuid)
7
7
  unpacked = Killbill::Plugin::ActiveMerchant::Utils.unpack_uuid(packed)
8
8
  unpacked.should == uuid
9
9
  end
10
10
 
11
- it "should respect leading 0s" do
12
- uuid = "0ae18a4c-be57-44c3-84ba-a82962a2de03"
11
+ it 'should respect leading 0s' do
12
+ uuid = '0ae18a4c-be57-44c3-84ba-a82962a2de03'
13
13
  0.upto(35) do |i|
14
14
  # Skip hyphens
15
15
  next if [8, 13, 18, 23].include?(i)
16
- uuid[i] = '0'
17
- packed = Killbill::Plugin::ActiveMerchant::Utils.compact_uuid(uuid)
16
+ uuid[i] = '0'
17
+ packed = Killbill::Plugin::ActiveMerchant::Utils.compact_uuid(uuid)
18
18
  unpacked = Killbill::Plugin::ActiveMerchant::Utils.unpack_uuid(packed)
19
19
  unpacked.should == uuid
20
20
  end
21
21
  end
22
+
23
+ it 'should implement a thread-safe LRU cache' do
24
+ require 'benchmark'
25
+
26
+ runs = 2
27
+ cache_size = 50
28
+ nb_threads = 200
29
+ keys_per_thread = 1000
30
+
31
+ cache = nil
32
+ bm = Benchmark.bm do |x|
33
+ runs.times do |n|
34
+ x.report("run ##{n}:") do
35
+ cache = ::Killbill::Plugin::ActiveMerchant::Utils::BoundedLRUCache.new(Proc.new { |value| -1 }, cache_size)
36
+
37
+ threads = (0..nb_threads).map do |i|
38
+ Thread.new do
39
+ (0..keys_per_thread).each do |j|
40
+ key = 1001 * i + j
41
+ value = rand(2000)
42
+ cache[key] = value
43
+ cache[key].should satisfy { |cache_value| cache_value == -1 or cache_value == value }
44
+ end
45
+ end
46
+ end
47
+
48
+ threads.each { |thread| thread.join }
49
+ end
50
+ end
51
+ end
52
+
53
+ last_keys = cache.keys_to_a
54
+ last_values = cache.values_to_a
55
+ 0.upto(cache_size - 1) do |i|
56
+ # No overlap with test keys or values above
57
+ cache[-1 * i - 1] = -2
58
+
59
+ new_keys = cache.keys_to_a
60
+ new_values = cache.values_to_a
61
+
62
+ # Verify the changes we made
63
+ 0.upto(i) do |j|
64
+ idx = cache_size - j - 1
65
+ expected_key = -1 * (i - j) - 1
66
+ expected_value = -2
67
+
68
+ new_keys[idx].should eq(expected_key), "i=#{i}, j=#{j}, idx=#{idx}, expected_key=#{expected_key}, new_keys=#{new_keys.inspect}, last_keys=#{last_keys}"
69
+ new_values[idx].should eq(expected_value), "i=#{i}, j=#{j}, idx=#{idx}, expected_value=#{expected_value}, new_values=#{new_values.inspect}, last_values=#{last_values.inspect}"
70
+ end
71
+
72
+ # Check we didn't override older entries
73
+ new_keys.slice(0, cache_size - i - 1).should eq(last_keys.slice(i + 1, cache_size)), "i=#{i}, new_keys=#{new_keys.inspect}, last_keys=#{last_keys.inspect}"
74
+ new_values.slice(0, cache_size - i - 1).should eq(last_values.slice(i + 1, cache_size)), "i=#{i}, new_values=#{new_values.inspect}, last_values=#{last_values.inspect}"
75
+
76
+ # Check there is no change in cache size
77
+ cache.size.should == cache_size
78
+ end
79
+ end
22
80
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: killbill
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.9
4
+ version: 3.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kill Bill core team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-16 00:00:00.000000000 Z
11
+ date: 2014-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sinatra
@@ -255,6 +255,7 @@ files:
255
255
  - lib/killbill.rb
256
256
  - lib/killbill/creator.rb
257
257
  - lib/killbill/currency.rb
258
+ - lib/killbill/ext/active_merchant/jdbc_connection.rb
258
259
  - lib/killbill/ext/active_merchant/typhoeus_connection.rb
259
260
  - lib/killbill/gen/api/account.rb
260
261
  - lib/killbill/gen/api/account_api_exception.rb
@@ -404,6 +405,7 @@ files:
404
405
  - spec/killbill/base_plugin_spec.rb
405
406
  - spec/killbill/config_test.ru
406
407
  - spec/killbill/gen_conversions_spec.rb
408
+ - spec/killbill/helpers/configuration_spec.rb
407
409
  - spec/killbill/helpers/connection_spec.rb
408
410
  - spec/killbill/helpers/payment_method_spec.rb
409
411
  - spec/killbill/helpers/payment_plugin_spec.rb
@@ -455,6 +457,7 @@ test_files:
455
457
  - spec/killbill/base_plugin_spec.rb
456
458
  - spec/killbill/config_test.ru
457
459
  - spec/killbill/gen_conversions_spec.rb
460
+ - spec/killbill/helpers/configuration_spec.rb
458
461
  - spec/killbill/helpers/connection_spec.rb
459
462
  - spec/killbill/helpers/payment_method_spec.rb
460
463
  - spec/killbill/helpers/payment_plugin_spec.rb