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.
@@ -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
+
@@ -0,0 +1,3 @@
1
+ module Experian
2
+ VERSION = "0.1.0"
3
+ end
@@ -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