Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/forms/idv/api_document_verification_status_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def submit
errors: errors,
extra: {
remaining_attempts: remaining_attempts,
doc_auth_result: @async_state&.result&.[](:doc_auth_result),
},
)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand All @@ -32,6 +37,7 @@ function DocumentCaptureTroubleshootingOptions({
heading,
location = 'document_capture_troubleshooting_options',
showDocumentTips = true,
showInPersonOption = true,
hasErrors,
}: DocumentCaptureTroubleshootingOptionsProps) {
const { t } = useI18n();
Expand Down Expand Up @@ -71,7 +77,7 @@ function DocumentCaptureTroubleshootingOptions({
].filter(Boolean) as TroubleshootingOption[]
}
/>
{hasErrors && inPersonURL && (
{hasErrors && inPersonURL && showInPersonOption && (
<TroubleshootingOptions
isNewFeatures
heading={t('idv.troubleshooting.headings.are_you_near')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ function DocumentCapture({ isAsyncForm = false, onStepChange }) {
submissionError instanceof UploadFormEntriesError
? withProps({
remainingAttempts: submissionError.remainingAttempts,
isFailedResult: submissionError.isFailedResult,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just want to clarify for myself - is isFailedResult for any id upload failure of for the kind that would not trigger an option for in person proofing (ie barcode can't be read)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way it's being treated here is as a "hard failure", meaning that it's a document which was recognized as unacceptable (fraudulent, etc.). This is one of the results which can be returned from our document authentication vendors. It's also distinct from other non-successful results like "unknown" or "attention". The ticket is making it so that we can continue showing the troubleshooting option for certain unsuccessful results (unknown, attention), but not for failures.

# The authentication test results are unknown. We are not billed for these
UNKNOWN = ResultCode.new(0, 'Unknown', false).freeze
# The authentication test passed.
PASSED = ResultCode.new(1, 'Passed', true).freeze
# The authentication test failed.
FAILED = ResultCode.new(2, 'Failed', true).freeze
# The authentication test was skipped (was not run).
SKIPPED = ResultCode.new(3, 'Skipped', true).freeze
# The authentication test was inconclusive and further investigation is warranted.
CAUTION = ResultCode.new(4, 'Caution', true).freeze
# The authentication test results requires user attention.
ATTENTION = ResultCode.new(5, 'Attention', true).freeze

captureHints: submissionError.hints,
pii: submissionError.pii,
})(ReviewIssuesStep)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ interface ReviewIssuesStepValue {
interface ReviewIssuesStepProps extends FormStepComponentProps<ReviewIssuesStepValue> {
remainingAttempts: number;

isFailedResult: boolean;

captureHints: boolean;

pii?: PII;
Expand All @@ -70,6 +72,7 @@ function ReviewIssuesStep({
onError = () => {},
registerField = () => undefined,
remainingAttempts = Infinity,
isFailedResult = false,
pii,
captureHints = false,
}: ReviewIssuesStepProps) {
Expand Down Expand Up @@ -110,6 +113,7 @@ function ReviewIssuesStep({
<DocumentCaptureTroubleshootingOptions
location="post_submission_warning"
hasErrors={!!errors?.length}
showInPersonOption={!isFailedResult}
/>
}
>
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/packages/document-capture/context/upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/packages/document-capture/services/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export class UploadFormEntriesError extends FormError {

remainingAttempts = Infinity;

isFailedResult = false;

pii?: PII;

hints = false;
Expand Down Expand Up @@ -111,6 +113,8 @@ const upload: UploadImplementation = async function (payload, { method = 'POST',
error.hints = result.hints;
}

error.isFailedResult = !!result.result_failed;

throw error;
}

Expand Down
5 changes: 5 additions & 0 deletions app/presenters/image_upload_response_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
18 changes: 17 additions & 1 deletion app/services/doc_auth/mock/result_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
)
Expand Down Expand Up @@ -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
Comment on lines +85 to +87
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our developer docs specify that a YAML test file can include doc_auth_result as a top-level field, but we previously weren't actually passing this along as the extra[:doc_auth_result] value.

https://developers.login.gov/testing/#document-upload


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'
Expand Down
71 changes: 63 additions & 8 deletions spec/controllers/idv/doc_auth_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions spec/controllers/idv/image_uploads_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
)
Expand All @@ -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,
},
)
Expand Down
13 changes: 13 additions & 0 deletions spec/forms/idv/api_document_verification_status_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,22 @@ describe('DocumentCaptureTroubleshootingOptions', () => {
expect(ippButton).to.exist();
});
});

context('hasErrors and inPersonURL but showInPersonOption is false', () => {
const wrapper = ({ children }) => (
<FlowContext.Provider value={{ inPersonURL }}>{children}</FlowContext.Provider>
);

it('does not have link to IPP flow', () => {
const { queryAllByText, queryAllByRole } = render(
<DocumentCaptureTroubleshootingOptions hasErrors showInPersonOption={false} />,
{ wrapper },
);

expect(queryAllByText('components.troubleshooting_options.new_feature').length).to.equal(0);
expect(queryAllByRole('button').length).to.equal(0);
});
});
});

context('with document tips hidden', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
}),
}),
Expand All @@ -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');
Expand Down
30 changes: 30 additions & 0 deletions spec/presenters/image_upload_response_presenter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -190,6 +219,7 @@
it 'returns hash of properties' do
expected = {
success: false,
result_failed: false,
errors: [],
hints: true,
remaining_attempts: 3,
Expand Down
Loading