vaulted_billing 1.0.2 → 1.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.
- data/README.md +44 -34
- data/lib/vaulted_billing/chainable_hash.rb +7 -0
- data/lib/vaulted_billing/configuration.rb +13 -3
- data/lib/vaulted_billing/errors.rb +4 -0
- data/lib/vaulted_billing/gateway.rb +4 -4
- data/lib/vaulted_billing/gateways/authorize_net_cim.rb +60 -36
- data/lib/vaulted_billing/gateways/ipcommerce.rb +372 -0
- data/lib/vaulted_billing/gateways/nmi_customer_vault.rb +43 -24
- data/lib/vaulted_billing/http.rb +157 -0
- data/lib/vaulted_billing/version.rb +1 -1
- data/lib/vaulted_billing.rb +5 -2
- metadata +139 -138
- data/lib/vaulted_billing/https_interface.rb +0 -127
@@ -0,0 +1,372 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
require 'multi_xml'
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module VaultedBilling
|
6
|
+
module Gateways
|
7
|
+
##
|
8
|
+
# Interface to IPCommerce.
|
9
|
+
#
|
10
|
+
# == Example
|
11
|
+
#
|
12
|
+
# VaultedBilling::Gateways::IPCommerce.new(:username => 'identity-token', :service_key_store => XXX).tap do |ipc|
|
13
|
+
# customer = ipc.add_customer(Customer.new)
|
14
|
+
# credit_card = ipc.add_credit_card(customer, CreditCard.new)
|
15
|
+
# ipc.purchase(customer, credit_card, 10.00)
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
class Ipcommerce
|
19
|
+
include VaultedBilling::Gateway
|
20
|
+
|
21
|
+
Companies = {
|
22
|
+
2 => /^4\d{12}(\d{3})?$/, # Visa
|
23
|
+
3 => /^(5[1-5]\d{4}|677189)\d{10}$/, # MasterCard
|
24
|
+
4 => /^3[47]\d{13}$/, # American Express
|
25
|
+
5 => /^3(0[0-5]|[68]\d)\d{11}$/, # Diners Club
|
26
|
+
6 => /^(6011|65\d{2}|64[4-9]\d)\d{12}|(62\d{14})$/, # Discover
|
27
|
+
7 => /^35(28|29|[3-8]\d)\d{12}$/ # JCB
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
class ServiceKeyStore
|
31
|
+
UnavailableKeyError = Class.new(VaultedBilling::CredentialError)
|
32
|
+
|
33
|
+
attr_reader :identity_token
|
34
|
+
|
35
|
+
def initialize(identity_token)
|
36
|
+
@identity_token = identity_token
|
37
|
+
@expires_at = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def key
|
41
|
+
renew! unless valid?
|
42
|
+
read_key || raise(UnavailableKeyError, 'A service key could not be retrieved for this session.')
|
43
|
+
end
|
44
|
+
|
45
|
+
def read_key
|
46
|
+
@key
|
47
|
+
end
|
48
|
+
|
49
|
+
def store_key(key)
|
50
|
+
@key = key
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid?
|
54
|
+
@expires_at && (@expires_at > Time.now + 5.minutes)
|
55
|
+
end
|
56
|
+
|
57
|
+
def before_request(request)
|
58
|
+
request.delete "accept"
|
59
|
+
end
|
60
|
+
|
61
|
+
def renew!
|
62
|
+
@expires_at = Time.now + 30.minutes
|
63
|
+
@key = http.get.body.try(:[], 1...-1)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def http
|
68
|
+
@request ||= begin
|
69
|
+
urls = ["https://cws-01.cert.ipcommerce.com/REST/2.0.15/SvcInfo/token",
|
70
|
+
"https://cws-02.cert.ipcommerce.com/REST/2.0.15/SvcInfo/token"]
|
71
|
+
VaultedBilling::HTTP.new(self, urls, {
|
72
|
+
:headers => {'Content-Type' => 'application/json'},
|
73
|
+
:before_request => :before_request,
|
74
|
+
:basic_auth => [@identity_token, ""]
|
75
|
+
})
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_reader :service_key_store
|
81
|
+
|
82
|
+
def initialize(options = {})
|
83
|
+
@identity_token = options[:username] || VaultedBilling.config.ipcommerce.username
|
84
|
+
@raw_options = options[:raw_options] || VaultedBilling.config.ipcommerce.raw_options
|
85
|
+
@test_mode = options.has_key?(:test) ? options[:test] : (VaultedBilling.config.ipcommerce.test_mode || VaultedBilling.config.test_mode)
|
86
|
+
@application_id = options[:application_id] || @raw_options["application_id"]
|
87
|
+
@service_id = options[:service_id] || @raw_options["service_id"]
|
88
|
+
@service_key_store = options[:service_key_store] || ServiceKeyStore.new(@identity_token)
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
##
|
93
|
+
# A stub, since the IP Commerce only generates tokens during
|
94
|
+
# successful authorization transactions.
|
95
|
+
#
|
96
|
+
def add_customer(customer)
|
97
|
+
respond_with customer.to_vaulted_billing
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# A stub, since the IP Commerce only generates tokens during
|
102
|
+
# successful authorization transactions.
|
103
|
+
#
|
104
|
+
#--
|
105
|
+
# TODO: If necessary, this may be implemented by Authorizing the given card for $1.00, then voiding immediately.
|
106
|
+
#++
|
107
|
+
#
|
108
|
+
def add_customer_credit_card(customer, credit_card)
|
109
|
+
respond_with credit_card.to_vaulted_billing
|
110
|
+
end
|
111
|
+
|
112
|
+
def authorize(customer, credit_card, amount, options = {})
|
113
|
+
data = {
|
114
|
+
"__type" => "AuthorizeTransaction:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Rest",
|
115
|
+
:ApplicationProfileId => @application_id,
|
116
|
+
:MerchantProfileId => options[:merchant_profile_id],
|
117
|
+
:Transaction => {
|
118
|
+
:"__type" => "BankcardTransaction:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Bankcard",
|
119
|
+
:TransactionData => {
|
120
|
+
:Amount => "%.2f" % amount,
|
121
|
+
:CurrencyCode => 4,
|
122
|
+
:TransactionDateTime => Time.now.xmlschema,
|
123
|
+
:CustomerPresent => 0,
|
124
|
+
:EntryMode => 1,
|
125
|
+
:GoodsType => 0,
|
126
|
+
:IndustryType => 2,
|
127
|
+
:SignatureCaptured => false,
|
128
|
+
:OrderNumber => options[:order_id] || generate_order_number
|
129
|
+
},
|
130
|
+
:TenderData => {
|
131
|
+
:CardData => {
|
132
|
+
:CardholderName => nil,
|
133
|
+
:CardType => self.class.credit_card_type_id(credit_card.card_number),
|
134
|
+
:Expire => credit_card.expires_on.try(:strftime, "%m%y"),
|
135
|
+
:PAN => credit_card.card_number
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
response = http(options[:workflow_id] || @service_id).post(data)
|
142
|
+
transaction = new_transaction_from_response(response)
|
143
|
+
respond_with(transaction,
|
144
|
+
response,
|
145
|
+
:success => (transaction.code == 1))
|
146
|
+
end
|
147
|
+
|
148
|
+
def capture(transaction_id, amount, options = {})
|
149
|
+
data = {
|
150
|
+
:"__type" => "Capture:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Rest",
|
151
|
+
:ApplicationProfileId => @application_id,
|
152
|
+
:DifferenceData => {
|
153
|
+
:"__type" => "BankcardCapture:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Bankcard",
|
154
|
+
:TransactionId => transaction_id,
|
155
|
+
:Addendum => nil,
|
156
|
+
:Amount => "%.2f" % amount
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
response = http(options[:workflow_id] || @service_id, transaction_id).put(data)
|
161
|
+
transaction = new_transaction_from_response(response)
|
162
|
+
respond_with(transaction,
|
163
|
+
response,
|
164
|
+
:success => (transaction.code == 1))
|
165
|
+
end
|
166
|
+
|
167
|
+
def purchase(customer, credit_card, amount, options = {})
|
168
|
+
data = {
|
169
|
+
"__type" => "AuthorizeAndCaptureTransaction:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Rest",
|
170
|
+
:ApplicationProfileId => @application_id,
|
171
|
+
:MerchantProfileId => options[:merchant_profile_id],
|
172
|
+
:Transaction => {
|
173
|
+
:"__type" => "BankcardTransaction:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Bankcard",
|
174
|
+
:TransactionData => {
|
175
|
+
:Amount => "%.2f" % amount,
|
176
|
+
:CurrencyCode => 4,
|
177
|
+
:TransactionDateTime => Time.now.xmlschema,
|
178
|
+
:CustomerPresent => 0,
|
179
|
+
:EmployeeId => options[:employee_id],
|
180
|
+
:EntryMode => 1,
|
181
|
+
:GoodsType => 0,
|
182
|
+
:IndustryType => 2,
|
183
|
+
:OrderNumber => options[:order_id] || generate_order_number,
|
184
|
+
:SignatureCaptured => false
|
185
|
+
},
|
186
|
+
:TenderData => {
|
187
|
+
:CardData => {
|
188
|
+
:CardholderName => nil,
|
189
|
+
:CardType => self.class.credit_card_type_id(credit_card.card_number),
|
190
|
+
:Expire => credit_card.expires_on.try(:strftime, "%m%y"),
|
191
|
+
:PAN => credit_card.card_number
|
192
|
+
}
|
193
|
+
}
|
194
|
+
}
|
195
|
+
}
|
196
|
+
response = http(options[:workflow_id] || @service_id).post(data)
|
197
|
+
transaction = new_transaction_from_response(response)
|
198
|
+
respond_with(transaction,
|
199
|
+
response,
|
200
|
+
:success => (transaction.code == 1))
|
201
|
+
end
|
202
|
+
|
203
|
+
def refund(transaction_id, amount, options = {})
|
204
|
+
data = {
|
205
|
+
:"__type" => "ReturnById:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Rest",
|
206
|
+
:ApplicationProfileId => @application_id,
|
207
|
+
:MerchantProfileId => options[:merchant_profile_id],
|
208
|
+
:DifferenceData => {
|
209
|
+
:"__type" => "BankcardReturn:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Bankcard",
|
210
|
+
:TransactionId => transaction_id,
|
211
|
+
:Addendum => nil,
|
212
|
+
:Amount => "%.2f" % amount
|
213
|
+
}
|
214
|
+
}
|
215
|
+
|
216
|
+
response = http(options[:workflow_id] || @service_id).post(data)
|
217
|
+
transaction = new_transaction_from_response(response)
|
218
|
+
respond_with(transaction,
|
219
|
+
response,
|
220
|
+
:success => (transaction.code == 1))
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# A stub, since the IP Commerce only generates tokens during
|
225
|
+
# successful authorization transactions.
|
226
|
+
#
|
227
|
+
def remove_customer(customer)
|
228
|
+
respond_with customer.to_vaulted_billing
|
229
|
+
end
|
230
|
+
|
231
|
+
##
|
232
|
+
# A stub, since the IP Commerce only generates tokens during
|
233
|
+
# successful authorization transactions.
|
234
|
+
#
|
235
|
+
def remove_customer_credit_card(customer, credit_card)
|
236
|
+
respond_with credit_card.to_vaulted_billing
|
237
|
+
end
|
238
|
+
|
239
|
+
##
|
240
|
+
# A stub, since the IP Commerce only generates tokens during
|
241
|
+
# successful authorization transactions.
|
242
|
+
#
|
243
|
+
def update_customer(customer)
|
244
|
+
respond_with customer.to_vaulted_billing
|
245
|
+
end
|
246
|
+
|
247
|
+
##
|
248
|
+
# A stub, since the IP Commerce only generates tokens during
|
249
|
+
# successful authorization transactions.
|
250
|
+
#
|
251
|
+
def update_customer_credit_card(customer, credit_card)
|
252
|
+
respond_with credit_card.to_vaulted_billing
|
253
|
+
end
|
254
|
+
|
255
|
+
def void(transaction_id, options = {})
|
256
|
+
data = {
|
257
|
+
:"__type" => "Undo:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Rest",
|
258
|
+
:ApplicationProfileId => @application_id,
|
259
|
+
:DifferenceData => {
|
260
|
+
:"__type" => "BankcardUndo:http://schemas.ipcommerce.com/CWS/v2.0/Transactions/Bankcard",
|
261
|
+
:TransactionId => transaction_id,
|
262
|
+
:Addendum => nil
|
263
|
+
}
|
264
|
+
}
|
265
|
+
|
266
|
+
response = http(options[:workflow_id] || @service_id, transaction_id).put(data)
|
267
|
+
transaction = new_transaction_from_response(response)
|
268
|
+
respond_with(transaction,
|
269
|
+
response,
|
270
|
+
:success => (transaction.code == 1))
|
271
|
+
end
|
272
|
+
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
|
277
|
+
##
|
278
|
+
# Returns the name of the card company based on the given number, or
|
279
|
+
# nil if it is unrecognized.
|
280
|
+
#
|
281
|
+
# This was heavily lifted from ActiveMerchant.
|
282
|
+
#
|
283
|
+
def self.credit_card_type_id(number)
|
284
|
+
Companies.each do |company, pattern|
|
285
|
+
return company if number =~ pattern
|
286
|
+
end
|
287
|
+
return 1
|
288
|
+
end
|
289
|
+
|
290
|
+
|
291
|
+
def generate_order_number
|
292
|
+
(Time.now.to_f * 100000).to_i.to_s(36) + rand(60000000).to_s(36)
|
293
|
+
end
|
294
|
+
|
295
|
+
def http(*params)
|
296
|
+
urls = %W(
|
297
|
+
https://cws-01.cert.ipcommerce.com/REST/2.0.15/Txn/#{params.join('/')}
|
298
|
+
https://cws-02.cert.ipcommerce.com/REST/2.0.15/Txn/#{params.join('/')}
|
299
|
+
)
|
300
|
+
VaultedBilling::HTTP.new(self, urls, {
|
301
|
+
:headers => { 'Content-Type' => 'application/json' },
|
302
|
+
:before_request => :before_request,
|
303
|
+
:basic_auth => [@service_key_store.key, ""],
|
304
|
+
:on_success => :on_success
|
305
|
+
})
|
306
|
+
end
|
307
|
+
|
308
|
+
def before_request(request)
|
309
|
+
request.body = MultiJson.encode(request.body)
|
310
|
+
# request.delete "accept"
|
311
|
+
end
|
312
|
+
|
313
|
+
def on_success(response)
|
314
|
+
response.body = decode_body(response.body) || {}
|
315
|
+
response.success = [1, 2].include?(response.body['Status'])
|
316
|
+
end
|
317
|
+
|
318
|
+
def decode_body(string)
|
319
|
+
MultiJson.decode(string)
|
320
|
+
rescue MultiJson::DecodeError
|
321
|
+
parse_error(string)
|
322
|
+
end
|
323
|
+
|
324
|
+
def new_transaction_from_response(response)
|
325
|
+
if response.success?
|
326
|
+
Transaction.new({
|
327
|
+
:id => response.body['TransactionId'],
|
328
|
+
:avs_response => response.body['AVSResult'] == 1,
|
329
|
+
:cvv_response => response.body['CVResult'] == 1,
|
330
|
+
:authcode => response.body['ApprovalCode'],
|
331
|
+
:message => response.body['StatusMessage'],
|
332
|
+
:code => response.body['Status'],
|
333
|
+
:masked_card_number => response.body['MaskedPAN']
|
334
|
+
})
|
335
|
+
else
|
336
|
+
if errors = parse_validation_errors(response)
|
337
|
+
Transaction.new({
|
338
|
+
:message => errors.join("\n"),
|
339
|
+
:code => (response.body['ErrorResponse'] || {})['ErrorId']
|
340
|
+
})
|
341
|
+
else
|
342
|
+
Transaction.new({
|
343
|
+
:message => response.body ? (response.body['ErrorResponse'] || {})['Reason'] : nil,
|
344
|
+
:code => response.body ? (response.body['ErrorResponse'] || {})['ErrorId'] : nil
|
345
|
+
})
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def parse_validation_errors(response)
|
351
|
+
errors = ChainableHash.new.merge(response.body || {})
|
352
|
+
if errors['ErrorResponse']['ValidationErrors'].present?
|
353
|
+
[errors['ErrorResponse']['ValidationErrors']['ValidationError']].flatten.collect { |e| e['RuleMessage'] }
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def parse_error(response_body)
|
358
|
+
MultiXml.parse(response_body)
|
359
|
+
rescue MultiXml::ParseError
|
360
|
+
end
|
361
|
+
|
362
|
+
def respond_with(object, response = nil, options = {}, &block)
|
363
|
+
super(object, options, &block).tap do |o|
|
364
|
+
if response
|
365
|
+
o.raw_response = response.raw_response.try(:body)
|
366
|
+
o.connection_error = response.connection_error
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
@@ -13,16 +13,16 @@ module VaultedBilling
|
|
13
13
|
#
|
14
14
|
class NmiCustomerVault
|
15
15
|
include VaultedBilling::Gateway
|
16
|
-
|
16
|
+
attr_accessor :use_test_uri
|
17
17
|
|
18
18
|
def initialize(options = {})
|
19
|
-
|
19
|
+
@live_uri = "https://secure.nmi.com/api/transact.php"
|
20
20
|
|
21
21
|
options = HashWithIndifferentAccess.new(options)
|
22
22
|
@username = options[:username] || VaultedBilling.config.nmi_customer_vault.username
|
23
23
|
@password = options[:password] || VaultedBilling.config.nmi_customer_vault.password
|
24
24
|
@raw_options = options[:raw_options] || VaultedBilling.config.nmi_customer_vault.raw_options
|
25
|
-
|
25
|
+
@use_test_uri = options.has_key?(:test) ? options[:test] : (VaultedBilling.config.nmi_customer_vault.test_mode || VaultedBilling.config.test_mode)
|
26
26
|
end
|
27
27
|
|
28
28
|
##
|
@@ -53,79 +53,91 @@ module VaultedBilling
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def add_customer_credit_card(customer, credit_card)
|
56
|
-
|
56
|
+
data = storage_data('add_customer', customer.to_vaulted_billing, credit_card.to_vaulted_billing)
|
57
|
+
response = http.post(data)
|
57
58
|
respond_with(credit_card, response, :success => response.success?) do |c|
|
58
59
|
c.vault_id = response.body['customer_vault_id']
|
59
60
|
end
|
60
61
|
end
|
61
62
|
|
62
63
|
def update_customer_credit_card(customer, credit_card)
|
63
|
-
|
64
|
+
data = storage_data('update_customer', customer.to_vaulted_billing, credit_card.to_vaulted_billing)
|
65
|
+
response = http.post(data)
|
64
66
|
respond_with(credit_card, response, :success => response.success?)
|
65
67
|
end
|
66
68
|
|
67
69
|
def remove_customer_credit_card(customer, credit_card)
|
68
|
-
|
70
|
+
data = core_data.merge({
|
69
71
|
:customer_vault => 'delete_customer',
|
70
72
|
:customer_vault_id => credit_card.to_vaulted_billing.vault_id
|
71
|
-
}).to_querystring
|
73
|
+
}).to_querystring
|
74
|
+
response = http.post(data)
|
72
75
|
respond_with(credit_card, response, :success => response.success?)
|
73
76
|
end
|
74
77
|
|
75
|
-
def purchase(customer, credit_card, amount)
|
76
|
-
|
78
|
+
def purchase(customer, credit_card, amount, options = {})
|
79
|
+
data = transaction_data('sale', {
|
77
80
|
:customer_vault_id => credit_card.to_vaulted_billing.vault_id,
|
78
81
|
:amount => amount
|
79
|
-
})
|
82
|
+
})
|
83
|
+
response = http.post(data)
|
80
84
|
respond_with(new_transaction_from_response(response.body),
|
81
85
|
response,
|
82
86
|
:success => response.success?)
|
83
87
|
end
|
84
88
|
|
85
|
-
def authorize(customer, credit_card, amount)
|
86
|
-
|
89
|
+
def authorize(customer, credit_card, amount, options = {})
|
90
|
+
data = transaction_data('auth', {
|
87
91
|
:customer_vault_id => credit_card.to_vaulted_billing.vault_id,
|
88
92
|
:amount => amount
|
89
|
-
})
|
93
|
+
})
|
94
|
+
response = http.post(data)
|
90
95
|
respond_with(new_transaction_from_response(response.body),
|
91
96
|
response,
|
92
97
|
:success => response.success?)
|
93
98
|
end
|
94
99
|
|
95
|
-
def capture(transaction_id, amount)
|
96
|
-
|
100
|
+
def capture(transaction_id, amount, options = {})
|
101
|
+
data = transaction_data('capture', {
|
97
102
|
:transactionid => transaction_id,
|
98
103
|
:amount => amount
|
99
|
-
})
|
104
|
+
})
|
105
|
+
response = http.post(data)
|
100
106
|
respond_with(new_transaction_from_response(response.body),
|
101
107
|
response,
|
102
108
|
:success => response.success?)
|
103
109
|
end
|
104
110
|
|
105
111
|
def refund(transaction_id, amount, options = {})
|
106
|
-
|
112
|
+
data = transaction_data('refund', {
|
107
113
|
:transactionid => transaction_id,
|
108
114
|
:amount => amount
|
109
|
-
})
|
115
|
+
})
|
116
|
+
response = http.post(data)
|
110
117
|
respond_with(new_transaction_from_response(response.body),
|
111
118
|
response,
|
112
119
|
:success => response.success?)
|
113
120
|
end
|
114
121
|
|
115
|
-
def void(transaction_id)
|
116
|
-
|
122
|
+
def void(transaction_id, options = {})
|
123
|
+
data = transaction_data('void', {
|
117
124
|
:transactionid => transaction_id
|
118
|
-
})
|
125
|
+
})
|
126
|
+
response = http.post(data)
|
119
127
|
respond_with(new_transaction_from_response(response.body),
|
120
128
|
response,
|
121
129
|
:success => response.success?)
|
122
130
|
end
|
123
131
|
|
132
|
+
def uri
|
133
|
+
@live_uri
|
134
|
+
end
|
135
|
+
|
124
136
|
|
125
137
|
protected
|
126
138
|
|
127
139
|
|
128
|
-
def
|
140
|
+
def on_error(response, exception)
|
129
141
|
response.body = {
|
130
142
|
'response' => '3',
|
131
143
|
'responsetext' => 'A communication problem has occurred.',
|
@@ -134,14 +146,21 @@ module VaultedBilling
|
|
134
146
|
response.success = false
|
135
147
|
end
|
136
148
|
|
137
|
-
def
|
138
|
-
response.body = Hash.from_querystring(response.body)
|
149
|
+
def on_complete(response)
|
150
|
+
response.body = Hash.from_querystring(response.body.to_s) unless response.body.is_a?(Hash)
|
139
151
|
response.success = response.body['response'] == '1'
|
140
152
|
end
|
141
153
|
|
142
154
|
|
143
155
|
private
|
144
156
|
|
157
|
+
def http
|
158
|
+
VaultedBilling::HTTP.new(self, uri, {
|
159
|
+
:headers => {'Content-Type' => 'text/xml'},
|
160
|
+
:on_complete => :on_complete,
|
161
|
+
:on_error => :on_error
|
162
|
+
})
|
163
|
+
end
|
145
164
|
|
146
165
|
def core_data
|
147
166
|
{
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module VaultedBilling
|
4
|
+
class HTTP
|
5
|
+
HTTP_ERRORS = [
|
6
|
+
Timeout::Error,
|
7
|
+
Errno::ETIMEDOUT,
|
8
|
+
Errno::EINVAL,
|
9
|
+
Errno::ECONNRESET,
|
10
|
+
Errno::ECONNREFUSED,
|
11
|
+
Errno::EHOSTUNREACH,
|
12
|
+
EOFError,
|
13
|
+
Net::HTTPBadResponse,
|
14
|
+
Net::HTTPHeaderSyntaxError,
|
15
|
+
Net::ProtocolError
|
16
|
+
] unless defined?(HTTP_ERRORS)
|
17
|
+
|
18
|
+
|
19
|
+
class Response
|
20
|
+
attr_accessor :code
|
21
|
+
attr_accessor :message
|
22
|
+
attr_accessor :body
|
23
|
+
attr_accessor :success
|
24
|
+
attr_accessor :raw_response
|
25
|
+
attr_accessor :connection_error
|
26
|
+
alias :connection_error? :connection_error
|
27
|
+
|
28
|
+
def initialize(http_response)
|
29
|
+
if http_response
|
30
|
+
self.raw_response = http_response
|
31
|
+
self.code = http_response.code
|
32
|
+
self.message = http_response.message
|
33
|
+
self.body = http_response.body
|
34
|
+
self.success = ((http_response.code =~ /^2\d{2}/) == 0)
|
35
|
+
self.connection_error = false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
alias :success? :success
|
40
|
+
alias :status_code :code
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :uri
|
44
|
+
|
45
|
+
def initialize(caller, uri, options = {})
|
46
|
+
@uri = [uri].flatten.compact.collect { |u| URI.parse(u.to_s).normalize }
|
47
|
+
@headers = options[:headers] || {}
|
48
|
+
@basic_auth = options[:basic_auth]
|
49
|
+
@content_type = options[:content_type]
|
50
|
+
@caller = caller
|
51
|
+
@before_request = options[:before_request]
|
52
|
+
@on_success = options[:on_success]
|
53
|
+
@on_error = options[:on_error]
|
54
|
+
@on_complete = options[:on_complete]
|
55
|
+
end
|
56
|
+
|
57
|
+
def post(body, options = {})
|
58
|
+
request(:post, uri.dup, body, options)
|
59
|
+
end
|
60
|
+
|
61
|
+
def get(options = {})
|
62
|
+
request(:get, uri.dup, nil, options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def put(body, options = {})
|
66
|
+
request(:put, uri.dup, body, options)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
|
72
|
+
def log(level, string)
|
73
|
+
if VaultedBilling.logger?
|
74
|
+
VaultedBilling.logger.send(level) { string }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def request(method, uris, body = nil, options = {})
|
79
|
+
uri = uris.shift || raise(ArgumentError, "URI is empty")
|
80
|
+
|
81
|
+
request = case method
|
82
|
+
when :get
|
83
|
+
Net::HTTP::Get
|
84
|
+
when :put
|
85
|
+
Net::HTTP::Put
|
86
|
+
when :post
|
87
|
+
Net::HTTP::Post
|
88
|
+
else
|
89
|
+
raise ArugmentError
|
90
|
+
end.new(uri.path)
|
91
|
+
|
92
|
+
request.initialize_http_header(@headers.merge(options[:headers] || {}).reverse_merge({
|
93
|
+
'User-Agent' => user_agent_string
|
94
|
+
}))
|
95
|
+
|
96
|
+
request.body = body if body
|
97
|
+
set_basic_auth request, options[:basic_auth] || @basic_auth
|
98
|
+
set_content_type request, options[:content_type] || @content_type
|
99
|
+
|
100
|
+
response = Net::HTTP.new(uri.host, uri.port).tap do |https|
|
101
|
+
https.read_timeout = options[:read_timeout] || 60
|
102
|
+
https.open_timeout = options[:open_timeout] || 60
|
103
|
+
https.use_ssl = true
|
104
|
+
https.ca_file = VaultedBilling.config.ca_file
|
105
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
106
|
+
end
|
107
|
+
|
108
|
+
run_callback(:before_request, options[:before_request] || @before_request, request)
|
109
|
+
http_response = run_request(request, response, options)
|
110
|
+
|
111
|
+
if http_response.connection_error && uris.present?
|
112
|
+
request(method, uris, body, options)
|
113
|
+
else
|
114
|
+
run_callback(:on_complete, options[:on_complete] || @on_complete, http_response)
|
115
|
+
http_response
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def run_callback(type, callback, *payload)
|
120
|
+
case callback
|
121
|
+
when Proc
|
122
|
+
callback.call(*payload)
|
123
|
+
when String, Symbol
|
124
|
+
@caller.send(callback, *payload)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def run_request(request, response, options)
|
129
|
+
log :debug, "%s %s to %s" % [request.class.name.split('::').last, request.body.inspect, uri.to_s]
|
130
|
+
|
131
|
+
http_response = Response.new(response.request(request))
|
132
|
+
log :info, "Response code %s (HTTP %s), %s" % [http_response.message, http_response.code.presence || '0', http_response.body.inspect]
|
133
|
+
run_callback(:on_success, options[:on_success] || @on_success, http_response)
|
134
|
+
http_response
|
135
|
+
rescue *HTTP_ERRORS
|
136
|
+
log :info, "HTTP Error: %s - %s" % [$!.class.name, $!.message]
|
137
|
+
Response.new(nil).tap do |request_response|
|
138
|
+
request_response.success = false
|
139
|
+
request_response.message = "%s - %s" % [$!.class.name, $!.message]
|
140
|
+
request_response.connection_error = true
|
141
|
+
run_callback(:on_error, options[:on_error] || @on_error, request_response, $!)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def set_content_type(request, content)
|
146
|
+
request.set_content_type(content) if content
|
147
|
+
end
|
148
|
+
|
149
|
+
def set_basic_auth(request, auth)
|
150
|
+
request.basic_auth(auth.first, auth.last) if auth
|
151
|
+
end
|
152
|
+
|
153
|
+
def user_agent_string
|
154
|
+
"vaulted_billing/%s (Rubygems; Ruby %s %s)" % [VaultedBilling::Version, RUBY_VERSION, RUBY_PLATFORM]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|