diff --git a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb new file mode 100644 index 00000000000..6fbe3ab1bb7 --- /dev/null +++ b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb @@ -0,0 +1,87 @@ +module DocAuth + module LexisNexis + module DocPiiReader + PII_EXCLUDES = %w[ + Age + DocSize + DOB_Day + DOB_Month + DOB_Year + ExpirationDate_Day + ExpirationDate_Month + ExpirationDate_Year + FullName + Portrait + Sex + ].freeze + + private + + def read_pii(true_id_product) + return {} unless true_id_product&.dig(:IDAUTH_FIELD_DATA).present? + pii = {} + PII_INCLUDES.each do |true_id_key, idp_key| + pii[idp_key] = true_id_product[:IDAUTH_FIELD_DATA][true_id_key] + end + pii[:state_id_type] = DocAuth::Response::ID_TYPE_SLUGS[pii[:state_id_type]] + + dob = parse_date( + year: pii.delete(:dob_year), + month: pii.delete(:dob_month), + day: pii.delete(:dob_day), + ) + pii[:dob] = dob if dob + + exp_date = parse_date( + year: pii.delete(:state_id_expiration_year), + month: pii.delete(:state_id_expiration_month), + day: pii.delete(:state_id_expiration_day), + ) + pii[:state_id_expiration] = exp_date if exp_date + + issued_date = parse_date( + year: pii.delete(:state_id_issued_year), + month: pii.delete(:state_id_issued_month), + day: pii.delete(:state_id_issued_day), + ) + pii[:state_id_issued] = issued_date if issued_date + + pii + end + + PII_INCLUDES = { + 'Fields_FirstName' => :first_name, + 'Fields_MiddleName' => :middle_name, + 'Fields_Surname' => :last_name, + 'Fields_AddressLine1' => :address1, + 'Fields_AddressLine2' => :address2, + 'Fields_City' => :city, + 'Fields_State' => :state, + 'Fields_PostalCode' => :zipcode, + 'Fields_DOB_Year' => :dob_year, + 'Fields_DOB_Month' => :dob_month, + 'Fields_DOB_Day' => :dob_day, + 'Fields_DocumentNumber' => :state_id_number, + 'Fields_IssuingStateCode' => :state_id_jurisdiction, + 'Fields_xpirationDate_Day' => :state_id_expiration_day, # this is NOT a typo + 'Fields_ExpirationDate_Month' => :state_id_expiration_month, + 'Fields_ExpirationDate_Year' => :state_id_expiration_year, + 'Fields_IssueDate_Day' => :state_id_issued_day, + 'Fields_IssueDate_Month' => :state_id_issued_month, + 'Fields_IssueDate_Year' => :state_id_issued_year, + 'Fields_DocumentClassName' => :state_id_type, + 'Fields_CountryCode' => :issuing_country_code, + }.freeze + + def parse_date(year:, month:, day:) + Date.new(year.to_i, month.to_i, day.to_i).to_s if year.to_i.positive? + rescue ArgumentError + message = { + event: 'Failure to parse TrueID date', + }.to_json + Rails.logger.info(message) + nil + end + end + end +end diff --git a/app/services/doc_auth/lexis_nexis/image_metrics_reader.rb b/app/services/doc_auth/lexis_nexis/image_metrics_reader.rb new file mode 100644 index 00000000000..953d31fb8fb --- /dev/null +++ b/app/services/doc_auth/lexis_nexis/image_metrics_reader.rb @@ -0,0 +1,29 @@ +module DocAuth + module LexisNexis + module ImageMetricsReader + private + + def read_image_metrics(true_id_product) + image_metrics = {} + return image_metrics unless true_id_product&.dig(:ParameterDetails).present? + true_id_product[:ParameterDetails].each do |detail| + next unless detail[:Group] == 'IMAGE_METRICS_RESULT' + + inner_val = detail.dig(:Values).collect { |value| value.dig(:Value) } + image_metrics[detail[:Name]] = inner_val + end + + transform_metrics(image_metrics) + end + + def transform_metrics(img_metrics) + new_metrics = {} + img_metrics['Side']&.each_with_index do |side, i| + new_metrics[side.downcase.to_sym] = img_metrics.transform_values { |v| v[i] } + end + + new_metrics + end + end + end +end diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb index 9e326260ed4..f23cd0b65b9 100644 --- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb +++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb @@ -4,56 +4,23 @@ module DocAuth module LexisNexis module Responses class TrueIdResponse < DocAuth::Response + include ImageMetricsReader + include DocPiiReader include ClassificationConcern include SelfieConcern - PII_EXCLUDES = %w[ - Age - DocSize - DOB_Day - DOB_Month - DOB_Year - ExpirationDate_Day - ExpirationDate_Month - ExpirationDate_Year - FullName - Portrait - Sex - ].freeze - - PII_INCLUDES = { - 'Fields_FirstName' => :first_name, - 'Fields_MiddleName' => :middle_name, - 'Fields_Surname' => :last_name, - 'Fields_AddressLine1' => :address1, - 'Fields_AddressLine2' => :address2, - 'Fields_City' => :city, - 'Fields_State' => :state, - 'Fields_PostalCode' => :zipcode, - 'Fields_DOB_Year' => :dob_year, - 'Fields_DOB_Month' => :dob_month, - 'Fields_DOB_Day' => :dob_day, - 'Fields_DocumentNumber' => :state_id_number, - 'Fields_IssuingStateCode' => :state_id_jurisdiction, - 'Fields_xpirationDate_Day' => :state_id_expiration_day, # this is NOT a typo - 'Fields_ExpirationDate_Month' => :state_id_expiration_month, - 'Fields_ExpirationDate_Year' => :state_id_expiration_year, - 'Fields_IssueDate_Day' => :state_id_issued_day, - 'Fields_IssueDate_Month' => :state_id_issued_month, - 'Fields_IssueDate_Year' => :state_id_issued_year, - 'Fields_DocumentClassName' => :state_id_type, - 'Fields_CountryCode' => :issuing_country_code, - }.freeze + attr_reader :config, :http_response def initialize(http_response, config, liveness_checking_enabled = false) @config = config @http_response = http_response @liveness_checking_enabled = liveness_checking_enabled + @pii_from_doc = read_pii(true_id_product) super( success: successful_result?, errors: error_messages, extra: extra_attributes, - pii_from_doc: pii_from_doc, + pii_from_doc: @pii_from_doc, ) rescue StandardError => e NewRelic::Agent.notice_error(e) @@ -72,7 +39,7 @@ def successful_result? def error_messages return {} if successful_result? - if true_id_product&.dig(:AUTHENTICATION_RESULT).present? + if with_authentication_result? ErrorGenerator.new(config).generate_doc_auth_errors(response_info) elsif true_id_product.present? ErrorGenerator.wrapped_general_error(@liveness_checking_enabled) @@ -82,7 +49,7 @@ def error_messages end def extra_attributes - if true_id_product&.dig(:AUTHENTICATION_RESULT).present? + if with_authentication_result? attrs = response_info.merge(true_id_product[:AUTHENTICATION_RESULT]) attrs.reject! do |k, _v| PII_EXCLUDES.include?(k) || k.start_with?('Alert_') @@ -98,38 +65,6 @@ def extra_attributes basic_logging_info.merge(attrs) end - def pii_from_doc - return {} unless true_id_product&.dig(:IDAUTH_FIELD_DATA).present? - pii = {} - PII_INCLUDES.each do |true_id_key, idp_key| - pii[idp_key] = true_id_product[:IDAUTH_FIELD_DATA][true_id_key] - end - pii[:state_id_type] = DocAuth::Response::ID_TYPE_SLUGS[pii[:state_id_type]] - - dob = parse_date( - year: pii.delete(:dob_year), - month: pii.delete(:dob_month), - day: pii.delete(:dob_day), - ) - pii[:dob] = dob if dob - - exp_date = parse_date( - year: pii.delete(:state_id_expiration_year), - month: pii.delete(:state_id_expiration_month), - day: pii.delete(:state_id_expiration_day), - ) - pii[:state_id_expiration] = exp_date if exp_date - - issued_date = parse_date( - year: pii.delete(:state_id_issued_year), - month: pii.delete(:state_id_issued_month), - day: pii.delete(:state_id_issued_day), - ) - pii[:state_id_issued] = issued_date if issued_date - - pii - end - def attention_with_barcode? return false unless doc_auth_result_attention? @@ -143,10 +78,14 @@ def billed? end def doc_auth_success? - transaction_status_passed? && + # really it's everything else excluding selfie + ((transaction_status_passed? && true_id_product.present? && product_status_passed? && doc_auth_result_passed? + ) || + attention_with_barcode? + ) && id_type_supported? end # @return [:success, :fail, :not_processed] @@ -230,7 +169,7 @@ def create_response_info alert_failure_count: alerts[:failed]&.count.to_i, log_alert_results: log_alert_formatter.log_alerts(alerts), portrait_match_results: portrait_match_results, - image_metrics: parse_image_metrics, + image_metrics: read_image_metrics(true_id_product), address_line2_present: !pii_from_doc[:address2].blank?, classification_info: classification_info, liveness_enabled: @liveness_checking_enabled, @@ -255,7 +194,7 @@ def all_passed? end def selfie_result - response_info&.dig(:portrait_match_results, :FaceMatchResult) + portrait_match_results&.dig(:FaceMatchResult) end def product_status_passed? @@ -324,7 +263,7 @@ def parsed_alerts return @new_alerts if defined?(@new_alerts) @new_alerts = { passed: [], failed: [] } - return @new_alerts unless true_id_product&.dig(:AUTHENTICATION_RESULT).present? + return @new_alerts unless with_authentication_result? all_alerts = true_id_product[:AUTHENTICATION_RESULT].select do |key| key.start_with?('Alert_') end @@ -363,28 +302,6 @@ def combine_alert_data(all_alerts, alert_name, region_details) new_alert_data end - def parse_image_metrics - image_metrics = {} - return image_metrics unless true_id_product&.dig(:ParameterDetails).present? - true_id_product[:ParameterDetails].each do |detail| - next unless detail[:Group] == 'IMAGE_METRICS_RESULT' - - inner_val = detail.dig(:Values).collect { |value| value.dig(:Value) } - image_metrics[detail[:Name]] = inner_val - end - - transform_metrics(image_metrics) - end - - def transform_metrics(img_metrics) - new_metrics = {} - img_metrics['Side']&.each_with_index do |side, i| - new_metrics[side.downcase.to_sym] = img_metrics.transform_values { |v| v[i] } - end - - new_metrics - end - # Generate a hash for image references information that can be linked to Alert # @return A hash with region_id => {:key : 'What region', :side: 'Front|Back'} def parse_document_region @@ -432,14 +349,8 @@ def transform_document_region(region_details, image_sides) end end - def parse_date(year:, month:, day:) - Date.new(year.to_i, month.to_i, day.to_i).to_s if year.to_i.positive? - rescue ArgumentError - message = { - event: 'Failure to parse TrueID date', - }.to_json - Rails.logger.info(message) - nil + def with_authentication_result? + true_id_product&.dig(:AUTHENTICATION_RESULT).present? end end end diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index 8beb314726c..4cb188025cc 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -131,7 +131,10 @@ def self.create_network_error_response end def doc_auth_success? - doc_auth_result_from_uploaded_file == 'Passed' || errors.blank? + (doc_auth_result_from_uploaded_file == 'Passed' || + errors.blank? || + attention_with_barcode? + ) && id_type_supported? end def selfie_status diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 1ce36b10b1b..7eb9b77643a 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -431,6 +431,7 @@ def get_decision_product(resp) end it 'produces reasonable output for a malformed TrueID response' do + allow(NewRelic::Agent).to receive(:notice_error) output = described_class.new(failure_response_malformed, config).to_h expect(output[:success]).to eq(false) @@ -654,6 +655,13 @@ def get_decision_product(resp) expect(response.doc_auth_success?).to eq(false) end end + + context 'when attention barcode read' do + let(:response) { described_class.new(attention_barcode_read, config) } + it 'returns true' do + expect(response.doc_auth_success?).to eq(true) + end + end end describe '#selfie_status' do