diff --git a/app/controllers/idv/capture_doc_status_controller.rb b/app/controllers/idv/capture_doc_status_controller.rb index 9e294d79439..670721751a6 100644 --- a/app/controllers/idv/capture_doc_status_controller.rb +++ b/app/controllers/idv/capture_doc_status_controller.rb @@ -20,7 +20,8 @@ def status :too_many_requests elsif confirmed_barcode_attention_result? || user_has_establishing_in_person_enrollment? :ok - elsif session_result.blank? || pending_barcode_attention_confirmation? + elsif session_result.blank? || pending_barcode_attention_confirmation? || + redo_document_capture_pending? :accepted elsif !session_result.success? :unauthorized @@ -68,11 +69,13 @@ def user_has_establishing_in_person_enrollment? end def confirmed_barcode_attention_result? - had_barcode_attention_result? && !document_capture_session.ocr_confirmation_pending? + !redo_document_capture_pending? && had_barcode_attention_result? && + !document_capture_session.ocr_confirmation_pending? end def pending_barcode_attention_confirmation? - had_barcode_attention_result? && document_capture_session.ocr_confirmation_pending? + !redo_document_capture_pending? && had_barcode_attention_result? && + document_capture_session.ocr_confirmation_pending? end def had_barcode_attention_result? @@ -90,5 +93,12 @@ def idv_session service_provider: current_sp, ) end + + def redo_document_capture_pending? + return unless session_result&.dig(:captured_at) + return unless document_capture_session.requested_at + + document_capture_session.requested_at > session_result.captured_at + end end end diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 2e4eb7c7879..6f24377a3e6 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -20,6 +20,9 @@ def show def update idv_session.redo_document_capture = nil # done with this redo + # Not used in standard flow, here for data consistency with hybrid flow. + document_capture_session.confirm_ocr + result = handle_stored_result analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index 2241569439d..b6f9e2678d2 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -6,13 +6,9 @@ class DocumentCaptureController < ApplicationController before_action :check_valid_document_capture_session before_action :override_csp_to_allow_acuant + before_action :confirm_document_capture_needed, only: :show def show - if document_capture_session&.load_result&.success? - redirect_to idv_hybrid_mobile_capture_complete_url - return - end - analytics.idv_doc_auth_document_capture_visited(**analytics_arguments) Funnel::DocAuth::RegisterStep.new(document_capture_user.id, sp_session[:issuer]). @@ -22,6 +18,7 @@ def show end def update + document_capture_session.confirm_ocr result = handle_stored_result analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) @@ -69,6 +66,20 @@ def handle_stored_result failure(I18n.t('doc_auth.errors.general.network_error'), extra) end end + + def confirm_document_capture_needed + return unless stored_result&.success? + return if redo_document_capture_pending? + + redirect_to idv_hybrid_mobile_capture_complete_url + end + + def redo_document_capture_pending? + return unless stored_result&.dig(:captured_at) + return unless document_capture_session.requested_at + + document_capture_session.requested_at > stored_result.captured_at + end end end end diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index d0c239dea7e..622aec47f95 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -25,6 +25,7 @@ def update # The doc capture flow will have fetched the results already. We need # to fetch them again here to add the PII to this session handle_document_verification_success(document_capture_session_result) + idv_session.redo_document_capture = nil redirect_to idv_ssn_url end diff --git a/app/models/document_capture_session.rb b/app/models/document_capture_session.rb index 95e0b2bf218..ac9bd7a8215 100644 --- a/app/models/document_capture_session.rb +++ b/app/models/document_capture_session.rb @@ -14,6 +14,7 @@ def store_result_from_response(doc_auth_response) ) session_result.success = doc_auth_response.success? session_result.pii = doc_auth_response.pii_from_doc + session_result.captured_at = Time.zone.now session_result.attention_with_barcode = doc_auth_response.attention_with_barcode? EncryptedRedisStructStorage.store( session_result, @@ -28,6 +29,7 @@ def store_failed_auth_image_fingerprint(front_image_fingerprint, back_image_fing id: generate_result_id, ) session_result.success = false + session_result.captured_at = Time.zone.now session_result.add_failed_front_image!(front_image_fingerprint) if front_image_fingerprint session_result.add_failed_back_image!(back_image_fingerprint) if back_image_fingerprint EncryptedRedisStructStorage.store( @@ -52,20 +54,6 @@ def create_doc_auth_session save! end - def store_doc_auth_result(result:, pii:) - EncryptedRedisStructStorage.store( - DocumentCaptureSessionAsyncResult.new( - id: result_id, - pii: pii, - result: result, - status: DocumentCaptureSessionAsyncResult::DONE, - ), - expires_in: IdentityConfig.store.async_wait_timeout_seconds, - ) - self.ocr_confirmation_pending = result[:attention_with_barcode] - save! - end - def load_proofing_result EncryptedRedisStructStorage.load(result_id, type: ProofingSessionAsyncResult) end @@ -99,6 +87,12 @@ def expired? Time.zone.now end + def confirm_ocr + return unless self.ocr_confirmation_pending + + update!(ocr_confirmation_pending: false) + end + private def generate_result_id diff --git a/app/services/document_capture_session_result.rb b/app/services/document_capture_session_result.rb index 6400dbcf592..1a5bdb794bc 100644 --- a/app/services/document_capture_session_result.rb +++ b/app/services/document_capture_session_result.rb @@ -8,9 +8,10 @@ :attention_with_barcode, :failed_front_image_fingerprints, :failed_back_image_fingerprints, + :captured_at, keyword_init: true, allowed_members: [:id, :success, :attention_with_barcode, :failed_front_image_fingerprints, - :failed_back_image_fingerprints], + :failed_back_image_fingerprints, :captured_at], ) do def self.redis_key_prefix 'dcs:result' diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 8de517414af..6792a7a059b 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -3,7 +3,16 @@ RSpec.describe Idv::DocumentCaptureController do include IdvHelper - let(:document_capture_session_uuid) { 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e' } + let(:document_capture_session_requested_at) { Time.zone.now } + + let!(:document_capture_session) do + DocumentCaptureSession.create!( + user: user, + requested_at: document_capture_session_requested_at, + ) + end + + let(:document_capture_session_uuid) { document_capture_session&.uuid } let(:user) { create(:user) } @@ -184,5 +193,16 @@ expect(user.reload.establishing_in_person_enrollment).to be_nil end end + + context 'ocr confirmation pending' do + before do + subject.document_capture_session.ocr_confirmation_pending = true + end + + it 'confirms ocr' do + put :update + expect(subject.document_capture_session.ocr_confirmation_pending).to be_falsey + end + end end end diff --git a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb index fcacb00a32a..5f1ebbfbe4d 100644 --- a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb @@ -15,6 +15,8 @@ let(:document_capture_session_uuid) { document_capture_session&.uuid } let(:document_capture_session_requested_at) { Time.zone.now } + let(:document_capture_session_result_captured_at) { Time.zone.now + 1.second } + let(:document_capture_session_result_success) { true } let(:ab_test_args) do { sample_bucket1: :sample_value1, sample_bucket2: :sample_value2 } @@ -102,20 +104,42 @@ end end end + + context 'stored_result already exists' do + before do + stub_document_capture_session_result + end + + it 'redirects to document capture complete' do + get :show + expect(response).to redirect_to idv_hybrid_mobile_capture_complete_url + end + + context 'document capture re-requested' do + let(:document_capture_session_result_captured_at) do + document_capture_session_requested_at - 5.minutes + end + context 'with successful stored_result' do + it 'renders the show template' do + get :show + expect(response).to render_template :show + end + end + + context 'with failed stored_result' do + let(:document_capture_session_result_success) { false } + it 'renders the show template' do + get :show + expect(response).to render_template :show + end + end + end + end end describe '#update' do before do - allow_any_instance_of(DocumentCaptureSession).to receive(:load_result).and_return( - DocumentCaptureSessionResult.new( - id: 1234, - success: true, - pii: { - state: 'WA', - }, - attention_with_barcode: true, - ), - ) + stub_document_capture_session_result end context 'with no user id in session' do @@ -161,6 +185,17 @@ put :update expect(response).to redirect_to idv_hybrid_mobile_capture_complete_url end + + context 'ocr confirmation pending' do + before do + subject.document_capture_session.ocr_confirmation_pending = true + end + + it 'confirms ocr' do + put :update + expect(subject.document_capture_session.ocr_confirmation_pending).to be_falsey + end + end end end @@ -172,4 +207,18 @@ controller.extra_view_variables end end + + def stub_document_capture_session_result + allow_any_instance_of(DocumentCaptureSession).to receive(:load_result).and_return( + DocumentCaptureSessionResult.new( + id: 1234, + success: document_capture_session_result_success, + pii: { + state: 'WA', + }, + attention_with_barcode: true, + captured_at: document_capture_session_result_captured_at, + ), + ) + end end diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index 4745b5de372..5b1bcdceedc 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -140,14 +140,26 @@ allow(subject).to receive(:document_capture_session).and_return(document_capture_session) end - it 'redirects to ssn page when successful' do - put :update + context 'document capture session successful' do + it 'redirects to ssn page' do + put :update - expect(response).to redirect_to(idv_ssn_url) + 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') + pc = ProofingComponent.find_by(user_id: user.id) + expect(pc.document_check).to eq('mock') + expect(pc.document_type).to eq('state_id') + end + + context 'redo document capture' do + before do + subject.idv_session.redo_document_capture = true + end + it 'resets redo_document capture to nil in idv_session' do + put :update + expect(subject.idv_session.redo_document_capture).to be_nil + end + end end context 'document capture session canceled' do diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb index e48caf2b801..c336ef8f9ef 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb @@ -139,7 +139,7 @@ ) end - it 'it shows capture complete on mobile and error page on desktop', js: true do + it 'shows capture complete on mobile and error page on desktop', js: true do user = nil perform_in_browser(:desktop) do @@ -174,4 +174,134 @@ end end end + + context 'barcode read error on mobile (redo document capture)' do + it 'continues to ssn on desktop when user selects Continue', js: true do + user = nil + + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + mock_doc_auth_attention_with_barcode + attach_and_submit_images + click_idv_continue + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_content(t('doc_auth.headings.capture_complete').tr(' ', ' ')) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_current_path(idv_verify_info_path, wait: 10) + + # verify pii is displayed + expect(page).to have_text('DAVID') + expect(page).to have_text('SAMPLE') + expect(page).to have_text('123 ABC AVE') + + warning_link_text = t('doc_auth.headings.capture_scan_warning_link') + click_link warning_link_text + + expect(current_path).to eq(idv_hybrid_handoff_path) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + perform_in_browser(:mobile) do + visit @sms_link + + DocAuth::Mock::DocAuthMockClient.reset! + attach_and_submit_images + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_verify_info_path, wait: 10) + + # verify orig pii no longer displayed + expect(page).not_to have_text('DAVID') + expect(page).not_to have_text('SAMPLE') + expect(page).not_to have_text('123 ABC AVE') + # verify new pii from redo is displayed + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + + click_idv_continue + end + end + end + + context 'barcode read error on desktop, redo document capture on mobile' do + it 'continues to ssn on desktop when user selects Continue', js: true do + user = nil + + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_attention_with_barcode + attach_and_submit_images + click_idv_continue + expect(page).to have_current_path(idv_ssn_path, wait: 10) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_current_path(idv_verify_info_path, wait: 10) + + # verify pii is displayed + expect(page).to have_text('DAVID') + expect(page).to have_text('SAMPLE') + expect(page).to have_text('123 ABC AVE') + + warning_link_text = t('doc_auth.headings.capture_scan_warning_link') + click_link warning_link_text + + expect(current_path).to eq(idv_hybrid_handoff_path) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + perform_in_browser(:mobile) do + visit @sms_link + + DocAuth::Mock::DocAuthMockClient.reset! + attach_and_submit_images + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_verify_info_path, wait: 10) + + # verify orig pii no longer displayed + expect(page).not_to have_text('DAVID') + expect(page).not_to have_text('SAMPLE') + expect(page).not_to have_text('123 ABC AVE') + # verify new pii from redo is displayed + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + + click_idv_continue + end + end + end end diff --git a/spec/models/document_capture_session_spec.rb b/spec/models/document_capture_session_spec.rb index 6d61bd49392..f140962c30b 100644 --- a/spec/models/document_capture_session_spec.rb +++ b/spec/models/document_capture_session_spec.rb @@ -61,38 +61,6 @@ end end - describe '#store_doc_auth_result' do - it 'generates a result ID stores the result encrypted in redis' do - record = DocumentCaptureSession.new(result_id: SecureRandom.uuid) - - record.store_doc_auth_result( - result: doc_auth_response.to_h, - pii: doc_auth_response.pii_from_doc, - ) - - result_id = record.result_id - key = EncryptedRedisStructStorage.key(result_id, type: DocumentCaptureSessionAsyncResult) - data = REDIS_POOL.with { |client| client.get(key) } - expect(data).to be_a(String) - expect(data).to_not include('Testy') - expect(data).to_not include('Testerson') - expect(record.ocr_confirmation_pending).to eq(false) - end - - context 'with attention with barcode response' do - before { allow(doc_auth_response).to receive(:attention_with_barcode?).and_return(true) } - - it 'sets record as pending ocr confirmation' do - record = DocumentCaptureSession.new(result_id: SecureRandom.uuid) - record.store_doc_auth_result( - result: doc_auth_response.to_h, - pii: doc_auth_response.pii_from_doc, - ) - expect(record.ocr_confirmation_pending).to eq(true) - end - end - end - describe '#expired?' do before do allow(IdentityConfig.store).to receive(:doc_capture_request_valid_for_minutes).and_return(15) @@ -138,5 +106,27 @@ expect(result.failed_front_image?(nil)).to eq(false) expect(result.failed_back_image?(nil)).to eq(false) end + + it 'saves failed image finterprints' do + record = DocumentCaptureSession.new(result_id: SecureRandom.uuid) + + record.store_failed_auth_image_fingerprint( + 'fingerprint1', nil + ) + old_result = record.load_result + + record.store_failed_auth_image_fingerprint( + 'fingerprint2', 'fingerprint3' + ) + new_result = record.load_result + + expect(old_result.failed_front_image?('fingerprint1')).to eq(true) + expect(old_result.failed_front_image?('fingerprint2')).to eq(false) + expect(old_result.failed_back_image?('fingerprint3')).to eq(false) + + expect(new_result.failed_front_image?('fingerprint1')).to eq(true) + expect(new_result.failed_front_image?('fingerprint2')).to eq(true) + expect(new_result.failed_back_image?('fingerprint3')).to eq(true) + end end end diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index b89572cf6c5..c331efa85b7 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -273,17 +273,6 @@ def mock_doc_auth_acuant_error_unknown ) end - def set_up_document_capture_result(uuid:, idv_result:) - dcs = DocumentCaptureSession.where(uuid: uuid).first_or_create - dcs.create_doc_auth_session - if idv_result - dcs.store_doc_auth_result( - result: idv_result.except(:pii_from_doc), - pii: idv_result[:pii_from_doc], - ) - end - end - def verify_phone_otp choose_idv_otp_delivery_method_sms fill_in_code_with_last_phone_otp