diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index 241c1c35a9b..d56dd51de67 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -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], } diff --git a/app/forms/idv/doc_pii_form.rb b/app/forms/idv/doc_pii_form.rb index 07acb09c7fd..d9afff38fe9 100644 --- a/app/forms/idv/doc_pii_form.rb +++ b/app/forms/idv/doc_pii_form.rb @@ -6,22 +6,9 @@ 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) @@ -29,12 +16,7 @@ def initialize(pii:, attention_with_barcode: false) @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 @@ -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', 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] @@ -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 attr_reader :pii_from_doc @@ -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 diff --git a/app/forms/idv/doc_pii_passport.rb b/app/forms/idv/doc_pii_passport.rb new file mode 100644 index 00000000000..417be6b871c --- /dev/null +++ b/app/forms/idv/doc_pii_passport.rb @@ -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, + :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 diff --git a/app/forms/idv/doc_pii_state_id.rb b/app/forms/idv/doc_pii_state_id.rb new file mode 100644 index 00000000000..751ad30fe86 --- /dev/null +++ b/app/forms/idv/doc_pii_state_id.rb @@ -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 diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 03271de8fba..0a513410e6e 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -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 @@ -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, @@ -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:, diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 1f31e38aaee..a4b9606bc72 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -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), ) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 4704ed2e2af..d6681bf0236 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -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' => { @@ -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 @@ -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, @@ -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, diff --git a/spec/features/idv/doc_auth/socure_document_capture_spec.rb b/spec/features/idv/doc_auth/socure_document_capture_spec.rb index c2734d51653..f7a0ae0e0cb 100644 --- a/spec/features/idv/doc_auth/socure_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/socure_document_capture_spec.rb @@ -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 diff --git a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb index dc7fa90cb63..bb848f82c0f 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb @@ -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 diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index 6302f31f993..d1842a2b2ce 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -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 diff --git a/spec/forms/idv/doc_pii_form_spec.rb b/spec/forms/idv/doc_pii_form_spec.rb index 4fa18c4e8e6..73ee9ce1a86 100644 --- a/spec/forms/idv/doc_pii_form_spec.rb +++ b/spec/forms/idv/doc_pii_form_spec.rb @@ -7,10 +7,43 @@ let(:subject) { Idv::DocPiiForm.new(pii: pii) } let(:valid_dob) { (Time.zone.today - (IdentityConfig.store.idv_min_age_years + 1).years).to_s } let(:valid_state_id_expiration) { Time.zone.today.to_s } + let(:state_id_type) { 'drivers_license' } let(:too_young_dob) do (Time.zone.today - (IdentityConfig.store.idv_min_age_years - 1).years).to_s end - let(:good_pii) do + let(:mrz) do + 'P