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