Skip to content
2 changes: 1 addition & 1 deletion app/forms/idv/api_image_upload_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def extra_attributes
submit_attempts: submit_attempts,
remaining_submit_attempts: remaining_submit_attempts,
user_id: user_uuid,
pii_like_keypaths: DocPiiForm.pii_like_keypaths,
pii_like_keypaths: DocPiiForm.pii_like_keypaths(document_type: document_type),
flow_path: params[:flow_path],
}

Expand Down
62 changes: 25 additions & 37 deletions app/forms/idv/doc_pii_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,17 @@ class DocPiiForm

validate :name_valid?
validate :dob_valid?
validates_presence_of :address1, { message: proc {
I18n.t('doc_auth.errors.alerts.address_check')
} }
validate :zipcode_valid?
validates :jurisdiction, :state, inclusion: { in: Idp::Constants::STATE_AND_TERRITORY_CODES,
message: proc {
I18n.t('doc_auth.errors.general.no_liveness')
} }

validates_presence_of :state_id_number, { message: proc {
I18n.t('doc_auth.errors.general.no_liveness')
} }
validate :state_id_expired?
validate :state_id_or_passport

attr_reader :first_name, :last_name, :dob, :address1, :state, :zipcode, :attention_with_barcode,
:jurisdiction, :state_id_number, :state_id_expiration
attr_reader :first_name, :last_name, :dob, :state_id_type, :attention_with_barcode
alias_method :attention_with_barcode?, :attention_with_barcode

def initialize(pii:, attention_with_barcode: false)
@pii_from_doc = pii
@first_name = pii[:first_name]
@last_name = pii[:last_name]
@dob = pii[:dob]
@address1 = pii[:address1]
@state = pii[:state]
@zipcode = pii[:zipcode]
@jurisdiction = pii[:state_id_jurisdiction]
@state_id_number = pii[:state_id_number]
@state_id_expiration = pii[:state_id_expiration]
@state_id_type = pii[:state_id_type]
@attention_with_barcode = attention_with_barcode
end

Expand All @@ -43,19 +25,27 @@ def submit
success: valid?,
errors: errors,
extra: {
pii_like_keypaths: self.class.pii_like_keypaths,
pii_like_keypaths: self.class.pii_like_keypaths(document_type: state_id_type),
attention_with_barcode: attention_with_barcode?,
id_issued_status: pii_from_doc[:state_id_issued].present? ? 'present' : 'missing',
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.

I'm thinking would we want something like "N/A" here if we are checking passports rather than state id?

id_expiration_status: pii_from_doc[:state_id_expiration].present? ? 'present' : 'missing',
passport_issued_status: pii_from_doc[:passport_issued].present? ? 'present' : 'missing',
passport_expiration_status: pii_from_doc[:passport_expiration].present? ?
'present' : 'missing',
},
)
response.pii_from_doc = pii_from_doc
response
end

def self.pii_like_keypaths
def self.pii_like_keypaths(document_type:)
keypaths = [[:pii]]
attrs = %i[name dob dob_min_age address1 state zipcode jurisdiction state_id_number]
document_attrs = document_type&.downcase == 'passport' ?
DocPiiPassport.pii_like_keypaths :
DocPiiStateId.pii_like_keypaths

attrs = %i[name dob dob_min_age] + document_attrs

attrs.each do |k|
keypaths << [:errors, k]
keypaths << [:error_details, k]
Expand Down Expand Up @@ -84,6 +74,7 @@ def self.present_error(existing_errors)

PII_ERROR_KEYS = %i[name dob address1 state zipcode jurisdiction state_id_number
dob_min_age].freeze
STATE_ID_TYPES = ['drivers_license', 'state_id_card', 'identification_card'].freeze
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.

It was in a test suite causing this to fail. I see it may have been taken out on the latest branch so I will remove it.

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.

this was spec where not having 'identification_card' in STATE_ID_TYPES was causing it to fail https://github.com/18F/identity-idp/blob/main/spec/jobs/socure_docv_results_job_spec.rb#L120

Copy link
Copy Markdown
Contributor

@amirbey amirbey Mar 28, 2025

Choose a reason for hiding this comment

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

@theabrad ... i don't think you should remove identification_card here ... after digging deeper, i deleted my initial comment but I then realized why you had to add it and I created LG-16008 to clean it up 👍🏿

Copy link
Copy Markdown
Contributor

@amirbey amirbey Mar 27, 2025

Choose a reason for hiding this comment

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

i see you had to add identificatIon_card because it looks like we're processing the id type differently across vendors 😬
👀 LG-16008


attr_reader :pii_from_doc

Expand All @@ -108,22 +99,19 @@ def dob_valid?
end
end

def state_id_expired?
# temporary fix, tracked for removal in LG-15600
return if IdentityConfig.store.socure_docv_verification_data_test_mode &&
DateParser.parse_legacy(state_id_expiration) == Date.parse('2020-01-01')

if state_id_expiration && DateParser.parse_legacy(state_id_expiration).past?
errors.add(:state_id_expiration, generic_error, type: :state_id_expiration)
def state_id_or_passport
case state_id_type
when *STATE_ID_TYPES
state_id_validation = DocPiiStateId.new(pii: pii_from_doc)
state_id_validation.valid? || errors.merge!(state_id_validation.errors)
when 'passport'
passport_validation = DocPiiPassport.new(pii: pii_from_doc)
passport_validation.valid? || errors.merge!(passport_validation.errors)
else
errors.add(:no_document, generic_error, type: :no_document)
end
end

def zipcode_valid?
return if zipcode.is_a?(String) && zipcode.present?

errors.add(:zipcode, generic_error, type: :zipcode)
end

def generic_error
I18n.t('doc_auth.errors.general.no_liveness')
end
Expand Down
52 changes: 52 additions & 0 deletions app/forms/idv/doc_pii_passport.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Idv
class DocPiiPassport
include ActiveModel::Model

validates :birth_place,
:passport_issued,
:nationality_code,
:mrz,
presence: { message: proc { I18n.t('doc_auth.errors.general.no_liveness') } }

validates :issuing_country_code,
:nationality_code,
inclusion: {
in: 'USA', message: proc { I18n.t('doc_auth.errors.general.no_liveness') }
}

validate :passport_expired?

attr_reader :birth_place, :passport_expiration, :passport_issued, :state_id_type,
Copy link
Copy Markdown
Contributor

@amirbey amirbey Mar 27, 2025

Choose a reason for hiding this comment

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

seems like we should move away from this state_id_type naming ... created a ticket LG-16007

:issuing_country_code, :nationality_code, :mrz

def initialize(pii:)
@pii_from_doc = pii
@birth_place = pii[:birth_place]
@passport_expiration = pii[:passport_expiration]
@passport_issued = pii[:passport_issued]
@issuing_country_code = pii[:issuing_country_code]
@nationality_code = pii[:nationality_code]
@mrz = pii[:mrz]
end

def self.pii_like_keypaths
%i[birth_place passport_issued issuing_country_code nationality_code mrz]
end

private

attr_reader :pii_from_doc

def generic_error
I18n.t('doc_auth.errors.general.no_liveness')
end

def passport_expired?
if passport_expiration && DateParser.parse_legacy(passport_expiration).past?
errors.add(:passport_expiration, generic_error, type: :passport_expiration)
end
end
end
end
65 changes: 65 additions & 0 deletions app/forms/idv/doc_pii_state_id.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module Idv
class DocPiiStateId
include ActiveModel::Model

validates_presence_of :address1, { message: proc {
I18n.t('doc_auth.errors.alerts.address_check')
} }

validate :zipcode_valid?
validates :jurisdiction, :state, inclusion: { in: Idp::Constants::STATE_AND_TERRITORY_CODES,
message: proc {
I18n.t('doc_auth.errors.general.no_liveness')
} }

validates_presence_of :state_id_number, { message: proc {
I18n.t('doc_auth.errors.general.no_liveness')
} }
validate :state_id_expired?

attr_reader :address1, :state, :zipcode, :attention_with_barcode, :jurisdiction,
:state_id_number, :state_id_expiration
alias_method :attention_with_barcode?, :attention_with_barcode

def initialize(pii:)
@pii_from_doc = pii
@address1 = pii[:address1]
@state = pii[:state]
@zipcode = pii[:zipcode]
@jurisdiction = pii[:state_id_jurisdiction]
@state_id_number = pii[:state_id_number]
@state_id_expiration = pii[:state_id_expiration]
@attention_with_barcode = attention_with_barcode
end

def self.pii_like_keypaths
%i[address1 state zipcode jurisdiction state_id_number]
end

private

attr_reader :pii_from_doc

def generic_error
I18n.t('doc_auth.errors.general.no_liveness')
end

def state_id_expired?
# temporary fix, tracked for removal in LG-15600
return if IdentityConfig.store.socure_docv_verification_data_test_mode &&
DateParser.parse_legacy(state_id_expiration) == Date.parse('2020-01-01')

if state_id_expiration && DateParser.parse_legacy(state_id_expiration).past?
errors.add(:state_id_expiration, generic_error, type: :state_id_expiration)
end
end

def zipcode_valid?
return if zipcode.is_a?(String) && zipcode.present?

errors.add(:zipcode, generic_error, type: :zipcode)
end
end
end
6 changes: 6 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2082,6 +2082,8 @@ def idv_doc_auth_submitted_image_upload_vendor(
# @param [Boolean] liveness_checking_required Whether or not the selfie is required
# @param ["present","missing"] id_issued_status Status of state_id_issued field presence
# @param ["present","missing"] id_expiration_status Status of state_id_expiration field presence
# @param ["present","missing"] passport_issued_status Status of passport_issued field presence
# @param ["present","missing"] passport_expiration_status Status of passport_expiration field
# @param [Boolean] attention_with_barcode Whether result was attention with barcode
# @param [Integer] submit_attempts Times that user has tried submitting
# @param [String] front_image_fingerprint Fingerprint of front image data
Expand All @@ -2098,6 +2100,8 @@ def idv_doc_auth_submitted_pii_validation(
attention_with_barcode:,
id_issued_status:,
id_expiration_status:,
passport_issued_status:,
passport_expiration_status:,
submit_attempts:,
errors: nil,
error_details: nil,
Expand All @@ -2118,6 +2122,8 @@ def idv_doc_auth_submitted_pii_validation(
attention_with_barcode:,
id_issued_status:,
id_expiration_status:,
passport_issued_status:,
passport_expiration_status:,
submit_attempts:,
remaining_submit_attempts:,
flow_path:,
Expand Down
12 changes: 12 additions & 0 deletions spec/controllers/idv/image_uploads_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,8 @@
classification_info: a_kind_of(Hash),
id_issued_status: 'present',
id_expiration_status: 'present',
passport_issued_status: 'missing',
passport_expiration_status: 'missing',
document_type: an_instance_of(String),
)

Expand Down Expand Up @@ -543,6 +545,8 @@
),
id_issued_status: 'missing',
id_expiration_status: 'present',
passport_issued_status: 'missing',
passport_expiration_status: 'missing',
document_type: an_instance_of(String),
)
end
Expand Down Expand Up @@ -621,6 +625,8 @@
),
id_issued_status: 'missing',
id_expiration_status: 'present',
passport_issued_status: 'missing',
passport_expiration_status: 'missing',
document_type: an_instance_of(String),
)
end
Expand Down Expand Up @@ -696,6 +702,8 @@
classification_info: hash_including(:Front, :Back),
id_issued_status: 'missing',
id_expiration_status: 'present',
passport_issued_status: 'missing',
passport_expiration_status: 'missing',
document_type: an_instance_of(String),
)
end
Expand Down Expand Up @@ -770,6 +778,8 @@
classification_info: hash_including(:Front, :Back),
id_issued_status: 'missing',
id_expiration_status: 'present',
passport_issued_status: 'missing',
passport_expiration_status: 'missing',
document_type: an_instance_of(String),
)
end
Expand Down Expand Up @@ -845,6 +855,8 @@
classification_info: hash_including(:Front, :Back),
id_issued_status: 'missing',
id_expiration_status: 'present',
passport_issued_status: 'missing',
passport_expiration_status: 'missing',
document_type: an_instance_of(String),
)
end
Expand Down
8 changes: 4 additions & 4 deletions spec/features/idv/analytics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@
},
'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: false, doc_auth_result: 'Passed', liveness_checking_required: boolean, document_type: an_instance_of(String)),
'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', document_type: an_instance_of(String)
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)
},
'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 ssn visited' => {
Expand Down Expand Up @@ -364,7 +364,7 @@
},
'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'hybrid', attention_with_barcode: false, doc_auth_result: 'Passed', liveness_checking_required: boolean),
'IdV: doc auth image upload vendor pii validation' => {
success: true, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'hybrid', 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', document_type: an_instance_of(String)
success: true, user_id: user.uuid, submit_attempts: 1, remaining_submit_attempts: 3, flow_path: 'hybrid', 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)
},
'IdV: doc auth document_capture submitted' => {
success: true, flow_path: 'hybrid', step: 'document_capture', analytics_id: 'Doc Auth', selfie_check_required: boolean, liveness_checking_required: boolean
Expand Down Expand Up @@ -486,7 +486,7 @@
},
'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: false, doc_auth_result: 'Passed', liveness_checking_required: boolean),
'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', document_type: an_instance_of(String)
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)
},
'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,
Expand Down Expand Up @@ -730,7 +730,7 @@
},
'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: false, doc_auth_result: 'Passed'),
'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), selfie_image_fingerprint: an_instance_of(String), liveness_checking_required: boolean, classification_info: {}, id_issued_status: 'present', id_expiration_status: 'present', document_type: an_instance_of(String)
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), selfie_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)
},
'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,
Expand Down
2 changes: 1 addition & 1 deletion spec/features/idv/doc_auth/socure_document_capture_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@

context 'Pii validation fails' do
before do
allow_any_instance_of(Idv::DocPiiForm).to receive(:zipcode).and_return(:invalid_junk)
allow_any_instance_of(Idv::DocPiiStateId).to receive(:zipcode).and_return(:invalid_junk)
end

it 'presents as a type 1 error' do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@

context 'Pii validation fails' do
before do
allow_any_instance_of(Idv::DocPiiForm).to receive(:zipcode).and_return(:invalid_junk)
allow_any_instance_of(Idv::DocPiiStateId).to receive(:zipcode).and_return(:invalid_junk)
end

it 'presents as a type 1 error', js: true do
Expand Down
4 changes: 3 additions & 1 deletion spec/forms/idv/api_image_upload_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -558,10 +558,12 @@
success: false,
errors: { doc_pii: 'bad' },
extra: {
pii_like_keypaths: pii_like_keypaths,
pii_like_keypaths: pii_like_keypaths_state_id,
attention_with_barcode: false,
id_issued_status: 'missing',
id_expiration_status: 'missing',
passport_issued_status: 'missing',
passport_expiration_status: 'missing',
},
)
end
Expand Down
Loading