diff --git a/app/services/proofing/aamva/applicant.rb b/app/services/proofing/aamva/applicant.rb index a567329ca1e..d704cfe8708 100644 --- a/app/services/proofing/aamva/applicant.rb +++ b/app/services/proofing/aamva/applicant.rb @@ -21,6 +21,8 @@ module Aamva :state_id_number, :state_id_jurisdiction, :state_id_type, + :state_id_issued, + :state_id_expiration, keyword_init: true, ).freeze @@ -64,6 +66,8 @@ def self.from_proofer_applicant(applicant) state_id_number: applicant.dig(:state_id_number)&.gsub(/[^\w\d]/, ''), state_id_jurisdiction: applicant[:state_id_jurisdiction], state_id_type: applicant[:state_id_type], + state_id_issued: applicant[:state_id_issued], + state_id_expiration: applicant[:state_id_expiration], ) end end.freeze diff --git a/app/services/proofing/aamva/proofer.rb b/app/services/proofing/aamva/proofer.rb index c73841282d8..48616af80d0 100644 --- a/app/services/proofing/aamva/proofer.rb +++ b/app/services/proofing/aamva/proofer.rb @@ -21,6 +21,18 @@ class Proofer ], ).freeze + ADDRESS_ATTRIBUTES = [ + :address1, + :address2, + :city, + :state, + :zipcode, + ].to_set.freeze + + OPTIONAL_ADDRESS_ATTRIBUTES = [:address2].freeze + + REQUIRED_ADDRESS_ATTRIBUTES = (ADDRESS_ATTRIBUTES - OPTIONAL_ADDRESS_ATTRIBUTES).freeze + attr_reader :config # Instance methods @@ -31,6 +43,7 @@ def initialize(config) def proof(applicant) aamva_applicant = Aamva::Applicant.from_proofer_applicant(OpenStruct.new(applicant)) + response = Aamva::VerificationClient.new( config, ).send_verification_request( @@ -55,6 +68,7 @@ def build_result_from_response(verification_response) exception: nil, vendor_name: 'aamva:state_id', transaction_id: verification_response.transaction_locator_id, + requested_attributes: requested_attributes(verification_response).index_with(1), verified_attributes: verified_attributes(verification_response), ) end @@ -73,30 +87,30 @@ def parse_verification_errors(verification_response) errors end - def verified_attributes(verification_response) - attributes = Set.new - results = verification_response.verification_results - - attributes.add :address if address_verified?(results) + def requested_attributes(verification_response) + attributes = verification_response. + verification_results.filter { |_, verified| !verified.nil? }. + keys. + to_set - results.delete :address1 - results.delete :address2 - results.delete :city - results.delete :state - results.delete :zipcode + normalize_address_attributes(attributes) + end - results.each do |attribute, verified| - attributes.add attribute if verified - end + def verified_attributes(verification_response) + attributes = verification_response. + verification_results.filter { |_, verified| verified }. + keys. + to_set - attributes + normalize_address_attributes(attributes) end - def address_verified?(results) - results[:address1] && - results[:city] && - results[:state] && - results[:zipcode] + def normalize_address_attributes(attribute_set) + all_present = REQUIRED_ADDRESS_ATTRIBUTES & attribute_set == REQUIRED_ADDRESS_ATTRIBUTES + + (attribute_set - ADDRESS_ATTRIBUTES).tap do |result| + result.add(:address) if all_present + end end def send_to_new_relic(result) diff --git a/app/services/proofing/aamva/request/verification_request.rb b/app/services/proofing/aamva/request/verification_request.rb index df38c8b2d3f..69e45d65dd8 100644 --- a/app/services/proofing/aamva/request/verification_request.rb +++ b/app/services/proofing/aamva/request/verification_request.rb @@ -62,34 +62,43 @@ def add_user_provided_data_to_body user_provided_data_map.each do |xpath, data| REXML::XPath.first(document, xpath).add_text(data) end - add_street_address_line_2_to_rexml_document(document) if applicant.address2.present? + + add_optional_element( + 'ns2:AddressDeliveryPointText', + value: applicant.address2, + document:, + after: '//ns1:Address/ns2:AddressDeliveryPointText', + ) + + add_optional_element( + 'ns2:DriverLicenseIssueDate', + value: applicant.state_id_data.state_id_issued, + document:, + inside: '//ns:verifyDriverLicenseDataRequest', + ) + + add_optional_element( + 'ns2:DriverLicenseExpirationDate', + value: applicant.state_id_data.state_id_expiration, + document:, + inside: '//ns:verifyDriverLicenseDataRequest', + ) + @body = document.to_s end - def add_street_address_line_2_to_rexml_document(document) - old_address_node = document.delete_element('//ns1:Address') - new_address_node = old_address_node.clone - old_address_node.children.each do |child_node| - next unless child_node.node_type == :element + def add_optional_element(name, value:, document:, inside: nil, after: nil) + return if value.blank? - new_element = child_node.clone - new_element.add_text(child_node.text) - new_address_node.add_element(new_element) + el = REXML::Element.new(name) + el.text = value - if child_node.name == 'AddressDeliveryPointText' - new_address_node.add_element(address_line_2_element) - end + if inside + REXML::XPath.first(document, inside).add_element(el) + elsif after + sibling = REXML::XPath.first(document, after) + sibling.parent.insert_after(sibling, el) end - REXML::XPath.first( - document, - '//ns:verifyDriverLicenseDataRequest', - ).add_element(new_address_node) - end - - def address_line_2_element - element = REXML::Element.new('ns2:AddressDeliveryPointText') - element.add_text(applicant.address2) - element end def build_request_body diff --git a/app/services/proofing/aamva/response/verification_response.rb b/app/services/proofing/aamva/response/verification_response.rb index b26c6e20f69..adcbdadfb7b 100644 --- a/app/services/proofing/aamva/response/verification_response.rb +++ b/app/services/proofing/aamva/response/verification_response.rb @@ -8,6 +8,8 @@ module Aamva module Response class VerificationResponse VERIFICATION_ATTRIBUTES_MAP = { + 'DriverLicenseExpirationDateMatchIndicator' => :state_id_expiration, + 'DriverLicenseIssueDateMatchIndicator' => :state_id_issued, 'DriverLicenseNumberMatchIndicator' => :state_id_number, 'DocumentCategoryMatchIndicator' => :state_id_type, 'PersonBirthDateMatchIndicator' => :dob, @@ -62,6 +64,7 @@ def success? REQUIRED_VERIFICATION_ATTRIBUTES.each do |verification_attribute| return false unless verification_results[verification_attribute] end + true end diff --git a/app/services/proofing/state_id_result.rb b/app/services/proofing/state_id_result.rb index 459603b67d5..275303357d9 100644 --- a/app/services/proofing/state_id_result.rb +++ b/app/services/proofing/state_id_result.rb @@ -11,6 +11,7 @@ class StateIdResult :success, :vendor_name, :transaction_id, + :requested_attributes, :verified_attributes def initialize( @@ -19,6 +20,7 @@ def initialize( exception: nil, vendor_name: nil, transaction_id: '', + requested_attributes: [], verified_attributes: [] ) @success = success @@ -26,6 +28,7 @@ def initialize( @exception = exception @vendor_name = vendor_name @transaction_id = transaction_id + @requested_attributes = requested_attributes @verified_attributes = verified_attributes end diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index 639d69011fd..c41649e4b7c 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -88,7 +88,7 @@ expect(result_context_stages_state_id[:success]).to eq(true) expect(result_context_stages_state_id[:timed_out]).to eq(false) expect(result_context_stages_state_id[:transaction_id]).to eq('1234-abcd-efgh') - expect(result_context_stages_state_id[:verified_attributes]).to eq( + expect(result_context_stages_state_id[:verified_attributes]).to match_array( %w[address state_id_number state_id_type dob last_name first_name], ) @@ -165,7 +165,7 @@ expect(result_context_stages_state_id[:success]).to eq(true) expect(result_context_stages_state_id[:timed_out]).to eq(false) expect(result_context_stages_state_id[:transaction_id]).to eq('1234-abcd-efgh') - expect(result_context_stages_state_id[:verified_attributes]).to eq( + expect(result_context_stages_state_id[:verified_attributes]).to match_array( %w[address state_id_number state_id_type dob last_name first_name], ) @@ -249,7 +249,7 @@ # result[:context][:stages][:state_id] expect(result_context_stages_state_id[:vendor_name]).to eq('aamva:state_id') expect(result_context_stages_state_id[:success]).to eq(true) - expect(result_context_stages_state_id[:verified_attributes]).to eq( + expect(result_context_stages_state_id[:verified_attributes]).to match_array( %w[address state_id_number state_id_type dob last_name first_name], ) end @@ -490,7 +490,7 @@ expect(result_context_stages_state_id[:success]).to eq(true) expect(result_context_stages_state_id[:timed_out]).to eq(false) expect(result_context_stages_state_id[:transaction_id]).to eq('1234-abcd-efgh') - expect(result_context_stages_state_id[:verified_attributes]).to eq( + expect(result_context_stages_state_id[:verified_attributes]).to match_array( %w[address state_id_number state_id_type dob last_name first_name], ) diff --git a/spec/services/proofing/aamva/proofer_spec.rb b/spec/services/proofing/aamva/proofer_spec.rb index eee30ed3cb1..6afb19420a8 100644 --- a/spec/services/proofing/aamva/proofer_spec.rb +++ b/spec/services/proofing/aamva/proofer_spec.rb @@ -67,6 +67,20 @@ ].to_set, ) end + + it 'includes requested_attributes' do + result = subject.proof(state_id_data) + expect(result.requested_attributes).to eq( + { + dob: 1, + state_id_number: 1, + state_id_type: 1, + last_name: 1, + first_name: 1, + address: 1, + }, + ) + end end context 'when verification is unsuccessful' do @@ -98,6 +112,20 @@ ].to_set, ) end + + it 'includes requested_attributes' do + result = subject.proof(state_id_data) + expect(result.requested_attributes).to eq( + { + dob: 1, + state_id_number: 1, + state_id_type: 1, + last_name: 1, + first_name: 1, + address: 1, + }, + ) + end end context 'when verification attributes are missing' do @@ -128,6 +156,43 @@ ].to_set, ) end + + it 'includes requested_attributes' do + result = subject.proof(state_id_data) + expect(result.requested_attributes).to eq( + { + state_id_number: 1, + state_id_type: 1, + last_name: 1, + first_name: 1, + address: 1, + }, + ) + end + end + + context 'when issue / expiration present' do + let(:state_id_data) do + { + state_id_number: '1234567890', + state_id_jurisdiction: 'VA', + state_id_type: 'drivers_license', + state_id_issued: '2023-04-05', + state_id_expiration: '2030-01-02', + } + end + + it 'includes them' do + expect(Proofing::Aamva::Request::VerificationRequest).to receive(:new).with( + hash_including( + applicant: satisfy do |a| + expect(a.state_id_data.state_id_issued).to eql('2023-04-05') + expect(a.state_id_data.state_id_expiration).to eql('2030-01-02') + end, + ), + ) + subject.proof(state_id_data) + end end context 'when AAMVA throws an exception' do diff --git a/spec/services/proofing/aamva/request/verification_request_spec.rb b/spec/services/proofing/aamva/request/verification_request_spec.rb index 2f379d5a911..19279b21218 100644 --- a/spec/services/proofing/aamva/request/verification_request_spec.rb +++ b/spec/services/proofing/aamva/request/verification_request_spec.rb @@ -71,6 +71,20 @@ ], ) end + + it 'includes issue date if present' do + applicant.state_id_data.state_id_issued = '2024-05-06' + expect(subject.body).to include( + '2024-05-06', + ) + end + + it 'includes expiration date if present' do + applicant.state_id_data.state_id_expiration = '2030-01-02' + expect(subject.body).to include( + '2030-01-02', + ) + end end describe '#headers' do diff --git a/spec/services/proofing/aamva/response/verification_response_spec.rb b/spec/services/proofing/aamva/response/verification_response_spec.rb index 47d57cba506..2bd0287eb63 100644 --- a/spec/services/proofing/aamva/response/verification_response_spec.rb +++ b/spec/services/proofing/aamva/response/verification_response_spec.rb @@ -13,6 +13,8 @@ end let(:verification_results) do { + state_id_expiration: nil, + state_id_issued: nil, state_id_number: true, state_id_type: true, dob: true,