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 +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
|