killbill-stripe 0.1.0

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.
@@ -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