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 +4 -4
- data/NEWS +9 -0
- data/VERSION +1 -1
- data/lib/killbill/ext/active_merchant/jdbc_connection.rb +69 -0
- data/lib/killbill/helpers/active_merchant.rb +3 -1
- data/lib/killbill/helpers/active_merchant/active_record/models/helpers.rb +1 -1
- data/lib/killbill/helpers/active_merchant/active_record/models/transaction.rb +5 -0
- data/lib/killbill/helpers/active_merchant/configuration.rb +22 -8
- data/lib/killbill/helpers/active_merchant/gateway.rb +5 -2
- data/lib/killbill/helpers/active_merchant/payment_plugin.rb +174 -124
- data/lib/killbill/helpers/active_merchant/utils.rb +62 -5
- data/spec/killbill/helpers/configuration_spec.rb +115 -0
- data/spec/killbill/helpers/connection_spec.rb +2 -1
- data/spec/killbill/helpers/utils_spec.rb +65 -7
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53241b3ff5ba7293ab885ae36942071b396cdb03
|
|
4
|
+
data.tar.gz: 6c78b3cb59a3c28140b30fdfbcc080425858f5ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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=
|
|
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 :
|
|
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
|
-
@@
|
|
19
|
-
@@
|
|
20
|
-
@@test = @@config[gateway_name][:test]
|
|
17
|
+
@@logger = logger
|
|
18
|
+
@@logger.log_level = Logger::DEBUG if (@@config[:logger] || {})[:debug]
|
|
21
19
|
|
|
22
|
-
@@
|
|
20
|
+
@@currency_conversions = @@config[:currency_conversions]
|
|
21
|
+
@@kb_apis = kb_apis
|
|
23
22
|
|
|
24
|
-
@@
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
77
|
+
last_transaction = @transaction_model.captures_from_kb_payment_id(kb_payment_id, context.tenant_id).last
|
|
128
78
|
if last_transaction.nil?
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
486
|
-
::Killbill::Plugin::ActiveMerchant.
|
|
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=
|
|
23
|
+
def initialize(proc, max_size=10000)
|
|
24
24
|
@proc = proc
|
|
25
25
|
@max_size = max_size
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
|
5
|
-
uuid
|
|
6
|
-
packed
|
|
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
|
|
12
|
-
uuid =
|
|
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]
|
|
17
|
-
packed
|
|
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.
|
|
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-
|
|
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
|