diff --git a/app/jobs/address_proofing_job.rb b/app/jobs/address_proofing_job.rb index 982bfb94c12..bfcc53d77b4 100644 --- a/app/jobs/address_proofing_job.rb +++ b/app/jobs/address_proofing_job.rb @@ -27,15 +27,8 @@ def perform(user_id:, issuer:, result_id:, encrypted_arguments:, trace_id:) ) Db::ProofingCost::AddUserProofingCost.call(user_id, :lexis_nexis_address) - result = proofer_result.to_h - result[:context] = { stages: [address: address_proofer.class.vendor_name] } - result[:transaction_id] = proofer_result.transaction_id - - result[:timed_out] = proofer_result.timed_out? - result[:exception] = proofer_result.exception.inspect if proofer_result.exception - document_capture_session = DocumentCaptureSession.new(result_id: result_id) - document_capture_session.store_proofing_result(result) + document_capture_session.store_proofing_result(proofer_result.to_h) ensure logger.info( { diff --git a/app/services/proofing/lexis_nexis/phone_finder/proofer.rb b/app/services/proofing/lexis_nexis/phone_finder/proofer.rb index 1671dfd93b4..26aebab79df 100644 --- a/app/services/proofing/lexis_nexis/phone_finder/proofer.rb +++ b/app/services/proofing/lexis_nexis/phone_finder/proofer.rb @@ -1,26 +1,19 @@ module Proofing module LexisNexis module PhoneFinder - class Proofer < LexisNexis::Proofer - vendor_name 'lexisnexis:phone_finder' + class Proofer + attr_reader :config - required_attributes :uuid, - :first_name, - :last_name, - :dob, - :ssn, - :phone - - optional_attributes :uuid_prefix - - stage :address - - proof do |applicant, result| - proof_applicant(applicant, result) + def initialize(config) + @config = LexisNexis::Proofer::Config.new(config) end - def send_verification_request(applicant) - VerificationRequest.new(config: config, applicant: applicant).send + def proof(applicant) + response = VerificationRequest.new(config: config, applicant: applicant).send + return Proofing::LexisNexis::PhoneFinder::Result.new(response) + rescue => exception + NewRelic::Agent.notice_error(exception) + ResultWithException.new(exception) end end end diff --git a/app/services/proofing/lexis_nexis/phone_finder/result.rb b/app/services/proofing/lexis_nexis/phone_finder/result.rb new file mode 100644 index 00000000000..a41bf82ff37 --- /dev/null +++ b/app/services/proofing/lexis_nexis/phone_finder/result.rb @@ -0,0 +1,58 @@ +module Proofing + module LexisNexis + module PhoneFinder + class Result + attr_reader :verification_response + + delegate( + :reference, + :verification_status, + :verification_errors, + to: :verification_response, + ) + + def initialize(verification_response) + @verification_response = verification_response + end + + def errors + return @errors if defined?(@errors) + + @errors = {} + verification_errors.each do |key, value| + @errors[key] ||= [] + @errors[key].push(value) + end + @errors + end + + def exception + nil + end + + def success? + verification_response.verification_status == 'passed' + end + + def timed_out? + false + end + + def transaction_id + verification_response.conversation_id + end + + def to_h + { + exception: exception, + errors: errors, + success: success?, + timed_out: timed_out?, + transaction_id: transaction_id, + vendor_name: 'lexisnexis:phone_finder', + } + end + end + end + end +end diff --git a/app/services/proofing/lexis_nexis/phone_finder/result_with_exception.rb b/app/services/proofing/lexis_nexis/phone_finder/result_with_exception.rb new file mode 100644 index 00000000000..bf7549a92f6 --- /dev/null +++ b/app/services/proofing/lexis_nexis/phone_finder/result_with_exception.rb @@ -0,0 +1,35 @@ +module Proofing + module LexisNexis + module PhoneFinder + class ResultWithException + attr_reader :exception + + def initialize(exception) + @exception = exception + end + + def success? + false + end + + def errors + {} + end + + def timed_out? + exception.is_a?(Proofing::TimeoutError) + end + + def to_h + { + success: success?, + errors: errors, + exception: exception, + timed_out: timed_out?, + vendor_name: 'lexisnexis:phone_finder', + } + end + end + end + end +end diff --git a/app/services/proofing/mock/address_mock_client.rb b/app/services/proofing/mock/address_mock_client.rb index 71ee3887b6c..028e7d8161d 100644 --- a/app/services/proofing/mock/address_mock_client.rb +++ b/app/services/proofing/mock/address_mock_client.rb @@ -1,35 +1,69 @@ module Proofing module Mock - class AddressMockClient < Proofing::Base - vendor_name 'AddressMock' - - required_attributes :uuid, - :first_name, - :last_name, - :dob, - :ssn, - :phone - - optional_attributes :uuid_prefix - - stage :address - + class AddressMockClient UNVERIFIABLE_PHONE_NUMBER = '7035555555' PROOFER_TIMEOUT_PHONE_NUMBER = '7035555888' FAILED_TO_CONTACT_PHONE_NUMBER = '7035555999' TRANSACTION_ID = 'address-mock-transaction-id-123' - proof do |applicant, result| + AddressMockClientResult = Struct.new(:success, :errors, :exception, keyword_init: true) do + def success? + success + end + + def transaction_id + TRANSACTION_ID + end + + def to_h + { + exception: exception, + errors: errors, + success: success, + timed_out: exception.is_a?(Proofing::TimeoutError), + transaction_id: transaction_id, + vendor_name: 'AddressMock', + } + end + end + + def proof(applicant) plain_phone = applicant[:phone].gsub(/\D/, '').delete_prefix('1') if plain_phone == UNVERIFIABLE_PHONE_NUMBER - result.add_error(:phone, 'The phone number could not be verified.') + unverifiable_phone_result elsif plain_phone == FAILED_TO_CONTACT_PHONE_NUMBER - raise 'Failed to contact proofing vendor' + failed_to_contact_vendor_result elsif plain_phone == PROOFER_TIMEOUT_PHONE_NUMBER - raise Proofing::TimeoutError, 'address mock timeout' + timeout_result + else + AddressMockClientResult.new(success: true, errors: {}, exception: nil) end - result.transaction_id = TRANSACTION_ID - result.context[:message] = 'some context for the mock address proofer' + end + + private + + def unverifiable_phone_result + AddressMockClientResult.new( + success: false, + errors: { phone: ['The phone number could not be verified.'] }, + exception: nil, + ) + end + + def failed_to_contact_vendor_result + AddressMockClientResult.new( + success: false, + errors: {}, + exception: RuntimeError.new('Failed to contact proofing vendor'), + ) + end + + def timeout_result + AddressMockClientResult.new( + success: false, + errors: {}, + exception: Proofing::TimeoutError.new('address mock timeout'), + ) end end end diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb index dbae6fed8da..7224b4a5b63 100644 --- a/spec/controllers/idv/phone_controller_spec.rb +++ b/spec/controllers/idv/phone_controller_spec.rb @@ -320,14 +320,13 @@ stub_analytics allow(@analytics).to receive(:track_event) - context = { stages: [{ address: 'AddressMock' }] } result = { success: true, new_phone_added: true, errors: {}, pii_like_keypaths: [[:errors, :phone], [:context, :stages, :address]], vendor: { - context: context, + vendor_name: 'AddressMock', exception: nil, timed_out: false, transaction_id: 'address-mock-transaction-id-123', @@ -369,7 +368,6 @@ stub_analytics allow(@analytics).to receive(:track_event) - context = { stages: [{ address: 'AddressMock' }] } result = { success: false, new_phone_added: true, @@ -378,7 +376,7 @@ }, pii_like_keypaths: [[:errors, :phone], [:context, :stages, :address]], vendor: { - context: context, + vendor_name: 'AddressMock', exception: nil, timed_out: false, transaction_id: 'address-mock-transaction-id-123', diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index d57f25ff86d..e8e775cde66 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -33,7 +33,7 @@ 'IdV: doc auth optional verify_wait submitted' => { success: true, errors: {}, address_edited: false, proofing_results: { exception: nil, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', timed_out: false, context: { should_proof_state_id: true, stages: { resolution: { client: 'ResolutionMock', errors: {}, exception: nil, success: true, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc' }, state_id: { client: 'StateIdMock', errors: {}, success: true, timed_out: false, exception: nil, transaction_id: 'state-id-mock-transaction-id-456', state: 'MT', state_id_jurisdiction: 'ND' } } } }, ssn_is_unique: true, step: 'verify_wait_step_show' }, 'IdV: phone of record visited' => {}, 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202' }, - 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, context: { stages: [{ address: 'AddressMock' }] }, transaction_id: 'address-mock-transaction-id-123', timed_out: false }, new_phone_added: false }, + 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false }, new_phone_added: false }, 'IdV: final resolution' => { success: true }, 'IdV: personal key visited' => {}, 'IdV: personal key submitted' => {}, @@ -113,7 +113,7 @@ 'IdV: in person proofing optional verify_wait submitted' => { success: true, step: 'verify_wait_step_show', address_edited: false, ssn_is_unique: true }, 'IdV: phone of record visited' => {}, 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202' }, - 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, context: { stages: [{ address: 'AddressMock' }] }, transaction_id: 'address-mock-transaction-id-123', timed_out: false }, new_phone_added: false }, + 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false }, new_phone_added: false }, 'IdV: Phone OTP delivery Selection Visited' => {}, 'IdV: Phone OTP Delivery Selection Submitted' => { success: true, otp_delivery_preference: 'sms' }, 'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202' }, diff --git a/spec/jobs/address_proofing_job_spec.rb b/spec/jobs/address_proofing_job_spec.rb index 9bc65c29636..96d26d169bc 100644 --- a/spec/jobs/address_proofing_job_spec.rb +++ b/spec/jobs/address_proofing_job_spec.rb @@ -78,16 +78,11 @@ result = document_capture_session.load_proofing_result[:result] - expect(result).to eq( - exception: nil, - errors: {}, - success: true, - timed_out: false, - transaction_id: conversation_id, - context: { stages: [ - { address: 'lexisnexis:phone_finder' }, - ] }, - ) + expect(result[:exception]).to be_nil + expect(result[:errors]).to eq({}) + expect(result[:success]).to be true + expect(result[:timed_out]).to be false + expect(result[:vendor_name]).to eq('lexisnexis:phone_finder') end it 'adds cost data' do diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb index 51a67e3453a..0b13603cd16 100644 --- a/spec/services/idv/agent_spec.rb +++ b/spec/services/idv/agent_spec.rb @@ -179,7 +179,7 @@ issuer: issuer, ) result = document_capture_session.load_proofing_result[:result] - expect(result[:context][:stages]).to include({ address: 'AddressMock' }) + expect(result[:vendor_name]).to eq('AddressMock') expect(result[:success]).to eq true end @@ -189,7 +189,7 @@ document_capture_session, trace_id: trace_id, user_id: user_id, issuer: issuer ) result = document_capture_session.load_proofing_result[:result] - expect(result[:context][:stages]).to include({ address: 'AddressMock' }) + expect(result[:vendor_name]).to eq('AddressMock') expect(result[:success]).to eq false end end diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index 7a06442e9cb..eb219503f70 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -49,10 +49,9 @@ let(:throttle) { Throttle.new(throttle_type: :proof_address, user: user) } it 'succeeds with good params' do - context = { stages: [{ address: 'AddressMock' }] } extra = { vendor: { - context: context, + vendor_name: 'AddressMock', exception: nil, timed_out: false, transaction_id: 'address-mock-transaction-id-123', @@ -79,10 +78,9 @@ end it 'fails with bad params' do - context = { stages: [{ address: 'AddressMock' }] } extra = { vendor: { - context: context, + vendor_name: 'AddressMock', exception: nil, timed_out: false, transaction_id: 'address-mock-transaction-id-123', diff --git a/spec/services/proofing/lexis_nexis/instant_verify/proofing_spec.rb b/spec/services/proofing/lexis_nexis/instant_verify/proofing_spec.rb index 7e18580ad24..4a449f2cdf5 100644 --- a/spec/services/proofing/lexis_nexis/instant_verify/proofing_spec.rb +++ b/spec/services/proofing/lexis_nexis/instant_verify/proofing_spec.rb @@ -16,6 +16,7 @@ zipcode: '70802-12345', } end + let(:verification_request) do Proofing::LexisNexis::InstantVerify::VerificationRequest.new( applicant: applicant, diff --git a/spec/services/proofing/lexis_nexis/phone_finder/proofing_spec.rb b/spec/services/proofing/lexis_nexis/phone_finder/proofing_spec.rb index 45dc5b3c68f..95056776de8 100644 --- a/spec/services/proofing/lexis_nexis/phone_finder/proofing_spec.rb +++ b/spec/services/proofing/lexis_nexis/phone_finder/proofing_spec.rb @@ -12,6 +12,7 @@ phone: '5551231234', } end + let(:verification_request) do Proofing::LexisNexis::PhoneFinder::VerificationRequest.new( applicant: applicant, @@ -26,24 +27,63 @@ end describe '#proof' do - subject(:result) { instance.proof(applicant) } + context 'when the response is a success' do + let(:response_body) { LexisNexisFixtures.phone_finder_success_response_json } + + it 'is a successful result' do + stub_request(:post, verification_request.url). + to_return(body: LexisNexisFixtures.phone_finder_success_response_json, status: 200) + + result = instance.proof(applicant) - before do - stub_request(:post, verification_request.url). - to_return(body: response_body, status: 200) + expect(result.success?).to eq(true) + expect(result.errors).to eq({}) + end end context 'when the response is a failure' do - let(:response_body) do - LexisNexisFixtures.instant_verify_date_of_birth_fail_response_json - end + let(:response_body) { LexisNexisFixtures.phone_finder_fail_response_json } it 'is a failure result' do + stub_request(:post, verification_request.url). + to_return(body: LexisNexisFixtures.phone_finder_fail_response_json, status: 200) + + result = instance.proof(applicant) + expect(result.success?).to eq(false) expect(result.errors).to include( base: include(a_kind_of(String)), - 'Execute Instant Verify': include(a_kind_of(Hash)), + 'PhoneFinder Checks': include(a_kind_of(Hash)), ) + expect(result.transaction_id).to eq('31000000000000') + expect(result.reference).to eq('Reference1') + end + end + + context 'when the request times out' do + it 'retuns a timeout result' do + stub_request(:post, verification_request.url).to_timeout + + result = instance.proof(applicant) + + expect(result.success?).to eq(false) + expect(result.errors).to eq({}) + expect(result.exception).to be_a(Proofing::TimeoutError) + expect(result.timed_out?).to eq(true) + end + end + + context 'when an error is raised' do + it 'returns a result with an exception' do + stub_request(:post, verification_request.url).to_raise(RuntimeError.new('fancy test error')) + + result = instance.proof(applicant) + + expect(result.success?).to eq(false) + expect(result.errors).to eq({}) + expect(result.exception).to be_a(RuntimeError) + expect(result.exception.message).to eq('fancy test error') + expect(result.timed_out?).to eq(false) end end end diff --git a/spec/services/proofing/lexis_nexis/phone_finder/result_spec.rb b/spec/services/proofing/lexis_nexis/phone_finder/result_spec.rb new file mode 100644 index 00000000000..25eec56f3fa --- /dev/null +++ b/spec/services/proofing/lexis_nexis/phone_finder/result_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +describe Proofing::LexisNexis::PhoneFinder::Result do + let(:response_body) { LexisNexisFixtures.instant_verify_success_response_json } + let(:response_status) { 200 } + let(:http_response) do + Faraday::Response.new(response_body: response_body, status: response_status) + end + let(:verification_response) do + Proofing::LexisNexis::Response.new(http_response) + end + + subject { described_class.new(verification_response) } + + it 'renders LexisNexis metadata' do + # expected values originate in the fixture + expect(subject.reference).to eq('Reference1') + expect(subject.transaction_id).to eq('123456') + end + + context 'successful response' do + it 'returns a successful verified result' do + expect(subject.success?).to eq(true) + expect(subject.verification_errors).to eq({}) + end + end + + context 'failed to match response' do + let(:response_body) { LexisNexisFixtures.phone_finder_fail_response_json } + + it 'returns a failed to match verified result' do + expect(subject.success?).to eq(false) + expect(subject.verification_errors).to include( + :base, + :PhoneFinder, + ) + end + end + + context 'error response' do + let(:response_body) { LexisNexisFixtures.instant_verify_error_response_json } + + it 'returns an error result' do + expect(subject.success?).to eq(false) + expect(subject.verification_errors).to match( + base: a_string_including('invalid_transaction_initiate'), + ) + end + end +end diff --git a/spec/services/proofing/lexis_nexis/phone_finder/result_with_exception_spec.rb b/spec/services/proofing/lexis_nexis/phone_finder/result_with_exception_spec.rb new file mode 100644 index 00000000000..19f140c90ed --- /dev/null +++ b/spec/services/proofing/lexis_nexis/phone_finder/result_with_exception_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +RSpec.describe Proofing::LexisNexis::PhoneFinder::ResultWithException do + let(:exception) { StandardError.new('test message') } + + subject { described_class.new(exception) } + + describe '#timed_out?' do + context 'with a timeout error' do + let(:exception) { Proofing::TimeoutError.new('hi') } + + it { expect(subject.timed_out?).to eq(true) } + end + + context 'with a error that is not a timeout error' do + let(:exception) { StandardError.new('test message') } + + it { expect(subject.timed_out?).to eq(false) } + end + end + + describe '#to_h' do + it 'returns a hash verion of the result' do + expect(subject.to_h).to eq( + success: false, + errors: {}, + exception: exception, + timed_out: false, + vendor_name: 'lexisnexis:phone_finder', + ) + end + end +end diff --git a/spec/services/proofing/mock/address_mock_client_spec.rb b/spec/services/proofing/mock/address_mock_client_spec.rb index ff9a6b05caf..30f903f3739 100644 --- a/spec/services/proofing/mock/address_mock_client_spec.rb +++ b/spec/services/proofing/mock/address_mock_client_spec.rb @@ -1,8 +1,76 @@ require 'rails_helper' RSpec.describe Proofing::Mock::AddressMockClient do - it_behaves_like_mock_proofer( - mock_proofer_class: Proofing::Mock::AddressMockClient, - real_proofer_class: Proofing::LexisNexis::PhoneFinder::Proofer, - ) + describe '#proof' do + let(:transaction_id) { 'address-mock-transaction-id-123' } + + context 'with a phone number that passes' do + it 'returns a successful result' do + result = subject.proof(phone: '2025551000') + + expect(result.success?).to eq(true) + expect(result.errors).to eq({}) + expect(result.transaction_id).to eq(transaction_id) + expect(result.to_h).to eq( + success: true, + errors: {}, + exception: nil, + timed_out: false, + transaction_id: transaction_id, + vendor_name: 'AddressMock', + ) + end + end + + context 'with a phone number that fails to match the user' do + it 'returns a proofing failed result' do + result = subject.proof(phone: '7035555555') + + expect(result.success?).to eq(false) + expect(result.errors).to eq(phone: ['The phone number could not be verified.']) + expect(result.to_h).to eq( + success: false, + errors: { phone: ['The phone number could not be verified.'] }, + exception: nil, + timed_out: false, + transaction_id: transaction_id, + vendor_name: 'AddressMock', + ) + end + end + + context 'with a phone number that raises an exception' do + it 'returns a result with an exception' do + result = subject.proof(phone: '7035555999') + + expect(result.success?).to eq(false) + expect(result.errors).to eq({}) + expect(result.to_h).to eq( + success: false, + errors: {}, + exception: RuntimeError.new('Failed to contact proofing vendor'), + timed_out: false, + transaction_id: transaction_id, + vendor_name: 'AddressMock', + ) + end + end + + context 'with a phone number that times out' do + it 'returns a result with a timeout exception' do + result = subject.proof(phone: '7035555888') + + expect(result.success?).to eq(false) + expect(result.errors).to eq({}) + expect(result.to_h).to eq( + success: false, + errors: {}, + exception: Proofing::TimeoutError.new('address mock timeout'), + timed_out: true, + transaction_id: transaction_id, + vendor_name: 'AddressMock', + ) + end + end + end end