killbill 3.1.9 → 3.1.10

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