diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index daeb55b5288..b22ef897a99 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -21,7 +21,10 @@ def show ) @post_office_enabled = IdentityConfig.store.in_person_proofing_enabled && IdentityConfig.store.in_person_proofing_opt_in_enabled && - IdentityConfig.store.in_person_doc_auth_button_enabled + IdentityConfig.store.in_person_doc_auth_button_enabled && + Idv::InPersonConfig.enabled_for_issuer?( + decorated_sp_session.sp_issuer, + ) @selfie_required = idv_session.selfie_check_required @idv_how_to_verify_form = Idv::HowToVerifyForm.new set_how_to_verify_presenter diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index ade934dfecd..826ebeab053 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -47,7 +47,8 @@ def submit if client_response.success? doc_pii_response = validate_pii_from_doc(client_response) - if doc_pii_response.success? && passport_submittal + + if doc_pii_response.success? && passport_requested? && passport_submittal mrz_response = validate_mrz(client_response) end end @@ -185,7 +186,7 @@ def post_images_to_client def document_type return nil if document_capture_session.nil? - @document_type ||= document_capture_session.passport_requested? \ + @document_type ||= passport_requested? \ ? 'Passport' : 'DriversLicense' end @@ -289,6 +290,7 @@ def determine_response(form_response:, client_response:, doc_pii_response:, mrz_ # doc_pii validation failed return doc_pii_response if doc_pii_response.present? && !doc_pii_response.success? + # mrz validation failed return mrz_response if mrz_response.present? && !mrz_response.success? client_response @@ -507,6 +509,10 @@ def user_uuid document_capture_session&.user&.uuid end + def passport_requested? + !!document_capture_session&.passport_requested? + end + def rate_limiter @rate_limiter ||= RateLimiter.new( user: document_capture_session.user, @@ -535,11 +541,12 @@ def store_failed_images(client_response, doc_pii_response) } end # doc auth failed due to non network error or doc_pii is not valid + failed_front_fingerprint = nil + failed_back_fingerprint = nil + failed_passport_fingerprint = nil + if client_response && !client_response.success? && !client_response.network_error? errors_hash = client_response.errors&.to_h || {} - failed_front_fingerprint = nil - failed_back_fingerprint = nil - failed_passport_fingerprint = nil if errors_hash[:front] || errors_hash[:back] || errors_hash[:passport] if errors_hash[:front] diff --git a/app/presenters/duplicate_profiles_detected_presenter.rb b/app/presenters/duplicate_profiles_detected_presenter.rb index 0cd2eb6f8de..ddf9092179e 100644 --- a/app/presenters/duplicate_profiles_detected_presenter.rb +++ b/app/presenters/duplicate_profiles_detected_presenter.rb @@ -14,10 +14,6 @@ def heading I18n.t('duplicate_profiles_detected.heading') end - def intro - I18n.t('duplicate_profiles_detected.intro', app_name: APP_NAME) - end - def associated_profiles profile_ids = [user.active_profile] + user_session[:duplicate_profile_ids] profiles = Profile.where(id: profile_ids) diff --git a/app/services/idv/proofing_components.rb b/app/services/idv/proofing_components.rb index c549046c6e1..4045fa72dff 100644 --- a/app/services/idv/proofing_components.rb +++ b/app/services/idv/proofing_components.rb @@ -11,7 +11,7 @@ def document_check end def document_type - return 'state_id' if idv_session.remote_document_capture_complete? + idv_session.pii_from_doc&.id_doc_type end def source_check diff --git a/app/views/duplicate_profiles_detected/show.html.erb b/app/views/duplicate_profiles_detected/show.html.erb index c7ba523bfb3..e7db0eb1590 100644 --- a/app/views/duplicate_profiles_detected/show.html.erb +++ b/app/views/duplicate_profiles_detected/show.html.erb @@ -43,7 +43,11 @@
- <%= link_to(t('duplicate_profiles_detected.dont_recognize_account'), '/') %> + <%= render ButtonComponent.new( + url: root_url, + method: :get, + unstyled: true, + ).with_content(t('duplicate_profiles_detected.dont_recognize_account')) %>
@@ -58,6 +62,14 @@ class: 'usa-button usa-button usa-button--outline usa-button--wide usa-button--big', ).with_content(t('duplicate_profiles_detected.sign_out')) %>
+ +
+ <%= render ButtonComponent.new( + url: root_url, + method: :get, + unstyled: true, + ).with_content(t('duplicate_profiles_detected.cant_access')) %> +
<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 7d68615aac8..d6420ff26ad 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -734,7 +734,8 @@ doc_auth.tips.document_capture_selfie_text1: Remove any items covering your face doc_auth.tips.document_capture_selfie_text2: Take photo in a well-lit place doc_auth.tips.document_capture_selfie_text3: Keep your expression neutral doc_auth.tips.document_capture_selfie_text4: Make sure your whole face is visible within the green circle -duplicate_profiles_detected.accounts_list.heading: Accounts with the same SSN +duplicate_profiles_detected.accounts_list.heading: Accounts with the same verified information +duplicate_profiles_detected.cant_access: 'I can’t access an account' duplicate_profiles_detected.connected_acct_html: ' Connected agencies: %{count}' duplicate_profiles_detected.created_at_html: ' Created: %{timestamp_html}' duplicate_profiles_detected.delete_duplicates.details_html: Sign in, authenticate, and delete the account from the ‘Your account’ page. %{link_html} @@ -743,14 +744,14 @@ duplicate_profiles_detected.delete_duplicates.link: How to delete your account. duplicate_profiles_detected.dont_recognize_account: I don’t recognize an account above duplicate_profiles_detected.duplicate: Duplicate duplicate_profiles_detected.get_help: Get Help -duplicate_profiles_detected.heading: You have multiple accounts with the same SSN -duplicate_profiles_detected.intro: For security purposes, you can only verify your identity on one %{app_name} account. Learn more about duplicate accounts +duplicate_profiles_detected.heading: We found other accounts that may be yours +duplicate_profiles_detected.intro: The %{app_name} requires that you only have one identity verified %{app_name} account. Learn more about duplicate accounts duplicate_profiles_detected.intro2: 'You need to delete the duplicate accounts before signing into %{app_name}. Here’s what to do:' duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}' duplicate_profiles_detected.never_logged_in: Never logged in duplicate_profiles_detected.select_an_account.details: Keep the account that you’ve connected to the most agencies. That way you don’t have to reconnect to all the agencies you use. duplicate_profiles_detected.select_an_account.heading: Choose an account to keep -duplicate_profiles_detected.sign_back_in.details: Once you’ve deleted the duplicate accounts return to %{app_name} and sign in. +duplicate_profiles_detected.sign_back_in.details: Go back to the %{app_name} website and sign in using the one %{app_name} account you kept. duplicate_profiles_detected.sign_back_in.heading: Sign back into %{app_name} with one account duplicate_profiles_detected.sign_out: Sign out duplicate_profiles_detected.signed_in: Signed In diff --git a/config/locales/es.yml b/config/locales/es.yml index fc6c148a4b9..9dd3d83ded0 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -745,7 +745,8 @@ doc_auth.tips.document_capture_selfie_text1: Quite cualquier prenda o accesorio doc_auth.tips.document_capture_selfie_text2: Tómese la foto en un lugar bien iluminado doc_auth.tips.document_capture_selfie_text3: Mantenga una expresión neutral doc_auth.tips.document_capture_selfie_text4: Revise que se vea su rostro completo dentro del círculo verde. -duplicate_profiles_detected.accounts_list.heading: Accounts with the same SSN +duplicate_profiles_detected.accounts_list.heading: Accounts with the same verified information +duplicate_profiles_detected.cant_access: 'I can’t access an account' duplicate_profiles_detected.connected_acct_html: ' Connected agencies: %{count}' duplicate_profiles_detected.created_at_html: ' Created: %{timestamp_html}' duplicate_profiles_detected.delete_duplicates.details_html: Sign in, authenticate, and delete the account from the ‘Your account’ page. %{link_html} @@ -754,14 +755,14 @@ duplicate_profiles_detected.delete_duplicates.link: How to delete your account. duplicate_profiles_detected.dont_recognize_account: I don’t recognize an account above duplicate_profiles_detected.duplicate: Duplicate duplicate_profiles_detected.get_help: Get Help -duplicate_profiles_detected.heading: You have multiple accounts with the same SSN -duplicate_profiles_detected.intro: For security purposes, you can only verify your identity on one %{app_name} account. Learn more about duplicate accounts +duplicate_profiles_detected.heading: We found other accounts that may be yours +duplicate_profiles_detected.intro: The %{app_name} requires that you only have one identity verified %{app_name} account. Learn more about duplicate accounts duplicate_profiles_detected.intro2: 'You need to delete the duplicate accounts before signing into %{app_name}. Here’s what to do:' duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}' duplicate_profiles_detected.never_logged_in: Never logged in duplicate_profiles_detected.select_an_account.details: Keep the account that you’ve connected to the most agencies. That way you don’t have to reconnect to all the agencies you use. duplicate_profiles_detected.select_an_account.heading: Choose an account to keep -duplicate_profiles_detected.sign_back_in.details: Once you’ve deleted the duplicate accounts return to %{app_name} and sign in. +duplicate_profiles_detected.sign_back_in.details: Go back to the %{app_name} website and sign in using the one %{app_name} account you kept. duplicate_profiles_detected.sign_back_in.heading: Sign back into %{app_name} with one account duplicate_profiles_detected.sign_out: Sign out duplicate_profiles_detected.signed_in: Signed In diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6792e48d9a5..a68d3add9e4 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -734,7 +734,8 @@ doc_auth.tips.document_capture_selfie_text1: Enlevez tout ce qui cache votre vis doc_auth.tips.document_capture_selfie_text2: Prenez votre photo dans un endroit bien éclairé doc_auth.tips.document_capture_selfie_text3: Gardez une expression neutre doc_auth.tips.document_capture_selfie_text4: Assurez-vous que l’ensemble de votre visage est visible dans le cercle vert -duplicate_profiles_detected.accounts_list.heading: Accounts with the same SSN +duplicate_profiles_detected.accounts_list.heading: Accounts with the same verified information +duplicate_profiles_detected.cant_access: 'I can’t access an account' duplicate_profiles_detected.connected_acct_html: ' Connected agencies: %{count}' duplicate_profiles_detected.created_at_html: ' Created: %{timestamp_html}' duplicate_profiles_detected.delete_duplicates.details_html: Sign in, authenticate, and delete the account from the ‘Your account’ page. %{link_html} @@ -743,14 +744,14 @@ duplicate_profiles_detected.delete_duplicates.link: How to delete your account. duplicate_profiles_detected.dont_recognize_account: I don’t recognize an account above duplicate_profiles_detected.duplicate: Duplicate duplicate_profiles_detected.get_help: Get Help -duplicate_profiles_detected.heading: You have multiple accounts with the same SSN -duplicate_profiles_detected.intro: For security purposes, you can only verify your identity on one %{app_name} account. Learn more about duplicate accounts +duplicate_profiles_detected.heading: We found other accounts that may be yours +duplicate_profiles_detected.intro: The %{app_name} requires that you only have one identity verified %{app_name} account. Learn more about duplicate accounts duplicate_profiles_detected.intro2: 'You need to delete the duplicate accounts before signing into %{app_name}. Here’s what to do:' duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}' duplicate_profiles_detected.never_logged_in: Never logged in duplicate_profiles_detected.select_an_account.details: Keep the account that you’ve connected to the most agencies. That way you don’t have to reconnect to all the agencies you use. duplicate_profiles_detected.select_an_account.heading: Choose an account to keep -duplicate_profiles_detected.sign_back_in.details: Once you’ve deleted the duplicate accounts return to %{app_name} and sign in. +duplicate_profiles_detected.sign_back_in.details: Go back to the %{app_name} website and sign in using the one %{app_name} account you kept. duplicate_profiles_detected.sign_back_in.heading: Sign back into %{app_name} with one account duplicate_profiles_detected.sign_out: Sign out duplicate_profiles_detected.signed_in: Signed In diff --git a/config/locales/zh.yml b/config/locales/zh.yml index e685a8cb342..91b313df73f 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -745,7 +745,8 @@ doc_auth.tips.document_capture_selfie_text1: 摘掉任何遮盖您面孔的东 doc_auth.tips.document_capture_selfie_text2: 在光线明亮的地方拍照 doc_auth.tips.document_capture_selfie_text3: 保持中性表情 doc_auth.tips.document_capture_selfie_text4: 确保您整个面孔都可以在绿色圆圈里看到 -duplicate_profiles_detected.accounts_list.heading: Accounts with the same SSN +duplicate_profiles_detected.accounts_list.heading: Accounts with the same verified information +duplicate_profiles_detected.cant_access: 'I can’t access an account' duplicate_profiles_detected.connected_acct_html: ' Connected agencies: %{count}' duplicate_profiles_detected.created_at_html: ' Created: %{timestamp_html}' duplicate_profiles_detected.delete_duplicates.details_html: Sign in, authenticate, and delete the account from the ‘Your account’ page. %{link_html} @@ -754,14 +755,14 @@ duplicate_profiles_detected.delete_duplicates.link: How to delete your account. duplicate_profiles_detected.dont_recognize_account: I don’t recognize an account above duplicate_profiles_detected.duplicate: Duplicate duplicate_profiles_detected.get_help: Get Help -duplicate_profiles_detected.heading: You have multiple accounts with the same SSN -duplicate_profiles_detected.intro: For security purposes, you can only verify your identity on one %{app_name} account. Learn more about duplicate accounts +duplicate_profiles_detected.heading: We found other accounts that may be yours +duplicate_profiles_detected.intro: The %{app_name} requires that you only have one identity verified %{app_name} account. Learn more about duplicate accounts duplicate_profiles_detected.intro2: 'You need to delete the duplicate accounts before signing into %{app_name}. Here’s what to do:' duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}' duplicate_profiles_detected.never_logged_in: Never logged in duplicate_profiles_detected.select_an_account.details: Keep the account that you’ve connected to the most agencies. That way you don’t have to reconnect to all the agencies you use. duplicate_profiles_detected.select_an_account.heading: Choose an account to keep -duplicate_profiles_detected.sign_back_in.details: Once you’ve deleted the duplicate accounts return to %{app_name} and sign in. +duplicate_profiles_detected.sign_back_in.details: Go back to the %{app_name} website and sign in using the one %{app_name} account you kept. duplicate_profiles_detected.sign_back_in.heading: Sign back into %{app_name} with one account duplicate_profiles_detected.sign_out: Sign out duplicate_profiles_detected.signed_in: Signed In diff --git a/db/primary_migrate/20250714184424_add_notes_to_device_profiling_result.rb b/db/primary_migrate/20250714184424_add_notes_to_device_profiling_result.rb new file mode 100644 index 00000000000..fab99596e8a --- /dev/null +++ b/db/primary_migrate/20250714184424_add_notes_to_device_profiling_result.rb @@ -0,0 +1,9 @@ +class AddNotesToDeviceProfilingResult < ActiveRecord::Migration[8.0] + def up + add_column :device_profiling_results, :notes, :string, comment: 'sensitive=false' + end + + def down + remove_column :device_profiling_results, :notes + end +end diff --git a/db/schema.rb b/db/schema.rb index 1745735842d..4177b8da75a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_06_11_195441) do +ActiveRecord::Schema[8.0].define(version: 2025_07_14_184424) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_catalog.plpgsql" @@ -96,6 +96,7 @@ t.string "profiling_type", comment: "sensitive=false" t.datetime "created_at", null: false, comment: "sensitive=false" t.datetime "updated_at", null: false, comment: "sensitive=false" + t.string "notes", comment: "sensitive=false" t.index ["user_id"], name: "index_device_profiling_results_on_user_id" end diff --git a/docs/attempts-api/schemas/events/shared/EventProperties.yml b/docs/attempts-api/schemas/events/shared/EventProperties.yml index 80d61a0b346..28ffac2b782 100644 --- a/docs/attempts-api/schemas/events/shared/EventProperties.yml +++ b/docs/attempts-api/schemas/events/shared/EventProperties.yml @@ -12,7 +12,8 @@ properties: description: Known more commonly as ephemeral port numbers associated with the Client IP – This is NOT the CSP server port 443 or 80. device_id: type: string - description: Cookie device unique identifier. This value is securely randomly generated server-side as 64 bytes and displayed as a string of hexadecimal characters. + description: | + Cookie device unique identifier. This value is securely randomly generated server-side as a string of 128 hexadecimal characters using the SecureRandom Ruby library. google_analytics_cookies: type: object description: | diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index 09a3b09870c..b4471b792be 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -123,6 +123,29 @@ module Vendors zipcode: '59010-1234', }.freeze + MOCK_IDV_APPLICANT_STATE_ID = { + address1: '1 FAKE RD', + address2: '', + city: 'GREAT FALLS', + dob: '1938-10-06', + eye_color: nil, + first_name: 'FAKEY', + height: 72, + issuing_country_code: 'US', + last_name: 'MCFAKERSON', + middle_name: nil, + name_suffix: 'JR', + state: MOCK_IDV_APPLICANT_STATE, + 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', + id_doc_type: 'state_id', + sex: 'male', + weight: nil, + zipcode: '59010-1234', + }.freeze + MOCK_IPP_APPLICANT = { first_name: 'FAKEY', last_name: 'MCFAKERSON', diff --git a/lib/tasks/device_profiling.rake b/lib/tasks/device_profiling.rake new file mode 100644 index 00000000000..e25f2ea8048 --- /dev/null +++ b/lib/tasks/device_profiling.rake @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +namespace :device_profiling do + desc 'Approve rejected device profiling results to pass for list of UUIDs' + task :approve_rejected_users, [:user_uuids] => :environment do |_task, _args| + user_uuids = ARGV[1] + + if user_uuids.blank? + puts 'Error: user_uuids is required' + exit 1 + end + + # Parse UUIDs + uuid_list = user_uuids.split(',').map(&:strip).reject(&:blank?) + + puts "Processing #{uuid_list.count} user UUID(s)" + puts "Action: Change 'reject' to 'pass' (skip if already 'pass')" + puts '' + + total_users_processed = 0 + total_results_updated = 0 + skipped_already_passed = 0 + users_with_no_results = 0 + + uuid_list.each do |user_uuid| + total_users_processed += 1 + + begin + # Find user by UUID + user = User.find_by(uuid: user_uuid) + if user.blank? + puts "User not found: #{user_uuid}" + next + end + + # Find device profiling results for this user (reject or pass) + result = DeviceProfilingResult.where( + user_id: user.id, + profiling_type: DeviceProfilingResult::PROFILING_TYPES[:account_creation], + ).first + + if result.nil? + users_with_no_results += 1 + puts "No device profiling results found for: #{user_uuid} (#{user.email})" + next + end + + # Check if already passed + + if result.review_status == 'pass' + skipped_already_passed += 1 + puts "Already passed: #{user_uuid}" + next + end + + # Update rejected results to pass + puts "Updating rejected result for: #{user_uuid}" + result.update!(review_status: 'pass', notes: 'Manually overridden') + total_results_updated += 1 + + puts "Successfully updated result for: #{user_uuid}" + + # Log for audit + rescue => e + puts "Error processing #{user_uuid}: #{e.message}" + end + end + + puts '' + puts '=' * 80 + puts 'SUMMARY:' + puts "Total users processed: #{total_users_processed}" + puts "Results updated (reject → pass): #{total_results_updated}" + puts "Users already passed: #{skipped_already_passed}" + puts "Users with no results: #{users_with_no_results}" + + puts 'Task completed successfully!' + end +end diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index f5e047c99d6..3d0a388ac18 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -180,7 +180,7 @@ idv_session: subject.idv_session, ) expect(proofing_components.document_check).to eq('mock') - expect(proofing_components.document_type).to eq('state_id') + expect(proofing_components.document_type).to eq('drivers_license') end context 'redo document capture' do diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 4df76ffd99b..541ba235873 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -43,7 +43,7 @@ let(:base_proofing_components) do { document_check: 'mock', - document_type: 'state_id', + document_type: 'drivers_license', source_check: 'StateIdMock', resolution_check: 'ResolutionMock', residential_resolution_check: 'ResidentialAddressNotRequired', @@ -243,22 +243,22 @@ 'IdV: doc auth image upload vendor pii validation' => { success: true, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean, classification_info: {}, id_issued_status: 'present', id_expiration_status: 'present', passport_issued_status: 'missing', passport_expiration_status: 'missing', document_type: an_instance_of(String), id_doc_type: an_instance_of(String) }, - 'IdV: doc auth document_capture submitted' => hash_including(success: true, flow_path: 'standard', step: 'document_capture', analytics_id: 'Doc Auth', selfie_check_required: boolean, liveness_checking_required: boolean, proofing_components: { document_check: 'mock', document_type: 'state_id' }), + 'IdV: doc auth document_capture submitted' => hash_including(success: true, flow_path: 'standard', step: 'document_capture', analytics_id: 'Doc Auth', selfie_check_required: boolean, liveness_checking_required: boolean, proofing_components: { document_check: 'mock', document_type: 'drivers_license' }), 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth ssn submitted' => { success: true, flow_path: 'standard', step: 'ssn', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, idv_threatmetrix_response_body: ( if threatmetrix_response_body.present? @@ -371,19 +371,19 @@ }, 'IdV: doc auth ssn visited' => { flow_path: 'hybrid', step: 'ssn', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth ssn submitted' => { success: true, flow_path: 'hybrid', step: 'ssn', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth verify visited' => { flow_path: 'hybrid', step: 'verify', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth verify submitted' => { flow_path: 'hybrid', step: 'verify', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, idv_threatmetrix_response_body: ( if threatmetrix_response_body.present? @@ -490,23 +490,23 @@ }, 'IdV: doc auth document_capture submitted' => { success: true, flow_path: 'standard', step: 'document_capture', analytics_id: 'Doc Auth', selfie_check_required: boolean, liveness_checking_required: boolean, - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth ssn submitted' => { success: true, flow_path: 'standard', step: 'ssn', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, idv_threatmetrix_response_body: ( if threatmetrix_response_body.present? @@ -734,26 +734,26 @@ }, 'IdV: doc auth document_capture submitted' => { success: true, flow_path: 'standard', step: 'document_capture', analytics_id: 'Doc Auth', selfie_check_required: boolean, liveness_checking_required: true, - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, :idv_selfie_image_added => { acuant_version: kind_of(String), captureAttempts: 1, fingerprint: 'aIzxkX_iMtoxFOURZr55qkshs53emQKUOr7VfTf6G1Q', flow_path: 'standard', height: 38, mimeType: 'image/png', size: 3694, source: 'upload', width: 284, liveness_checking_required: boolean, selfie_attempts: 0 }, 'IdV: doc auth ssn visited' => { flow_path: 'standard', step: 'ssn', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth ssn submitted' => { success: true, flow_path: 'standard', step: 'ssn', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', analytics_id: 'Doc Auth', - proofing_components: { document_check: 'mock', document_type: 'state_id' } + proofing_components: { document_check: 'mock', document_type: 'drivers_license' } }, idv_threatmetrix_response_body: ( if threatmetrix_response_body.present? diff --git a/spec/features/idv/cancel_spec.rb b/spec/features/idv/cancel_spec.rb index 6eb3a6ea745..b954e52ec49 100644 --- a/spec/features/idv/cancel_spec.rb +++ b/spec/features/idv/cancel_spec.rb @@ -121,7 +121,7 @@ expect(page).to have_content(t('idv.cancel.headings.prompt.standard')) expect(fake_analytics).to have_logged_event( 'IdV: cancellation visited', - proofing_components: { document_check: 'mock', document_type: 'state_id' }, + proofing_components: { document_check: 'mock', document_type: 'drivers_license' }, request_came_from: 'idv/ssn#show', step: 'ssn', ) @@ -137,7 +137,7 @@ expect(fake_analytics).to have_logged_event( 'IdV: cancellation go back', - proofing_components: { document_check: 'mock', document_type: 'state_id' }, + proofing_components: { document_check: 'mock', document_type: 'drivers_license' }, step: 'ssn', ) @@ -147,7 +147,7 @@ expect(fake_analytics).to have_logged_event( 'IdV: start over', - proofing_components: { document_check: 'mock', document_type: 'state_id' }, + proofing_components: { document_check: 'mock', document_type: 'drivers_license' }, step: 'ssn', ) @@ -159,7 +159,7 @@ expect(fake_analytics).to have_logged_event( 'IdV: cancellation confirmed', step: 'ssn', - proofing_components: { document_check: 'mock', document_type: 'state_id' }, + proofing_components: { document_check: 'mock', document_type: 'drivers_license' }, ) end end diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index 46433236e22..eaf9bb0d3be 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -382,14 +382,17 @@ def verify_no_upload_photos_section_and_link(page) end context 'when sp ipp is not available' do let(:sp_ipp_enabled) { false } + describe 'when selfie is required by sp' do let(:facial_match_required) { true } it 'shows selfie version of top content, no ipp option section, no upload section' do verify_handoff_page_selfie_version_content(page) verify_no_upload_photos_section_and_link(page) + verify_handoff_page_no_ipp_option_shown(page) end end + describe 'when selfie is not required by sp' do let(:facial_match_required) { false } it 'shows non selfie version of top content and upload section, diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index 8014a0618e4..43f91f61c9f 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -342,7 +342,7 @@ def validate_enter_password_submit(user) 'source_check' => 'StateIdMock', 'threatmetrix' => true, 'address_check' => 'lexis_nexis_address', - 'document_type' => 'state_id', + 'document_type' => 'drivers_license', 'document_check' => 'mock', 'residential_resolution_check' => 'ResidentialAddressNotRequired', 'resolution_check' => 'ResolutionMock', diff --git a/spec/features/idv/proofing_components_spec.rb b/spec/features/idv/proofing_components_spec.rb index 63d4f16c426..4d1f266dd1b 100644 --- a/spec/features/idv/proofing_components_spec.rb +++ b/spec/features/idv/proofing_components_spec.rb @@ -30,7 +30,7 @@ it 'records proofing components' do proofing_components = user.active_profile.proofing_components expect(proofing_components['document_check']).to eq('mock') - expect(proofing_components['document_type']).to eq('state_id') + expect(proofing_components['document_type']).to eq('drivers_license') expect(proofing_components['source_check']).to eq('StateIdMock') end end diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index 9a5e1f37372..0bb1e0b4dac 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -30,6 +30,7 @@ let(:front_image) { DocAuthImageFixtures.document_front_image_multipart } let(:back_image) { DocAuthImageFixtures.document_back_image_multipart } let(:passport_image) { nil } + let(:passport_requested) { false } let(:selfie_image) { nil } let(:liveness_checking_required) { false } let(:front_image_file_name) { 'front.jpg' } @@ -89,6 +90,8 @@ before do allow(IdentityConfig.store).to receive(:doc_escrow_enabled).and_return doc_escrow_enabled allow(writer).to receive(:write).and_return result + allow_any_instance_of(DocumentCaptureSession).to receive(:passport_requested?) + .and_return(passport_requested) end describe '#valid?' do @@ -899,6 +902,7 @@ ) end let(:response) { form.submit } + let(:passport_requested) { true } before do allow_any_instance_of(described_class) @@ -919,6 +923,7 @@ }, ) end + let(:document_type) { 'Passport' } before do allow_any_instance_of(DocAuth::Mock::DosPassportApiClient) @@ -955,6 +960,7 @@ context 'Passport MRZ validation succeeds' do let(:passport_image) { DocAuthImageFixtures.passport_passed_yaml } + let(:document_type) { 'Passport' } let(:successful_passport_mrz_response) do DocAuth::Response.new( @@ -1018,6 +1024,63 @@ expect(response.errors[:passport]).to eq(message) end end + + context 'User submits passport but passport is not requested' do + let(:passport_requested) { false } + let(:passport_image) { DocAuthImageFixtures.passport_passed_yaml } + + it 'does not do MRZ validation' do + expect_any_instance_of(DocAuth::Mock::DosPassportApiClient).to_not receive(:fetch) + + response + end + + it 'does not call the MRZ analytics event' do + response + + expect(fake_analytics).to_not have_logged_event( + :idv_dos_passport_verification, + ) + end + end + + context 'Passport doc auth succeeds but PII validation fails' do + let(:passport_image) { DocAuthImageFixtures.passport_passed_yaml } + let(:successful_doc_auth_response) do + DocAuth::Mock::ResultResponse.new( + passport_image.read, + image_config, + ) + end + let(:failed_pii_response) do + Idv::DocAuthFormResponse.new( + success: false, + errors: { doc_pii: 'bad' }, + extra: { + pii_like_keypaths: pii_like_keypaths_passport, + attention_with_barcode: false, + id_issued_status: 'missing', + id_expiration_status: 'missing', + passport_issued_status: 'missing', + passport_expiration_status: 'missing', + }, + ) + end + + before do + allow_any_instance_of(described_class) + .to receive(:post_images_to_client) + .and_return(successful_doc_auth_response) + allow_any_instance_of(Idv::DocPiiForm).to receive(:submit).and_return(failed_pii_response) + end + + it 'does not raise NameError when passport fingerprint variable is accessed' do + expect { form.submit }.not_to raise_error + + response = form.submit + expect(response.success?).to eq(false) + end + end end describe 'image source' do diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb index 8cb0cc2847b..b64af08c570 100644 --- a/spec/services/idv/agent_spec.rb +++ b/spec/services/idv/agent_spec.rb @@ -147,7 +147,7 @@ expect(ResolutionProofingJob).to receive(:perform_later).with( hash_including( proofing_components: { - document_type: 'state_id', + document_type: 'drivers_license', }, ), ) diff --git a/spec/services/idv/analytics_events_enhancer_spec.rb b/spec/services/idv/analytics_events_enhancer_spec.rb index 1cf65613a41..225769c75d5 100644 --- a/spec/services/idv/analytics_events_enhancer_spec.rb +++ b/spec/services/idv/analytics_events_enhancer_spec.rb @@ -100,7 +100,7 @@ def track_event(_event, **kwargs) expect(analytics.called_kwargs).to eql( extra: true, proofing_components: { - document_type: 'state_id', + document_type: 'drivers_license', }, ) end diff --git a/spec/services/idv/proofing_components_spec.rb b/spec/services/idv/proofing_components_spec.rb index 97645db4bae..251fbb50da5 100644 --- a/spec/services/idv/proofing_components_spec.rb +++ b/spec/services/idv/proofing_components_spec.rb @@ -26,8 +26,6 @@ end describe '#to_h' do - let(:pii_from_doc) { Idp::Constants::MOCK_IDV_APPLICANT } - before do allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return('test_vendor') idv_session.mark_verify_info_step_complete! @@ -40,18 +38,58 @@ idv_session.doc_auth_vendor = 'feedabee' end - it 'returns expected result' do - expect(subject.to_h).to eql( - { - document_check: 'feedabee', - document_type: 'state_id', - source_check: 'aamva', - resolution_check: 'lexis_nexis', - address_check: 'gpo_letter', - threatmetrix: true, - threatmetrix_review_status: 'pass', - }, - ) + context 'with drivers_license' do + let(:pii_from_doc) { Idp::Constants::MOCK_IDV_APPLICANT } + + it 'returns expected result' do + expect(subject.to_h).to eql( + { + document_check: 'feedabee', + document_type: 'drivers_license', + source_check: 'aamva', + resolution_check: 'lexis_nexis', + address_check: 'gpo_letter', + threatmetrix: true, + threatmetrix_review_status: 'pass', + }, + ) + end + end + + context 'with state_id' do + let(:pii_from_doc) { Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID } + + it 'returns expected result' do + expect(subject.to_h).to eql( + { + document_check: 'feedabee', + document_type: 'state_id', + source_check: 'aamva', + resolution_check: 'lexis_nexis', + address_check: 'gpo_letter', + threatmetrix: true, + threatmetrix_review_status: 'pass', + }, + ) + end + end + + context 'with passport' do + let(:pii_from_doc) { Idp::Constants::MOCK_IDV_PROOFING_PASSPORT_APPLICANT } + + it 'returns expected result' do + expect(subject.to_h).to eql( + { + document_check: 'feedabee', + document_type: 'passport', + source_check: 'aamva', + resolution_check: 'lexis_nexis', + address_check: 'gpo_letter', + threatmetrix: true, + threatmetrix_review_status: 'pass', + }, + ) + end end end @@ -93,7 +131,15 @@ let(:pii_from_doc) { Idp::Constants::MOCK_IDV_APPLICANT } it 'returns doc auth vendor' do - expect(subject.document_type).to eql('state_id') + expect(subject.document_type).to eql('drivers_license') + end + end + + context 'after doc auth completed successfully with passport' do + let(:pii_from_doc) { Idp::Constants::MOCK_IDV_PROOFING_PASSPORT_APPLICANT } + + it 'returns doc auth vendor' do + expect(subject.document_type).to eql('passport') end end end