diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index f5e0afbdc04..bb716947409 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -79,6 +79,23 @@ def ssn_rate_limiter ) end + def address_exception?(result) + result.extra.dig( + :proofing_results, + :context, + :stages, + :resolution, + :exception, + ).present? && + result.extra.dig( + :proofing_results, + :context, + :stages, + :resolution, + :attributes_requiring_additional_verification, + ) == ['address'] + end + def idv_failure(result) proofing_results_exception = result.extra.dig(:proofing_results, :exception) has_exception = proofing_results_exception.present? @@ -89,6 +106,7 @@ def idv_failure(result) :state_id, :mva_exception, ).present? + is_address_exception = address_exception?(result) is_threatmetrix_exception = result.extra.dig( :proofing_results, :context, @@ -113,6 +131,9 @@ def idv_failure(result) elsif has_exception && is_mva_exception idv_failure_log_warning redirect_to state_id_warning_url + elsif has_exception && is_address_exception + idv_failure_log_address_warning + redirect_to address_warning_url elsif (has_exception && is_threatmetrix_exception) || (!has_exception && resolution_failed) idv_failure_log_warning @@ -147,6 +168,13 @@ def idv_failure_log_error ) end + def idv_failure_log_address_warning + analytics.idv_doc_auth_address_warning_visited( + step_name: STEP_NAME, + remaining_submit_attempts: resolution_rate_limiter.remaining_count, + ) + end + def idv_failure_log_warning analytics.idv_doc_auth_warning_visited( step_name: STEP_NAME, @@ -166,6 +194,10 @@ def state_id_warning_url idv_session_errors_state_id_warning_url(flow: flow_param) end + def address_warning_url + idv_session_errors_address_warning_url(flow: flow_param) + end + def warning_url idv_session_errors_warning_url(flow: flow_param) end diff --git a/app/controllers/idv/session_errors_controller.rb b/app/controllers/idv/session_errors_controller.rb index 129ab660980..a080321464d 100644 --- a/app/controllers/idv/session_errors_controller.rb +++ b/app/controllers/idv/session_errors_controller.rb @@ -16,28 +16,26 @@ def exception end def warning - rate_limiter = RateLimiter.new( - user: idv_session_user, - rate_limit_type: :idv_resolution, - ) - @step_indicator_steps = step_indicator_steps - @remaining_submit_attempts = rate_limiter.remaining_count - log_event(based_on_limiter: rate_limiter) + @remaining_submit_attempts = resolution_rate_limiter.remaining_count + log_event(based_on_limiter: resolution_rate_limiter) end def state_id_warning log_event end + def address_warning + @step_indicator_steps = step_indicator_steps + @address_path = idv_address_url + @remaining_submit_attempts = resolution_rate_limiter.remaining_count + log_event(based_on_limiter: resolution_rate_limiter) + end + def failure - rate_limiter = RateLimiter.new( - user: idv_session_user, - rate_limit_type: :idv_resolution, - ) - @expires_at = rate_limiter.expires_at + @expires_at = resolution_rate_limiter.expires_at @sp_name = decorated_sp_session.sp_name - log_event(based_on_limiter: rate_limiter) + log_event(based_on_limiter: resolution_rate_limiter) end def ssn_failure @@ -63,6 +61,13 @@ def rate_limited private + def resolution_rate_limiter + @resolution_rate_limiter ||= RateLimiter.new( + user: idv_session_user, + rate_limit_type: :idv_resolution, + ) + end + def confirm_two_factor_authenticated_or_user_id_in_session return if session[:doc_capture_user_id].present? diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 0a513410e6e..96cf10d4c4c 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1226,6 +1226,18 @@ def idv_consent_checkbox_toggled(checked:, **extra) ) end + # @param [String] step_name + # @param [Integer] remaining_submit_attempts (previously called "remaining_attempts") + # The user was sent to a warning page during the IDV flow + def idv_doc_auth_address_warning_visited(step_name:, remaining_submit_attempts:, **extra) + track_event( + :idv_doc_auth_address_warning_visited, + step_name: step_name, + remaining_submit_attempts: remaining_submit_attempts, + **extra, + ) + end + # User has consented to share information with document upload and may # view the "hybrid handoff" step next unless "skip_hybrid_handoff" param is true # @param [Boolean] success Whether form validation was successful diff --git a/app/views/idv/session_errors/address_warning.html.erb b/app/views/idv/session_errors/address_warning.html.erb new file mode 100644 index 00000000000..0578ccc848c --- /dev/null +++ b/app/views/idv/session_errors/address_warning.html.erb @@ -0,0 +1,26 @@ +<% self.title = t('titles.failure.information_not_verified') %> + +<% content_for(:pre_flash_content) do %> + <%= render StepIndicatorComponent.new( + steps: @step_indicator_steps, + current_step: :verify_info, + locale_scope: 'idv', + class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', + ) %> +<% end %> + +<%= render StatusPageComponent.new(status: :warning) do |c| %> + <% c.with_header { t('idv.warning.address.heading') } %> + +

<%= t('idv.failure.address.warning') %>

+

<%= t('idv.failure.attempts_html', count: @remaining_submit_attempts) %>

+ + <% c.with_action_button( + url: @address_path, + class: 'margin-bottom-2', + ) { t('idv.forgot_password.try_again') } %> +<% end %> + +<%= render PageFooterComponent.new do %> + <%= link_to t('links.cancel'), idv_cancel_path(step: :invalid_session) %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 2d28ebe903c..5a64a5470e3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1062,6 +1062,7 @@ idv.errors.pattern_mismatch.zipcode: Enter a 5 or 9 digit ZIP Code idv.errors.pattern_mismatch.zipcode_five: Enter a 5 digit ZIP Code idv.errors.technical_difficulties: We are having technical difficulties idv.errors.try_again_later: Try again later. +idv.failure.address.warning: Please check the information you entered and try again. If your mailing address is different than your residential address, try entering the address where you currently live. idv.failure.attempts_html.one: You can try 1 more time. Then you must wait 6 hours before trying again. idv.failure.attempts_html.other: You can try %{count} more times. Then you must wait 6 hours before trying again. idv.failure.button.try_online: Try again online @@ -1206,6 +1207,7 @@ idv.unavailable.idv_explanation.without_sp: The agency that you are trying to ac idv.unavailable.next_steps_html: '%{status_page_link_html} or exit %{app_name} and try again later.' idv.unavailable.status_page_link: Get updates on our status page idv.unavailable.technical_difficulties: Unfortunately, we are having technical difficulties and cannot verify your identity at this time. +idv.warning.address.heading: We couldn’t find records matching your address idv.warning.sessions.heading: We couldn’t find records matching your personal information idv.warning.state_id.cancel_button: Exit %{app_name} idv.warning.state_id.explanation: Unfortunately, we’re experiencing technical difficulties with IDs from your state and are currently unable to verify your information. diff --git a/config/locales/es.yml b/config/locales/es.yml index 69a76bea1ed..baa8d4f1fff 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1073,6 +1073,7 @@ idv.errors.pattern_mismatch.zipcode: Ingrese un código postal de 5 o 9 dígitos idv.errors.pattern_mismatch.zipcode_five: Ingrese un código postal de 5 dígitos. idv.errors.technical_difficulties: Estamos teniendo problemas técnicos idv.errors.try_again_later: Vuelva a intentarlo más tarde. +idv.failure.address.warning: Revise la información que ingresó y vuelva a intentarlo. Si su dirección postal no es igual que su domicilio, intente ingresar la dirección donde vive actualmente. idv.failure.attempts_html.one: Puede intentarlo una vez más. Luego debe esperar 6 horas antes de volver a intentarlo. idv.failure.attempts_html.other: Puede intentarlo %{count} veces más. Luego debe esperar 6 horas antes de volver a intentarlo. idv.failure.button.try_online: Vuelva a intentarlo en línea @@ -1217,6 +1218,7 @@ idv.unavailable.idv_explanation.without_sp: La agencia a la que está intentando idv.unavailable.next_steps_html: '%{status_page_link_html} o salga de %{app_name} y vuelva a intentarlo más tarde.' idv.unavailable.status_page_link: Obtenga las actualizaciones en nuestra página de estado idv.unavailable.technical_difficulties: Lamentablemente, tenemos problemas técnicos y no podemos verificar su identidad en este momento. +idv.warning.address.heading: No encontramos registros que coincidan con su dirección idv.warning.sessions.heading: No encontramos registros que coincidan con sus datos personales idv.warning.state_id.cancel_button: Salir de %{app_name} idv.warning.state_id.explanation: Lamentablemente, tenemos problemas técnicos con las identificaciones de su estado y no podemos verificar su información en este momento. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 62083c54e82..ccc51df6818 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1062,6 +1062,7 @@ idv.errors.pattern_mismatch.zipcode: Saisissez un code postal à 5 ou 9 chiffres idv.errors.pattern_mismatch.zipcode_five: Saisissez un code postal à 5 chiffres idv.errors.technical_difficulties: Nous rencontrons des difficultés techniques idv.errors.try_again_later: Veuillez réessayer ultérieurement. +idv.failure.address.warning: Veuillez vérifier les informations que vous avez saisies et réessayer. Si votre adresse postale est différente de votre adresse personnelle, veuillez saisir l’adresse où vous résidez actuellement. idv.failure.attempts_html.one: Vous avez encore un essai. Vous devrez ensuite attendre 6 heures avant de réessayer. idv.failure.attempts_html.other: Vous pouvez encore essayer %{count} fois de plus. Ensuite vous devrez ensuite attendre 6 heures avant de réessayer. idv.failure.button.try_online: Réessayer en ligne @@ -1206,6 +1207,7 @@ idv.unavailable.idv_explanation.without_sp: L’organisme auquel vous essayez d idv.unavailable.next_steps_html: '%{status_page_link_html} ou quittez le site %{app_name} et réessayez plus tard.' idv.unavailable.status_page_link: Obtenir les dernières informations sur notre page d’état des systèmes. idv.unavailable.technical_difficulties: Malheureusement, nous rencontrons des difficultés techniques et ne pouvons pas vérifier votre identité pour le moment. +idv.warning.address.heading: Nous n’avons pas trouvé d’informations correspondant à votre adresse idv.warning.sessions.heading: Nous n’avons pas trouvé de dossiers correspondant à vos informations personnelles idv.warning.state_id.cancel_button: Quitter %{app_name} idv.warning.state_id.explanation: Malheureusement, nous rencontrons des difficultés techniques avec les pièces d’identité de votre État et ne sommes pas en mesure de vérifier vos informations pour le moment. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 87ac370a2d7..7a77ea6ed2f 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1075,6 +1075,7 @@ idv.errors.pattern_mismatch.zipcode: 输入 5 或 9 位的邮编 idv.errors.pattern_mismatch.zipcode_five: 输入 5 位的邮编 idv.errors.technical_difficulties: 我们目前遇到技术困难 idv.errors.try_again_later: 请稍后再试。 +idv.failure.address.warning: 请检查一下你输入的信息,然后再试一下。如果你的邮寄地址与居住地址不同,请尝试输入你目前居住的地址。 idv.failure.attempts_html.one: 您可以再试1次。 然后您必须等6个小时才能再试。 idv.failure.attempts_html.other: 您可以再试%{count}次。 然后您必须等6个小时才能再试。 idv.failure.button.try_online: 在网上再试一下 @@ -1219,6 +1220,7 @@ idv.unavailable.idv_explanation.without_sp: 你试图访问的机构需要确保 idv.unavailable.next_steps_html: '%{status_page_link_html} 或者退出 %{app_name},稍后再试。' idv.unavailable.status_page_link: 在我们的状态页面获得最新信息。 idv.unavailable.technical_difficulties: 很遗憾,我们这边现在遇到技术困难,目前无法验证你的身份。 +idv.warning.address.heading: 我们找不到与你地址匹配的记录 idv.warning.sessions.heading: 我们找不到与你个人信息匹配的记录 idv.warning.state_id.cancel_button: 退出 %{app_name} idv.warning.state_id.explanation: 遗憾的是,处理来自你所在州的身份证件时我们遇到技术困难,目前无法验证你的信息。 diff --git a/config/routes.rb b/config/routes.rb index c4362f8cb7b..1ca8eff16ad 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -425,6 +425,7 @@ put '/enter_password' => 'enter_password#create' get '/session/errors/warning' => 'session_errors#warning' get '/session/errors/state_id_warning' => 'session_errors#state_id_warning' + get '/session/errors/address_warning' => 'session_errors#address_warning' get '/session/errors/failure' => 'session_errors#failure' get '/session/errors/ssn_failure' => 'session_errors#ssn_failure' get '/session/errors/exception' => 'session_errors#exception' diff --git a/spec/controllers/idv/session_errors_controller_spec.rb b/spec/controllers/idv/session_errors_controller_spec.rb index 3bf0665bb44..bdda32818c1 100644 --- a/spec/controllers/idv/session_errors_controller_spec.rb +++ b/spec/controllers/idv/session_errors_controller_spec.rb @@ -234,6 +234,68 @@ end end + describe '#address_warning' do + let(:action) { :address_warning } + let(:template) { 'idv/session_errors/address_warning' } + + subject(:response) { get action } + it_behaves_like 'an idv session errors controller action' + + context 'with rate limit attempts' do + let(:user) { create(:user) } + + before do + RateLimiter.new(rate_limit_type: :idv_resolution, user: user).increment! + end + + it 'assigns remaining count' do + response + + expect(assigns(:remaining_submit_attempts)).to be_kind_of(Numeric) + end + + it 'assigns URL to try again' do + response + + expect(assigns(:address_path)).to eq(idv_address_url) + end + + it 'logs an event with attempts remaining' do + response + + expect(@analytics).to have_logged_event( + 'IdV: session error visited', + type: action.to_s, + remaining_submit_attempts: IdentityConfig.store.idv_max_attempts - 1, + ) + end + end + + context 'while rate limited' do + let(:user) { create(:user) } + + before do + RateLimiter.new(rate_limit_type: :idv_resolution, user: user).increment_to_limited! + end + + it 'assigns expiration time' do + get action + + expect(assigns(:expires_at)).not_to eq(Time.zone.now) + end + + it 'logs an event with attempts remaining' do + get action + + expect(@analytics).to have_logged_event( + 'IdV: session error visited', + type: 'address_warning', + remaining_submit_attempts: 0, + ) + end + end + end + describe '#failure' do let(:action) { :failure } let(:template) { 'idv/session_errors/failure' } diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 62050d44865..a8433782519 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -543,6 +543,79 @@ end end + context 'when instant verify address proofing results in an exception' do + let(:document_capture_session) do + DocumentCaptureSession.create(user:) + end + let(:success) { false } + let(:errors) { {} } + let(:exception) { nil } + let(:error_attributes) { nil } + let(:vendor_name) { 'instantverify_placeholder' } + let(:async_state) do + # Here we're trying to match the store to redis -> read from redis flow this data travels + adjudicated_result = Proofing::Resolution::ResultAdjudicator.new( + state_id_result: Proofing::StateIdResult.new(success: true), + phone_finder_result: Proofing::AddressResult.new( + success: success, + errors: {}, + exception: exception, + vendor_name: 'instant_verify_test', + ), + device_profiling_result: Proofing::DdpResult.new(success: true), + ipp_enrollment_in_progress: false, + residential_resolution_result: Proofing::Resolution::Result.new(success: true), + resolution_result: Proofing::Resolution::Result.new( + success: success, + errors: {}, + exception: 'fake exception', + vendor_name: vendor_name, + attributes_requiring_additional_verification: error_attributes, + ), + same_address_as_id: nil, + should_proof_state_id: true, + applicant_pii: Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN, + ).adjudicated_result.to_h + + document_capture_session.create_proofing_session + + document_capture_session.store_proofing_result(adjudicated_result) + + document_capture_session.load_proofing_result + end + before do + allow(controller).to receive(:load_async_state).and_return(async_state) + end + + context 'address is the only exception' do + let(:error_attributes) { ['address'] } + + it 'redirects user to address warning' do + put :show + expect(response).to redirect_to idv_session_errors_address_warning_url + end + + it 'logs an event' do + get :show + + expect(@analytics).to have_logged_event( + :idv_doc_auth_address_warning_visited, + step_name: 'verify_info', + remaining_submit_attempts: kind_of(Numeric), + ) + end + end + + context 'there are more instant verify exceptions' do + let(:error_attributes) { ['address', 'dob', 'ssn'] } + + it 'redirects user to address warning' do + put :show + expect(response).to redirect_to idv_session_errors_exception_url + end + end + end + context 'when the resolution proofing job fails and there is no exception' do before do allow(controller).to receive(:load_async_state).and_return(async_state) diff --git a/spec/views/idv/session_errors/address_warning.html.erb_spec.rb b/spec/views/idv/session_errors/address_warning.html.erb_spec.rb new file mode 100644 index 00000000000..1db17993a6a --- /dev/null +++ b/spec/views/idv/session_errors/address_warning.html.erb_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe 'idv/session_errors/address_warning.html.erb' do + let(:sp_name) { nil } + let(:address_path) { '/example/path' } + let(:remaining_submit_attempts) { 5 } + let(:user_session) { {} } + + before do + decorated_sp_session = instance_double(ServiceProviderSession, sp_name: sp_name) + allow(view).to receive(:decorated_sp_session).and_return(decorated_sp_session) + allow(view).to receive(:user_session).and_return(user_session) + + assign(:remaining_submit_attempts, remaining_submit_attempts) + assign(:address_path, address_path) + + @step_indicator_steps = Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS + + render + end + + it 'shows a primary action' do + expect(rendered).to have_link(t('idv.failure.button.warning'), href: address_path) + end + + it 'shows remaining attempts' do + expect(rendered).to have_text( + strip_tags( + t('idv.failure.attempts_html', count: remaining_submit_attempts), + ), + ) + end + + it 'shows a cancel link' do + expect(rendered).to have_link( + t('links.cancel'), + href: idv_cancel_path(step: :invalid_session), + ) + end + + context 'with a nil user_session' do + let(:user_session) { nil } + + it 'does not render troubleshooting option to retake photos' do + expect(rendered).to have_link(t('idv.failure.button.warning'), href: address_path) + expect(rendered).to have_text( + strip_tags( + t('idv.failure.attempts_html', count: remaining_submit_attempts), + ), + ) + expect(rendered).to have_link( + t('links.cancel'), + href: idv_cancel_path(step: :invalid_session), + ) + end + end +end