From de2bed1dc1784b6be730555f9952a6ee79432d1d Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Tue, 26 Nov 2024 12:10:06 -0500 Subject: [PATCH 01/17] Add new fields to `Pii::StateId` (#11543) We are planning to validate additional data that appears on the state ID with the issuing source via DLDV. In order to facilitate this new feature this change adds fields for the new attributes to the `StateId` struct. This struct is used to represent the State ID data when it is in the IdV session. [skip changelog] --- .../doc_auth/lexis_nexis/doc_pii_reader.rb | 5 +++++ .../socure/responses/docv_result_response.rb | 5 +++++ app/services/idv/session.rb | 3 ++- app/services/pii/state_id.rb | 7 ++++++- lib/idp/constants.rb | 7 ++++++- .../idv/image_uploads_controller_spec.rb | 5 +++++ .../idv/personal_key_controller_spec.rb | 5 +++++ spec/jobs/resolution_proofing_job_spec.rb | 5 +++++ spec/models/document_capture_session_spec.rb | 5 +++++ .../responses/true_id_response_spec.rb | 10 ++++++++++ .../mock/doc_auth_mock_client_spec.rb | 15 ++++++++++++++ .../doc_auth/mock/result_response_spec.rb | 20 +++++++++++++++++++ 12 files changed, 89 insertions(+), 3 deletions(-) diff --git a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb index 5aa9439d645..5b3947fda27 100644 --- a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb +++ b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb @@ -31,6 +31,7 @@ def read_pii(true_id_product) first_name: id_auth_field_data['Fields_FirstName'], last_name: id_auth_field_data['Fields_Surname'], middle_name: id_auth_field_data['Fields_MiddleName'], + name_suffix: nil, address1: id_auth_field_data['Fields_AddressLine1'], address2: id_auth_field_data['Fields_AddressLine2'], city: id_auth_field_data['Fields_City'], @@ -41,6 +42,10 @@ def read_pii(true_id_product) month: id_auth_field_data['Fields_DOB_Month'], day: id_auth_field_data['Fields_DOB_Day'], ), + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_expiration: parse_date( year: id_auth_field_data['Fields_ExpirationDate_Year'], month: id_auth_field_data['Fields_ExpirationDate_Month'], diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb index aaf4413da27..aa0c0ae7a8e 100644 --- a/app/services/doc_auth/socure/responses/docv_result_response.rb +++ b/app/services/doc_auth/socure/responses/docv_result_response.rb @@ -90,12 +90,17 @@ def read_pii first_name: get_data(DATA_PATHS[:first_name]), middle_name: get_data(DATA_PATHS[:middle_name]), last_name: get_data(DATA_PATHS[:last_name]), + name_suffix: nil, address1: get_data(DATA_PATHS[:address1]), address2: get_data(DATA_PATHS[:address2]), city: get_data(DATA_PATHS[:city]), state: get_data(DATA_PATHS[:state]), zipcode: get_data(DATA_PATHS[:zipcode]), dob: parse_date(get_data(DATA_PATHS[:dob])), + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_number: get_data(DATA_PATHS[:document_number]), state_id_issued: parse_date(get_data(DATA_PATHS[:issue_date])), state_id_expiration: parse_date(get_data(DATA_PATHS[:expiration_date])), diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 4f4683997de..f21a017ad64 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -230,7 +230,8 @@ def pii_from_doc=(new_pii_from_doc) def pii_from_doc return nil if session[:pii_from_doc].blank? - Pii::StateId.new(**session[:pii_from_doc].slice(*Pii::StateId.members)) + state_id_data = Pii::StateId.members.index_with { |key| session[:pii_from_doc][key] } + Pii::StateId.new(**state_id_data) end def updated_user_address=(updated_user_address) diff --git a/app/services/pii/state_id.rb b/app/services/pii/state_id.rb index 713766d68ee..60ddbd24988 100644 --- a/app/services/pii/state_id.rb +++ b/app/services/pii/state_id.rb @@ -6,17 +6,22 @@ module Pii :first_name, :last_name, :middle_name, + :name_suffix, :address1, :address2, :city, :state, + :zipcode, :dob, + :sex, + :height, + :weight, + :eye_color, :state_id_expiration, :state_id_issued, :state_id_jurisdiction, :state_id_number, :state_id_type, - :zipcode, :issuing_country_code, ) end diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index 77510830474..cfa5c0e560f 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -100,17 +100,22 @@ module Vendors address2: nil, city: 'GREAT FALLS', dob: '1938-10-06', + eye_color: nil, first_name: 'FAKEY', + height: nil, + issuing_country_code: 'US', last_name: 'MCFAKERSON', middle_name: nil, + name_suffix: nil, state: 'MT', state_id_expiration: '2099-12-31', state_id_issued: '2019-12-31', state_id_jurisdiction: MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, state_id_number: '1111111111111', state_id_type: 'drivers_license', + sex: nil, + weight: nil, zipcode: '59010-1234', - issuing_country_code: 'US', }.freeze MOCK_IPP_APPLICANT = { diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 7d83a3b7989..26975325175 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -444,10 +444,15 @@ first_name: first_name, last_name: last_name, middle_name: nil, + name_suffix: nil, address1: address1, state: state, state_id_type: state_id_type, dob: dob, + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_jurisdiction: jurisdiction, state_id_number: state_id_number, zipcode: zipcode, diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index 33946f4bbe1..47e85d5f938 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -13,6 +13,11 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs) # These keys are present in our applicant fixture but # are not actually supported in Pii::Attributes keys_to_ignore = %i[ + name_suffix + sex + height + weight + eye_color state_id_expiration state_id_issued state_id_number diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index 0ce568616d6..87daae5116b 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -557,6 +557,7 @@ first_name: 'FAKEY', middle_name: nil, last_name: 'MCFAKERSON', + name_suffix: nil, address1: '1 FAKE RD', identity_doc_address1: '1 FAKE RD', identity_doc_address2: nil, @@ -570,6 +571,10 @@ state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, ssn: '900-66-1234', state_id_jurisdiction: 'ND', state_id_expiration: '2099-12-31', diff --git a/spec/models/document_capture_session_spec.rb b/spec/models/document_capture_session_spec.rb index b1056460a49..0219451dd6e 100644 --- a/spec/models/document_capture_session_spec.rb +++ b/spec/models/document_capture_session_spec.rb @@ -8,11 +8,16 @@ first_name: 'Testy', last_name: 'Testy', middle_name: nil, + name_suffix: nil, address1: '123 ABC AVE', address2: nil, city: 'ANYTOWN', state: 'MD', dob: '1986-07-01', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_expiration: '2099-10-15', state_id_issued: '2016-10-15', state_id_jurisdiction: 'MD', diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 7847a830725..3ca01b5c9fd 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -127,11 +127,16 @@ first_name: 'DAVID', last_name: 'SAMPLE', middle_name: 'LICENSE', + name_suffix: nil, address1: '123 ABC AVE', address2: 'APT 3E', city: 'ANYTOWN', state: 'MD', dob: '1986-07-01', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_expiration: '2099-10-15', state_id_issued: '2016-10-15', state_id_jurisdiction: 'MD', @@ -326,11 +331,16 @@ def get_decision_product(resp) first_name: 'DAVID', last_name: 'SAMPLE', middle_name: 'LICENSE', + name_suffix: nil, address1: '123 ABC AVE', address2: nil, city: 'ANYTOWN', state: 'MD', dob: '1986-10-13', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_expiration: '2099-10-15', state_id_issued: '2016-10-15', state_id_jurisdiction: 'MD', diff --git a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb index 8206bbe528f..afe8ee4117b 100644 --- a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb +++ b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb @@ -28,12 +28,17 @@ first_name: 'FAKEY', middle_name: nil, last_name: 'MCFAKERSON', + name_suffix: nil, address1: '1 FAKE RD', address2: nil, city: 'GREAT FALLS', state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_number: '1111111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', @@ -84,12 +89,17 @@ first_name: 'Susan', middle_name: 'Q', last_name: 'Smith', + name_suffix: nil, address1: '1 Microsoft Way', address2: 'Apt 3', city: 'Bayside', state: 'NY', zipcode: '11364', dob: '1938-10-06', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_number: '111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', @@ -127,12 +137,17 @@ first_name: 'Susan', middle_name: nil, last_name: 'MCFAKERSON', + name_suffix: nil, address1: '1 FAKE RD', address2: nil, city: 'GREAT FALLS', state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_number: '1111111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', diff --git a/spec/services/doc_auth/mock/result_response_spec.rb b/spec/services/doc_auth/mock/result_response_spec.rb index 7988ac33622..548c8e49ba8 100644 --- a/spec/services/doc_auth/mock/result_response_spec.rb +++ b/spec/services/doc_auth/mock/result_response_spec.rb @@ -58,12 +58,17 @@ first_name: 'Susan', middle_name: 'Q', last_name: 'Smith', + name_suffix: nil, address1: '1 Microsoft Way', address2: 'Apt 3', city: 'Bayside', state: 'NY', zipcode: '11364', dob: '1938-10-06', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_number: '111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', @@ -302,6 +307,7 @@ first_name: 'Susan', middle_name: 'Q', last_name: 'Smith', + name_suffix: nil, address1: '1 Microsoft Way', address2: 'Apt 3', city: 'Bayside', @@ -310,6 +316,10 @@ state_id_number: '123456789', zipcode: '11364', dob: '1938-10-06', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_type: 'drivers_license', state_id_expiration: '2089-12-31', state_id_issued: '2009-12-31', @@ -356,12 +366,17 @@ first_name: 'Susan', middle_name: nil, last_name: 'MCFAKERSON', + name_suffix: nil, address1: '1 FAKE RD', address2: nil, city: 'GREAT FALLS', state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_number: '1111111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', @@ -504,12 +519,17 @@ first_name: 'Susan', middle_name: 'Q', last_name: 'Smith', + name_suffix: nil, address1: '1 Microsoft Way', address2: 'Apt 3', city: 'Bayside', state: 'NY', zipcode: '11364', dob: '1938-10-06', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_number: '111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', From 61aa8f993a8aefc610bd1e32326726eff64c1101 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:09:48 -0500 Subject: [PATCH 02/17] Fetch latest origin as part of deploy PR script (#11563) changelog: Internal, Deploy, Fetch latest origin as part of deploy PR script --- scripts/create-deploy-pr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/create-deploy-pr b/scripts/create-deploy-pr index 310d4103beb..4557e1b6040 100755 --- a/scripts/create-deploy-pr +++ b/scripts/create-deploy-pr @@ -51,7 +51,7 @@ function get_next_rc { LAST_RC="$1"; shift MAJOR=$(echo "$LAST_RC" | sed -E 's/\.[0-9]+//') MINOR=$(echo "$LAST_RC" | sed -E 's/[0-9]+(\.|$)//') - + if [ "$PATCH" == "1" ]; then # Doing a patch, so increment minor version by 1 if [ -z "$MINOR" ]; then @@ -77,6 +77,7 @@ function get_staging_sha { } check_gh_configuration +git fetch $GIT_REMOTE RC_BRANCH=stages/rc-$(date +'%Y-%m-%d') if git rev-parse "$GIT_REMOTE/$RC_BRANCH" > /dev/null 2>&1; then From 9d8431d92fdad5e33931a8b81f8050e7b577086d Mon Sep 17 00:00:00 2001 From: Matt Wagner Date: Tue, 26 Nov 2024 13:30:25 -0500 Subject: [PATCH 03/17] LG-15056 | Socure A/B test (#11544) changelog: Upcoming Features, Socure, Model Socure shadow mode as an A/B test --- app/jobs/resolution_proofing_job.rb | 13 +++++++- config/application.yml.default | 1 + config/initializers/ab_tests.rb | 8 +++++ lib/ab_test.rb | 2 +- lib/identity_config.rb | 1 + spec/config/initializers/ab_tests_spec.rb | 40 +++++++++++++++++++++++ spec/jobs/resolution_proofing_job_spec.rb | 2 +- 7 files changed, 64 insertions(+), 3 deletions(-) diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index b8cd4e08a95..1d2c1aab43c 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -74,7 +74,7 @@ def perform( timing: timer.results, ) - if IdentityConfig.store.idv_socure_shadow_mode_enabled + if use_shadow_mode?(user:) SocureShadowModeProofingJob.perform_later( document_capture_session_result_id: document_capture_session&.result_id, encrypted_arguments:, @@ -85,6 +85,17 @@ def perform( end end + def use_shadow_mode?(user:) + IdentityConfig.store.idv_socure_shadow_mode_enabled && + AbTests::SOCURE_IDV_SHADOW_MODE.bucket( + request: nil, + service_provider: nil, + session: nil, + user:, + user_session: nil, + ) == :shadow_mode_enabled + end + private # @return [CallbackLogData] diff --git a/config/application.yml.default b/config/application.yml.default index 3e72fd32c6f..a7cb8389166 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -384,6 +384,7 @@ socure_docv_webhook_secret_key: '' socure_docv_webhook_secret_key_queue: '[]' socure_idplus_api_key: '' socure_idplus_base_url: '' +socure_idplus_shadow_mode_percent: 0 socure_idplus_timeout_in_seconds: 5 socure_reason_code_api_key: '' socure_reason_code_base_url: '' diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index ea7288631d0..fff91a2dd65 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -96,4 +96,12 @@ def self.all IdentityConfig.store.recommend_webauthn_platform_for_sms_ab_test_authentication_percent, }, ).freeze + + SOCURE_IDV_SHADOW_MODE = AbTest.new( + experiment_name: 'Socure shadow mode', + should_log: ['IdV: doc auth verify proofing results'].to_set, + buckets: { + shadow_mode_enabled: IdentityConfig.store.socure_idplus_shadow_mode_percent, + }, + ).freeze end diff --git a/lib/ab_test.rb b/lib/ab_test.rb index e92cf24b1a1..1d7be784b49 100644 --- a/lib/ab_test.rb +++ b/lib/ab_test.rb @@ -35,7 +35,7 @@ def initialize( # @param [ActionDispatch::Request] request # @param [String,nil] service_provider Issuer string for the service provider associated with # the current session. - # @params [Hash] session + # @param [Hash] session # @param [User] user # @param [Hash] user_session def bucket(request:, service_provider:, session:, user:, user_session:) diff --git a/lib/identity_config.rb b/lib/identity_config.rb index bf04825a906..12ecd3a7fc1 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -420,6 +420,7 @@ def self.store config.add(:socure_docv_webhook_secret_key, type: :string) config.add(:socure_idplus_api_key, type: :string) config.add(:socure_idplus_base_url, type: :string) + config.add(:socure_idplus_shadow_mode_percent, type: :integer) config.add(:socure_idplus_timeout_in_seconds, type: :integer) config.add(:socure_reason_code_api_key, type: :string) config.add(:socure_reason_code_base_url, type: :string) diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index e89511a3f05..8c14e6f0924 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -261,4 +261,44 @@ end end end + + describe 'SOCURE_IDV_SHADOW_MODE' do + let(:user) { create(:user) } + + subject(:bucket) do + AbTests::SOCURE_IDV_SHADOW_MODE.bucket( + request: nil, + service_provider: nil, + session: nil, + user:, + user_session: nil, + ) + end + + before do + allow(IdentityConfig.store).to receive( + :socure_idplus_shadow_mode_percent, + ).and_return(0) + reload_ab_tests + end + + context 'when the A/B test is disabled' do + it 'does not return a bucket' do + expect(bucket).to be_nil + end + end + + context 'when the A/B test is enabled' do + before do + allow(IdentityConfig.store).to receive( + :socure_idplus_shadow_mode_percent, + ).and_return(100) + reload_ab_tests + end + + it 'returns a bucket' do + expect(bucket).to eq :shadow_mode_enabled + end + end + end end diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index 87daae5116b..e583d9bb113 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -538,7 +538,7 @@ context 'socure shadow mode' do context 'turned on' do before do - allow(IdentityConfig.store).to receive(:idv_socure_shadow_mode_enabled).and_return(true) + allow(instance).to receive(:use_shadow_mode?).and_return(true) end it 'schedules a SocureShadowModeProofingJob' do From 3384b65c238fe17ecc237b2fe3790aea5c127e54 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Tue, 26 Nov 2024 14:11:36 -0500 Subject: [PATCH 04/17] Start logging year-of-birth in biographical info hash (#11532) In #11267 we quit logging the user's year-of-birth while we waited for additional security and privacy review. We received approval to start logging this attribute. This commit undoes the changes in #11267 so the year-of-birth is now logged [skip changelog] --- app/services/proofing/resolution/result_adjudicator.rb | 1 + spec/features/idv/analytics_spec.rb | 2 ++ spec/jobs/socure_shadow_mode_proofing_job_spec.rb | 2 ++ spec/services/proofing/resolution/result_adjudicator_spec.rb | 2 ++ 4 files changed, 7 insertions(+) diff --git a/app/services/proofing/resolution/result_adjudicator.rb b/app/services/proofing/resolution/result_adjudicator.rb index 09053d21216..908327ac585 100644 --- a/app/services/proofing/resolution/result_adjudicator.rb +++ b/app/services/proofing/resolution/result_adjudicator.rb @@ -123,6 +123,7 @@ def biographical_info StringRedacter.redact_alphanumeric(state_id_number) end { + birth_year: applicant_pii[:dob]&.to_date&.year, state: applicant_pii[:state], identity_doc_address_state: applicant_pii[:identity_doc_address_state], state_id_jurisdiction: applicant_pii[:state_id_jurisdiction], diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index dbe44ada457..b224ab7321e 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -116,6 +116,7 @@ }, }, biographical_info: { + birth_year: 1938, identity_doc_address_state: nil, state: 'MT', state_id_jurisdiction: 'ND', @@ -158,6 +159,7 @@ }, }, biographical_info: { + birth_year: 1938, identity_doc_address_state: 'ND', state: 'MT', state_id_jurisdiction: 'ND', diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index 9416a03e907..92e93072894 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -106,6 +106,7 @@ }, }, biographical_info: { + birth_year: 1938, identity_doc_address_state: nil, same_address_as_id: nil, state: 'MT', @@ -257,6 +258,7 @@ threatmetrix_review_status: 'pass', timed_out: false, biographical_info: { + birth_year: 1938, identity_doc_address_state: nil, same_address_as_id: nil, state: 'MT', diff --git a/spec/services/proofing/resolution/result_adjudicator_spec.rb b/spec/services/proofing/resolution/result_adjudicator_spec.rb index c63a10e4c0a..6e8aba8bbfb 100644 --- a/spec/services/proofing/resolution/result_adjudicator_spec.rb +++ b/spec/services/proofing/resolution/result_adjudicator_spec.rb @@ -102,6 +102,7 @@ result = subject.adjudicated_result expect(result.extra[:biographical_info]).to eq( + birth_year: 1938, state: 'MT', identity_doc_address_state: nil, state_id_jurisdiction: 'ND', @@ -118,6 +119,7 @@ result = subject.adjudicated_result expect(result.extra[:biographical_info]).to eq( + birth_year: 1938, state: 'MT', identity_doc_address_state: 'MT', state_id_jurisdiction: 'ND', From fa067d23782450a556689d398c7c55d433bf029b Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Tue, 26 Nov 2024 14:52:55 -0500 Subject: [PATCH 05/17] Return new attributes from doc auth mock client (#11561) In #11559 we will be reading new attributes from the document during document authentication. This commit updates the doc auth mock client to match this behavior. This will help us identify any issues these new attributes may cause in lower environments. [skip changelog] --- lib/idp/constants.rb | 6 ++-- spec/jobs/resolution_proofing_job_spec.rb | 6 ++-- .../mock/doc_auth_mock_client_spec.rb | 21 ++++++++------ .../doc_auth/mock/result_response_spec.rb | 28 ++++++++++++------- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index cfa5c0e560f..0f2d44f86ca 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -102,18 +102,18 @@ module Vendors dob: '1938-10-06', eye_color: nil, first_name: 'FAKEY', - height: nil, + height: 72, issuing_country_code: 'US', last_name: 'MCFAKERSON', middle_name: nil, - name_suffix: nil, + name_suffix: 'JR', state: 'MT', state_id_expiration: '2099-12-31', state_id_issued: '2019-12-31', state_id_jurisdiction: MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, state_id_number: '1111111111111', state_id_type: 'drivers_license', - sex: nil, + sex: 'male', weight: nil, zipcode: '59010-1234', }.freeze diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index e583d9bb113..8b7261c0876 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -557,7 +557,7 @@ first_name: 'FAKEY', middle_name: nil, last_name: 'MCFAKERSON', - name_suffix: nil, + name_suffix: 'JR', address1: '1 FAKE RD', identity_doc_address1: '1 FAKE RD', identity_doc_address2: nil, @@ -571,8 +571,8 @@ state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', - sex: nil, - height: nil, + sex: 'male', + height: 72, weight: nil, eye_color: nil, ssn: '900-66-1234', diff --git a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb index afe8ee4117b..ddfade0d576 100644 --- a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb +++ b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb @@ -28,15 +28,15 @@ first_name: 'FAKEY', middle_name: nil, last_name: 'MCFAKERSON', - name_suffix: nil, + name_suffix: 'JR', address1: '1 FAKE RD', address2: nil, city: 'GREAT FALLS', state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', - sex: nil, - height: nil, + sex: 'male', + height: 72, weight: nil, eye_color: nil, state_id_number: '1111111111111', @@ -55,12 +55,15 @@ first_name: Susan last_name: Smith middle_name: Q + name_suffix: 'SR' address1: 1 Microsoft Way address2: Apt 3 city: Bayside state: NY zipcode: '11364' dob: 1938-10-06 + sex: 'female' + height: 66 state_id_number: '111111111' state_id_jurisdiction: ND state_id_type: drivers_license @@ -89,15 +92,15 @@ first_name: 'Susan', middle_name: 'Q', last_name: 'Smith', - name_suffix: nil, + name_suffix: 'SR', address1: '1 Microsoft Way', address2: 'Apt 3', city: 'Bayside', state: 'NY', zipcode: '11364', dob: '1938-10-06', - sex: nil, - height: nil, + sex: 'female', + height: 66, weight: nil, eye_color: nil, state_id_number: '111111111', @@ -137,15 +140,15 @@ first_name: 'Susan', middle_name: nil, last_name: 'MCFAKERSON', - name_suffix: nil, + name_suffix: 'JR', address1: '1 FAKE RD', address2: nil, city: 'GREAT FALLS', state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', - sex: nil, - height: nil, + sex: 'male', + height: 72, weight: nil, eye_color: nil, state_id_number: '1111111111111', diff --git a/spec/services/doc_auth/mock/result_response_spec.rb b/spec/services/doc_auth/mock/result_response_spec.rb index 548c8e49ba8..f79c503e1fb 100644 --- a/spec/services/doc_auth/mock/result_response_spec.rb +++ b/spec/services/doc_auth/mock/result_response_spec.rb @@ -34,12 +34,15 @@ first_name: Susan last_name: Smith middle_name: Q + name_suffix: address1: 1 Microsoft Way address2: Apt 3 city: Bayside state: NY zipcode: '11364' dob: 1938-10-06 + sex: female + height: 66 state_id_number: '111111111' state_id_jurisdiction: ND state_id_type: drivers_license @@ -65,8 +68,8 @@ state: 'NY', zipcode: '11364', dob: '1938-10-06', - sex: nil, - height: nil, + sex: 'female', + height: 66, weight: nil, eye_color: nil, state_id_number: '111111111', @@ -282,13 +285,15 @@ first_name: Susan last_name: Smith middle_name: Q + name_suffix: address1: 1 Microsoft Way address2: Apt 3 city: Bayside state: NY zipcode: '11364' dob: 10/06/1938 - phone: +1 314-555-1212 + sex: female + height: 66 state_id_number: '123456789' state_id_type: drivers_license state_id_jurisdiction: 'NY' @@ -316,8 +321,8 @@ state_id_number: '123456789', zipcode: '11364', dob: '1938-10-06', - sex: nil, - height: nil, + sex: 'female', + height: 66, weight: nil, eye_color: nil, state_id_type: 'drivers_license', @@ -366,15 +371,15 @@ first_name: 'Susan', middle_name: nil, last_name: 'MCFAKERSON', - name_suffix: nil, + name_suffix: 'JR', address1: '1 FAKE RD', address2: nil, city: 'GREAT FALLS', state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', - sex: nil, - height: nil, + sex: 'male', + height: 72, weight: nil, eye_color: nil, state_id_number: '1111111111111', @@ -495,12 +500,15 @@ first_name: Susan last_name: Smith middle_name: Q + name_suffix: address1: 1 Microsoft Way address2: Apt 3 city: Bayside state: NY zipcode: 11364 dob: 1938-10-06 + sex: female + height: 66 state_id_number: '111111111' state_id_jurisdiction: ND state_id_type: drivers_license @@ -526,8 +534,8 @@ state: 'NY', zipcode: '11364', dob: '1938-10-06', - sex: nil, - height: nil, + sex: 'female', + height: 66, weight: nil, eye_color: nil, state_id_number: '111111111', From c7835cde0521cc695b0a983f1cbfeeba93be886b Mon Sep 17 00:00:00 2001 From: Colter <59977618+colter-nattrass@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:14:19 -0700 Subject: [PATCH 06/17] LG-15160 adding feature flag to table_summary job (#11556) * changelog: Internal, Reporting, feature flag Adding Feature flag to table summary export job to prevent errors in New Relic in elevated environments --------- Co-authored-by: Zach Margolis Co-authored-by: Aaron Nagucki --- app/jobs/data_warehouse/base_job.rb | 4 ++++ .../table_summary_stats_export_job.rb | 2 ++ config/application.yml.default | 1 + lib/identity_config.rb | 1 + .../table_summary_stats_export_job_spec.rb | 15 ++++++++++++++- 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/jobs/data_warehouse/base_job.rb b/app/jobs/data_warehouse/base_job.rb index 5db5f079a27..85ee26caa3d 100644 --- a/app/jobs/data_warehouse/base_job.rb +++ b/app/jobs/data_warehouse/base_job.rb @@ -42,5 +42,9 @@ def upload_file_to_s3_bucket(path:, body:, content_type:, bucket: bucket_name) logger.debug("#{class_name}: upload completed to #{url}") url end + + def data_warehouse_disabled? + !IdentityConfig.store.data_warehouse_enabled + end end end diff --git a/app/jobs/data_warehouse/table_summary_stats_export_job.rb b/app/jobs/data_warehouse/table_summary_stats_export_job.rb index 7fef9cabee5..44622a17620 100644 --- a/app/jobs/data_warehouse/table_summary_stats_export_job.rb +++ b/app/jobs/data_warehouse/table_summary_stats_export_job.rb @@ -5,6 +5,8 @@ class TableSummaryStatsExportJob < BaseJob REPORT_NAME = 'table_summary_stats' def perform(timestamp) + return if data_warehouse_disabled? + data = fetch_table_max_ids_and_counts(timestamp) upload_to_s3(data, timestamp) end diff --git a/config/application.yml.default b/config/application.yml.default index a7cb8389166..019961b58f6 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -67,6 +67,7 @@ compromised_password_randomizer_value: 1000 country_phone_number_overrides: '{}' dashboard_api_token: '' dashboard_url: https://dashboard.demo.login.gov +data_warehouse_enabled: false database_advisory_locks_enabled: false database_host: '' database_name: '' diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 12ecd3a7fc1..3b22d72d80b 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -85,6 +85,7 @@ def self.store config.add(:country_phone_number_overrides, type: :json) config.add(:dashboard_api_token, type: :string) config.add(:dashboard_url, type: :string) + config.add(:data_warehouse_enabled, type: :boolean) config.add(:database_advisory_locks_enabled, type: :boolean) config.add(:database_host, type: :string) config.add(:database_name, type: :string) diff --git a/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb b/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb index 45e022abb9f..980f93ce836 100644 --- a/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb +++ b/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb @@ -6,6 +6,7 @@ let(:expected_bucket) { 'login-gov-analytics-export-test-1234-us-west-2' } let(:test_on_tables) { ['users'] } let(:s3_data_warehouse_bucket_prefix) { 'login-gov-analytics-export' } + let(:data_warehouse_enabled) { true } let(:expected_json) do { @@ -30,7 +31,8 @@ allow(Identity::Hostdata).to receive(:aws_region).and_return('us-west-1') allow(IdentityConfig.store).to receive(:s3_data_warehouse_bucket_prefix). and_return(s3_data_warehouse_bucket_prefix) - + allow(IdentityConfig.store).to receive(:data_warehouse_enabled). + and_return(data_warehouse_enabled) Aws.config[:s3] = { stub_responses: { put_object: {}, @@ -44,6 +46,17 @@ add_data_to_tables end + context 'when data_warehouse_enabled is false' do + let(:data_warehouse_enabled) { false } + + it 'does not perform the job' do + allow(IdentityConfig.store).to receive(:data_warehouse_enabled). + and_return(data_warehouse_enabled) + expect(job).not_to receive(:fetch_table_max_ids_and_counts) + expect(job).not_to receive(:upload_file_to_s3_bucket) + end + end + context 'when database tables contain data' do it 'generates correct JSON from database tables' do json_data = job.fetch_table_max_ids_and_counts(timestamp) From 8a60da4e313efd8390bd827f0b324050413ddbc3 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Tue, 26 Nov 2024 16:44:45 -0500 Subject: [PATCH 07/17] LG-14985 Read additional document data from TrueID when configured to do so (#11559) We are working to send additional data to AAMVA when it is present on documents. This commit lays groundwork for this by reading those new attributes when configured to do so. We are waiting on security approval before we start writing these new attributes into the Login.gov session. For that reason the reading of these attributes is feature flagged and disabled by default. changelog: Internal, Document Authentication, Read additional document data from TrueID when configured to do so * cleanup --- .../doc_auth/lexis_nexis/doc_pii_reader.rb | 42 ++++++++++++++++++- config/application.yml.default | 1 + lib/identity_config.rb | 1 + .../responses/true_id_response_spec.rb | 16 +++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb index 5b3947fda27..0c6e5b2556b 100644 --- a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb +++ b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb @@ -27,7 +27,7 @@ def read_pii(true_id_product) state_id_type_slug = id_auth_field_data['Fields_DocumentClassName'] state_id_type = DocAuth::Response::ID_TYPE_SLUGS[state_id_type_slug] - Pii::StateId.new( + state_id_data = Pii::StateId.new( first_name: id_auth_field_data['Fields_FirstName'], last_name: id_auth_field_data['Fields_Surname'], middle_name: id_auth_field_data['Fields_MiddleName'], @@ -61,6 +61,16 @@ def read_pii(true_id_product) state_id_type: state_id_type, issuing_country_code: id_auth_field_data['Fields_CountryCode'], ) + + if IdentityConfig.store.doc_auth_read_additional_pii_attributes_enabled + state_id_data = state_id_data.with( + name_suffix: id_auth_field_data['Fields_NameSuffix'], + sex: parse_sex_value(id_auth_field_data['Fields_Sex']), + height: parse_height_value(id_auth_field_data['Fields_Height']), + ) + end + + state_id_data end def parse_date(year:, month:, day:) @@ -72,6 +82,36 @@ def parse_date(year:, month:, day:) Rails.logger.info(message) nil end + + def parse_sex_value(sex_attribute) + # A value of "non-binary" or "not-specified" may appear on a document. However, at this time + # the DLDV `PersonSexCode` input can only process values that correspond to "male" or + # "female". + # + # From the DLDV User Guide Version 2.1 - 28: + # + # Since 2017, a growing number of states have allowed a person to select "not specified" + # or "non-binary" for their sex on the application for a credential. While Male and + # Female can be verified, the non-binary value cannot be verified at this time. + # + # This code will return `nil` for those cases with the intent that they will not be verified + # against the DLDV where they will not be recognized + # + case sex_attribute&.upcase + when 'M' + 'male' + when 'F' + 'female' + end + end + + def parse_height_value(height_attribute) + height_match_data = height_attribute&.match(/(?\d)'(?\d{1,2})"/) + + return unless height_match_data + + height_match_data[:feet].to_i * 12 + height_match_data[:inches].to_i + end end end end diff --git a/config/application.yml.default b/config/application.yml.default index 019961b58f6..81b1cc3ebcc 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -103,6 +103,7 @@ doc_auth_error_sharpness_threshold: 40 doc_auth_max_attempts: 5 doc_auth_max_capture_attempts_before_native_camera: 3 doc_auth_max_submission_attempts_before_native_camera: 3 +doc_auth_read_additional_pii_attributes_enabled: false doc_auth_selfie_desktop_test_mode: false doc_auth_socure_wait_polling_refresh_max_seconds: 15 doc_auth_socure_wait_polling_timeout_minutes: 2 diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 3b22d72d80b..d85fbef073d 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -121,6 +121,7 @@ def self.store config.add(:doc_auth_max_attempts, type: :integer) config.add(:doc_auth_max_capture_attempts_before_native_camera, type: :integer) config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer) + config.add(:doc_auth_read_additional_pii_attributes_enabled, type: :boolean) config.add(:doc_auth_selfie_desktop_test_mode, type: :boolean) config.add(:doc_auth_socure_wait_polling_refresh_max_seconds, type: :integer) config.add(:doc_auth_socure_wait_polling_timeout_minutes, type: :integer) diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 3ca01b5c9fd..2d3584ce73d 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -249,6 +249,22 @@ expect(response.success?).to eq(false) end end + + context 'when doc_auth_read_additional_pii_attributes_enabled is enabled' do + let(:success_response_body) { LexisNexisFixtures.true_id_response_success } + + it 'reads the additional PII attributes' do + allow(IdentityConfig.store).to receive(:doc_auth_read_additional_pii_attributes_enabled). + and_return(true) + + pii_from_doc = response.pii_from_doc + + expect(pii_from_doc.first_name).to eq('LICENSE') + expect(pii_from_doc.name_suffix).to eq('JR') + expect(pii_from_doc.sex).to eq('male') + expect(pii_from_doc.height).to eq(68) + end + end end context 'when there is no address line 2' do From 345afa2c313a7833e1264fb27d216ca6ed8da2ce Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Wed, 27 Nov 2024 09:18:38 -0500 Subject: [PATCH 08/17] LG-14741 max email add new email link (#11551) * changelog: User-Facing Improvements, Authentication, SP Email Selection max email limit * rubocop fixes * Add check for when user goes through completion screen * add spec for making sure variable is being assigned --- .../selected_email_controller.rb | 1 + .../sign_up/select_email_controller.rb | 1 + .../selected_email/edit.html.erb | 16 +++++++------- app/views/sign_up/select_email/show.html.erb | 16 +++++++------- .../selected_email_controller_spec.rb | 13 ++++++++++++ .../sign_up/select_email_controller_spec.rb | 13 ++++++++++++ .../selected_email/edit.html.erb_spec.rb | 21 +++++++++++++++++++ .../select_email/show.html.erb_spec.rb | 21 +++++++++++++++++++ 8 files changed, 88 insertions(+), 14 deletions(-) diff --git a/app/controllers/accounts/connected_accounts/selected_email_controller.rb b/app/controllers/accounts/connected_accounts/selected_email_controller.rb index 9add839cf12..f782b052491 100644 --- a/app/controllers/accounts/connected_accounts/selected_email_controller.rb +++ b/app/controllers/accounts/connected_accounts/selected_email_controller.rb @@ -12,6 +12,7 @@ class SelectedEmailController < ApplicationController def edit @identity = identity @select_email_form = build_select_email_form + @can_add_email = EmailPolicy.new(current_user).can_add_email? analytics.sp_select_email_visited end diff --git a/app/controllers/sign_up/select_email_controller.rb b/app/controllers/sign_up/select_email_controller.rb index 41bc0258206..2d691d85d8a 100644 --- a/app/controllers/sign_up/select_email_controller.rb +++ b/app/controllers/sign_up/select_email_controller.rb @@ -14,6 +14,7 @@ def show @user_emails = user_emails @last_sign_in_email_address = last_email @select_email_form = build_select_email_form + @can_add_email = EmailPolicy.new(current_user).can_add_email? analytics.sp_select_email_visited(needs_completion_screen_reason:) end diff --git a/app/views/accounts/connected_accounts/selected_email/edit.html.erb b/app/views/accounts/connected_accounts/selected_email/edit.html.erb index 3347832195c..c32e05f522b 100644 --- a/app/views/accounts/connected_accounts/selected_email/edit.html.erb +++ b/app/views/accounts/connected_accounts/selected_email/edit.html.erb @@ -33,13 +33,15 @@ <%= f.submit(t('help_text.requested_attributes.select_email_link'), class: 'margin-top-1') %> <% end %> - <%= render ButtonComponent.new( - url: add_email_path(in_select_email_flow: true), - outline: true, - big: true, - wide: true, - class: 'margin-top-2', - ).with_content(t('account.index.email_add')) %> + <% if @can_add_email %> + <%= render ButtonComponent.new( + url: add_email_path(in_select_email_flow: true), + outline: true, + big: true, + wide: true, + class: 'margin-top-2', + ).with_content(t('account.index.email_add')) %> + <% end %> <% c.with_footer { link_to t('forms.buttons.back'), account_connected_accounts_path } %> <% end %> diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb index ed505209855..4a967d2c97d 100644 --- a/app/views/sign_up/select_email/show.html.erb +++ b/app/views/sign_up/select_email/show.html.erb @@ -29,13 +29,15 @@ <%= f.submit t('help_text.requested_attributes.select_email_link'), class: 'margin-top-1' %> <% end %> - <%= render ButtonComponent.new( - url: add_email_path(in_select_email_flow: true), - outline: true, - big: true, - wide: true, - class: 'margin-top-2', - ).with_content(t('account.index.email_add')) %> + <% if @can_add_email %> + <%= render ButtonComponent.new( + url: add_email_path(in_select_email_flow: true), + outline: true, + big: true, + wide: true, + class: 'margin-top-2', + ).with_content(t('account.index.email_add')) %> + <% end %> <%= render PageFooterComponent.new do %> <%= link_to t('forms.buttons.back'), sign_up_completed_path %> diff --git a/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb b/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb index 4be4fb6fd52..0b66e62c194 100644 --- a/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb +++ b/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb @@ -28,6 +28,7 @@ expect(assigns(:identity)).to be_kind_of(ServiceProviderIdentity) expect(assigns(:select_email_form)).to be_kind_of(SelectEmailForm) + expect(assigns(:can_add_email)).to eq(true) end context 'with an identity parameter not associated with the user' do @@ -59,6 +60,18 @@ expect(response).to be_not_found end end + + context 'when users has max number of emails' do + before do + allow(user).to receive(:email_address_count).and_return(2) + allow(IdentityConfig.store).to receive(:max_emails_per_account).and_return(2) + end + + it 'can add email variable set to false' do + response + expect(assigns(:can_add_email)).to eq(false) + end + end end describe '#update' do diff --git a/spec/controllers/sign_up/select_email_controller_spec.rb b/spec/controllers/sign_up/select_email_controller_spec.rb index 73adfe3dbb4..4966281dccc 100644 --- a/spec/controllers/sign_up/select_email_controller_spec.rb +++ b/spec/controllers/sign_up/select_email_controller_spec.rb @@ -47,6 +47,7 @@ expect(assigns(:user_emails)).to all be_kind_of(EmailAddress) expect(assigns(:last_sign_in_email_address)).to be_kind_of(String) expect(assigns(:select_email_form)).to be_kind_of(SelectEmailForm) + expect(assigns(:can_add_email)).to eq(true) end context 'with selected email to share feature disabled' do @@ -69,6 +70,18 @@ expect(response).to redirect_to(sign_up_completed_path) end end + + context 'when users has max number of emails' do + before do + allow(user).to receive(:email_address_count).and_return(2) + allow(IdentityConfig.store).to receive(:max_emails_per_account).and_return(2) + end + + it 'can add email variable set to false' do + response + expect(assigns(:can_add_email)).to eq(false) + end + end end describe '#create' do diff --git a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb index 454ddb9144b..a0e0fe82a02 100644 --- a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb +++ b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb @@ -20,6 +20,7 @@ allow(view).to receive(:current_user).and_return(user) @identity = identity @select_email_form = SelectEmailForm.new(user:, identity:) + @can_add_email = true end it 'renders introduction text' do @@ -35,4 +36,24 @@ expect(inputs).to be_logically_grouped(t('titles.select_email')) expect(rendered).to have_content(identity.display_name) end + + it 'renders a button to allow users to add email' do + expect(rendered).to have_link( + t('account.index.email_add'), + href: add_email_path(in_select_email_flow: true), + ) + end + + context 'if user has reached max number of emails' do + before do + @can_add_email = false + end + + it 'does not render add email button' do + expect(rendered).not_to have_link( + t('account.index.email_add'), + href: add_email_path(in_select_email_flow: true), + ) + end + end end diff --git a/spec/views/sign_up/select_email/show.html.erb_spec.rb b/spec/views/sign_up/select_email/show.html.erb_spec.rb index 30d16c34b92..86352280d0e 100644 --- a/spec/views/sign_up/select_email/show.html.erb_spec.rb +++ b/spec/views/sign_up/select_email/show.html.erb_spec.rb @@ -12,6 +12,7 @@ @user_emails = user.confirmed_email_addresses @select_email_form = SelectEmailForm.new(user:) @sp_name = 'Test Service Provider' + @can_add_email = true end it 'renders introduction text' do @@ -24,4 +25,24 @@ expect(rendered).to include('michael.motorist@email.com') expect(rendered).to include('michael.motorist2@email.com') end + + it 'renders a button to allow users to add email' do + expect(rendered).to have_link( + t('account.index.email_add'), + href: add_email_path(in_select_email_flow: true), + ) + end + + context 'if user has reached max number of emails' do + before do + @can_add_email = false + end + + it 'does not render add email button' do + expect(rendered).not_to have_link( + t('account.index.email_add'), + href: add_email_path(in_select_email_flow: true), + ) + end + end end From 23f509e7a9ecd3991109cfde27be3cbfdbeac1f8 Mon Sep 17 00:00:00 2001 From: A Shukla Date: Wed, 27 Nov 2024 09:17:00 -0600 Subject: [PATCH 09/17] Lg-14277 verification data analytics event (#11509) * added method definition * changelog: Upcoming Features, socure, socure analytics logging * Resolving pr comments, still needs work * Try to fix test * still trying * Moving test and adding tests * Fixing lint * Resolving Pr comments, fixing perform later * Resolving PR comments * cleaning up varibles and using extra_attributes method * Resolving PR comments * Removing certain nil attributes and resolving PR comments, making more readable * Resolving pr comments removing unneeded code * Fixing rebase * Resolving PR request for adding logging test to feature test * Resolving PR comments --- app/controllers/socure_webhook_controller.rb | 2 +- app/jobs/socure_docv_results_job.rb | 61 ++++++++---- app/services/analytics_events.rb | 95 +++++++++++++++++++ .../socure/responses/docv_result_response.rb | 60 +++++++++--- app/services/idv/analytics_events_enhancer.rb | 1 + .../doc_auth/socure_document_capture_spec.rb | 14 ++- spec/jobs/socure_docv_results_job_spec.rb | 45 ++++++++- .../requests/docv_result_request_spec.rb | 1 + 8 files changed, 246 insertions(+), 33 deletions(-) diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb index 0d0354da7d1..0028767330d 100644 --- a/app/controllers/socure_webhook_controller.rb +++ b/app/controllers/socure_webhook_controller.rb @@ -38,7 +38,7 @@ def fetch_results if IdentityConfig.store.ruby_workers_idv_enabled SocureDocvResultsJob.perform_later(document_capture_session_uuid: dcs.uuid) else - SocureDocvResultsJob.perform_now(document_capture_session_uuid: dcs.uuid) + SocureDocvResultsJob.perform_now(document_capture_session_uuid: dcs.uuid, async: false) end end diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb index ad3fa7bca04..a2d78f7ca50 100644 --- a/app/jobs/socure_docv_results_job.rb +++ b/app/jobs/socure_docv_results_job.rb @@ -3,35 +3,48 @@ class SocureDocvResultsJob < ApplicationJob queue_as :high_socure_docv - attr_reader :document_capture_session_uuid + attr_reader :document_capture_session_uuid, :async # @param [String] document_capture_session_uuid - def perform(document_capture_session_uuid:) + def perform(document_capture_session_uuid:, async: true) @document_capture_session_uuid = document_capture_session_uuid + @async = async - dcs = DocumentCaptureSession.find_by(uuid: document_capture_session_uuid) - raise "DocumentCaptureSession not found: #{document_capture_session_uuid}" if !dcs + raise "DocumentCaptureSession not found: #{document_capture_session_uuid}" unless + document_capture_session - # analytics = create_analytics( - # user: dcs.user, - # service_provider_issuer: dcs.issuer, - # ) - - result = socure_document_verification_result - dcs.store_result_from_response(result) + timer = JobHelpers::Timer.new + response = timer.time('vendor_request') do + socure_document_verification_result + end + log_verification_request( + docv_result_response: response, + vendor_request_time_in_ms: timer.results['vendor_request'], + ) + document_capture_session.store_result_from_response(response) end private - def create_analytics( - user:, - service_provider_issuer: - ) - Analytics.new( - user:, + def analytics + @analytics ||= Analytics.new( + user: document_capture_session.user, request: nil, - sp: service_provider_issuer, session: {}, + sp: document_capture_session.issuer, + ) + end + + def log_verification_request(docv_result_response:, vendor_request_time_in_ms:) + analytics.idv_socure_verification_data_requested( + **docv_result_response.to_h.merge( + docv_transaction_token: document_capture_session.socure_docv_transaction_token, + submit_attempts: rate_limiter&.attempts, + remaining_submit_attempts: rate_limiter&.remaining_count, + vendor_request_time_in_ms:, + async:, + ).except(:attention_with_barcode, :selfie_live, :selfie_quality_good, + :selfie_status).compact, ) end @@ -40,4 +53,16 @@ def socure_document_verification_result document_capture_session_uuid:, ).fetch end + + def document_capture_session + @document_capture_session ||= + DocumentCaptureSession.find_by(uuid: document_capture_session_uuid) + end + + def rate_limiter + @rate_limiter ||= RateLimiter.new( + user: document_capture_session.user, + rate_limit_type: :idv_doc_auth, + ) + end end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 2bc84a7d2cb..257cb23c201 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -4708,6 +4708,101 @@ def idv_socure_shadow_mode_proofing_result_missing(**extra) track_event(:idv_socure_shadow_mode_proofing_result_missing, **extra) end + # @param [Boolean] success Whether form validation was successful + # @param [Hash] errors Errors resulting from form validation + # @param [String] exception + # @param [Boolean] billed + # @param [String] docv_transaction_token socure transaction token + # @param [Hash] customer_profile socure customer profile + # @param [String] reference_id socure interal id for transaction + # @param [Hash] reason_codes socure internal reason codes for accept reject decision + # @param [Hash] document_type type of socument submitted (Drivers Licenese, etc.) + # @param [Hash] decision accept or reject of given ID + # @param [String] user_id internal id of socure user + # @param [String] state state of ID + # @param [String] state_id_type type of state issued ID + # @param [Boolean] async whether or not this worker is running asynchronously + # @param [Integer] submit_attempts Times that user has tried submitting (previously called + # "attempts") + # @param [Integer] remaining_submit_attempts (previously called "remaining_attempts") + # @param ["hybrid","standard"] flow_path Document capture user flow + # @param [Float] vendor_request_time_in_ms Time it took to upload images & get a response. + # @param [Boolean] doc_type_supported + # @param [Boolean] doc_auth_success + # @param [Boolean] liveness_checking_required Whether or not the selfie is required + # @param [Boolean] liveness_enabled Whether or not the selfie result is included in response + # @param [String] vendor which 2rd party we are using for doc auth + # @param [Boolean] address_line2_present wether or not we have an address that uses the 2nd line + # @param [String] zip_code zip code from state issued ID + # @param [String] birth_year Birth year from document + # @param [Integer] issue_year Year document was issued + # @param [Boolean] biometric_comparison_required does doc auth require biometirc + # The request for socure verification was sent + def idv_socure_verification_data_requested( + success:, + errors:, + async:, + customer_profile:, + reference_id:, + reason_codes:, + document_type:, + decision:, + state:, + state_id_type:, + submit_attempts:, + remaining_submit_attempts:, + liveness_checking_required:, + issue_year:, + vendor_request_time_in_ms:, + doc_type_supported:, + doc_auth_success:, + vendor:, + address_line2_present:, + zip_code:, + birth_year:, + liveness_enabled:, + biometric_comparison_required:, + docv_transaction_token: nil, + user_id: nil, + exception: nil, + flow_path: nil, + billed: nil, + **extra + ) + track_event( + :idv_socure_verification_data_requested, + success:, + errors:, + exception:, + billed:, + docv_transaction_token:, + customer_profile:, + reference_id:, + reason_codes:, + document_type:, + decision:, + user_id:, + state:, + state_id_type:, + async:, + submit_attempts:, + remaining_submit_attempts:, + flow_path:, + liveness_checking_required:, + vendor_request_time_in_ms:, + doc_type_supported:, + doc_auth_success:, + vendor:, + address_line2_present:, + zip_code:, + birth_year:, + issue_year:, + liveness_enabled:, + biometric_comparison_required:, + **extra, + ) + end + # @param [String] step # @param [String] location # @param [Hash,nil] proofing_components User's current proofing components diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb index aa0c0ae7a8e..d2beacb7966 100644 --- a/app/services/doc_auth/socure/responses/docv_result_response.rb +++ b/app/services/doc_auth/socure/responses/docv_result_response.rb @@ -30,9 +30,13 @@ class DocvResultResponse < DocAuth::Response document_number: %w[documentVerification documentData documentNumber], issue_date: %w[documentVerification documentData issueDate], expiration_date: %w[documentVerification documentData expirationDate], + customer_profile: %w[customerProfile], + socure_customer_user_id: %w[customerProfile customerUserId], + socure_user_id: %w[customerProfile userId], }.freeze - def initialize(http_response:, biometric_comparison_required: false) + def initialize(http_response:, + biometric_comparison_required: false) @http_response = http_response @biometric_comparison_required = biometric_comparison_required @pii_from_doc = read_pii @@ -63,6 +67,28 @@ def selfie_status :not_processed end + def extra_attributes + { + reference_id: get_data(DATA_PATHS[:reference_id]), + decision: get_data(DATA_PATHS[:decision]), + biometric_comparison_required: biometric_comparison_required, + customer_profile: get_data(DATA_PATHS[:customer_profile]), + reason_codes: get_data(DATA_PATHS[:reason_codes]), + document_type: get_data(DATA_PATHS[:document_type]), + state: state, + state_id_type: state_id_type, + flow_path: nil, + liveness_checking_required: @biometric_comparison_required, + issue_year: state_id_issued&.year, + doc_auth_success: successful_result?, + vendor: 'Socure', + address_line2_present: address2.present?, + zip_code: zipcode, + birth_year: dob&.year, + liveness_enabled: @biometric_comparison_required, + } + end + private def successful_result? @@ -77,14 +103,6 @@ def error_messages } end - def extra_attributes - { - reference_id: get_data(DATA_PATHS[:reference_id]), - decision: get_data(DATA_PATHS[:decision]), - biometric_comparison_required: biometric_comparison_required, - } - end - def read_pii Pii::StateId.new( first_name: get_data(DATA_PATHS[:first_name]), @@ -92,7 +110,7 @@ def read_pii last_name: get_data(DATA_PATHS[:last_name]), name_suffix: nil, address1: get_data(DATA_PATHS[:address1]), - address2: get_data(DATA_PATHS[:address2]), + address2:, city: get_data(DATA_PATHS[:city]), state: get_data(DATA_PATHS[:state]), zipcode: get_data(DATA_PATHS[:zipcode]), @@ -102,7 +120,7 @@ def read_pii weight: nil, eye_color: nil, state_id_number: get_data(DATA_PATHS[:document_number]), - state_id_issued: parse_date(get_data(DATA_PATHS[:issue_date])), + state_id_issued:, state_id_expiration: parse_date(get_data(DATA_PATHS[:expiration_date])), state_id_type: state_id_type, state_id_jurisdiction: get_data(DATA_PATHS[:issuing_state]), @@ -124,11 +142,31 @@ def parsed_response_body end end + def state + get_data(DATA_PATHS[:state]) + end + + def zipcode + get_data(DATA_PATHS[:zipcode]) + end + + def state_id_issued + parse_date(get_data(DATA_PATHS[:issue_date])) + end + def state_id_type type = get_data(DATA_PATHS[:id_type]) type&.gsub(/\W/, '')&.underscore end + def dob + parse_date(get_data(DATA_PATHS[:dob])) + end + + def address2 + get_data(DATA_PATHS[:address2]) + end + def parse_date(date_string) Date.parse(date_string) rescue ArgumentError, TypeError diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb index 6f67339f580..810e933438d 100644 --- a/app/services/idv/analytics_events_enhancer.rb +++ b/app/services/idv/analytics_events_enhancer.rb @@ -31,6 +31,7 @@ module AnalyticsEventsEnhancer idv_doc_auth_ssn_visited idv_doc_auth_submitted_image_upload_form idv_doc_auth_submitted_image_upload_vendor + idv_socure_verification_data_requested idv_doc_auth_submitted_pii_validation idv_doc_auth_verify_proofing_results idv_doc_auth_verify_submitted diff --git a/spec/features/idv/doc_auth/socure_document_capture_spec.rb b/spec/features/idv/doc_auth/socure_document_capture_spec.rb index ca3e1702bb9..be8b28388fc 100644 --- a/spec/features/idv/doc_auth/socure_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/socure_document_capture_spec.rb @@ -66,7 +66,9 @@ context 'successfully processes image on last attempt' do before do + allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) DocAuth::Mock::DocAuthMockClient.reset! + allow(Analytics).to receive(:new).and_return(fake_analytics) end it 'proceeds to the next page with valid info' do @@ -76,13 +78,15 @@ socure_docv_upload_documents( docv_transaction_token: @docv_transaction_token, ) - visit idv_socure_document_capture_update_path expect(page).to have_current_path(idv_ssn_url) visit idv_socure_document_capture_path expect(page).to have_current_path(idv_session_errors_rate_limited_path) + expect(fake_analytics).to have_logged_event( + :idv_socure_verification_data_requested, + ) end end end @@ -119,6 +123,11 @@ end context 'standard mobile flow' do + before do + allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) + allow(Analytics).to receive(:new).and_return(fake_analytics) + end + it 'proceeds to the next page with valid info' do perform_in_browser(:mobile) do visit_idp_from_oidc_sp_with_ial2 @@ -135,6 +144,9 @@ expect(page).to have_current_path(idv_ssn_url) expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY') + expect(fake_analytics).to have_logged_event( + :idv_socure_verification_data_requested, + ) fill_out_ssn_form_ok click_idv_continue diff --git a/spec/jobs/socure_docv_results_job_spec.rb b/spec/jobs/socure_docv_results_job_spec.rb index 5ded063eaa9..92bfa049db3 100644 --- a/spec/jobs/socure_docv_results_job_spec.rb +++ b/spec/jobs/socure_docv_results_job_spec.rb @@ -5,6 +5,7 @@ RSpec.describe SocureDocvResultsJob do let(:job) { described_class.new } let(:user) { create(:user) } + let(:fake_analytics) { FakeAnalytics.new } let(:document_capture_session) do DocumentCaptureSession.create(user:).tap do |dcs| dcs.socure_docv_transaction_token = '1234' @@ -15,17 +16,19 @@ let(:decision_value) { 'accept' } let(:expiration_date) { "#{1.year.from_now.year}-01-01" } - let(:analytics) { FakeAnalytics.new } - before do allow(IdentityConfig.store).to receive(:socure_idplus_base_url). and_return(socure_idplus_base_url) + allow(Analytics).to receive(:new).and_return(fake_analytics) end describe '#perform' do subject(:perform) do job.perform(document_capture_session_uuid: document_capture_session_uuid) end + subject(:perform_now) do + job.perform(document_capture_session_uuid: document_capture_session_uuid, async: false) + end let(:socure_response_body) do # ID+ v3.0 API Predictive Document Verification response @@ -69,6 +72,24 @@ } end + let(:expected_socure_log) do + { + success: true, + issue_year: 2020, + vendor: 'Socure', + submit_attempts: 0, + remaining_submit_attempts: 4, + state: 'NY', + zip_code: '10001', + doc_auth_success: true, + document_type: { + type: 'Drivers License', + country: 'USA', + state: 'NY', + }, + } + end + before do stub_request(:post, 'https://example.com/api/3.0/EmailAuthScore'). to_return( @@ -91,6 +112,26 @@ expect(document_capture_session_result.selfie_status).to eq(:not_processed) end + it 'expect fake analytics to have logged idv_socure_verification_data_requested' do + perform + expect(fake_analytics).to have_logged_event( + :idv_socure_verification_data_requested, + hash_including( + expected_socure_log.merge({ async: true }), + ), + ) + end + + it 'expect log with perform_now to have async eq false' do + perform_now + expect(fake_analytics).to have_logged_event( + :idv_socure_verification_data_requested, + hash_including( + expected_socure_log.merge({ async: false }), + ), + ) + end + context 'when the document capture session does not exist' do let(:document_capture_session_uuid) { '1234' } diff --git a/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb index 2fe380431d8..ae7a67a63c7 100644 --- a/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb +++ b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb @@ -3,6 +3,7 @@ RSpec.describe DocAuth::Socure::Requests::DocvResultRequest do let(:document_capture_session_uuid) { 'fake uuid' } let(:biometric_comparison_required) { false } + let(:fake_analytics) { FakeAnalytics.new } subject(:docv_result_request) do described_class.new( From 247ce080911b9c167d0820efe60c29c959df1797 Mon Sep 17 00:00:00 2001 From: Shane Chesnutt Date: Wed, 27 Nov 2024 10:48:52 -0500 Subject: [PATCH 10/17] LG-14748 Add in person warning to password reset email (#11547) changelog: User-facing Improvements, In-person Proofing, Add warning banner to password reset email when the user has an in-progress in-person enrollment --- app/mailers/user_mailer.rb | 3 +- .../reset_password_instructions.html.erb | 10 +- .../shared/_in_person_warning_banner.html.erb | 11 ++ config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/fr.yml | 1 + config/locales/zh.yml | 1 + spec/i18n_spec.rb | 1 + spec/mailers/previews/user_mailer_preview.rb | 21 +++ spec/mailers/user_mailer_spec.rb | 146 ++++++++++++++++++ 10 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 app/views/user_mailer/shared/_in_person_warning_banner.html.erb diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index ca8c1f2f0a5..32678fad843 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -67,7 +67,8 @@ def reset_password_instructions(token:, request_id:) @token = token @request_id = request_id @gpo_verification_pending_profile = user.gpo_verification_pending_profile? - @hide_title = @gpo_verification_pending_profile + @in_person_verification_pending_profile = user.in_person_pending_profile? + @hide_title = @gpo_verification_pending_profile || @in_person_verification_pending_profile mail(to: email_address.email, subject: t('user_mailer.reset_password_instructions.subject')) end end diff --git a/app/views/user_mailer/reset_password_instructions.html.erb b/app/views/user_mailer/reset_password_instructions.html.erb index 8a4616aa5e6..bed12ed7ed7 100644 --- a/app/views/user_mailer/reset_password_instructions.html.erb +++ b/app/views/user_mailer/reset_password_instructions.html.erb @@ -15,7 +15,15 @@

<%= @header || message.subject %>

- <% end %> +<% end %> + +<% if @in_person_verification_pending_profile %> + <%= render 'user_mailer/shared/in_person_warning_banner' %> +

+ <%= @header || message.subject %> +

+<% end %> +

<%= t( 'user_mailer.reset_password_instructions.header', diff --git a/app/views/user_mailer/shared/_in_person_warning_banner.html.erb b/app/views/user_mailer/shared/_in_person_warning_banner.html.erb new file mode 100644 index 00000000000..10fe28efa28 --- /dev/null +++ b/app/views/user_mailer/shared/_in_person_warning_banner.html.erb @@ -0,0 +1,11 @@ + + + + + + +
+ <%= image_tag('email/warning.png', width: 16, height: 14, alt: 'warning icon', style: 'margin-top: 5px;') %> + +

<%= t('user_mailer.reset_password_instructions.in_person_warning_description_html') %>

+
diff --git a/config/locales/en.yml b/config/locales/en.yml index f528ad32963..2ba1e98735e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1942,6 +1942,7 @@ user_mailer.reset_password_instructions.footer: This link expires in %{expires} user_mailer.reset_password_instructions.gpo_letter_description: If you reset your password, the verification code in your letter will no longer work and you’ll have to verify your identity again. user_mailer.reset_password_instructions.gpo_letter_header: Your letter is on the way user_mailer.reset_password_instructions.header: To finish resetting your password, please click the link below or copy and paste the entire link into your browser. +user_mailer.reset_password_instructions.in_person_warning_description_html: If you reset your password now, your barcode will not work at the Post Office. You’ll have to restart the identity verification process from the beginning. user_mailer.reset_password_instructions.link_text: Reset your password user_mailer.reset_password_instructions.subject: Reset your password user_mailer.signup_with_your_email.help_html: If you did not request a new account or suspect an error, please visit the %{app_name_html} %{help_link_html} or %{contact_link_html}. diff --git a/config/locales/es.yml b/config/locales/es.yml index 3640e2aa666..d93a88dbcf3 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1954,6 +1954,7 @@ user_mailer.reset_password_instructions.footer: Este vínculo vence en %{expires user_mailer.reset_password_instructions.gpo_letter_description: Si restablece su contraseña, el código de verificación que recibió en su carta ya no funcionará y tendrá que volver a verificar su identidad. user_mailer.reset_password_instructions.gpo_letter_header: Su carta está en camino user_mailer.reset_password_instructions.header: Para terminar de restablecer su contraseña, haga clic en el enlace de abajo o copie y pegue el enlace completo en su navegador. +user_mailer.reset_password_instructions.in_person_warning_description_html: If you reset your password now, your barcode will not work at the Post Office. You’ll have to restart the identity verification process from the beginning. user_mailer.reset_password_instructions.link_text: Restablezca su contraseña user_mailer.reset_password_instructions.subject: Restablezca su contraseña user_mailer.signup_with_your_email.help_html: Si usted no solicitó una cuenta nueva o sospecha que hubo un error, visite la %{help_link_html} de %{app_name_html} o %{contact_link_html}. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 72ae134d8f5..e20fa43644b 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1942,6 +1942,7 @@ user_mailer.reset_password_instructions.footer: Ce lien expire dans %{expires} h user_mailer.reset_password_instructions.gpo_letter_description: Si vous réinitialisez votre mot de passe, le code de vérification contenu dans votre lettre ne fonctionnera plus et vous devrez reconfirmer votre identité. user_mailer.reset_password_instructions.gpo_letter_header: Votre lettre est en route user_mailer.reset_password_instructions.header: Pour terminer la réinitialisation de votre mot de passe, veuillez cliquer sur le lien ci-dessous ou copier et coller le lien complet dans votre navigateur. +user_mailer.reset_password_instructions.in_person_warning_description_html: If you reset your password now, your barcode will not work at the Post Office. You’ll have to restart the identity verification process from the beginning. user_mailer.reset_password_instructions.link_text: Réinitialiser votre mot de passe user_mailer.reset_password_instructions.subject: Réinitialiser votre mot de passe user_mailer.signup_with_your_email.help_html: Si vous n’avez pas demandé un nouveau compte ou soupçonnez qu’une erreur s’est produite, veuillez visiter le %{help_link_html} de %{app_name_html} ou %{contact_link_html}. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 62cd87be425..89061d000ad 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1955,6 +1955,7 @@ user_mailer.reset_password_instructions.footer: 这一链接 %{expires} 小时 user_mailer.reset_password_instructions.gpo_letter_description: 如果你重设密码,信件中的一次性代码就会失效,你需要再次验证身份。 user_mailer.reset_password_instructions.gpo_letter_header: 你的信件已寄出。 user_mailer.reset_password_instructions.header: 要完成重设密码,请点击下面的链接或把整个链接复制并黏贴进浏览器。 +user_mailer.reset_password_instructions.in_person_warning_description_html: If you reset your password now, your barcode will not work at the Post Office. You’ll have to restart the identity verification process from the beginning. user_mailer.reset_password_instructions.link_text: 重设你的密码 user_mailer.reset_password_instructions.subject: 重设你的密码 user_mailer.signup_with_your_email.help_html: 如果你没有要求一封新电邮或怀疑有错, 请访问 %{app_name_html}的 %{help_link_html} 或者 %{contact_link_html}。 diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 220d6842536..960aa40598c 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -74,6 +74,7 @@ class BaseTask { key: 'time.formats.event_timestamp', locales: %i[zh] }, { key: 'time.formats.full_date', locales: %i[es] }, # format is the same in Spanish and English { key: 'time.formats.sms_date' }, # for us date format + { key: 'user_mailer.reset_password_instructions.in_person_warning_description_html', locales: %i[es fr zh] }, # Temporary until spanish, french, and chinese translations come in. { key: 'webauthn_platform_recommended.cta' }, # English-only A/B test { key: 'webauthn_platform_recommended.description_private_html' }, # English-only A/B test { key: 'webauthn_platform_recommended.description_secure_account' }, # English-only A/B test diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 5aa21140cc6..4bf1af6242b 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -26,6 +26,14 @@ def reset_password_instructions_with_pending_gpo_letter ) end + def reset_password_instructions_with_pending_in_person_warning + UserMailer.with( + user: user_with_pending_in_person_profile, email_address: email_address_record, + ).reset_password_instructions( + token: SecureRandom.hex, request_id: SecureRandom.hex, + ) + end + def password_changed UserMailer.with(user: user, email_address: email_address_record). password_changed(disavowal_token: SecureRandom.hex) @@ -313,6 +321,19 @@ def user_with_pending_gpo_letter raw_user end + def user_with_pending_in_person_profile + raw_user = user + in_person_pending_profile = unsaveable( + Profile.new( + user: raw_user, + active: false, + in_person_verification_pending_at: Time.zone.now, + ), + ) + raw_user.send(:instance_variable_set, :@pending_profile, in_person_pending_profile) + raw_user + end + def email_address 'email@example.com' end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 15a4ffb1fda..8155d26713a 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -113,6 +113,152 @@ end end + describe '#reset_password_instructions' do + let(:token) { SecureRandom.hex } + let(:request_id) { SecureRandom.uuid } + let(:mail) do + UserMailer.with( + user: user, + email_address: email_address, + ).reset_password_instructions(token:, request_id:) + end + let(:locale) { 'es' } + + before do + I18n.locale = locale + end + + it_behaves_like 'a system email' + it_behaves_like 'an email that respects user email locale preference' + + context 'when the user has gpo verfication pending' do + let(:user) { create(:user, :with_pending_gpo_profile) } + + it 'sends to the current email address' do + expect(mail.to).to eq [email_address.email] + end + + it 'renders the subject' do + expect(mail.subject).to eq t('user_mailer.reset_password_instructions.subject') + end + + it 'renders the gpo warning alert' do + expect(mail.html_part.body).to have_content( + t('user_mailer.reset_password_instructions.gpo_letter_description'), + ) + end + + it 'does not render the in person warning banner' do + expect(mail.html_part.body).not_to have_content( + strip_tags( + t('user_mailer.reset_password_instructions.in_person_warning_description_html'), + ), + ) + end + + it 'renders the reset password instructions' do + expect(mail.html_part.body).to have_content( + t('user_mailer.reset_password_instructions.header'), + ) + end + + it 'renders the reset password button' do + expect(mail.html_part.body).to have_link( + t('user_mailer.reset_password_instructions.link_text'), + href: edit_user_password_url( + reset_password_token: token, + locale: locale, + request_id: request_id, + ), + ) + end + end + + context 'when the user has in person verfication pending' do + let(:user) { create(:user, :with_pending_in_person_enrollment) } + + it 'sends to the current email address' do + expect(mail.to).to eq [email_address.email] + end + + it 'renders the subject' do + expect(mail.subject).to eq t('user_mailer.reset_password_instructions.subject') + end + + it 'renders the in person warning banner' do + expect(mail.html_part.body).to have_content( + strip_tags( + t('user_mailer.reset_password_instructions.in_person_warning_description_html'), + ), + ) + end + + it 'does not render the gpo warning alert' do + expect(mail.html_part.body).not_to have_content( + t('user_mailer.reset_password_instructions.gpo_letter_description'), + ) + end + + it 'renders the reset password instructions' do + expect(mail.html_part.body).to have_content( + t('user_mailer.reset_password_instructions.header'), + ) + end + + it 'renders the reset password button' do + expect(mail.html_part.body).to have_link( + t('user_mailer.reset_password_instructions.link_text'), + href: edit_user_password_url( + reset_password_token: token, + locale: locale, + request_id: request_id, + ), + ) + end + end + + context 'when the user does not have any verification pending' do + it 'sends to the current email address' do + expect(mail.to).to eq [email_address.email] + end + + it 'renders the subject' do + expect(mail.subject).to eq t('user_mailer.reset_password_instructions.subject') + end + + it 'does not render the gpo warning alert' do + expect(mail.html_part.body).not_to have_content( + t('user_mailer.reset_password_instructions.gpo_letter_description'), + ) + end + + it 'does not render the in person warning banner' do + expect(mail.html_part.body).not_to have_content( + strip_tags( + t('user_mailer.reset_password_instructions.in_person_warning_description_html'), + ), + ) + end + + it 'renders the reset password instructions' do + expect(mail.html_part.body).to have_content( + t('user_mailer.reset_password_instructions.header'), + ) + end + + it 'renders the reset password button' do + expect(mail.html_part.body).to have_link( + t('user_mailer.reset_password_instructions.link_text'), + href: edit_user_password_url( + reset_password_token: token, + locale: locale, + request_id: request_id, + ), + ) + end + end + end + describe '#password_changed' do let(:mail) do UserMailer.with( From 92d2b2f4e2fc56f6e96ee186e5547d16f4ab530f Mon Sep 17 00:00:00 2001 From: KeithNava <134446588+KeithNava@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:27:19 -0500 Subject: [PATCH 11/17] LG-13006: remove skip_doc_auth from session (#11569) * feat: remove skip_doc_auth from session * changelog: Internal, In-person proofing, remove old skip_doc_auth variable from session --- app/services/idv/session.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index f21a017ad64..071ce403a80 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -26,7 +26,6 @@ module Idv # @attr resolution_successful [Boolean, nil] # @attr selfie_check_performed [Boolean, nil] # @attr selfie_check_required [Boolean, nil] - # @attr skip_doc_auth [Boolean, nil] # @attr skip_doc_auth_from_handoff [Boolean, nil] # @attr skip_doc_auth_from_how_to_verify [Boolean, nil] # @attr skip_hybrid_handoff [Boolean, nil] @@ -69,7 +68,6 @@ class Session resolution_successful selfie_check_performed selfie_check_required - skip_doc_auth skip_doc_auth_from_handoff skip_doc_auth_from_how_to_verify skip_hybrid_handoff From 3d244b63b429bf222c60d04cc8e3a53e3a13d09f Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:02:31 -0500 Subject: [PATCH 12/17] Remove unused event parameter from RedirectController (#11576) changelog: Internal, Analytics, Remove unused event parameter from RedirectController --- app/controllers/redirect/redirect_controller.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/controllers/redirect/redirect_controller.rb b/app/controllers/redirect/redirect_controller.rb index c4e90625644..d63025667e9 100644 --- a/app/controllers/redirect/redirect_controller.rb +++ b/app/controllers/redirect/redirect_controller.rb @@ -17,14 +17,8 @@ def partner_params }.compact end - def redirect_to_and_log(url, event: nil, tracker_method: analytics.method(:external_redirect)) - if event - # Once all events have been moved to tracker methods, we can remove the event: param - analytics.track_event(event, redirect_url: url, **location_params) - else - tracker_method.call(redirect_url: url, **location_params) - end - + def redirect_to_and_log(url, tracker_method: analytics.method(:external_redirect)) + tracker_method.call(redirect_url: url, **location_params) redirect_url = UriService.add_params(url, partner_params) redirect_to(redirect_url, allow_other_host: true) end From f94b900fd4032eb030d7346b4fca8b80188e4674 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:46:11 -0500 Subject: [PATCH 13/17] Exempt additional WebAuthn error logging as expected (#11577) changelog: Internal, Error Logging, Exempt additional WebAuthn error logging as expected --- .../webauthn/is-expected-error.spec.ts | 30 +++++++++++ .../packages/webauthn/is-expected-error.ts | 50 +++++++++++-------- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/app/javascript/packages/webauthn/is-expected-error.spec.ts b/app/javascript/packages/webauthn/is-expected-error.spec.ts index 58834e29d05..725753896f4 100644 --- a/app/javascript/packages/webauthn/is-expected-error.spec.ts +++ b/app/javascript/packages/webauthn/is-expected-error.spec.ts @@ -36,4 +36,34 @@ describe('isExpectedWebauthnError', () => { expect(result).to.be.true(); }); + + it('returns true for a NotReadableError Android credential manager incompatibility', () => { + const error = new DOMException( + 'An unknown error occurred while talking to the credential manager.', + 'NotReadableError', + ); + const result = isExpectedWebauthnError(error); + + expect(result).to.be.true(); + }); + + it('returns false for NotSupportedError when not during verification', () => { + const error = new DOMException( + 'The user agent does not support public key credentials.', + 'NotSupportedError', + ); + const result = isExpectedWebauthnError(error); + + expect(result).to.be.false(); + }); + + it('returns true for NotSupportedError during verification', () => { + const error = new DOMException( + 'The user agent does not support public key credentials.', + 'NotSupportedError', + ); + const result = isExpectedWebauthnError(error, { isVerifying: true }); + + expect(result).to.be.true(); + }); }); diff --git a/app/javascript/packages/webauthn/is-expected-error.ts b/app/javascript/packages/webauthn/is-expected-error.ts index 7ee1c48babc..9e889cbd037 100644 --- a/app/javascript/packages/webauthn/is-expected-error.ts +++ b/app/javascript/packages/webauthn/is-expected-error.ts @@ -1,19 +1,34 @@ import isUserVerificationScreenLockError from './is-user-verification-screen-lock-error'; /** - * Set of expected DOM exceptions, which occur based on some user behavior that is not noteworthy: - * - * - Declining permissions - * - Timeout due to inactivity - * - Invalid state such as duplicate key enrollment - * - * @see https://webidl.spec.whatwg.org/#idl-DOMException + * Functions to test whether an error is expected and should not be logged for further analysis. */ -const EXPECTED_DOM_EXCEPTIONS: Set = new Set([ - 'NotAllowedError', - 'TimeoutError', - 'InvalidStateError', -]); +const EXPECTED_ERRORS: Array<(error: Error, options: IsExpectedErrorOptions) => boolean> = [ + // A user who is unable to complete due to following DOMException reasons is not noteworthy: + // + // - Declining permissions + // - Timeout due to inactivity + // - Invalid state such as duplicate key enrollment + (error) => + error.name === 'NotAllowedError' || + error.name === 'TimeoutError' || + error.name === 'InvalidStateError', + // Some indication of incompatibilities on specific Android devices, either phone itself or + // through credential manager. + // + // See: https://community.bitwarden.com/t/android-mobile-yubikey-5-nfc-webauth/51732 + // See: https://www.reddit.com/r/GooglePixel/comments/17enqf3/pixel_7_pro_unable_to_setup_passkeys/ + (error) => + error.name === 'NotReadableError' && + error.message === 'An unknown error occurred while talking to the credential manager.', + // A user can choose to authenticate with Face or Touch Unlock from another device from what + // they set up from, which may not necessarily support platform authenticators. + (error, { isVerifying }) => isVerifying && isUserVerificationScreenLockError(error), + (error, { isVerifying }) => + isVerifying && + error.name === 'NotSupportedError' && + error.message === 'The user agent does not support public key credentials.', +]; interface IsExpectedErrorOptions { /** @@ -22,14 +37,9 @@ interface IsExpectedErrorOptions { isVerifying: boolean; } -function isExpectedWebauthnError( +const isExpectedWebauthnError = ( error: Error, - { isVerifying }: Partial = {}, -): boolean { - return ( - (error instanceof DOMException && EXPECTED_DOM_EXCEPTIONS.has(error.name)) || - (!!isVerifying && isUserVerificationScreenLockError(error)) - ); -} + { isVerifying = false }: Partial = {}, +): boolean => EXPECTED_ERRORS.some((isExpected) => isExpected(error, { isVerifying })); export default isExpectedWebauthnError; From 9004d0297085ba30781e2cee74e462b8c366f356 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Mon, 2 Dec 2024 13:36:31 -0800 Subject: [PATCH 14/17] LG-13763: Stop writing to the proofing_components table (#11564) * Stop writing to the proofing_components table Just stop. These values are now generated dynamically as we need them, then serialized to the Profile. [skip changelog] * Update LinkSentController spec * Update UspsLocationsController + spec I moved idv_session to be public on the controller (matching the behavior of other controllers that use IdvSessionConcern). Then updated tests to use dynamically-generated proofing components. This requires reloading the user object. I created https://cm-jira.usa.gov/browse/LG-15167 to address computing the proofing component value without using the User object * Move proofing_component check to end of end-to-end spec We don't have access to idv_session here so we have to wait until the proofing component has been written to the database so we can access it. * Remove proofing components from pw reset disavowal form spec We were clearing out the _user's_ proofing component here, but not doing anything with the profile. * Remove references to ProofingComponent from specs * Don't check proofing_component in ResolutionProofingJob * Remove ProofingComponent model --- .../concerns/idv/document_capture_concern.rb | 11 -------- .../concerns/idv/verify_info_concern.rb | 17 +----------- .../idv/by_mail/request_letter_controller.rb | 2 -- .../in_person/usps_locations_controller.rb | 12 ++------- app/controllers/idv/link_sent_controller.rb | 1 - .../idv/personal_key_controller.rb | 5 ---- .../password_reset_from_disavowal_form.rb | 1 - app/forms/reset_password_form.rb | 1 - app/jobs/resolution_proofing_job.rb | 8 ------ app/models/proofing_component.rb | 5 ---- app/models/user.rb | 1 - app/services/idv/phone_step.rb | 3 --- app/services/idv/steps/doc_auth_base_step.rb | 15 ----------- .../idv/step_indicator_concern_spec.rb | 1 - .../idv/enter_password_controller_spec.rb | 1 - .../usps_locations_controller_spec.rb | 26 ++++++++++++++++--- .../idv/link_sent_controller_spec.rb | 11 +++++--- .../idv/personal_key_controller_spec.rb | 1 - spec/features/idv/end_to_end_idv_spec.rb | 17 +++++++----- ...password_reset_from_disavowal_form_spec.rb | 22 ---------------- spec/forms/gpo_verify_form_spec.rb | 8 ------ spec/jobs/resolution_proofing_job_spec.rb | 12 --------- spec/models/user_spec.rb | 4 --- spec/services/idv/session_spec.rb | 2 -- 24 files changed, 44 insertions(+), 143 deletions(-) delete mode 100644 app/models/proofing_component.rb diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb index e02aa9b69b2..edcfaa0f4d8 100644 --- a/app/controllers/concerns/idv/document_capture_concern.rb +++ b/app/controllers/concerns/idv/document_capture_concern.rb @@ -8,7 +8,6 @@ module DocumentCaptureConcern def handle_stored_result(user: current_user, store_in_session: true) if stored_result&.success? && selfie_requirement_met? - save_proofing_components(user) extract_pii_from_doc(user, store_in_session: store_in_session) flash[:success] = t('doc_auth.headings.capture_complete') successful_response @@ -18,16 +17,6 @@ def handle_stored_result(user: current_user, store_in_session: true) end end - def save_proofing_components(user) - return unless user - - component_attributes = { - document_check: doc_auth_vendor, - document_type: 'state_id', - } - ProofingComponent.create_or_find_by(user: user).update(component_attributes) - end - def successful_response FormResponse.new(success: true) end diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index f3c0ac15f3a..fc71a949363 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -267,26 +267,11 @@ def summarize_result_and_rate_limit(summary_result) proofing_results_exception = summary_result.extra.dig(:proofing_results, :exception) resolution_rate_limiter.increment! if proofing_results_exception.blank? - if summary_result.success? - add_proofing_components(summary_result) - else + if !summary_result.success? idv_failure(summary_result) end end - def add_proofing_components(summary_result) - ProofingComponent.create_or_find_by(user: current_user).update( - resolution_check: Idp::Constants::Vendors::LEXIS_NEXIS, - source_check: summary_result.extra.dig( - :proofing_results, - :context, - :stages, - :state_id, - :vendor_name, - ), - ) - end - def load_async_state dcs_uuid = idv_session.verify_info_step_document_capture_session_uuid dcs = DocumentCaptureSession.find_by(uuid: dcs_uuid) diff --git a/app/controllers/idv/by_mail/request_letter_controller.rb b/app/controllers/idv/by_mail/request_letter_controller.rb index b7799d71181..e78689f9488 100644 --- a/app/controllers/idv/by_mail/request_letter_controller.rb +++ b/app/controllers/idv/by_mail/request_letter_controller.rb @@ -48,8 +48,6 @@ def update_tracking log_letter_requested_analytics(resend: false) create_user_event(:gpo_mail_sent, current_user) - - ProofingComponent.find_or_create_by(user: current_user).update(address_check: 'gpo_letter') end def confirm_mail_not_rate_limited diff --git a/app/controllers/idv/in_person/usps_locations_controller.rb b/app/controllers/idv/in_person/usps_locations_controller.rb index 36412215794..35c8366801e 100644 --- a/app/controllers/idv/in_person/usps_locations_controller.rb +++ b/app/controllers/idv/in_person/usps_locations_controller.rb @@ -63,13 +63,9 @@ def update sponsor_id: enrollment_sponsor_id, ) - add_proofing_component - render json: { success: true }, status: :ok end - private - def idv_session if user_session && current_user @idv_session ||= Idv::Session.new( @@ -80,6 +76,8 @@ def idv_session end end + private + def document_capture_session if idv_session&.document_capture_session_uuid # standard flow DocumentCaptureSession.find_by(uuid: idv_session.document_capture_session_uuid) @@ -92,12 +90,6 @@ def proofer @proofer ||= EnrollmentHelper.usps_proofer end - def add_proofing_component - ProofingComponent. - create_or_find_by(user: current_or_hybrid_user). - update(document_check: Idp::Constants::Vendors::USPS) - end - def localized_locations(locations) return nil if locations.nil? locations.map do |location| diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index 2c16e0d1e6f..93a8137bc86 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -65,7 +65,6 @@ def analytics_arguments end def handle_document_verification_success - save_proofing_components(current_user) extract_pii_from_doc(current_user, store_in_session: true) idv_session.flow_path = 'hybrid' end diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb index 7cccf2d8924..358b1f97fd0 100644 --- a/app/controllers/idv/personal_key_controller.rb +++ b/app/controllers/idv/personal_key_controller.rb @@ -29,7 +29,6 @@ def show if pii_is_missing? redirect_to_retrieve_pii else - add_proofing_component finish_idv_session end end @@ -78,10 +77,6 @@ def next_step end end - def add_proofing_component - ProofingComponent.find_or_create_by(user: current_user).update(verified_at: Time.zone.now) - end - def finish_idv_session @code = personal_key @personal_key_generated_at = current_user.personal_key_generated_at diff --git a/app/forms/event_disavowal/password_reset_from_disavowal_form.rb b/app/forms/event_disavowal/password_reset_from_disavowal_form.rb index 06df1ca4e2c..9f418501e0f 100644 --- a/app/forms/event_disavowal/password_reset_from_disavowal_form.rb +++ b/app/forms/event_disavowal/password_reset_from_disavowal_form.rb @@ -36,7 +36,6 @@ def mark_profile_inactive user.active_profile&.deactivate(:password_reset) Funnel::DocAuth::ResetSteps.call(@user.id) - user.proofing_component&.destroy end def extra_analytics_attributes diff --git a/app/forms/reset_password_form.rb b/app/forms/reset_password_form.rb index adcf084577e..d07ffa8ebdd 100644 --- a/app/forms/reset_password_form.rb +++ b/app/forms/reset_password_form.rb @@ -65,7 +65,6 @@ def mark_profile_inactive active_profile.deactivate(:password_reset) Funnel::DocAuth::ResetSteps.call(user.id) - user.proofing_component&.destroy end # It is possible for an account that is resetting their password to be "invalid". diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index 1d2c1aab43c..cf5b3b9cbe3 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -119,7 +119,6 @@ def make_vendor_proofing_requests( ) log_threatmetrix_info(result.device_profiling_result, user) - add_threatmetrix_proofing_component(user.id, result.device_profiling_result) if user.present? CallbackLogData.new( device_profiling_success: result.device_profiling_result.success?, @@ -150,11 +149,4 @@ def logger_info_hash(hash) def progressive_proofer @progressive_proofer ||= Proofing::Resolution::ProgressiveProofer.new end - - def add_threatmetrix_proofing_component(user_id, threatmetrix_result) - ProofingComponent. - create_or_find_by(user_id: user_id). - update(threatmetrix: FeatureManagement.proofing_device_profiling_collecting_enabled?, - threatmetrix_review_status: threatmetrix_result.review_status) - end end diff --git a/app/models/proofing_component.rb b/app/models/proofing_component.rb deleted file mode 100644 index 55ef4d750f0..00000000000 --- a/app/models/proofing_component.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class ProofingComponent < ApplicationRecord - belongs_to :user -end diff --git a/app/models/user.rb b/app/models/user.rb index 5d53cc38dac..88c89cc9daf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -44,7 +44,6 @@ class User < ApplicationRecord has_many :backup_code_configurations, dependent: :destroy has_many :document_capture_sessions, dependent: :destroy has_one :registration_log, dependent: :destroy - has_one :proofing_component, dependent: :destroy has_many :service_providers, through: :identities, source: :service_provider_record diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index 0ed264ffefd..eb13660adce 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -132,9 +132,6 @@ def failed_due_to_timeout_or_exception? def update_idv_session idv_session.applicant = applicant idv_session.mark_phone_step_started! - - ProofingComponent.find_or_create_by(user: idv_session.current_user). - update(address_check: 'lexis_nexis_address') end def start_phone_confirmation_session diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb index 52306235d87..09434827548 100644 --- a/app/services/idv/steps/doc_auth_base_step.rb +++ b/app/services/idv/steps/doc_auth_base_step.rb @@ -9,21 +9,6 @@ def initialize(flow) private - def save_proofing_components - return unless current_user - - doc_auth_vendor = DocAuthRouter.doc_auth_vendor( - discriminator: flow_session[document_capture_session_uuid_key], - analytics: @flow.analytics, - ) - - component_attributes = { - document_check: doc_auth_vendor, - document_type: 'state_id', - } - ProofingComponent.create_or_find_by(user: current_user).update(component_attributes) - end - def user_id_from_token flow_session[:doc_capture_user_id] end diff --git a/spec/controllers/concerns/idv/step_indicator_concern_spec.rb b/spec/controllers/concerns/idv/step_indicator_concern_spec.rb index 7c52970f208..702fadf58f9 100644 --- a/spec/controllers/concerns/idv/step_indicator_concern_spec.rb +++ b/spec/controllers/concerns/idv/step_indicator_concern_spec.rb @@ -87,7 +87,6 @@ def force_gpo context 'via current idv session' do before do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) create(:in_person_enrollment, :establishing, user: user) end diff --git a/spec/controllers/idv/enter_password_controller_spec.rb b/spec/controllers/idv/enter_password_controller_spec.rb index 7c8554f6f6d..5bbaab77b78 100644 --- a/spec/controllers/idv/enter_password_controller_spec.rb +++ b/spec/controllers/idv/enter_password_controller_spec.rb @@ -414,7 +414,6 @@ def show stub_request_enroll subject.idv_session.applicant = Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID_WITH_PHONE - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end diff --git a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb index 9ba5c554b0b..489df56f68c 100644 --- a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb +++ b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb @@ -374,11 +374,20 @@ end it 'updates proofing component vendor' do - expect(user.proofing_component&.document_check).to be_nil + proofing_components = Idv::ProofingComponents.new( + idv_session: controller.idv_session, + session: controller.session, + user_session: controller.user_session, + user:, + ) + + expect(proofing_components.document_check).to be_nil response - expect(user.proofing_component.document_check).to eq Idp::Constants::Vendors::USPS + user.reload + + expect(proofing_components.document_check).to eq Idp::Constants::Vendors::USPS end end @@ -404,11 +413,20 @@ end it 'updates proofing component vendor' do - expect(user.proofing_component&.document_check).to be_nil + proofing_components = Idv::ProofingComponents.new( + idv_session: controller.idv_session, + session: controller.session, + user_session: controller.user_session, + user:, + ) + + expect(proofing_components.document_check).to be_nil response - expect(user.proofing_component.document_check).to eq Idp::Constants::Vendors::USPS + user.reload + + expect(proofing_components.document_check).to eq Idp::Constants::Vendors::USPS end end diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index fea657a0e01..9a49ea0c1cd 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -168,9 +168,14 @@ expect(response).to redirect_to(idv_ssn_url) - pc = ProofingComponent.find_by(user_id: user.id) - expect(pc.document_check).to eq('mock') - expect(pc.document_type).to eq('state_id') + proofing_components = Idv::ProofingComponents.new( + idv_session: subject.idv_session, + session: subject.session, + user_session: subject.user_session, + user:, + ) + expect(proofing_components.document_check).to eq('mock') + expect(proofing_components.document_type).to eq('state_id') end context 'redo document capture' do diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index 47e85d5f938..8238bc1b0f6 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -502,7 +502,6 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs) end before do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index dc2c5123915..8c516ce67bd 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -33,7 +33,7 @@ validate_verify_info_page complete_verify_step - validate_verify_info_submit(user) + validate_verify_info_submit validate_phone_page try_to_skip_ahead_from_phone @@ -265,12 +265,8 @@ def validate_verify_info_page expect(page).to have_text(DocAuthHelper::GOOD_SSN) end - def validate_verify_info_submit(user) + def validate_verify_info_submit expect(page).to have_content(t('doc_auth.forms.doc_success')) - expect(user.proofing_component.resolution_check).to eq(Idp::Constants::Vendors::LEXIS_NEXIS) - expect(user.proofing_component.source_check).to satisfy do |v| - Idp::Constants::Vendors::SOURCE_CHECK.include?(v) - end end def validate_phone_page @@ -333,6 +329,15 @@ def validate_enter_password_submit(user) profile = user.profiles.first expect(profile.active?).to eq true + expect(profile.proofing_components).to eql( + 'source_check' => 'StateIdMock', + 'threatmetrix' => true, + 'address_check' => 'lexis_nexis_address', + 'document_type' => 'state_id', + 'document_check' => 'mock', + 'resolution_check' => 'lexis_nexis', + 'threatmetrix_review_status' => 'pass', + ) expect(GpoConfirmation.count).to eq(0) end diff --git a/spec/forms/event_disavowal/password_reset_from_disavowal_form_spec.rb b/spec/forms/event_disavowal/password_reset_from_disavowal_form_spec.rb index cab54a33c7c..7d442e98706 100644 --- a/spec/forms/event_disavowal/password_reset_from_disavowal_form_spec.rb +++ b/spec/forms/event_disavowal/password_reset_from_disavowal_form_spec.rb @@ -26,26 +26,4 @@ expect(user.reload.valid_password?(new_password)).to eq(false) end end - - context 'user has an active profile' do - let(:user) { create(:user, :proofed) } - - it 'destroys the proofing component' do - ProofingComponent.create(user_id: user.id, document_check: 'mock') - - subject.submit(password: new_password) - - expect(user.reload.proofing_component).to be_nil - end - end - - context 'user does not have an active profile' do - it 'does not destroy the proofing component' do - ProofingComponent.create(user_id: user.id, document_check: 'mock') - - subject.submit(password: new_password) - - expect(user.reload.proofing_component).to_not be_nil - end - end end diff --git a/spec/forms/gpo_verify_form_spec.rb b/spec/forms/gpo_verify_form_spec.rb index 4aad640877a..6c50506aaa2 100644 --- a/spec/forms/gpo_verify_form_spec.rb +++ b/spec/forms/gpo_verify_form_spec.rb @@ -156,10 +156,6 @@ ) end - let(:proofing_components) do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) - end - before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end @@ -194,10 +190,6 @@ ) end - let(:proofing_components) do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) - end - before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index 8b7261c0876..3b448919173 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -138,10 +138,6 @@ expect(result_context_stages_threatmetrix[:response_body]).to eq( JSON.parse(LexisNexisFixtures.ddp_success_redacted_response_json, symbolize_names: true), ) - - proofing_component = user.proofing_component - expect(proofing_component.threatmetrix).to equal(true) - expect(proofing_component.threatmetrix_review_status).to eq('pass') end end @@ -340,10 +336,6 @@ expect(result_context_stages_threatmetrix[:client]).to eq('tmx_disabled') expect(@threatmetrix_stub).to_not have_been_requested - - proofing_component = user.proofing_component - expect(proofing_component.threatmetrix).to equal(false) - expect(proofing_component.threatmetrix_review_status).to eq('pass') end end @@ -463,10 +455,6 @@ expect(result_context_stages_threatmetrix[:response_body]).to eq( JSON.parse(LexisNexisFixtures.ddp_success_redacted_response_json, symbolize_names: true), ) - - proofing_component = user.proofing_component - expect(proofing_component.threatmetrix).to equal(true) - expect(proofing_component.threatmetrix_review_status).to eq('pass') end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 94c77ed8f6c..288f591a368 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -10,7 +10,6 @@ it { is_expected.to have_one(:account_reset_request) } it { is_expected.to have_many(:phone_configurations) } it { is_expected.to have_many(:webauthn_configurations) } - it { is_expected.to have_one(:proofing_component) } it { is_expected.to have_many(:in_person_enrollments).dependent(:destroy) } it { is_expected.to have_one(:pending_in_person_enrollment). @@ -333,9 +332,6 @@ describe '#has_in_person_enrollment?' do it 'returns the establishing IPP enrollment that has an address' do - ProofingComponent.find_or_create_by(user: user). - update!(document_check: Idp::Constants::Vendors::USPS) - expect(user.has_in_person_enrollment?).to eq(true) end end diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb index df023971d8d..09dd6ec941a 100644 --- a/spec/services/idv/session_spec.rb +++ b/spec/services/idv/session_spec.rb @@ -193,7 +193,6 @@ let(:profile) { subject.profile } before do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) subject.user_phone_confirmation = true subject.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.with_indifferent_access @@ -272,7 +271,6 @@ before do allow(UspsInPersonProofing::EnrollmentHelper).to receive(:schedule_in_person_enrollment) - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) subject.user_phone_confirmation = true subject.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.with_indifferent_access From 2cf5663c22dc66681f293d79d62a4365756d4510 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 3 Dec 2024 07:50:21 -0600 Subject: [PATCH 15/17] LG-15119 query optimization (#11574) * changelog: Internal, Reporting, Optimize Query Adjust query and adjust for opted-in flows Co-authored-by: Mitchell Henke Co-authored-by: Zach Margolis --- app/models/profile.rb | 11 ++++++++ .../reporting/agency_and_sp_report.rb | 23 ++++++++-------- .../reporting/total_user_count_report.rb | 24 ++++++++--------- bin/rspec | 27 +++++++++++++++++++ spec/factories/profiles.rb | 2 +- 5 files changed, 62 insertions(+), 25 deletions(-) create mode 100755 bin/rspec diff --git a/app/models/profile.rb b/app/models/profile.rb index be986e03cf4..2052eb33eaf 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true class Profile < ApplicationRecord + # IDV levels equivalent to facial match FACIAL_MATCH_IDV_LEVELS = %w[unsupervised_with_selfie in_person].to_set.freeze + # Facial match through IAL2 opt-in flow + FACIAL_MATCH_OPT_IN = %w[unsupervised_with_selfie].to_set.freeze belongs_to :user # rubocop:disable Rails/InverseOf @@ -52,6 +55,14 @@ def self.verified where.not(verified_at: nil) end + def self.facial_match + where(idv_level: FACIAL_MATCH_IDV_LEVELS) + end + + def self.facial_match_opt_in + where(idv_level: FACIAL_MATCH_OPT_IN) + end + def self.fraud_rejection where.not(fraud_rejection_at: nil) end diff --git a/app/services/reporting/agency_and_sp_report.rb b/app/services/reporting/agency_and_sp_report.rb index 2ab63c8320d..d58744e3a31 100644 --- a/app/services/reporting/agency_and_sp_report.rb +++ b/app/services/reporting/agency_and_sp_report.rb @@ -48,12 +48,12 @@ def agency_and_sp_emailable_report end def active_agencies - @active_agencies ||= Agreements::PartnerAccountStatus.find_by(name: 'active'). - partner_accounts. - includes(:agency). - where('became_partner <= ?', report_date). - map(&:agency). - uniq + @active_agencies ||= Agency.joins(:partner_accounts). + where(partner_accounts: { + partner_account_status: Agreements::PartnerAccountStatus.find_by(name: 'active'), + became_partner: ..report_date, + }). + distinct end def service_providers @@ -67,11 +67,12 @@ def service_providers end def facial_match_issuers - @facial_match_issuers ||= Profile.where(active: true).where( - 'verified_at <= ?', - report_date.end_of_day, - ).where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS). - pluck(:initiating_service_provider_issuer).uniq + @facial_match_issuers ||= Reports::BaseReport.transaction_with_timeout do + Profile.active.facial_match_opt_in. + where('verified_at <= ?', report_date.end_of_day). + distinct. + pluck(:initiating_service_provider_issuer) + end end end end diff --git a/app/services/reporting/total_user_count_report.rb b/app/services/reporting/total_user_count_report.rb index 62ac028906d..b521f7a8f97 100644 --- a/app/services/reporting/total_user_count_report.rb +++ b/app/services/reporting/total_user_count_report.rb @@ -83,33 +83,30 @@ def total_user_count def verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where( - 'verified_at <= ?', - end_date, + Profile.active.where( + 'verified_at <= ?', end_date ).count - verified_facial_match_user_count end end def verified_facial_match_user_count @verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where( - 'verified_at <= ?', - end_date, - ).where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count + Profile.active.facial_match_opt_in.where( + 'verified_at <= ?', end_date + ).count end end def new_verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: current_month).count - + Profile.active.where(verified_at: current_month).count - new_verified_facial_match_user_count end end def new_verified_facial_match_user_count @new_verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: current_month). - where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count + Profile.active.facial_match_opt_in.where(verified_at: current_month).count end end @@ -121,15 +118,16 @@ def annual_total_user_count def annual_verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: annual_start_date..annual_end_date).count - + Profile.active.where(verified_at: annual_start_date..annual_end_date).count - annual_verified_facial_match_user_count end end def annual_verified_facial_match_user_count @annual_verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: annual_start_date..annual_end_date). - where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count + Profile.active.facial_match_opt_in.where( + verified_at: annual_start_date..annual_end_date, + ).count end end diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 00000000000..cb53ebe5f00 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/spec/factories/profiles.rb b/spec/factories/profiles.rb index 66eead40d4c..c1483a146d4 100644 --- a/spec/factories/profiles.rb +++ b/spec/factories/profiles.rb @@ -80,7 +80,7 @@ end trait :facial_match_proof do - idv_level { :in_person } + idv_level { :unsupervised_with_selfie } initiating_service_provider_issuer { 'urn:gov:gsa:openidconnect:inactive:sp:test' } end From f392784c01c997578f1708e3dc9a0818e323870e Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Tue, 3 Dec 2024 09:56:41 -0500 Subject: [PATCH 16/17] LG-14988 Send additional attributes to AAMVA (#11565) 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 +- spec/jobs/resolution_proofing_job_spec.rb | 27 +++- .../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 ++ 13 files changed, 373 insertions(+), 5 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 81b1cc3ebcc..ea3d95f4d9e 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 @@ -504,6 +505,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 d85fbef073d..2b721e2c7a7 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/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index 3b448919173..1af200134b2 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 ], ) @@ -209,9 +216,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 @@ -438,9 +452,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 ], ) 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 0a429414bc873b327d2072fd8e4df6598c9622c6 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Tue, 3 Dec 2024 10:36:24 -0500 Subject: [PATCH 17/17] LG-14277 verification data analytics event allow empty customer profile (#11579) * do not require customer profile for idv_socure_verification_data_requested analytics event * [skip changelog] --- app/jobs/socure_docv_results_job.rb | 2 +- app/services/analytics_events.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb index a2d78f7ca50..25729078575 100644 --- a/app/jobs/socure_docv_results_job.rb +++ b/app/jobs/socure_docv_results_job.rb @@ -44,7 +44,7 @@ def log_verification_request(docv_result_response:, vendor_request_time_in_ms:) vendor_request_time_in_ms:, async:, ).except(:attention_with_barcode, :selfie_live, :selfie_quality_good, - :selfie_status).compact, + :selfie_status), ) end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 257cb23c201..980d83a391f 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -4742,7 +4742,6 @@ def idv_socure_verification_data_requested( success:, errors:, async:, - customer_profile:, reference_id:, reason_codes:, document_type:, @@ -4762,6 +4761,7 @@ def idv_socure_verification_data_requested( birth_year:, liveness_enabled:, biometric_comparison_required:, + customer_profile: nil, docv_transaction_token: nil, user_id: nil, exception: nil,