experian 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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +157 -0
- data/Rakefile +9 -0
- data/experian.gemspec +24 -0
- data/lib/experian.rb +73 -0
- data/lib/experian/client.rb +29 -0
- data/lib/experian/connect_check.rb +29 -0
- data/lib/experian/connect_check/client.rb +19 -0
- data/lib/experian/connect_check/request.rb +116 -0
- data/lib/experian/connect_check/response.rb +103 -0
- data/lib/experian/constants.rb +41 -0
- data/lib/experian/error.rb +22 -0
- data/lib/experian/request.rb +23 -0
- data/lib/experian/response.rb +118 -0
- data/lib/experian/version.rb +3 -0
- data/test/client_test.rb +31 -0
- data/test/connect_check/client_test.rb +21 -0
- data/test/connect_check/request_test.rb +23 -0
- data/test/connect_check/response_test.rb +94 -0
- data/test/experian_test.rb +19 -0
- data/test/fixtures/connect_check/request.xml +43 -0
- data/test/fixtures/connect_check/response.xml +7 -0
- data/test/fixtures/connect_check/response_error.xml +7 -0
- data/test/fixtures/connect_check/response_error_legacy.xml +7 -0
- data/test/request_test.rb +10 -0
- data/test/response_test.rb +40 -0
- data/test/test_helper.rb +35 -0
- metadata +127 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
module Experian
|
2
|
+
module ConnectCheck
|
3
|
+
class Response < Experian::Response
|
4
|
+
|
5
|
+
def input_type
|
6
|
+
return unless connect_check_segment
|
7
|
+
connect_check_segment[7]
|
8
|
+
end
|
9
|
+
|
10
|
+
def credit_match_code
|
11
|
+
return unless connect_check_segment
|
12
|
+
connect_check_segment[8]
|
13
|
+
end
|
14
|
+
|
15
|
+
def credit_match_code_message
|
16
|
+
MATCH_CODES[credit_match_code]
|
17
|
+
end
|
18
|
+
|
19
|
+
def credit_class_code
|
20
|
+
return unless connect_check_segment
|
21
|
+
connect_check_segment[10]
|
22
|
+
end
|
23
|
+
|
24
|
+
def credit_score
|
25
|
+
return unless risk_score_segment
|
26
|
+
risk_score_segment[7..10].to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
def high_risk_address_alert
|
30
|
+
return unless connect_check_segment
|
31
|
+
connect_check_segment[11]
|
32
|
+
end
|
33
|
+
|
34
|
+
def credit_fraud_code
|
35
|
+
case statement_type_code
|
36
|
+
when 25 then 'X' # file frozen due to state legislation
|
37
|
+
when 26..31 then 'Y' # active credit alerts
|
38
|
+
else 'Z' # assume no fraud
|
39
|
+
end if success?
|
40
|
+
end
|
41
|
+
|
42
|
+
def statement_type_code
|
43
|
+
return unless consumer_statement_segment
|
44
|
+
consumer_statement_segment[7..8].to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
def customer_name_length
|
48
|
+
return unless connect_check_segment
|
49
|
+
connect_check_segment[23..25].to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def customer_name
|
53
|
+
return unless connect_check_segment
|
54
|
+
connect_check_segment[25, customer_name_length]
|
55
|
+
end
|
56
|
+
|
57
|
+
def customer_names
|
58
|
+
segments(335).map do |segment|
|
59
|
+
segment[9, segment[7..8].to_i]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def customer_addresses
|
64
|
+
segments(336).map do |segment|
|
65
|
+
segment[36, segment[34..35].to_i]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def customer_message_length
|
70
|
+
return unless connect_check_segment
|
71
|
+
connect_check_segment[25 + customer_name_length, 2].to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
def customer_message
|
75
|
+
return unless connect_check_segment
|
76
|
+
connect_check_segment[25 + customer_name_length + 2, customer_message_length]
|
77
|
+
end
|
78
|
+
|
79
|
+
def success?
|
80
|
+
super && !header_segment.nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def consumer_statement_segment
|
86
|
+
segment(365)
|
87
|
+
end
|
88
|
+
|
89
|
+
def connect_check_segment
|
90
|
+
segment(111)
|
91
|
+
end
|
92
|
+
|
93
|
+
def risk_score_segment
|
94
|
+
segment(125)
|
95
|
+
end
|
96
|
+
|
97
|
+
def header_segment
|
98
|
+
segment(110)
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Experian
|
2
|
+
module Constants
|
3
|
+
|
4
|
+
LOOKUP_SERVLET_URL = "http://www.experian.com/lookupServlet1"
|
5
|
+
ECALS_TIMEOUT = 86400 # 24 hours in seconds
|
6
|
+
|
7
|
+
LOOKUP_SERVICE_NAME = "AccessPoint"
|
8
|
+
LOOKUP_SERVICE_VERSION = 1.0
|
9
|
+
|
10
|
+
SERVICE_NAME = "NetConnect"
|
11
|
+
SERVICE_NAME_TEST = "NetConnectDemo"
|
12
|
+
|
13
|
+
SERVICE_VERSION = 2.0
|
14
|
+
|
15
|
+
ARF_VERSION = "06"
|
16
|
+
|
17
|
+
XML_NAMESPACE = "http://www.experian.com/NetConnect"
|
18
|
+
XML_SCHEMA_INSTANCE = "http://www.w3.org/2001/XMLSchema-instance"
|
19
|
+
XML_SCHEMA_LOCATION = "http://www.experian.com/NetConnect NetConnect.xsd"
|
20
|
+
XML_REQUEST_NAMESPACE = "http://www.experian.com/WebDelivery"
|
21
|
+
|
22
|
+
COMPLETION_CODES = {
|
23
|
+
"0000" => "Request processed successfully",
|
24
|
+
"1000" => "Invalid request format",
|
25
|
+
"1001" => "Invalid length",
|
26
|
+
"1002" => "Invalid length",
|
27
|
+
"1003" => "No XML request",
|
28
|
+
"1004" => "Invalid request parameter",
|
29
|
+
"2000" => "Authorization failure",
|
30
|
+
"4000" => "System error. Call Experian Technical Support at 1-800-854-7201."
|
31
|
+
}
|
32
|
+
|
33
|
+
ERROR_ACTION_INDICATORS = {
|
34
|
+
"I" => "Informative",
|
35
|
+
"C" => "Correct and/or resubmit",
|
36
|
+
"R" => "Report condition or database problem",
|
37
|
+
"S" => "Suspend"
|
38
|
+
}
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Experian
|
2
|
+
class Error < StandardError
|
3
|
+
|
4
|
+
def self.message(code)
|
5
|
+
"Experian: " + (codes[code] || "Error Code #{code}")
|
6
|
+
end
|
7
|
+
|
8
|
+
# Experian has hundreds of codes documented, only populated small set
|
9
|
+
def self.codes
|
10
|
+
{
|
11
|
+
1 => "System temporarily unavailable. Please resubmit",
|
12
|
+
53 => "Invalid social security number"
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class ClientError < Error; end
|
18
|
+
class ArgumentError < ClientError; end
|
19
|
+
class Forbidden < ClientError; end
|
20
|
+
class ServerError < Error; end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Experian
|
2
|
+
class Request
|
3
|
+
|
4
|
+
attr_reader :xml
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@options = options
|
8
|
+
@xml = build_request
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_request
|
12
|
+
xml = Builder::XmlMarkup.new(:indent => 2)
|
13
|
+
xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8')
|
14
|
+
xml.tag!("NetConnectRequest",
|
15
|
+
:xmlns => Experian::XML_NAMESPACE,
|
16
|
+
'xmlns:xsi' => Experian::XML_SCHEMA_INSTANCE,
|
17
|
+
'xsi:schemaLocation' => Experian::XML_SCHEMA_LOCATION) do
|
18
|
+
yield xml if block_given?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
|
3
|
+
module Experian
|
4
|
+
class Response
|
5
|
+
|
6
|
+
attr_reader :xml
|
7
|
+
|
8
|
+
def initialize(xml)
|
9
|
+
@xml = xml
|
10
|
+
@response = parse_xml_response
|
11
|
+
end
|
12
|
+
|
13
|
+
def error_message
|
14
|
+
@response["ErrorMessage"] || (Experian::Error.message(error_code) if error_code)
|
15
|
+
end
|
16
|
+
|
17
|
+
def host_response
|
18
|
+
@response["HostResponse"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def completion_code
|
22
|
+
@response["CompletionCode"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def completion_message
|
26
|
+
Experian::COMPLETION_CODES[completion_code]
|
27
|
+
end
|
28
|
+
|
29
|
+
def transaction_id
|
30
|
+
@response["TransactionId"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def segments(segment_id = nil)
|
34
|
+
@segments ||= host_response ? host_response.split("@") : []
|
35
|
+
|
36
|
+
if segment_id
|
37
|
+
@segments.select { |segment| segment.length >= 3 ? segment[0..2].to_i == segment_id : false }
|
38
|
+
else
|
39
|
+
@segments
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def segment(segment_id)
|
44
|
+
segments(segment_id).first
|
45
|
+
end
|
46
|
+
|
47
|
+
def success?
|
48
|
+
completion_code == "0000"
|
49
|
+
end
|
50
|
+
|
51
|
+
def error?
|
52
|
+
completion_code != "0000" || !error_segment.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
# error_segment returns the entire host response (segments 100, 200, 900)
|
56
|
+
# since error responses do not separate segments with "@".
|
57
|
+
def error_segment
|
58
|
+
segment(100)
|
59
|
+
end
|
60
|
+
|
61
|
+
# The error message segment is embedded in the error segment :(
|
62
|
+
def error_message_segment
|
63
|
+
return unless error_segment
|
64
|
+
error_segment[error_segment.index("200")..-1]
|
65
|
+
end
|
66
|
+
|
67
|
+
def error_code
|
68
|
+
return unless error_segment
|
69
|
+
error_message_segment[6..8].to_i
|
70
|
+
end
|
71
|
+
|
72
|
+
def error_action_indicator
|
73
|
+
return unless error_segment
|
74
|
+
error_message_segment[9]
|
75
|
+
end
|
76
|
+
|
77
|
+
def error_action_indicator_message
|
78
|
+
Experian::ERROR_ACTION_INDICATORS[error_action_indicator]
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def parse_xml_response
|
84
|
+
xml = REXML::Document.new(@xml)
|
85
|
+
root = REXML::XPath.first(xml, "//NetConnectResponse")
|
86
|
+
if root
|
87
|
+
parse_element(root)
|
88
|
+
else
|
89
|
+
raise Experian::ClientError, "Invalid xml response from Experian"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# parse xml node elements recursively into hash
|
94
|
+
def parse_element(node)
|
95
|
+
if node.has_elements?
|
96
|
+
response = {}
|
97
|
+
node.elements.each do |e|
|
98
|
+
key = e.name
|
99
|
+
value = parse_element(e)
|
100
|
+
if response.has_key?(key)
|
101
|
+
if response[key].is_a?(Array)
|
102
|
+
response[key].push(value)
|
103
|
+
else
|
104
|
+
response[key] = [response[key], value]
|
105
|
+
end
|
106
|
+
else
|
107
|
+
response[key] = parse_element(e)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
else
|
111
|
+
response = node.text
|
112
|
+
end
|
113
|
+
response
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Experian::Client do
|
4
|
+
|
5
|
+
before do
|
6
|
+
stub_experian_uri_lookup
|
7
|
+
@client = Experian::Client.new
|
8
|
+
@client.stubs(:request).returns(stub(xml: "fake xml content"))
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should submit the request and return the response body" do
|
12
|
+
Excon::Connection.any_instance.expects(:post).returns(stub(body: "fake body content", headers: {}))
|
13
|
+
assert_equal "fake body content", @client.submit_request
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should set the correct content type header" do
|
17
|
+
assert_equal "application/x-www-form-urlencoded", @client.request_headers["Content-Type"]
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should set the body to the url encoded request xml" do
|
21
|
+
assert_equal "NETCONNECT_TRANSACTION=fake+xml+content", @client.request_body
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should raise a forbidden exception if logon location header returned" do
|
25
|
+
Excon::Connection.any_instance.expects(:post).returns(stub(headers: { "Location" => "sso_logon" }))
|
26
|
+
assert_raises(Experian::Forbidden) do
|
27
|
+
@client.submit_request
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Experian::ConnectCheck::Client do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@client = Experian::ConnectCheck::Client.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should perform a credit check" do
|
10
|
+
stub_experian_request("connect_check", "response.xml")
|
11
|
+
assert_kind_of Experian::ConnectCheck::Response,
|
12
|
+
@client.check_credit(first_name: "Homer", last_name: "Simpson", ssn: "123456789")
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should raise an ArgumentError if passed bad arguments" do
|
16
|
+
assert_raises(Experian::ArgumentError) do
|
17
|
+
@client.assert_check_credit_options({})
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Experian::ConnectCheck::Request do
|
4
|
+
|
5
|
+
it "creates a request in xml format" do
|
6
|
+
request = Experian::ConnectCheck::Request.new(
|
7
|
+
first_name: 'Homer',
|
8
|
+
last_name: 'Simpson',
|
9
|
+
ssn: '123456789',
|
10
|
+
reference_id: 'fakeReferenceId',
|
11
|
+
street: '10655 NorthBirch Street',
|
12
|
+
city: 'Springfield',
|
13
|
+
state: 'IL',
|
14
|
+
zip: '60611',
|
15
|
+
yob: '1951',
|
16
|
+
reference_number: '00234',
|
17
|
+
end_user: 'homer'
|
18
|
+
)
|
19
|
+
|
20
|
+
assert_equal request.xml, fixture("connect_check", "request.xml")
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Experian::ConnectCheck::Response do
|
4
|
+
|
5
|
+
describe "successful response" do
|
6
|
+
before do
|
7
|
+
stub_experian_request("connect_check", "response.xml")
|
8
|
+
@response = Experian::ConnectCheck.check_credit(first_name: "Homer", last_name: "Simpson", ssn: "123456789")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "receives a successful response" do
|
12
|
+
assert @response.success?
|
13
|
+
refute @response.error?
|
14
|
+
end
|
15
|
+
|
16
|
+
it "extracts the credit match code" do
|
17
|
+
assert_equal "C", @response.credit_match_code
|
18
|
+
end
|
19
|
+
|
20
|
+
it "maps the credit match code message" do
|
21
|
+
assert_equal "ID Match", @response.credit_match_code_message
|
22
|
+
end
|
23
|
+
|
24
|
+
it "extracts the credit class code" do
|
25
|
+
assert_equal "3", @response.credit_class_code
|
26
|
+
end
|
27
|
+
|
28
|
+
it "extracts the credit score" do
|
29
|
+
assert_equal 603, @response.credit_score
|
30
|
+
end
|
31
|
+
|
32
|
+
it "extracts the high risk address alert" do
|
33
|
+
assert_equal "X", @response.high_risk_address_alert
|
34
|
+
end
|
35
|
+
|
36
|
+
it "extracts the credit fraud code" do
|
37
|
+
assert_equal "Z", @response.credit_fraud_code
|
38
|
+
end
|
39
|
+
|
40
|
+
it "extracts the input type" do
|
41
|
+
assert_equal "S", @response.input_type
|
42
|
+
end
|
43
|
+
|
44
|
+
it "extracts the customer name" do
|
45
|
+
assert_equal "AMBLE,ROBERT", @response.customer_name
|
46
|
+
end
|
47
|
+
|
48
|
+
it "extracts the customer message" do
|
49
|
+
assert_equal "Fraud alert on account. Deposit required to complete enrollment.", @response.customer_message
|
50
|
+
end
|
51
|
+
|
52
|
+
it "extracts the statement type code" do
|
53
|
+
assert_equal 6, @response.statement_type_code
|
54
|
+
@response.customer_names
|
55
|
+
end
|
56
|
+
|
57
|
+
it "extracts the customer name aliases" do
|
58
|
+
assert_includes @response.customer_names, "ROBERT K AMBLE"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "extracts the customer addresses" do
|
62
|
+
assert_includes @response.customer_addresses, "6717 11TH AVE /BROOKLYN NY 112195904"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "error response" do
|
67
|
+
before do
|
68
|
+
stub_experian_request("connect_check", "response_error_legacy.xml")
|
69
|
+
@response = Experian::ConnectCheck.check_credit(first_name: "Homer", last_name: "Simpson", ssn: "123456789")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "receives an error response" do
|
73
|
+
refute @response.success?
|
74
|
+
assert @response.error?
|
75
|
+
end
|
76
|
+
|
77
|
+
it "extracts the error code" do
|
78
|
+
assert_equal 45, @response.error_code
|
79
|
+
end
|
80
|
+
|
81
|
+
it "extracts the error action indicator" do
|
82
|
+
assert_equal "C", @response.error_action_indicator
|
83
|
+
end
|
84
|
+
|
85
|
+
it "translates the error action indicator message" do
|
86
|
+
assert_equal "Correct and/or resubmit", @response.error_action_indicator_message
|
87
|
+
end
|
88
|
+
|
89
|
+
it "maps an error message based on code" do
|
90
|
+
assert_equal "Experian: Error Code 45", @response.error_message
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|