authorize_net 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/authorize_net/address.rb +22 -0
- data/lib/authorize_net/api.rb +325 -0
- data/lib/authorize_net/credit_card.rb +16 -0
- data/lib/authorize_net/customer_profile.rb +26 -0
- data/lib/authorize_net/data_object.rb +123 -0
- data/lib/authorize_net/error_handler.rb +137 -0
- data/lib/authorize_net/exception.rb +13 -0
- data/lib/authorize_net/payment_profile.rb +39 -0
- data/lib/authorize_net/request.rb +93 -0
- data/lib/authorize_net/response.rb +50 -0
- data/lib/authorize_net/transaction.rb +45 -0
- data/lib/authorize_net/util.rb +55 -0
- data/lib/authorize_net.rb +30 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: af027737f355d0be9ecdf632b089872b67c055ba
|
4
|
+
data.tar.gz: af7373a60654eb83262507dd8b5b3475bf0018f1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 67c4b05bbea66632978c76b3d7e35ce210746fba8cdd7bd1cf1479a5499d863d3df2da4cc55396d049524a92d52076ac5a091a231424fa085bc67a0eef1d5f50
|
7
|
+
data.tar.gz: 3045de76b817e9e898478829c768c7165be7db6ffb0b96f13065fe13be8e89f4a1800aaf2df6704d729d32b5d03fc35f212b3efc4f85c6c9cb252852ef24f067
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'authorize_net/data_object'
|
2
|
+
|
3
|
+
class AuthorizeNet::Address < AuthorizeNet::DataObject
|
4
|
+
|
5
|
+
ATTRIBUTES = {
|
6
|
+
:first_name => {:key => "firstName"},
|
7
|
+
:last_name => {:key => "lastName"},
|
8
|
+
:company => nil,
|
9
|
+
:address => nil,
|
10
|
+
:city => nil,
|
11
|
+
:state => nil,
|
12
|
+
:zip => nil,
|
13
|
+
:country => nil,
|
14
|
+
:phone => nil,
|
15
|
+
:fax => nil,
|
16
|
+
}
|
17
|
+
|
18
|
+
self::ATTRIBUTES.keys.each do |attr|
|
19
|
+
attr_accessor attr
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,325 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
# ===============================================================
|
4
|
+
# This class uses the AuthroizeRequest object to interact with
|
5
|
+
# the Authorize.Net API
|
6
|
+
#
|
7
|
+
# Add any new Authroize.Net API endpoints here
|
8
|
+
# ===============================================================
|
9
|
+
class AuthorizeNet::Api
|
10
|
+
|
11
|
+
def initialize(api_login_id, api_transaction_key, is_test_api)
|
12
|
+
@api_login_id = api_login_id
|
13
|
+
@api_transaction_key = api_transaction_key
|
14
|
+
@is_test_api = is_test_api
|
15
|
+
@logger = nil
|
16
|
+
@log_full_request = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def setLogger(logger)
|
20
|
+
@logger = logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def setLogFullRequest(log_full_request)
|
24
|
+
@log_full_request = log_full_request
|
25
|
+
end
|
26
|
+
|
27
|
+
# ===============================================================
|
28
|
+
# Charges the given credit card
|
29
|
+
# @param Number amount
|
30
|
+
# @param AuthorizeNet::CreditCard credit_card
|
31
|
+
# @param AuthorizeNet::Address billing_address
|
32
|
+
# @return {transaction_id}
|
33
|
+
# ===============================================================
|
34
|
+
def chargeCard(amount, credit_card, billing_address=nil)
|
35
|
+
xml_obj = getXmlAuth
|
36
|
+
xml_obj["transactionRequest"] = {
|
37
|
+
"transactionType" => "authCaptureTransaction",
|
38
|
+
"amount" => amount,
|
39
|
+
"payment" => {
|
40
|
+
"creditCard" => credit_card.to_h,
|
41
|
+
},
|
42
|
+
}
|
43
|
+
if !billing_address.nil?
|
44
|
+
xml_obj["transactionRequest"]["billTo"] = billing_address.to_h
|
45
|
+
end
|
46
|
+
|
47
|
+
response = sendRequest("createTransactionRequest", xml_obj)
|
48
|
+
if !response.nil?
|
49
|
+
return AuthorizeNet::Transaction.parse(response)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# ===============================================================
|
54
|
+
# Creates the CustomerProfile and charges the first listed
|
55
|
+
# PaymentProfile on AuthorizeNet
|
56
|
+
# @param Number amount
|
57
|
+
# @param AuthorizeNet::CustomerProfile customer_profile
|
58
|
+
# @return {customer_profile_id, payment_profile_id}
|
59
|
+
# ===============================================================
|
60
|
+
def chargeAndCreateProfile(amount, customer_profile)
|
61
|
+
if customer_profile.payment_profiles.empty?
|
62
|
+
raise "[AuthorizeNet] CustomerProfile in Api.chargeAndCreateProfile requires a PaymentProfile"
|
63
|
+
end
|
64
|
+
|
65
|
+
payment_profile = customer_profile.payment_profiles.first
|
66
|
+
xml_obj = getXmlAuth
|
67
|
+
xml_obj["transactionRequest"] = {
|
68
|
+
"transactionType" => "authCaptureTransaction",
|
69
|
+
"amount" => amount,
|
70
|
+
"payment" => {
|
71
|
+
"creditCard" => payment_profile.credit_card.to_h,
|
72
|
+
},
|
73
|
+
"profile" => {
|
74
|
+
"createProfile" => true,
|
75
|
+
},
|
76
|
+
"customer" => {
|
77
|
+
"id" => customer_profile.merchant_id,
|
78
|
+
"email" => customer_profile.email,
|
79
|
+
"description" => customer_profile.description,
|
80
|
+
"billTo" => payment_profile.billing_address.to_h,
|
81
|
+
},
|
82
|
+
}
|
83
|
+
|
84
|
+
response = sendRequest("createTransactionRequest", xml_obj)
|
85
|
+
|
86
|
+
if !response.nil?
|
87
|
+
return {
|
88
|
+
:transaction => AuthorizeNet::Transaction.parse(response),
|
89
|
+
:customer_profile_id => AuthorizeNet::Util.getXmlValue(
|
90
|
+
response, "customerProfileId"),
|
91
|
+
:payment_profile_id => AuthorizeNet::Util.getXmlValue(
|
92
|
+
response, "customerPaymentProfileIdList numericString"),
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# ===============================================================
|
98
|
+
# Charges the given profile and payment profile on Authorize.net
|
99
|
+
# @param Number amount
|
100
|
+
# @param String/Number customer_profile_id
|
101
|
+
# @param String/Number payment_profile_id
|
102
|
+
# @return transaction_id
|
103
|
+
# ===============================================================
|
104
|
+
def chargeProfile(amount, profile_id, payment_profile_id)
|
105
|
+
xml_obj = getXmlAuth
|
106
|
+
xml_obj["transactionRequest"] = {
|
107
|
+
"transactionType" => "authCaptureTransaction",
|
108
|
+
"amount" => amount,
|
109
|
+
"profile" => {
|
110
|
+
"customerProfileId" => profile_id,
|
111
|
+
"paymentProfile" => {
|
112
|
+
"paymentProfileId" => payment_profile_id,
|
113
|
+
},
|
114
|
+
},
|
115
|
+
}
|
116
|
+
|
117
|
+
response = sendRequest("createTransactionRequest", xml_obj)
|
118
|
+
if !response.nil?
|
119
|
+
return AuthorizeNet::Transaction.parse(response)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# ===============================================================
|
124
|
+
# Creates the given customer profile on Authorize.net
|
125
|
+
# @param AuthorizeNet::CustomerProfile customer_profile
|
126
|
+
# @param Number amount
|
127
|
+
# @param AuthorizeNet::ValidationMode validation_mode (optional)
|
128
|
+
# @return transaction_id
|
129
|
+
# ===============================================================
|
130
|
+
def createCustomerProfile(customer_profile, validation_mode=nil)
|
131
|
+
xml_obj = getXmlAuth
|
132
|
+
xml_obj["profile"] = customer_profile.to_h
|
133
|
+
|
134
|
+
addValidationMode!(xml_obj, validation_mode)
|
135
|
+
response = sendRequest("createCustomerProfileRequest", xml_obj)
|
136
|
+
|
137
|
+
if !response.nil?
|
138
|
+
return {
|
139
|
+
:customer_profile_id => AuthorizeNet::Util.getXmlValue(
|
140
|
+
response, "customerProfileId"),
|
141
|
+
:payment_profile_id => AuthorizeNet::Util.getXmlValue(
|
142
|
+
response, "customerPaymentProfileIdList numericString"),
|
143
|
+
}
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# ===============================================================
|
148
|
+
# Create Customer Payment Profile
|
149
|
+
# @param String/Number customer_profile_id
|
150
|
+
# @param AuthorizeNet::PaymentProfile payment_profile
|
151
|
+
# @param AuthorizeNet::ValidationMode validation_mode (optional)
|
152
|
+
# @return {customer_profile_id, payment_profile_id}
|
153
|
+
# ===============================================================
|
154
|
+
def createPaymentProfile(customer_profile_id, payment_profile, validation_mode=nil)
|
155
|
+
xml_obj = getXmlAuth
|
156
|
+
xml_obj["customerProfileId"] = customer_profile_id
|
157
|
+
xml_obj["paymentProfile"] = payment_profile.to_h
|
158
|
+
|
159
|
+
addValidationMode!(xml_obj, validation_mode)
|
160
|
+
response = sendRequest("createCustomerPaymentProfileRequest", xml_obj)
|
161
|
+
|
162
|
+
if !response.nil?
|
163
|
+
return {
|
164
|
+
:customer_profile_id => AuthorizeNet::Util.getXmlValue(
|
165
|
+
response, "customerProfileId"),
|
166
|
+
:payment_profile_id => AuthorizeNet::Util.getXmlValue(
|
167
|
+
response, "customerPaymentProfileId"),
|
168
|
+
}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# ===============================================================
|
173
|
+
# Delete Customer Payment Profile
|
174
|
+
# @param String/Number customer_profile_id
|
175
|
+
# @param String/Number payment_profile_id
|
176
|
+
# @return boolean is delete successful?
|
177
|
+
# ===============================================================
|
178
|
+
def deletePaymentProfile(customer_profile_id, payment_profile_id)
|
179
|
+
xml_obj = getXmlAuth
|
180
|
+
xml_obj["customerProfileId"] = customer_profile_id
|
181
|
+
xml_obj["customerPaymentProfileId"] = payment_profile_id
|
182
|
+
|
183
|
+
response = sendRequest("deleteCustomerPaymentProfileRequest", xml_obj)
|
184
|
+
return !response.nil?
|
185
|
+
end
|
186
|
+
|
187
|
+
# ===============================================================
|
188
|
+
# Validate Customer Payment Profile
|
189
|
+
# @param String/Number customer_profile_id
|
190
|
+
# @param String/Number payment_profile_id
|
191
|
+
# @param AuthorizeNet::ValidationMode::(String) validation_mode
|
192
|
+
# @return boolean is update successful?
|
193
|
+
# ===============================================================
|
194
|
+
def validatePaymentProfile(customer_profile_id, payment_profile_id, validation_mode)
|
195
|
+
xml_obj = getXmlAuth
|
196
|
+
xml_obj["customerProfileId"] = customer_profile_id
|
197
|
+
xml_obj["customerPaymentProfileId"] = payment_profile_id
|
198
|
+
xml_obj["validationMode"] = validation_mode
|
199
|
+
|
200
|
+
response = sendRequest("validateCustomerPaymentProfileRequest", xml_obj)
|
201
|
+
return !response.nil?
|
202
|
+
end
|
203
|
+
|
204
|
+
# ===============================================================
|
205
|
+
# Get customer profile information
|
206
|
+
# @param String/Number customer_profile_id
|
207
|
+
# @param String/Number customer_profile_id
|
208
|
+
# @return AuthorizeNet::CustomerProfile
|
209
|
+
# ===============================================================
|
210
|
+
def getCustomerProfile(customer_profile_id)
|
211
|
+
xml_obj = getXmlAuth
|
212
|
+
xml_obj["customerProfileId"] = customer_profile_id
|
213
|
+
|
214
|
+
response = sendRequest("getCustomerProfileRequest", xml_obj)
|
215
|
+
if response
|
216
|
+
return AuthorizeNet::CustomerProfile.parse(response)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# ===============================================================
|
221
|
+
# Gets transaction information
|
222
|
+
# @param String/Number customer_profile_id
|
223
|
+
# @param String/Number transaction_id
|
224
|
+
# @return AuthorizeNet::Transaction
|
225
|
+
# ===============================================================
|
226
|
+
def getTransactionInfo(transaction_id)
|
227
|
+
xml_obj = getXmlAuth
|
228
|
+
xml_obj["transId"] = transaction_id
|
229
|
+
|
230
|
+
response = sendRequest("getTransactionDetailsRequest", xml_obj)
|
231
|
+
if response
|
232
|
+
return AuthorizeNet::Transaction.parse(response)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
def getXmlAuth
|
241
|
+
return {
|
242
|
+
"merchantAuthentication" => {
|
243
|
+
"name" => @api_login_id,
|
244
|
+
"transactionKey" => @api_transaction_key,
|
245
|
+
}
|
246
|
+
}
|
247
|
+
end
|
248
|
+
|
249
|
+
def addValidationMode!(xml_obj, validation_mode)
|
250
|
+
if validation_mode
|
251
|
+
xml_obj["validationMode"] = validation_mode
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# =============================================
|
256
|
+
# Looks for potential errors in the response
|
257
|
+
# and raises an error if it finds any
|
258
|
+
# Passes through OK responses
|
259
|
+
# @throws AuthorizeNet::Exception
|
260
|
+
# =============================================
|
261
|
+
def handleResponse(raw_response)
|
262
|
+
logHttpResponse(raw_response)
|
263
|
+
|
264
|
+
response = AuthorizeNet::Response.parseXml(raw_response.read_body)
|
265
|
+
if response.result == AuthorizeNet::RESULT_OK && response.errors.nil?
|
266
|
+
return response.parsed_xml
|
267
|
+
else
|
268
|
+
logErrorResponse(response)
|
269
|
+
AuthorizeNet::ErrorHandler.handle(response)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# =============================================
|
274
|
+
# Send HTTP request to Authorize Net
|
275
|
+
# @param Net::HTTPResponse
|
276
|
+
# @return response
|
277
|
+
# =============================================
|
278
|
+
def sendRequest(type, xml_obj)
|
279
|
+
uri = @is_test_api ? AuthorizeNet::TEST_URI : AuthorizeNet::URI
|
280
|
+
request = AuthorizeNet::Request.new(type, xml_obj, uri)
|
281
|
+
|
282
|
+
if @logger.respond_to? :info
|
283
|
+
@logger.info(request.toLog(@log_full_request))
|
284
|
+
end
|
285
|
+
|
286
|
+
return handleResponse(request.postRequest)
|
287
|
+
end
|
288
|
+
|
289
|
+
# =============================================
|
290
|
+
# Log HTTP response from Authorize Net
|
291
|
+
# @param Net::HTTPResponse
|
292
|
+
# @return String log
|
293
|
+
# =============================================
|
294
|
+
def logHttpResponse(response)
|
295
|
+
if @logger.respond_to? :logHttpResponse
|
296
|
+
@logger.logHttpResponse(response)
|
297
|
+
elsif @logger.respond_to? :info
|
298
|
+
@logger.info("[AuthorizeNet] HTTP Response code=#{response.code} message=#{response.message}")
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# =============================================
|
303
|
+
# Returns a log string with http response data
|
304
|
+
# @param Net::HTTPResponse
|
305
|
+
# @throws RuntimeError
|
306
|
+
# =============================================
|
307
|
+
def logErrorResponse(response)
|
308
|
+
if @logger.respond_to? :info
|
309
|
+
@logger.info("[AuthorizeNet] Responded with resultCode=\"#{response.result}\"")
|
310
|
+
end
|
311
|
+
|
312
|
+
if !response.messages.nil? and @logger.respond_to? :info
|
313
|
+
response.messages.each do |msg|
|
314
|
+
@logger.info("[AuthorizeNet] Message code=\"#{msg[:code]}\" text=\"#{msg[:text]}\"")
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
if !response.errors.nil? and @logger.respond_to? :error
|
319
|
+
response.errors.each do |error|
|
320
|
+
@logger.error("[AuthorizeNet] Error code=\"#{error[:code]}\" text=\"#{error[:text]}\"")
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'authorize_net/data_object'
|
2
|
+
|
3
|
+
class AuthorizeNet::CreditCard < AuthorizeNet::DataObject
|
4
|
+
|
5
|
+
ATTRIBUTES = {
|
6
|
+
:card_num => {:key => "cardNumber"},
|
7
|
+
:expiration => {:key => "expirationDate"},
|
8
|
+
:security_code => {:key => "cardCode"},
|
9
|
+
:card_type => {:key => "cardType"},
|
10
|
+
}
|
11
|
+
|
12
|
+
self::ATTRIBUTES.keys.each do |attr|
|
13
|
+
attr_accessor attr
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'authorize_net/data_object'
|
2
|
+
require 'authorize_net/payment_profile'
|
3
|
+
|
4
|
+
class AuthorizeNet::CustomerProfile < AuthorizeNet::DataObject
|
5
|
+
|
6
|
+
ATTRIBUTES = {
|
7
|
+
:id => {:key => "customerProfileId"},
|
8
|
+
:merchant_id => {:key => "merchantCustomerId"},
|
9
|
+
:email => nil,
|
10
|
+
:description => nil,
|
11
|
+
:payment_profiles => {
|
12
|
+
:key => "paymentProfiles",
|
13
|
+
:type => AuthorizeNet::DataObject::TYPE_OBJECT_ARRAY,
|
14
|
+
:class => AuthorizeNet::PaymentProfile,
|
15
|
+
},
|
16
|
+
}
|
17
|
+
|
18
|
+
self::ATTRIBUTES.keys.each do |attr|
|
19
|
+
attr_accessor attr
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@payment_profiles = []
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# *** NOTE ***
|
2
|
+
# Data objects must have a static ATTRIBUTES hash followed by these lines
|
3
|
+
#
|
4
|
+
# self::ATTRIBUTES.keys.each do |attr|
|
5
|
+
# attr_accessor attr
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
class AuthorizeNet::DataObject
|
9
|
+
|
10
|
+
TYPE_ARRAY = :type_array
|
11
|
+
TYPE_OBJECT = :type_object
|
12
|
+
TYPE_OBJECT_ARRAY = :type_object_array
|
13
|
+
|
14
|
+
# =======================================================
|
15
|
+
# Parses XML from the values in the ATTRIBUTES hash in
|
16
|
+
# to the attributes of this object.
|
17
|
+
# =======================================================
|
18
|
+
def parse(xml)
|
19
|
+
if xml.nil? || !xml.respond_to?(:at_css)
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
self.class::ATTRIBUTES.keys.each do |attr|
|
24
|
+
spec = self.class::ATTRIBUTES[attr].to_h
|
25
|
+
xml_key = spec[:key] || attr.to_s
|
26
|
+
type = spec[:type]
|
27
|
+
type_class = spec[:class]
|
28
|
+
|
29
|
+
if (type == TYPE_OBJECT or type == TYPE_OBJECT_ARRAY) and type_class.nil?
|
30
|
+
raise "DataObject=#{self.class} Attribute=#{attr} of type #{type} must specify a class"
|
31
|
+
end
|
32
|
+
|
33
|
+
if type == TYPE_OBJECT
|
34
|
+
obj_xml = xml.at_css(xml_key)
|
35
|
+
send("#{attr}=", type_class.parse(obj_xml))
|
36
|
+
|
37
|
+
elsif type == TYPE_OBJECT_ARRAY
|
38
|
+
array_xml = xml.css(xml_key)
|
39
|
+
send("#{attr}=", array_xml.map{ |x| type_class.parse(x) })
|
40
|
+
|
41
|
+
elsif type == TYPE_ARRAY
|
42
|
+
array_xml = xml.css(xml_key)
|
43
|
+
send("#{attr}=", array_xml.map{ |x| x.inner_text })
|
44
|
+
|
45
|
+
else
|
46
|
+
send("#{attr}=", AuthorizeNet::Util.getXmlValue(xml, xml_key))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# =======================================================
|
52
|
+
# Turns this object into a hash using the keys specified
|
53
|
+
# as the values in ATTRIBUTES
|
54
|
+
#
|
55
|
+
# If the value in ATTRIBUTES is nil, use the String
|
56
|
+
# version of the attribute itself
|
57
|
+
# =======================================================
|
58
|
+
def to_h(include_blanks=false)
|
59
|
+
hash = {}
|
60
|
+
self.class::ATTRIBUTES.keys.each do |attr|
|
61
|
+
spec = self.class::ATTRIBUTES[attr].to_h
|
62
|
+
key = spec[:key] || attr.to_s
|
63
|
+
type = spec[:type]
|
64
|
+
value = send(attr)
|
65
|
+
|
66
|
+
if value.nil?
|
67
|
+
if include_blanks
|
68
|
+
hash[key] = nil
|
69
|
+
end
|
70
|
+
elsif type == TYPE_OBJECT
|
71
|
+
hash[key] = value.to_h
|
72
|
+
elsif type == TYPE_OBJECT_ARRAY
|
73
|
+
hash[key] = value.map{ |e| e.to_h }
|
74
|
+
else
|
75
|
+
hash[key] = value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
return hash
|
80
|
+
end
|
81
|
+
|
82
|
+
# =======================================================
|
83
|
+
# Turns this object into a hash using the keys specified
|
84
|
+
# as the keys in ATTRIBUTES
|
85
|
+
# =======================================================
|
86
|
+
def serialize
|
87
|
+
hash = {}
|
88
|
+
self.class::ATTRIBUTES.keys.each do |attr|
|
89
|
+
spec = self.class::ATTRIBUTES[attr].to_h
|
90
|
+
type = spec[:type]
|
91
|
+
value = send(attr)
|
92
|
+
|
93
|
+
if value.nil?
|
94
|
+
hash[attr] = nil
|
95
|
+
elsif type == TYPE_OBJECT
|
96
|
+
hash[attr] = value.serialize
|
97
|
+
elsif type == TYPE_OBJECT_ARRAY
|
98
|
+
hash[attr] = value.map{ |e| e.serialize }
|
99
|
+
else
|
100
|
+
hash[attr] = value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
return hash
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
class << self
|
109
|
+
# =============================================
|
110
|
+
# Parses xml into a new instance of this class
|
111
|
+
# =============================================
|
112
|
+
def parse(xml)
|
113
|
+
if xml.nil? || !xml.respond_to?(:at_css)
|
114
|
+
return
|
115
|
+
end
|
116
|
+
|
117
|
+
object = new
|
118
|
+
object.parse(xml)
|
119
|
+
return object
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
|
2
|
+
class AuthorizeNet::ErrorHandler
|
3
|
+
class << self
|
4
|
+
ERROR_FIELD_REGEXES = [
|
5
|
+
/'AnetApi\/xml\/v1\/schema\/AnetApiSchema\.xsd:([a-zA-Z]*)'/,
|
6
|
+
/The element '([a-zA-Z]*)' in namespace 'AnetApi\/xml\/v1\/schema\/AnetApiSchema.xsd'/
|
7
|
+
]
|
8
|
+
|
9
|
+
MESSAGE_CODES = {
|
10
|
+
"E00003" => :invalid_field,
|
11
|
+
"E00015" => :invalid_field_length,
|
12
|
+
"E00027" => :missing_required_field,
|
13
|
+
"E00039" => :duplicate_record_exists,
|
14
|
+
"E00041" => :customer_profile_info_required,
|
15
|
+
}
|
16
|
+
|
17
|
+
ERROR_CODES = {
|
18
|
+
"210" => :transaction_declined,
|
19
|
+
"6" => :invalid_card_number,
|
20
|
+
"7" => :invalid_expiration_date,
|
21
|
+
"8" => :expired_credit_card,
|
22
|
+
}
|
23
|
+
|
24
|
+
ERROR_FIELDS = {
|
25
|
+
:invalid_card_number => :card_number,
|
26
|
+
:invalid_expiration_date => :card_expiration,
|
27
|
+
:expired_credit_card => :card_expiration,
|
28
|
+
|
29
|
+
"cardNumber" => :card_number,
|
30
|
+
"expirationDate" => :card_expiration,
|
31
|
+
"cardCode" => :card_security_code,
|
32
|
+
}
|
33
|
+
|
34
|
+
# =============================================
|
35
|
+
# Creates an exception, populates it as well
|
36
|
+
# as possible, and then raises it
|
37
|
+
# @param AuthorizeNet::Response
|
38
|
+
# @throws AuthorizeNet::Exception
|
39
|
+
# =============================================
|
40
|
+
def handle(response)
|
41
|
+
exception = AuthorizeNet::Exception.new
|
42
|
+
|
43
|
+
if !response.errors.nil?
|
44
|
+
first_error = response.errors.first
|
45
|
+
exception.message = first_error[:text]
|
46
|
+
|
47
|
+
# Add errors to exception
|
48
|
+
response.errors.each do |error|
|
49
|
+
exception.errors << buildError(error)
|
50
|
+
end
|
51
|
+
|
52
|
+
raise exception
|
53
|
+
|
54
|
+
# If there are no errors, then the "messages" are probably errors... *sigh*
|
55
|
+
elsif !response.messages.nil? and response.result == AuthorizeNet::RESULT_ERROR
|
56
|
+
first_msg = response.messages.first
|
57
|
+
exception.message = first_msg[:text]
|
58
|
+
|
59
|
+
# Add messages (that are sometimes actually errors) to exception
|
60
|
+
response.messages.each do |msg|
|
61
|
+
exception.errors << buildError(msg)
|
62
|
+
end
|
63
|
+
|
64
|
+
raise exception
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
# =============================================
|
70
|
+
# Attempts to determine the error type and field
|
71
|
+
# for an error hash
|
72
|
+
# @param Hash error
|
73
|
+
# @return Hash error
|
74
|
+
# =============================================
|
75
|
+
def buildError(error)
|
76
|
+
code = error[:code]
|
77
|
+
text = error[:text]
|
78
|
+
type = getTypeFromCode(code)
|
79
|
+
field = nil
|
80
|
+
|
81
|
+
if !type.nil? and ERROR_FIELDS.has_key? type
|
82
|
+
field = ERROR_FIELDS[type]
|
83
|
+
else
|
84
|
+
field = getFieldFromText(text)
|
85
|
+
end
|
86
|
+
|
87
|
+
return {
|
88
|
+
:code => code,
|
89
|
+
:text => text,
|
90
|
+
:type => type,
|
91
|
+
:field => field,
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
# =============================================
|
96
|
+
# Attempts to determine the error type given
|
97
|
+
# an error code
|
98
|
+
# @param String code
|
99
|
+
# @return Symbol|nil type
|
100
|
+
# =============================================
|
101
|
+
def getTypeFromCode(code)
|
102
|
+
if ERROR_CODES.has_key? code
|
103
|
+
return ERROR_CODES[code]
|
104
|
+
elsif MESSAGE_CODES.has_key? code
|
105
|
+
return MESSAGE_CODES[code]
|
106
|
+
end
|
107
|
+
return nil
|
108
|
+
end
|
109
|
+
|
110
|
+
# =============================================
|
111
|
+
# Attempts to determine the error field given
|
112
|
+
# an error message
|
113
|
+
# @param String text
|
114
|
+
# @return Symbol|nil field
|
115
|
+
# =============================================
|
116
|
+
def getFieldFromText(text)
|
117
|
+
if text.nil?
|
118
|
+
return nil
|
119
|
+
end
|
120
|
+
|
121
|
+
ERROR_FIELD_REGEXES.each do |regex|
|
122
|
+
field_match = text.match(regex)
|
123
|
+
if !field_match.nil?
|
124
|
+
field = field_match[1]
|
125
|
+
|
126
|
+
if ERROR_FIELDS.keys.include? field
|
127
|
+
return ERROR_FIELDS[field]
|
128
|
+
end
|
129
|
+
return field
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
return nil
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class AuthorizeNet::Exception < Exception
|
2
|
+
|
3
|
+
GENERIC_ERROR_MESSAGE = "[AuthorizeNet] The Authorize.Net API returned an error"
|
4
|
+
|
5
|
+
attr_accessor :message
|
6
|
+
attr_accessor :errors
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@message = GENERIC_ERROR_MESSAGE
|
10
|
+
@errors = []
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'authorize_net/data_object'
|
2
|
+
require 'authorize_net/credit_card'
|
3
|
+
require 'authorize_net/address'
|
4
|
+
|
5
|
+
class AuthorizeNet::PaymentProfile < AuthorizeNet::DataObject
|
6
|
+
|
7
|
+
ATTRIBUTES = {
|
8
|
+
:id => {:key => "customerPaymentProfileId"},
|
9
|
+
:credit_card => {
|
10
|
+
:key => "creditCard",
|
11
|
+
:type => AuthorizeNet::DataObject::TYPE_OBJECT,
|
12
|
+
:class => AuthorizeNet::CreditCard,
|
13
|
+
},
|
14
|
+
:billing_address => {
|
15
|
+
:key => "billTo",
|
16
|
+
:type => AuthorizeNet::DataObject::TYPE_OBJECT,
|
17
|
+
:class => AuthorizeNet::Address,
|
18
|
+
},
|
19
|
+
}
|
20
|
+
|
21
|
+
self::ATTRIBUTES.keys.each do |attr|
|
22
|
+
attr_accessor attr
|
23
|
+
end
|
24
|
+
|
25
|
+
# Override
|
26
|
+
def to_h
|
27
|
+
hash = super
|
28
|
+
|
29
|
+
hash.delete('creditCard')
|
30
|
+
if !@credit_card.nil?
|
31
|
+
hash['payment'] = {
|
32
|
+
'creditCard' => @credit_card.to_h
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
return hash
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
# ===============================================================
|
5
|
+
# This class represents a request to the Authorize.Net API
|
6
|
+
#
|
7
|
+
# Add any logic that applies to ALL requests here
|
8
|
+
# ===============================================================
|
9
|
+
class AuthorizeNet::Request
|
10
|
+
|
11
|
+
attr_accessor :response
|
12
|
+
|
13
|
+
def initialize(type, data, uri)
|
14
|
+
@xml_data = data
|
15
|
+
@request_type = type
|
16
|
+
@uri = URI(uri)
|
17
|
+
@response = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
# =============================================
|
21
|
+
# Uses the given data to make a POST request
|
22
|
+
# =============================================
|
23
|
+
def postRequest
|
24
|
+
assertRequestData
|
25
|
+
assertRequestType
|
26
|
+
req = Net::HTTP::Post.new(@uri.request_uri)
|
27
|
+
req.add_field('Content-Type', 'text/xml')
|
28
|
+
req.body = buildXmlRequest
|
29
|
+
@response = sendRequest(req)
|
30
|
+
return @response
|
31
|
+
end
|
32
|
+
|
33
|
+
# =============================================
|
34
|
+
# Uses the given data to make a GET request
|
35
|
+
# =============================================
|
36
|
+
def getRequest
|
37
|
+
assertRequestType
|
38
|
+
req = Net::HTTP::Get.new(@uri.request_uri)
|
39
|
+
req.add_field('Content-Type', 'text/xml')
|
40
|
+
req.body = buildXmlRequest
|
41
|
+
end
|
42
|
+
|
43
|
+
# =============================================
|
44
|
+
# Make a log string for this request
|
45
|
+
# =============================================
|
46
|
+
def toLog(log_body)
|
47
|
+
log = "[AuthorizeNet] HTTP Request type=#{@request_type} uri=#{@uri}"
|
48
|
+
|
49
|
+
if log_body
|
50
|
+
log += " body=\"#{buildXmlRequest}\""
|
51
|
+
end
|
52
|
+
|
53
|
+
return log
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def assertRequestData
|
60
|
+
if @xml_data.nil?
|
61
|
+
raise "AuthorizeRequest has no xml data"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def assertRequestType
|
66
|
+
if @request_type.nil?
|
67
|
+
raise "AuthorizeRequest has no request type"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# =============================================
|
72
|
+
# Builds the full XML request using request
|
73
|
+
# type and the xml data object
|
74
|
+
# =============================================
|
75
|
+
def buildXmlRequest
|
76
|
+
xml_string = AuthorizeNet::XML_HEADER
|
77
|
+
xml_string += "<#{@request_type} xmlns=\"#{AuthorizeNet::XML_SCHEMA}\">"
|
78
|
+
xml_string += AuthorizeNet::Util.buildXmlFromObject(@xml_data)
|
79
|
+
xml_string += "</#{@request_type}>"
|
80
|
+
return xml_string
|
81
|
+
end
|
82
|
+
|
83
|
+
# =============================================
|
84
|
+
# Sends the input request to Authorize.Net
|
85
|
+
# =============================================
|
86
|
+
def sendRequest(req)
|
87
|
+
http = Net::HTTP.start(@uri.host, @uri.port, :use_ssl => @uri.scheme == 'https')
|
88
|
+
@response = http.request(req)
|
89
|
+
return @response
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class AuthorizeNet::Response
|
4
|
+
|
5
|
+
attr_accessor :result
|
6
|
+
attr_accessor :errors
|
7
|
+
attr_accessor :messages
|
8
|
+
attr_accessor :raw_xml
|
9
|
+
attr_accessor :parsed_xml
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
# =============================================
|
14
|
+
# Returns a populated response object
|
15
|
+
# @param String xml
|
16
|
+
# @return AuthorizeNet::Response
|
17
|
+
# =============================================
|
18
|
+
def parseXml(xml)
|
19
|
+
response = new
|
20
|
+
response.raw_xml = xml
|
21
|
+
response.parsed_xml = Nokogiri::XML.parse(xml)
|
22
|
+
response.result = AuthorizeNet::Util.getXmlValue(response.parsed_xml, "resultCode")
|
23
|
+
|
24
|
+
errors = response.parsed_xml.at_css("errors")
|
25
|
+
if !errors.nil?
|
26
|
+
response.errors = []
|
27
|
+
errors.css("error").each do |xml_error|
|
28
|
+
response.errors << {
|
29
|
+
:code => AuthorizeNet::Util.getXmlValue(xml_error, "errorCode"),
|
30
|
+
:text => AuthorizeNet::Util.getXmlValue(xml_error, "errorText"),
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
messages = response.parsed_xml.at_css("messages")
|
36
|
+
if !messages.nil?
|
37
|
+
response.messages = []
|
38
|
+
messages.css("message").each do |xml_msg|
|
39
|
+
response.messages << {
|
40
|
+
:code => AuthorizeNet::Util.getXmlValue(xml_msg, "code"),
|
41
|
+
:text => AuthorizeNet::Util.getXmlValue(xml_msg, "text"),
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
return response
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'authorize_net/data_object'
|
2
|
+
require 'authorize_net/address'
|
3
|
+
require 'authorize_net/customer_profile'
|
4
|
+
require 'authorize_net/credit_card'
|
5
|
+
require 'authorize_net/util'
|
6
|
+
|
7
|
+
class AuthorizeNet::Transaction < AuthorizeNet::DataObject
|
8
|
+
|
9
|
+
ATTRIBUTES = {
|
10
|
+
:id => {:key => "transId"},
|
11
|
+
:timestamp_local => {:key => "submitTimeLocal"},
|
12
|
+
:timestamp_utc => {:key => "submitTimeUTC"},
|
13
|
+
:type => {:key => "transactionType"},
|
14
|
+
:status => {:key => "transactionStatus"},
|
15
|
+
:account_num => {:key => "accountNumber"},
|
16
|
+
:account_type => {:key => "accountType"},
|
17
|
+
:auth_code => {:key => "authCode"},
|
18
|
+
:credit_card => {
|
19
|
+
:key => "creditCard",
|
20
|
+
:type => AuthorizeNet::DataObject::TYPE_OBJECT,
|
21
|
+
:class => AuthorizeNet::CreditCard,
|
22
|
+
},
|
23
|
+
:customer_profile => {
|
24
|
+
:key => "customer",
|
25
|
+
:type => AuthorizeNet::DataObject::TYPE_OBJECT,
|
26
|
+
:class => AuthorizeNet::CustomerProfile,
|
27
|
+
},
|
28
|
+
:billing_address => {
|
29
|
+
:key => "billTo",
|
30
|
+
:type => AuthorizeNet::DataObject::TYPE_OBJECT,
|
31
|
+
:class => AuthorizeNet::Address,
|
32
|
+
},
|
33
|
+
}
|
34
|
+
|
35
|
+
self::ATTRIBUTES.keys.each do |attr|
|
36
|
+
attr_accessor attr
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse(xml)
|
40
|
+
super
|
41
|
+
|
42
|
+
@customer_profile.merchant_id = AuthorizeNet::Util.getXmlValue(xml, 'customer id')
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class AuthorizeNet::Util
|
2
|
+
|
3
|
+
class << self
|
4
|
+
# ==============================================
|
5
|
+
# A wrapper for safely getting the inner value
|
6
|
+
# of an XML attribute only if it exists
|
7
|
+
#
|
8
|
+
# If multiple instances exist, return the first one
|
9
|
+
# ==============================================
|
10
|
+
def getXmlValue(xml, attr_string)
|
11
|
+
if !xml.respond_to? :at_css || attr_string.nil?
|
12
|
+
return nil
|
13
|
+
end
|
14
|
+
|
15
|
+
attr = xml.at_css(attr_string)
|
16
|
+
if !attr.nil?
|
17
|
+
return attr.inner_text
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# ==============================================
|
22
|
+
# Builds XML from Ruby Hashes/Arrays/Primitives
|
23
|
+
# ==============================================
|
24
|
+
def buildXmlFromObject(obj, parent_tag=nil)
|
25
|
+
xml = ""
|
26
|
+
has_parent = !parent_tag.nil?
|
27
|
+
|
28
|
+
# Arrays are formatted with the parent tag
|
29
|
+
# wrapping each of the array elements for some
|
30
|
+
# reason
|
31
|
+
if obj.is_a? Array
|
32
|
+
obj.each do |e|
|
33
|
+
xml += has_parent ? "<#{parent_tag}>" : ""
|
34
|
+
xml += buildXmlFromObject(e)
|
35
|
+
xml += has_parent ? "</#{parent_tag}>" : ""
|
36
|
+
end
|
37
|
+
|
38
|
+
elsif obj.is_a? Hash
|
39
|
+
xml += has_parent ? "<#{parent_tag}>" : ""
|
40
|
+
obj.keys.each do |key|
|
41
|
+
xml += buildXmlFromObject(obj[key], key.to_s)
|
42
|
+
end
|
43
|
+
xml += has_parent ? "</#{parent_tag}>" : ""
|
44
|
+
|
45
|
+
elsif !obj.nil?
|
46
|
+
xml += has_parent ? "<#{parent_tag}>" : ""
|
47
|
+
xml += obj.to_s
|
48
|
+
xml += has_parent ? "</#{parent_tag}>" : ""
|
49
|
+
end
|
50
|
+
|
51
|
+
return xml
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module AuthorizeNet
|
2
|
+
URI = "https://api.authorize.net/xml/v1/request.api"
|
3
|
+
TEST_URI = "https://apitest.authorize.net/xml/v1/request.api"
|
4
|
+
XML_SCHEMA = "AnetApi/xml/v1/schema/AnetApiSchema.xsd"
|
5
|
+
XML_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
|
6
|
+
RESULT_OK = "Ok"
|
7
|
+
RESULT_ERROR = "Error"
|
8
|
+
|
9
|
+
# ===============================================================
|
10
|
+
# Constants for types of authorize net credit card validation
|
11
|
+
#
|
12
|
+
# Live Mode - Executes a test charge on the credit card for $0.01
|
13
|
+
# that is immediately voided
|
14
|
+
# Test Mode - Does basic mathematical checks on card validity
|
15
|
+
# None - No validation, could be useful for integration tests?
|
16
|
+
# ===============================================================
|
17
|
+
module ValidationMode
|
18
|
+
LIVE = "liveMode"
|
19
|
+
TEST = "testMode"
|
20
|
+
NONE = "None"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# require all authorize-net files
|
25
|
+
Dir['lib/authorize_net/**/*.rb'].each do |filename|
|
26
|
+
match = filename.match(/lib\/(authorize_net\/.*).rb/)
|
27
|
+
if !match.nil?
|
28
|
+
require match[1]
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: authorize_net
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Avenir Interactive LLC
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.6.7.2
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.6.7.2
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - '='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.6.7.2
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.6.7.2
|
33
|
+
description: A RubyGem that interfaces with the Authorize.net payment gateway
|
34
|
+
email:
|
35
|
+
- info@avenirhq.com
|
36
|
+
executables: []
|
37
|
+
extensions: []
|
38
|
+
extra_rdoc_files: []
|
39
|
+
files:
|
40
|
+
- lib/authorize_net.rb
|
41
|
+
- lib/authorize_net/address.rb
|
42
|
+
- lib/authorize_net/api.rb
|
43
|
+
- lib/authorize_net/credit_card.rb
|
44
|
+
- lib/authorize_net/customer_profile.rb
|
45
|
+
- lib/authorize_net/data_object.rb
|
46
|
+
- lib/authorize_net/error_handler.rb
|
47
|
+
- lib/authorize_net/exception.rb
|
48
|
+
- lib/authorize_net/payment_profile.rb
|
49
|
+
- lib/authorize_net/request.rb
|
50
|
+
- lib/authorize_net/response.rb
|
51
|
+
- lib/authorize_net/transaction.rb
|
52
|
+
- lib/authorize_net/util.rb
|
53
|
+
homepage: https://github.com/AvenirHQ/authorize_net
|
54
|
+
licenses:
|
55
|
+
- MIT
|
56
|
+
metadata: {}
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 2.5.2
|
74
|
+
signing_key:
|
75
|
+
specification_version: 4
|
76
|
+
summary: API interface for Authorize.net payment gateway
|
77
|
+
test_files: []
|
78
|
+
has_rdoc:
|