diff --git a/app/forms/idv/api_document_verification_status_form.rb b/app/forms/idv/api_document_verification_status_form.rb index 08f675ccce7..414fa7e673b 100644 --- a/app/forms/idv/api_document_verification_status_form.rb +++ b/app/forms/idv/api_document_verification_status_form.rb @@ -18,6 +18,7 @@ def submit errors: errors, extra: { remaining_attempts: remaining_attempts, + doc_auth_result: @async_state&.result&.[](:doc_auth_result), }, ) end diff --git a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx index 3c3d7d6198b..885321ae284 100644 --- a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx @@ -22,6 +22,11 @@ interface DocumentCaptureTroubleshootingOptionsProps { */ showDocumentTips?: boolean; + /** + * Whether to include option to verify in person. + */ + showInPersonOption?: boolean; + /** * If there are any errors (toggles whether or not to show in person proofing option) */ @@ -32,6 +37,7 @@ function DocumentCaptureTroubleshootingOptions({ heading, location = 'document_capture_troubleshooting_options', showDocumentTips = true, + showInPersonOption = true, hasErrors, }: DocumentCaptureTroubleshootingOptionsProps) { const { t } = useI18n(); @@ -71,7 +77,7 @@ function DocumentCaptureTroubleshootingOptions({ ].filter(Boolean) as TroubleshootingOption[] } /> - {hasErrors && inPersonURL && ( + {hasErrors && inPersonURL && showInPersonOption && ( { remainingAttempts: number; + isFailedResult: boolean; + captureHints: boolean; pii?: PII; @@ -70,6 +72,7 @@ function ReviewIssuesStep({ onError = () => {}, registerField = () => undefined, remainingAttempts = Infinity, + isFailedResult = false, pii, captureHints = false, }: ReviewIssuesStepProps) { @@ -110,6 +113,7 @@ function ReviewIssuesStep({ } > diff --git a/app/javascript/packages/document-capture/context/upload.tsx b/app/javascript/packages/document-capture/context/upload.tsx index b02fac344a1..b24b844284b 100644 --- a/app/javascript/packages/document-capture/context/upload.tsx +++ b/app/javascript/packages/document-capture/context/upload.tsx @@ -94,6 +94,11 @@ export interface UploadErrorResponse { * Personally-identifiable information from OCR analysis. */ ocr_pii?: PII; + + /** + * Whether the unsuccessful result was the failure type. + */ + result_failed: boolean; } export type UploadImplementation = ( diff --git a/app/javascript/packages/document-capture/services/upload.ts b/app/javascript/packages/document-capture/services/upload.ts index e49d4a6519f..84b98fd1935 100644 --- a/app/javascript/packages/document-capture/services/upload.ts +++ b/app/javascript/packages/document-capture/services/upload.ts @@ -36,6 +36,8 @@ export class UploadFormEntriesError extends FormError { remainingAttempts = Infinity; + isFailedResult = false; + pii?: PII; hints = false; @@ -111,6 +113,8 @@ const upload: UploadImplementation = async function (payload, { method = 'POST', error.hints = result.hints; } + error.isFailedResult = !!result.result_failed; + throw error; } diff --git a/app/presenters/image_upload_response_presenter.rb b/app/presenters/image_upload_response_presenter.rb index b79c5e8c821..9b319a45c08 100644 --- a/app/presenters/image_upload_response_presenter.rb +++ b/app/presenters/image_upload_response_presenter.rb @@ -38,6 +38,7 @@ def as_json(*) json[:redirect] = idv_session_errors_throttled_url if remaining_attempts&.zero? json[:hints] = true if show_hints? json[:ocr_pii] = ocr_pii + json[:result_failed] = doc_auth_result_failed? json end end @@ -48,6 +49,10 @@ def url_options private + def doc_auth_result_failed? + @form_response.to_h[:doc_auth_result] == DocAuth::Acuant::ResultCodes::FAILED.name + end + def show_hints? @form_response.errors[:hints].present? || attention_with_barcode? end diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index 23b3cdf102f..e7b3d6ada50 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -12,7 +12,7 @@ def initialize(uploaded_file, config, liveness_enabled) errors: errors, pii_from_doc: pii_from_doc, extra: { - doc_auth_result: success? ? 'Passed' : 'Caution', + doc_auth_result: doc_auth_result, billed: true, }, ) @@ -78,6 +78,22 @@ def parsed_data_from_uploaded_file @parsed_data_from_uploaded_file = parse_uri || parse_yaml end + def doc_auth_result + doc_auth_result_from_uploaded_file || doc_auth_result_from_success + end + + def doc_auth_result_from_uploaded_file + parsed_data_from_uploaded_file&.[]('doc_auth_result') + end + + def doc_auth_result_from_success + if success? + DocAuth::Acuant::ResultCodes::PASSED.name + else + DocAuth::Acuant::ResultCodes::CAUTION.name + end + end + def parse_uri uri = URI.parse(uploaded_file.chomp) if uri.scheme == 'data' diff --git a/spec/controllers/idv/doc_auth_controller_spec.rb b/spec/controllers/idv/doc_auth_controller_spec.rb index d4b0ae65d6a..56ae560a832 100644 --- a/spec/controllers/idv/doc_auth_controller_spec.rb +++ b/spec/controllers/idv/doc_auth_controller_spec.rb @@ -235,6 +235,16 @@ attention_with_barcode: false, } end + let(:hard_fail_result) do + { + pii_from_doc: {}, + success: false, + errors: { front: 'Wrong document' }, + messages: ['message'], + attention_with_barcode: false, + doc_auth_result: 'Failed', + } + end it 'returns status of success' do set_up_document_capture_result( @@ -284,14 +294,15 @@ errors: [{ field: 'front', message: 'Wrong document' }], remaining_attempts: IdentityConfig.store.doc_auth_max_attempts, ocr_pii: nil, + result_failed: false, }.to_json, ) end - it 'returns status of fail with incomplete PII from doc auth' do + it 'returns status of hard fail' do set_up_document_capture_result( uuid: document_capture_session_uuid, - idv_result: bad_pii_result, + idv_result: hard_fail_result, ) put :update, params: { @@ -303,15 +314,40 @@ expect(response.body).to eq( { success: false, - errors: [{ field: 'pii', - message: I18n.t('doc_auth.errors.general.no_liveness') }], + errors: [{ field: 'front', message: 'Wrong document' }], remaining_attempts: IdentityConfig.store.doc_auth_max_attempts, ocr_pii: nil, + result_failed: true, }.to_json, ) - expect(@analytics).to have_received(:track_event).with( - 'IdV: ' + "#{Analytics::DOC_AUTH} verify_document_status submitted".downcase, { - errors: { pii: [I18n.t('doc_auth.errors.general.no_liveness')] }, + end + + it 'returns status of fail with incomplete PII from doc auth' do + set_up_document_capture_result( + uuid: document_capture_session_uuid, + idv_result: bad_pii_result, + ) + + expect(@analytics).to receive(:track_event).with( + "IdV: #{Analytics::DOC_AUTH.downcase} image upload vendor pii validation", include( + errors: include( + pii: [I18n.t('doc_auth.errors.general.no_liveness')], + ), + error_details: { pii: [I18n.t('doc_auth.errors.general.no_liveness')] }, + attention_with_barcode: false, + success: false, + remaining_attempts: IdentityConfig.store.doc_auth_max_attempts, + flow_path: 'standard', + pii_like_keypaths: [[:pii]], + user_id: nil, + ) + ) + + expect(@analytics).to receive(:track_event).with( + "IdV: #{Analytics::DOC_AUTH.downcase} verify_document_status submitted", include( + errors: include( + pii: [I18n.t('doc_auth.errors.general.no_liveness')], + ), error_details: { pii: [I18n.t('doc_auth.errors.general.no_liveness')] }, attention_with_barcode: false, success: false, @@ -320,7 +356,26 @@ flow_path: 'standard', step_count: 1, pii_like_keypaths: [[:pii]], - } + doc_auth_result: nil, + ) + ) + + put :update, + params: { + step: 'verify_document_status', + document_capture_session_uuid: document_capture_session_uuid, + } + + expect(response.status).to eq(400) + expect(response.body).to eq( + { + success: false, + errors: [{ field: 'pii', + message: I18n.t('doc_auth.errors.general.no_liveness') }], + remaining_attempts: IdentityConfig.store.doc_auth_max_attempts, + ocr_pii: nil, + result_failed: false, + }.to_json, ) end end diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 1691be5c6a0..cb60cad2084 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -159,6 +159,7 @@ success: false, errors: [{ field: 'front', message: 'Please fill in this field.' }], remaining_attempts: Throttle.max_attempts(:idv_doc_auth) - 2, + result_failed: false, ocr_pii: nil, }, ) @@ -176,6 +177,7 @@ errors: [{ field: 'limit', message: 'We could not verify your ID' }], redirect: idv_session_errors_throttled_url, remaining_attempts: 0, + result_failed: false, ocr_pii: nil, }, ) diff --git a/spec/forms/idv/api_document_verification_status_form_spec.rb b/spec/forms/idv/api_document_verification_status_form_spec.rb index 431f481d1ec..402d02817d9 100644 --- a/spec/forms/idv/api_document_verification_status_form_spec.rb +++ b/spec/forms/idv/api_document_verification_status_form_spec.rb @@ -86,5 +86,18 @@ response = form.submit expect(response.extra[:remaining_attempts]).to be_a_kind_of(Numeric) end + + it 'includes doc_auth_result' do + response = form.submit + expect(response.extra[:doc_auth_result]).to be_nil + + expect(async_state).to receive(:result).and_return(doc_auth_result: nil) + response = form.submit + expect(response.extra[:doc_auth_result]).to be_nil + + expect(async_state).to receive(:result).and_return(doc_auth_result: 'Failed') + response = form.submit + expect(response.extra[:doc_auth_result]).to eq('Failed') + end end end diff --git a/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx b/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx index 508402580de..2e5c889ebf3 100644 --- a/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx @@ -155,6 +155,22 @@ describe('DocumentCaptureTroubleshootingOptions', () => { expect(ippButton).to.exist(); }); }); + + context('hasErrors and inPersonURL but showInPersonOption is false', () => { + const wrapper = ({ children }) => ( + {children} + ); + + it('does not have link to IPP flow', () => { + const { queryAllByText, queryAllByRole } = render( + , + { wrapper }, + ); + + expect(queryAllByText('components.troubleshooting_options.new_feature').length).to.equal(0); + expect(queryAllByRole('button').length).to.equal(0); + }); + }); }); context('with document tips hidden', () => { diff --git a/spec/javascripts/packages/document-capture/services/upload-spec.js b/spec/javascripts/packages/document-capture/services/upload-spec.js index 379b9b4a94a..38eb6ab24a5 100644 --- a/spec/javascripts/packages/document-capture/services/upload-spec.js +++ b/spec/javascripts/packages/document-capture/services/upload-spec.js @@ -169,6 +169,7 @@ describe('document-capture/services/upload', () => { ], remaining_attempts: 3, hints: true, + result_failed: true, ocr_pii: { first_name: 'Fakey', last_name: 'McFakerson', dob: '1938-10-06' }, }), }), @@ -187,6 +188,7 @@ describe('document-capture/services/upload', () => { last_name: 'McFakerson', dob: '1938-10-06', }); + expect(error.isFailedResult).to.be.true(); expect(error.formEntryErrors[0]).to.be.instanceOf(UploadFormEntryError); expect(error.formEntryErrors[0].field).to.equal('front'); expect(error.formEntryErrors[0].message).to.equal('Please fill in this field'); diff --git a/spec/presenters/image_upload_response_presenter_spec.rb b/spec/presenters/image_upload_response_presenter_spec.rb index 439e0f990f5..760f052d253 100644 --- a/spec/presenters/image_upload_response_presenter_spec.rb +++ b/spec/presenters/image_upload_response_presenter_spec.rb @@ -115,6 +115,7 @@ it 'returns hash of properties' do expected = { success: false, + result_failed: false, errors: [{ field: :limit, message: t('errors.doc_auth.throttled_heading') }], redirect: idv_session_errors_throttled_url, remaining_attempts: 0, @@ -140,6 +141,7 @@ it 'returns hash of properties' do expected = { success: false, + result_failed: false, errors: [{ field: :front, message: t('doc_auth.errors.not_a_file') }], hints: true, remaining_attempts: 3, @@ -149,6 +151,32 @@ expect(presenter.as_json).to eq expected end + context 'hard fail' do + let(:form_response) do + FormResponse.new( + success: false, + errors: { + front: t('doc_auth.errors.not_a_file'), + hints: true, + }, + extra: { doc_auth_result: 'Failed', remaining_attempts: 3 }, + ) + end + + it 'returns hash of properties' do + expected = { + success: false, + result_failed: true, + errors: [{ field: :front, message: t('doc_auth.errors.not_a_file') }], + hints: true, + remaining_attempts: 3, + ocr_pii: nil, + } + + expect(presenter.as_json).to eq expected + end + end + context 'no remaining attempts' do let(:form_response) do FormResponse.new( @@ -164,6 +192,7 @@ it 'returns hash of properties' do expected = { success: false, + result_failed: false, errors: [{ field: :front, message: t('doc_auth.errors.not_a_file') }], hints: true, redirect: idv_session_errors_throttled_url, @@ -190,6 +219,7 @@ it 'returns hash of properties' do expected = { success: false, + result_failed: false, errors: [], hints: true, remaining_attempts: 3, diff --git a/spec/services/doc_auth/mock/result_response_spec.rb b/spec/services/doc_auth/mock/result_response_spec.rb index 81138cbd64b..721ab88dbe9 100644 --- a/spec/services/doc_auth/mock/result_response_spec.rb +++ b/spec/services/doc_auth/mock/result_response_spec.rb @@ -274,6 +274,34 @@ expect(response.exception).to eq(nil) expect(response.pii_from_doc).to eq({}) expect(response.attention_with_barcode?).to eq(false) + expect(response.extra).to eq( + doc_auth_result: DocAuth::Acuant::ResultCodes::PASSED.name, + billed: true, + ) + end + end + + context 'with a yaml file containing a failed result' do + let(:input) do + <<~YAML + doc_auth_result: Failed + YAML + end + + it 'returns a failed result' do + expect(response.success?).to eq(false) + expect(response.errors).to eq( + general: [DocAuth::Errors::BARCODE_READ_CHECK], + back: [DocAuth::Errors::FALLBACK_FIELD_LEVEL], + hints: true, + ) + expect(response.exception).to eq(nil) + expect(response.pii_from_doc).to eq({}) + expect(response.attention_with_barcode?).to eq(false) + expect(response.extra).to eq( + doc_auth_result: DocAuth::Acuant::ResultCodes::FAILED.name, + billed: true, + ) end end @@ -315,6 +343,10 @@ state_id_type: 'drivers_license', ) expect(response.attention_with_barcode?).to eq(false) + expect(response.extra).to eq( + doc_auth_result: DocAuth::Acuant::ResultCodes::PASSED.name, + billed: true, + ) end end end