From cb55ebdf34511bdfcf81263727ebb312f9e752e2 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Tue, 26 Nov 2024 13:34:19 -0500 Subject: [PATCH 1/2] LG-14988 Send additional attributes to AAMVA We are planning on sending additional attributes that we read off of documents to AAMVA. This commit modifies the AAMVA client to do so. The new attributes are: - Middle name - Name suffix - Sex - Height - Weight - Eye color These attributes are only sent if they are available in the applicant. For most of the attributes we do not currently read them from the document so they will not be sent until we enable the feature to do so. The exception to this is middle name. For that reason a feature flag controls whether we send middle name. We will log whether these attributes are sent and whether they are validated. None of these attributes are "required attributes" so a user will still pass if they are absent or if they do not match. changelog: Internal, AAMVA DLDV, Send additional attributes to AAMVA --- app/services/proofing/aamva/applicant.rb | 22 +++ .../aamva/request/verification_request.rb | 57 ++++++++ .../aamva/response/verification_response.rb | 6 + config/application.yml.default | 2 + lib/identity_config.rb | 1 + .../aamva/responses/verification_response.xml | 10 +- ...rification_response_namespaced_success.xml | 8 +- .../services/proofing/aamva/applicant_spec.rb | 8 + spec/services/proofing/aamva/proofer_spec.rb | 138 ++++++++++++++++++ .../request/verification_request_spec.rb | 81 ++++++++++ .../response/verification_response_spec.rb | 6 + .../aamva/verification_client_spec.rb | 12 ++ 12 files changed, 349 insertions(+), 2 deletions(-) diff --git a/app/services/proofing/aamva/applicant.rb b/app/services/proofing/aamva/applicant.rb index d704cfe8708..a0e697b0e8c 100644 --- a/app/services/proofing/aamva/applicant.rb +++ b/app/services/proofing/aamva/applicant.rb @@ -8,7 +8,13 @@ module Aamva :uuid, :first_name, :last_name, + :middle_name, + :name_suffix, :dob, + :height, + :sex, + :weight, + :eye_color, :state_id_data, :address1, :address2, @@ -32,7 +38,13 @@ def self.from_proofer_applicant(applicant) uuid: applicant[:uuid], first_name: applicant[:first_name], last_name: applicant[:last_name], + middle_name: applicant[:middle_name], + name_suffix: applicant[:name_suffix], dob: format_dob(applicant[:dob]), + sex: applicant[:sex], + height: format_height(applicant[:height]), + weight: applicant[:weight], + eye_color: applicant[:eye_color], state_id_data: format_state_id_data(applicant), address1: applicant[:address1], address2: applicant[:address2], @@ -70,6 +82,16 @@ def self.from_proofer_applicant(applicant) state_id_expiration: applicant[:state_id_expiration], ) end + + private_class_method def self.format_height(height) + return if height.nil? + + # From the AAMVA DLDV guide regarding formatting the height: + # + # The height is provided in feet-inches (i.e. 5 foot 10 inches is presented as "510"). + # + [(height / 12).to_s, (height % 12).to_s].join('') + end end.freeze end end diff --git a/app/services/proofing/aamva/request/verification_request.rb b/app/services/proofing/aamva/request/verification_request.rb index 84e25e75b34..32e440407dc 100644 --- a/app/services/proofing/aamva/request/verification_request.rb +++ b/app/services/proofing/aamva/request/verification_request.rb @@ -63,6 +63,45 @@ def add_user_provided_data_to_body REXML::XPath.first(document, xpath).add_text(data) end + if IdentityConfig.store.aamva_send_middle_name + add_optional_element( + 'nc:PersonMiddleName', + value: applicant.middle_name, + document:, + inside: '//nc:PersonName', + ) + end + + add_optional_element( + 'nc:PersonNameSuffixText', + value: applicant.name_suffix, + document:, + inside: '//nc:PersonName', + ) + + add_optional_element( + 'aa:PersonHeightMeasure', + value: applicant.height, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + + add_optional_element( + 'aa:PersonWeightMeasure', + value: applicant.weight, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + + add_optional_element( + 'aa:PersonEyeColorCode', + value: applicant.eye_color, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + + add_sex_code(applicant.sex, document) + add_optional_element( 'nc:AddressDeliveryPointText', value: applicant.address2, @@ -114,6 +153,24 @@ def add_state_id_type(id_type, document) end end + def add_sex_code(sex_value, document) + sex_code = case sex_value + when 'male' + 1 + when 'female' + 2 + end + + if sex_code + add_optional_element( + 'aa:PersonSexCode', + value: sex_code, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + end + end + def add_optional_element(name, value:, document:, inside: nil, after: nil) return if value.blank? diff --git a/app/services/proofing/aamva/response/verification_response.rb b/app/services/proofing/aamva/response/verification_response.rb index c0fc81d40d9..5c1a86d33d2 100644 --- a/app/services/proofing/aamva/response/verification_response.rb +++ b/app/services/proofing/aamva/response/verification_response.rb @@ -13,8 +13,14 @@ class VerificationResponse 'DriverLicenseNumberMatchIndicator' => :state_id_number, 'DocumentCategoryMatchIndicator' => :state_id_type, 'PersonBirthDateMatchIndicator' => :dob, + 'PersonHeightMatchIndicator' => :height, + 'PersonSexCodeMatchIndicator' => :sex, + 'PersonWeightMatchIndicator' => :weight, + 'PersonEyeColorMatchIndicator' => :eye_color, 'PersonLastNameExactMatchIndicator' => :last_name, 'PersonFirstNameExactMatchIndicator' => :first_name, + 'PersonMiddleNameExactMatchIndicator' => :middle_name, + 'PersonNameSuffixMatchIndicator' => :name_suffix, 'AddressLine1MatchIndicator' => :address1, 'AddressLine2MatchIndicator' => :address2, 'AddressCityMatchIndicator' => :city, diff --git a/config/application.yml.default b/config/application.yml.default index a7cb8389166..5ad1eba2cb8 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -20,6 +20,7 @@ aamva_cert_enabled: true aamva_private_key: '' aamva_public_key: '' aamva_send_id_type: true +aamva_send_middle_name: true aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","MA","MD","ME","MI","MO","MS","MT","NC","ND","NE","NJ","NM","NV","OH","OR","PA","RI","SC","SD","TN","TX","VA","VT","WA","WI","WV","WY"]' aamva_verification_request_timeout: 5.0 aamva_verification_url: https://example.org:12345/verification/url @@ -502,6 +503,7 @@ development: production: aamva_auth_url: 'https://authentication-cert.aamva.org/Authentication/Authenticate.svc' aamva_send_id_type: false + aamva_send_middle_name: false aamva_verification_url: 'https://verificationservices-cert.aamva.org:18449/dldv/2.1/online' available_locales: 'en,es,fr' disable_email_sending: false diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 12ecd3a7fc1..46cd0c3be9e 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -35,6 +35,7 @@ def self.store config.add(:aamva_private_key, type: :string) config.add(:aamva_public_key, type: :string) config.add(:aamva_send_id_type, type: :boolean) + config.add(:aamva_send_middle_name, type: :boolean) config.add(:aamva_supported_jurisdictions, type: :json) config.add(:aamva_verification_request_timeout, type: :float) config.add(:aamva_verification_url) diff --git a/spec/fixtures/proofing/aamva/responses/verification_response.xml b/spec/fixtures/proofing/aamva/responses/verification_response.xml index a55dc4e890c..9c32b010b25 100644 --- a/spec/fixtures/proofing/aamva/responses/verification_response.xml +++ b/spec/fixtures/proofing/aamva/responses/verification_response.xml @@ -11,16 +11,24 @@ true true true + true + true + true + true true true true true true true + true + true + true + true true true true true true true - \ No newline at end of file + diff --git a/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml b/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml index 6fdc71ded19..63e537697e1 100644 --- a/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml +++ b/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml @@ -20,8 +20,14 @@ true true true + true + true + true + true true true + true + true true true true @@ -30,4 +36,4 @@ - \ No newline at end of file + diff --git a/spec/services/proofing/aamva/applicant_spec.rb b/spec/services/proofing/aamva/applicant_spec.rb index b2172ce9bd5..cb53ffeed65 100644 --- a/spec/services/proofing/aamva/applicant_spec.rb +++ b/spec/services/proofing/aamva/applicant_spec.rb @@ -64,4 +64,12 @@ expect(aamva_applicant[:dob]).to eq('') end + + it 'should format the height' do + proofer_applicant[:height] = 73 + aamva_applicant = Proofing::Aamva::Applicant.from_proofer_applicant(proofer_applicant) + + # This is intended to describe 6'1" + expect(aamva_applicant[:height]).to eq('61') + end end diff --git a/spec/services/proofing/aamva/proofer_spec.rb b/spec/services/proofing/aamva/proofer_spec.rb index e14c2f44aa0..33bbdfb169f 100644 --- a/spec/services/proofing/aamva/proofer_spec.rb +++ b/spec/services/proofing/aamva/proofer_spec.rb @@ -338,6 +338,108 @@ def self.test_not_successful test_not_in_verified_attributes end end + + describe '#middle_name' do + let(:attribute) { :middle_name } + let(:match_indicator_name) { 'PersonMiddleNameExactMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#name_suffix' do + let(:attribute) { :name_suffix } + let(:match_indicator_name) { 'PersonNameSuffixMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#height' do + let(:attribute) { :height } + let(:match_indicator_name) { 'PersonHeightMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#sex' do + let(:attribute) { :sex } + let(:match_indicator_name) { 'PersonSexCodeMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#weight' do + let(:attribute) { :weight } + let(:match_indicator_name) { 'PersonWeightMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#eye_color' do + let(:attribute) { :eye_color } + let(:match_indicator_name) { 'PersonEyeColorMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end end context 'when verification is successful' do @@ -361,7 +463,13 @@ def self.test_not_successful state_id_type last_name first_name + middle_name + name_suffix address + height + sex + weight + eye_color ].to_set, ) end @@ -377,7 +485,13 @@ def self.test_not_successful state_id_type: 1, last_name: 1, first_name: 1, + middle_name: 1, + name_suffix: 1, address: 1, + height: 1, + sex: 1, + weight: 1, + eye_color: 1, }, ) end @@ -410,7 +524,13 @@ def self.test_not_successful state_id_type last_name first_name + middle_name + name_suffix address + height + sex + weight + eye_color ].to_set, ) end @@ -426,7 +546,13 @@ def self.test_not_successful state_id_type: 1, last_name: 1, first_name: 1, + middle_name: 1, + name_suffix: 1, address: 1, + height: 1, + sex: 1, + weight: 1, + eye_color: 1, }, ) end @@ -458,7 +584,13 @@ def self.test_not_successful state_id_type last_name first_name + middle_name + name_suffix address + height + sex + weight + eye_color ].to_set, ) end @@ -473,7 +605,13 @@ def self.test_not_successful state_id_type: 1, last_name: 1, first_name: 1, + middle_name: 1, + name_suffix: 1, address: 1, + height: 1, + sex: 1, + weight: 1, + eye_color: 1, }, ) end diff --git a/spec/services/proofing/aamva/request/verification_request_spec.rb b/spec/services/proofing/aamva/request/verification_request_spec.rb index 68c2a874d83..187b766fd01 100644 --- a/spec/services/proofing/aamva/request/verification_request_spec.rb +++ b/spec/services/proofing/aamva/request/verification_request_spec.rb @@ -89,6 +89,87 @@ ) end + it 'includes height if it is present' do + applicant.height = '63' + expect(subject.body).to include( + '63', + ) + end + + it 'includes weight if it is present' do + applicant.weight = 190 + expect(subject.body).to include( + '190', + ) + end + + it 'includes eye_color if it is present' do + applicant.eye_color = 'blu' + expect(subject.body).to include( + 'blu', + ) + end + + it 'includes name_suffix if it is present' do + applicant.name_suffix = 'JR' + expect(subject.body).to include( + 'JR', + ) + end + + context '#sex' do + context 'when the sex is male' do + it 'sends a sex code value of 1' do + applicant.sex = 'male' + expect(subject.body).to include( + '1', + ) + end + end + + context 'when the sex is female' do + it 'sends a sex code value of 2' do + applicant.sex = 'female' + expect(subject.body).to include( + '2', + ) + end + end + + context 'when the sex is blank' do + it 'does not send a sex code value' do + applicant.sex = nil + expect(subject.body).to_not include('') + end + end + end + + context '#middle_name' do + context 'when the feature flag is off' do + before do + allow(IdentityConfig.store).to receive(:aamva_send_middle_name).and_return(false) + end + + it 'does not add a PersonMiddleName node' do + applicant.middle_name = 'test_name' + expect(subject.body).to_not include('') + end + end + + context 'when the feature flag is on' do + before do + allow(IdentityConfig.store).to receive(:aamva_send_middle_name).and_return(true) + end + + it 'does add a PersonMiddleName node' do + applicant.middle_name = 'test_name' + expect(subject.body).to include( + 'test_name', + ) + end + end + end + context '#state_id_type' do context 'when the feature flag is off' do before do diff --git a/spec/services/proofing/aamva/response/verification_response_spec.rb b/spec/services/proofing/aamva/response/verification_response_spec.rb index 3c91ffb880a..322abe22f91 100644 --- a/spec/services/proofing/aamva/response/verification_response_spec.rb +++ b/spec/services/proofing/aamva/response/verification_response_spec.rb @@ -18,8 +18,14 @@ state_id_number: true, state_id_type: true, dob: true, + height: true, + sex: true, + weight: true, + eye_color: true, last_name: true, first_name: true, + middle_name: true, + name_suffix: true, address1: true, address2: true, city: true, diff --git a/spec/services/proofing/aamva/verification_client_spec.rb b/spec/services/proofing/aamva/verification_client_spec.rb index 25fefd7702e..14a765a2445 100644 --- a/spec/services/proofing/aamva/verification_client_spec.rb +++ b/spec/services/proofing/aamva/verification_client_spec.rb @@ -65,8 +65,14 @@ address2: true, city: true, dob: true, + height: true, + sex: true, + weight: true, + eye_color: true, first_name: true, last_name: true, + middle_name: true, + name_suffix: true, state: true, state_id_expiration: true, state_id_issued: true, @@ -96,8 +102,14 @@ address2: true, city: true, dob: false, + height: true, + sex: true, + weight: true, + eye_color: true, first_name: true, last_name: true, + middle_name: true, + name_suffix: true, state: true, state_id_expiration: true, state_id_issued: true, From 258ca64e5271ba3a491f68e372605d67cca4f998 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Tue, 26 Nov 2024 16:49:07 -0500 Subject: [PATCH 2/2] test fixup --- spec/jobs/resolution_proofing_job_spec.rb | 27 ++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index e583d9bb113..947194cb137 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -121,9 +121,16 @@ state_id_expiration state_id_issued state_id_number - state_id_type dob + state_id_type + dob last_name first_name + middle_name + name_suffix + height + sex + weight + eye_color ], ) @@ -213,9 +220,16 @@ state_id_expiration state_id_issued state_id_number - state_id_type dob + state_id_type + dob last_name first_name + middle_name + name_suffix + height + sex + weight + eye_color ], ) end @@ -446,9 +460,16 @@ state_id_expiration state_id_issued state_id_number - state_id_type dob + state_id_type + dob last_name first_name + middle_name + name_suffix + height + sex + weight + eye_color ], )