killbill-stripe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ configure do
2
+ # Usage: rackup -Ilib -E test
3
+ if development? or test?
4
+ Killbill::Stripe.initialize! unless Killbill::Stripe.initialized
5
+ end
6
+ end
7
+
8
+ helpers do
9
+ def plugin
10
+ Killbill::Stripe::PrivatePaymentPlugin.instance
11
+ end
12
+
13
+ def required_parameter!(parameter_name, parameter_value, message='must be specified!')
14
+ halt 400, "#{parameter_name} #{message}" if parameter_value.blank?
15
+ end
16
+ end
17
+
18
+ after do
19
+ # return DB connections to the Pool if required
20
+ ActiveRecord::Base.connection.close
21
+ end
22
+
23
+ # http://127.0.0.1:9292/plugins/killbill-stripe
24
+ get '/plugins/killbill-stripe' do
25
+ kb_account_id = request.GET['kb_account_id']
26
+ required_parameter! :kb_account_id, kb_account_id
27
+
28
+ # URL to Stripe.js
29
+ stripejs_url = Killbill::Stripe.config[:stripe][:stripejs_url] || 'https://js.stripe.com/v2/'
30
+ required_parameter! :stripejs_url, stripejs_url, 'is not configured'
31
+
32
+ # Public API key
33
+ publishable_key = Killbill::Stripe.config[:stripe][:api_publishable_key]
34
+ required_parameter! :publishable_key, publishable_key, 'is not configured'
35
+
36
+ # Redirect
37
+ success_page = params[:successPage] || '/plugins/killbill-stripe'
38
+ required_parameter! :success_page, success_page, 'is not specified'
39
+
40
+ locals = {
41
+ :stripejs_url => stripejs_url,
42
+ :publishable_key => publishable_key,
43
+ :kb_account_id => kb_account_id,
44
+ :success_page => success_page
45
+ }
46
+ erb :stripejs, :views => File.expand_path(File.dirname(__FILE__) + '/../views'), :locals => locals
47
+ end
48
+
49
+ # This is mainly for testing. Your application should redirect from the Stripe.js checkout above
50
+ # to a custom endpoint where you call the Kill Bill add payment method JAX-RS API.
51
+ # If you really want to use this endpoint, you'll have to call the Kill Bill refresh payment methods API
52
+ # to get a Kill Bill payment method id assigned.
53
+ post '/plugins/killbill-stripe' do
54
+ pm = plugin.add_payment_method params
55
+
56
+ status 201
57
+ redirect '/plugins/killbill-stripe/1.0/pms/' + pm.id.to_s
58
+ end
59
+
60
+ # curl -v http://127.0.0.1:9292/plugins/killbill-stripe/1.0/pms/1
61
+ get '/plugins/killbill-stripe/1.0/pms/:id', :provides => 'json' do
62
+ if pm = Killbill::Stripe::StripePaymentMethod.find_by_id(params[:id].to_i)
63
+ pm.to_json
64
+ else
65
+ status 404
66
+ end
67
+ end
68
+
69
+ # curl -v http://127.0.0.1:9292/plugins/killbill-stripe/1.0/transactions/1
70
+ get '/plugins/killbill-stripe/1.0/transactions/:id', :provides => 'json' do
71
+ if transaction = Killbill::Stripe::StripeTransaction.find_by_id(params[:id].to_i)
72
+ transaction.to_json
73
+ else
74
+ status 404
75
+ end
76
+ end
@@ -0,0 +1,38 @@
1
+ require 'logger'
2
+
3
+ module Killbill::Stripe
4
+ mattr_reader :logger
5
+ mattr_reader :config
6
+ mattr_reader :gateway
7
+ mattr_reader :kb_apis
8
+ mattr_reader :stripe_payment_description
9
+ mattr_reader :initialized
10
+ mattr_reader :test
11
+
12
+ def self.initialize!(logger=Logger.new(STDOUT), conf_dir=File.expand_path('../../../', File.dirname(__FILE__)), kb_apis = nil)
13
+ @@logger = logger
14
+ @@kb_apis = kb_apis
15
+
16
+ config_file = "#{conf_dir}/stripe.yml"
17
+ @@config = Properties.new(config_file)
18
+ @@config.parse!
19
+ @@test = @@config[:stripe][:test]
20
+
21
+ @@logger.log_level = Logger::DEBUG if (@@config[:logger] || {})[:debug]
22
+
23
+ @@stripe_payment_description = @@config[:stripe][:payment_description]
24
+
25
+ @@gateway = Killbill::Stripe::Gateway.from_config(@@config[:stripe])
26
+
27
+ if defined?(JRUBY_VERSION)
28
+ # See https://github.com/jruby/activerecord-jdbc-adapter/issues/302
29
+ require 'jdbc/mysql'
30
+ Jdbc::MySQL.load_driver(:require) if Jdbc::MySQL.respond_to?(:load_driver)
31
+ end
32
+
33
+ ActiveRecord::Base.establish_connection(@@config[:database])
34
+ ActiveRecord::Base.logger = @@logger
35
+
36
+ @@initialized = true
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ module Killbill::Stripe
2
+ class Properties
3
+ def initialize(file = 'stripe.yml')
4
+ @config_file = Pathname.new(file).expand_path
5
+ end
6
+
7
+ def parse!
8
+ raise "#{@config_file} is not a valid file" unless @config_file.file?
9
+ @config = YAML.load_file(@config_file.to_s)
10
+ validate!
11
+ end
12
+
13
+ def [](key)
14
+ @config[key]
15
+ end
16
+
17
+ private
18
+
19
+ def validate!
20
+ raise "Bad configuration for Stripe plugin. Config is #{@config.inspect}" if @config.blank? || !@config[:stripe] || !@config[:stripe][:api_secret_key]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,182 @@
1
+ module Killbill::Stripe
2
+ class StripePaymentMethod < ActiveRecord::Base
3
+ attr_accessible :kb_account_id,
4
+ :kb_payment_method_id,
5
+ :stripe_customer_id,
6
+ :stripe_card_id_or_token,
7
+ :cc_first_name,
8
+ :cc_last_name,
9
+ :cc_type,
10
+ :cc_exp_month,
11
+ :cc_exp_year,
12
+ :cc_last_4,
13
+ :address1,
14
+ :address2,
15
+ :city,
16
+ :state,
17
+ :zip,
18
+ :country
19
+
20
+ alias_attribute :external_payment_method_id, :stripe_card_id_or_token
21
+
22
+ def self.from_kb_account_id(kb_account_id)
23
+ find_all_by_kb_account_id_and_is_deleted(kb_account_id, false)
24
+ end
25
+
26
+ def self.stripe_customer_id_from_kb_account_id(kb_account_id)
27
+ pms = from_kb_account_id(kb_account_id)
28
+ return nil if pms.empty?
29
+
30
+ stripe_customer_ids = Set.new
31
+ pms.each { |pm| stripe_customer_ids << pm.stripe_customer_id }
32
+ raise "No Stripe customer id found for account #{kb_account_id}" if stripe_customer_ids.empty?
33
+ raise "Killbill account #{kb_account_id} mapping to multiple Stripe customers: #{stripe_customer_ids}" if stripe_customer_ids.size > 1
34
+ stripe_customer_ids.first
35
+ end
36
+
37
+ def self.from_kb_payment_method_id(kb_payment_method_id)
38
+ payment_methods = find_all_by_kb_payment_method_id_and_is_deleted(kb_payment_method_id, false)
39
+ raise "No payment method found for payment method #{kb_payment_method_id}" if payment_methods.empty?
40
+ raise "Killbill payment method mapping to multiple active Stripe tokens for payment method #{kb_payment_method_id}" if payment_methods.size > 1
41
+ payment_methods[0]
42
+ end
43
+
44
+ def self.mark_as_deleted!(kb_payment_method_id)
45
+ payment_method = from_kb_payment_method_id(kb_payment_method_id)
46
+ payment_method.is_deleted = true
47
+ payment_method.save!
48
+ end
49
+
50
+ # VisibleForTesting
51
+ def self.search_query(search_key, offset = nil, limit = nil)
52
+ t = self.arel_table
53
+
54
+ # Exact match for kb_account_id, kb_payment_method_id, stripe_customer_id, stripe_card_id_or_token, cc_type, cc_exp_month,
55
+ # cc_exp_year, cc_last_4, state and zip, partial match for the reset
56
+ where_clause = t[:kb_account_id].eq(search_key)
57
+ .or(t[:kb_payment_method_id].eq(search_key))
58
+ .or(t[:stripe_customer_id].eq(search_key))
59
+ .or(t[:stripe_card_id_or_token].eq(search_key))
60
+ .or(t[:cc_type].eq(search_key))
61
+ .or(t[:state].eq(search_key))
62
+ .or(t[:zip].eq(search_key))
63
+ .or(t[:cc_first_name].matches("%#{search_key}%"))
64
+ .or(t[:cc_last_name].matches("%#{search_key}%"))
65
+ .or(t[:address1].matches("%#{search_key}%"))
66
+ .or(t[:address2].matches("%#{search_key}%"))
67
+ .or(t[:city].matches("%#{search_key}%"))
68
+ .or(t[:country].matches("%#{search_key}%"))
69
+
70
+ # Coming from Kill Bill, search_key will always be a String. Check to see if it represents a numeric for numeric-only fields
71
+ if search_key.is_a?(Numeric) or search_key.to_s =~ /\A[-+]?[0-9]*\.?[0-9]+\Z/
72
+ where_clause = where_clause.or(t[:cc_exp_month].eq(search_key))
73
+ .or(t[:cc_exp_year].eq(search_key))
74
+ .or(t[:cc_last_4].eq(search_key))
75
+ end
76
+
77
+ # Remove garbage payment methods (added in the plugin but not reconcilied with Kill Bill yet)
78
+ query = t.where(where_clause)
79
+ .where(t[:kb_payment_method_id].not_eq(nil))
80
+ .order(t[:id])
81
+
82
+ if offset.blank? and limit.blank?
83
+ # true is for count distinct
84
+ query.project(t[:id].count(true))
85
+ else
86
+ query.skip(offset) unless offset.blank?
87
+ query.take(limit) unless limit.blank?
88
+ query.project(t[Arel.star])
89
+ # Not chainable
90
+ query.distinct
91
+ end
92
+ query
93
+ end
94
+
95
+ def self.search(search_key, offset = 0, limit = 100)
96
+ pagination = Killbill::Plugin::Model::Pagination.new
97
+ pagination.current_offset = offset
98
+ pagination.total_nb_records = self.count_by_sql(self.search_query(search_key))
99
+ pagination.max_nb_records = self.count
100
+ pagination.next_offset = (!pagination.total_nb_records.nil? && offset + limit >= pagination.total_nb_records) ? nil : offset + limit
101
+ # Reduce the limit if the specified value is larger than the number of records
102
+ actual_limit = [pagination.max_nb_records, limit].min
103
+ pagination.iterator = StreamyResultSet.new(actual_limit) do |offset,limit|
104
+ self.find_by_sql(self.search_query(search_key, offset, limit))
105
+ .map(&:to_payment_method_response)
106
+ end
107
+ pagination
108
+ end
109
+
110
+ def to_payment_method_response
111
+ properties = []
112
+ properties << create_pm_kv_info('token', external_payment_method_id)
113
+ properties << create_pm_kv_info('ccName', cc_name)
114
+ properties << create_pm_kv_info('ccType', cc_type)
115
+ properties << create_pm_kv_info('ccExpirationMonth', cc_exp_month)
116
+ properties << create_pm_kv_info('ccExpirationYear', cc_exp_year)
117
+ properties << create_pm_kv_info('ccLast4', cc_last_4)
118
+ properties << create_pm_kv_info('address1', address1)
119
+ properties << create_pm_kv_info('address2', address2)
120
+ properties << create_pm_kv_info('city', city)
121
+ properties << create_pm_kv_info('state', state)
122
+ properties << create_pm_kv_info('zip', zip)
123
+ properties << create_pm_kv_info('country', country)
124
+
125
+ pm_plugin = Killbill::Plugin::Model::PaymentMethodPlugin.new
126
+ pm_plugin.kb_payment_method_id = kb_payment_method_id
127
+ pm_plugin.external_payment_method_id = external_payment_method_id
128
+ pm_plugin.is_default_payment_method = is_default
129
+ pm_plugin.properties = properties
130
+ pm_plugin.type = 'CreditCard'
131
+ pm_plugin.cc_name = cc_name
132
+ pm_plugin.cc_type = cc_type
133
+ pm_plugin.cc_expiration_month = cc_exp_month
134
+ pm_plugin.cc_expiration_year = cc_exp_year
135
+ pm_plugin.cc_last4 = cc_last_4
136
+ pm_plugin.address1 = address1
137
+ pm_plugin.address2 = address2
138
+ pm_plugin.city = city
139
+ pm_plugin.state = state
140
+ pm_plugin.zip = zip
141
+ pm_plugin.country = country
142
+
143
+ pm_plugin
144
+ end
145
+
146
+ def to_payment_method_info_response
147
+ pm_info_plugin = Killbill::Plugin::Model::PaymentMethodInfoPlugin.new
148
+ pm_info_plugin.account_id = kb_account_id
149
+ pm_info_plugin.payment_method_id = kb_payment_method_id
150
+ pm_info_plugin.is_default = is_default
151
+ pm_info_plugin.external_payment_method_id = external_payment_method_id
152
+ pm_info_plugin
153
+ end
154
+
155
+ def is_default
156
+ # There is a concept of default credit card in Stripe but it's not exposed by the API
157
+ # Return false to let Kill Bill knows it's authoritative on the matter
158
+ false
159
+ end
160
+
161
+ def cc_name
162
+ if cc_first_name and cc_last_name
163
+ "#{cc_first_name} #{cc_last_name}"
164
+ elsif cc_first_name
165
+ cc_first_name
166
+ elsif cc_last_name
167
+ cc_last_name
168
+ else
169
+ nil
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def create_pm_kv_info(key, value)
176
+ prop = Killbill::Plugin::Model::PaymentMethodKVInfo.new
177
+ prop.key = key
178
+ prop.value = value
179
+ prop
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,242 @@
1
+ module Killbill::Stripe
2
+ class StripeResponse < ActiveRecord::Base
3
+ has_one :stripe_transaction
4
+ attr_accessible :api_call,
5
+ :kb_payment_id,
6
+ :message,
7
+ :authorization,
8
+ :fraud_review,
9
+ :test,
10
+ :params_id,
11
+ :params_object,
12
+ :params_created,
13
+ :params_livemode,
14
+ :params_paid,
15
+ :params_amount,
16
+ :params_currency,
17
+ :params_refunded,
18
+ :params_card_id,
19
+ :params_card_object,
20
+ :params_card_last4,
21
+ :params_card_type,
22
+ :params_card_exp_month,
23
+ :params_card_exp_year,
24
+ :params_card_fingerprint,
25
+ :params_card_customer,
26
+ :params_card_country,
27
+ :params_card_name,
28
+ :params_card_address_line1,
29
+ :params_card_address_line2,
30
+ :params_card_address_city,
31
+ :params_card_address_state,
32
+ :params_card_address_zip,
33
+ :params_card_address_country,
34
+ :params_card_cvc_check,
35
+ :params_card_address_line1_check,
36
+ :params_card_address_zip_check,
37
+ :params_captured,
38
+ :params_refunds,
39
+ :params_balance_transaction,
40
+ :params_failure_message,
41
+ :params_failure_code,
42
+ :params_amount_refunded,
43
+ :params_customer,
44
+ :params_invoice,
45
+ :params_description,
46
+ :params_dispute,
47
+ :params_metadata,
48
+ :params_error_type,
49
+ :params_error_message,
50
+ :avs_result_code,
51
+ :avs_result_message,
52
+ :avs_result_street_match,
53
+ :avs_result_postal_match,
54
+ :cvv_result_code,
55
+ :cvv_result_message,
56
+ :success
57
+
58
+ def stripe_txn_id
59
+ params_id || authorization
60
+ end
61
+
62
+ def self.from_response(api_call, kb_payment_id, response)
63
+ StripeResponse.new({
64
+ :api_call => api_call,
65
+ :kb_payment_id => kb_payment_id,
66
+ :message => response.message,
67
+ :authorization => response.authorization,
68
+ :fraud_review => response.fraud_review?,
69
+ :test => response.test?,
70
+ :params_id => extract(response, "id"),
71
+ :params_object => extract(response, "object"),
72
+ :params_created => extract(response, "created"),
73
+ :params_livemode => extract(response, "livemode"),
74
+ :params_paid => extract(response, "paid"),
75
+ :params_amount => extract(response, "amount"),
76
+ :params_currency => extract(response, "currency"),
77
+ :params_refunded => extract(response, "refunded"),
78
+ :params_card_id => extract(response, "card", "id"),
79
+ :params_card_object => extract(response, "card", "object"),
80
+ :params_card_last4 => extract(response, "card", "last4"),
81
+ :params_card_type => extract(response, "card", "type"),
82
+ :params_card_exp_month => extract(response, "card", "exp_month"),
83
+ :params_card_exp_year => extract(response, "card", "exp_year"),
84
+ :params_card_fingerprint => extract(response, "card", "fingerprint"),
85
+ :params_card_customer => extract(response, "card", "customer"),
86
+ :params_card_country => extract(response, "card", "country"),
87
+ :params_card_name => extract(response, "card", "name"),
88
+ :params_card_address_line1 => extract(response, "card", "address_line1"),
89
+ :params_card_address_line2 => extract(response, "card", "address_line2"),
90
+ :params_card_address_city => extract(response, "card", "address_city"),
91
+ :params_card_address_state => extract(response, "card", "address_state"),
92
+ :params_card_address_zip => extract(response, "card", "address_zip"),
93
+ :params_card_address_country => extract(response, "card", "address_country"),
94
+ :params_card_cvc_check => extract(response, "card", "cvc_check"),
95
+ :params_card_address_line1_check => extract(response, "card", "address_line1_check"),
96
+ :params_card_address_zip_check => extract(response, "card", "address_zip_check"),
97
+ :params_captured => extract(response, "captured"),
98
+ :params_refunds => extract(response, "refunds"),
99
+ :params_balance_transaction => extract(response, "balance_transaction"),
100
+ :params_failure_message => extract(response, "failure_message"),
101
+ :params_failure_code => extract(response, "failure_code"),
102
+ :params_amount_refunded => extract(response, "amount_refunded"),
103
+ :params_customer => extract(response, "customer"),
104
+ :params_invoice => extract(response, "invoice"),
105
+ :params_description => extract(response, "description"),
106
+ :params_dispute => extract(response, "dispute"),
107
+ :params_metadata => extract(response, "metadata"),
108
+ :params_error_type => extract(response, "error", "type"),
109
+ :params_error_message => extract(response, "error", "message"),
110
+ :avs_result_code => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.code : response.avs_result['code'],
111
+ :avs_result_message => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.message : response.avs_result['message'],
112
+ :avs_result_street_match => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.street_match : response.avs_result['street_match'],
113
+ :avs_result_postal_match => response.avs_result.kind_of?(ActiveMerchant::Billing::AVSResult) ? response.avs_result.postal_match : response.avs_result['postal_match'],
114
+ :cvv_result_code => response.cvv_result.kind_of?(ActiveMerchant::Billing::CVVResult) ? response.cvv_result.code : response.cvv_result['code'],
115
+ :cvv_result_message => response.cvv_result.kind_of?(ActiveMerchant::Billing::CVVResult) ? response.cvv_result.message : response.cvv_result['message'],
116
+ :success => response.success?
117
+ })
118
+ end
119
+
120
+ def to_payment_response
121
+ to_killbill_response :payment
122
+ end
123
+
124
+ def to_refund_response
125
+ to_killbill_response :refund
126
+ end
127
+
128
+ # VisibleForTesting
129
+ def self.search_query(api_call, search_key, offset = nil, limit = nil)
130
+ t = self.arel_table
131
+
132
+ # Exact matches only
133
+ where_clause = t[:authorization].eq(search_key)
134
+ .or(t[:params_id].eq(search_key))
135
+ .or(t[:params_card_id].eq(search_key))
136
+
137
+ # Only search successful payments and refunds
138
+ where_clause = where_clause.and(t[:api_call].eq(api_call))
139
+ .and(t[:success].eq(true))
140
+
141
+ query = t.where(where_clause)
142
+ .order(t[:id])
143
+
144
+ if offset.blank? and limit.blank?
145
+ # true is for count distinct
146
+ query.project(t[:id].count(true))
147
+ else
148
+ query.skip(offset) unless offset.blank?
149
+ query.take(limit) unless limit.blank?
150
+ query.project(t[Arel.star])
151
+ # Not chainable
152
+ query.distinct
153
+ end
154
+ query
155
+ end
156
+
157
+ def self.search(search_key, offset = 0, limit = 100, type = :payment)
158
+ api_call = type == :payment ? 'charge' : 'refund'
159
+ pagination = Killbill::Plugin::Model::Pagination.new
160
+ pagination.current_offset = offset
161
+ pagination.total_nb_records = self.count_by_sql(self.search_query(api_call, search_key))
162
+ pagination.max_nb_records = self.where(:api_call => api_call, :success => true).count
163
+ pagination.next_offset = (!pagination.total_nb_records.nil? && offset + limit >= pagination.total_nb_records) ? nil : offset + limit
164
+ # Reduce the limit if the specified value is larger than the number of records
165
+ actual_limit = [pagination.max_nb_records, limit].min
166
+ pagination.iterator = StreamyResultSet.new(actual_limit) do |offset,limit|
167
+ self.find_by_sql(self.search_query(api_call, search_key, offset, limit))
168
+ .map { |x| type == :payment ? x.to_payment_response : x.to_refund_response }
169
+ end
170
+ pagination
171
+ end
172
+
173
+ private
174
+
175
+ def to_killbill_response(type)
176
+ if stripe_transaction.nil?
177
+ amount_in_cents = nil
178
+ currency = nil
179
+ created_date = created_at
180
+ first_payment_reference_id = nil
181
+ second_payment_reference_id = nil
182
+ else
183
+ amount_in_cents = stripe_transaction.amount_in_cents
184
+ currency = stripe_transaction.currency
185
+ created_date = stripe_transaction.created_at
186
+ first_reference_id = params_balance_transaction
187
+ second_reference_id = stripe_transaction.stripe_txn_id
188
+ end
189
+
190
+ unless params_created.blank?
191
+ effective_date = DateTime.strptime(params_created.to_s, "%s") rescue nil
192
+ end
193
+ effective_date ||= created_date
194
+ gateway_error = message || params_error_message
195
+ gateway_error_code = params_error_type
196
+
197
+ if type == :payment
198
+ p_info_plugin = Killbill::Plugin::Model::PaymentInfoPlugin.new
199
+ p_info_plugin.kb_payment_id = kb_payment_id
200
+ p_info_plugin.amount = Money.new(amount_in_cents, currency).to_d if currency
201
+ p_info_plugin.currency = currency
202
+ p_info_plugin.created_date = created_date
203
+ p_info_plugin.effective_date = effective_date
204
+ p_info_plugin.status = (success ? :PROCESSED : :ERROR)
205
+ p_info_plugin.gateway_error = gateway_error
206
+ p_info_plugin.gateway_error_code = gateway_error_code
207
+ p_info_plugin.first_payment_reference_id = first_reference_id
208
+ p_info_plugin.second_payment_reference_id = second_reference_id
209
+ p_info_plugin
210
+ else
211
+ r_info_plugin = Killbill::Plugin::Model::RefundInfoPlugin.new
212
+ r_info_plugin.kb_payment_id = kb_payment_id
213
+ r_info_plugin.amount = Money.new(amount_in_cents, currency).to_d if currency
214
+ r_info_plugin.currency = currency
215
+ r_info_plugin.created_date = created_date
216
+ r_info_plugin.effective_date = effective_date
217
+ r_info_plugin.status = (success ? :PROCESSED : :ERROR)
218
+ r_info_plugin.gateway_error = gateway_error
219
+ r_info_plugin.gateway_error_code = gateway_error_code
220
+ r_info_plugin.first_refund_reference_id = first_reference_id
221
+ r_info_plugin.second_refund_reference_id = second_reference_id
222
+ r_info_plugin
223
+ end
224
+ end
225
+
226
+ def self.extract(response, key1, key2=nil, key3=nil)
227
+ return nil if response.nil? || response.params.nil?
228
+ level1 = response.params[key1]
229
+
230
+ if level1.nil? or (key2.nil? and key3.nil?)
231
+ return level1
232
+ end
233
+ level2 = level1[key2]
234
+
235
+ if level2.nil? or key3.nil?
236
+ return level2
237
+ else
238
+ return level2[key3]
239
+ end
240
+ end
241
+ end
242
+ end