18F/identity-idp

View on GitHub
app/services/doc_auth/mock/result_response.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

module DocAuth
  module Mock
    class ResultResponse < DocAuth::Response
      include DocAuth::ClassificationConcern
      include DocAuth::SelfieConcern
      include DocAuth::Mock::YmlLoaderConcern

      attr_reader :uploaded_file, :config

      def initialize(uploaded_file, config, selfie_required = false)
        @uploaded_file = uploaded_file.to_s
        @config = config
        @selfie_required = selfie_required
        super(
          success: success?,
          errors: errors,
          pii_from_doc: pii_from_doc,
          doc_type_supported: id_type_supported?,
          selfie_live: selfie_live?,
          selfie_quality_good: selfie_quality_good?,
          selfie_status: selfie_status,
          extra: {
            doc_auth_result: doc_auth_result,
            portrait_match_results: portrait_match_results,
            billed: true,
            classification_info: classification_info,
            workflow: workflow,
            liveness_checking_required: @selfie_required,
            **@response_info.to_h,
          },
        )
      end

      def errors
        @errors ||= begin
          file_data = parsed_data_from_uploaded_file

          if file_data.blank?
            {}
          else
            doc_auth_result = file_data.dig('doc_auth_result')
            image_metrics = file_data.dig('image_metrics')
            failed = file_data.dig('failed_alerts')&.dup
            passed = file_data.dig('passed_alerts')
            face_match_result = file_data.dig('portrait_match_results', 'FaceMatchResult')
            classification_info = file_data.dig('classification_info')&.symbolize_keys
            # Pass and doc type is ok
            has_fields = [
              doc_auth_result,
              image_metrics,
              failed,
              passed,
              face_match_result,
              classification_info,
            ].any?(&:present?)

            if has_fields
              # Error generator is not to be called when it's not failure
              # allows us to test successful results
              return {} if all_doc_capture_values_passing?(
                doc_auth_result, id_type_supported?
              )

              mock_args = {}
              mock_args[:doc_auth_result] = doc_auth_result if doc_auth_result.present?
              mock_args[:image_metrics] = image_metrics.symbolize_keys if image_metrics.present?
              mock_args[:failed] = failed.map!(&:symbolize_keys) unless failed.nil?
              mock_args[:passed] = passed.map!(&:symbolize_keys) if passed.present?
              mock_args[:liveness_enabled] = face_match_result ? true : false
              mock_args[:classification_info] = classification_info if classification_info.present?
              @response_info = create_response_info(**mock_args)
              ErrorGenerator.new(config).generate_doc_auth_errors(@response_info)
            elsif file_data.include?(:general) # general is the key for errors from parsing
              file_data
            end
          end
        end
      end

      def pii_from_doc
        if parsed_data_from_uploaded_file.present?
          parsed_pii_from_doc
        else
          Pii::StateId.new(**Idp::Constants::MOCK_IDV_APPLICANT)
        end
      end

      def success?
        doc_auth_success? && (@selfie_required ? selfie_passed? : true)
      end

      def attention_with_barcode?
        parsed_alerts == [ATTENTION_WITH_BARCODE_ALERT]
      end

      def self.create_network_error_response
        errors = { network: true }
        DocAuth::Response.new(
          success: false,
          errors: errors,
          exception: Faraday::TimeoutError.new,
          extra: { vendor: 'Mock' },
        )
      end

      def doc_auth_success?
        (doc_auth_result_from_uploaded_file == 'Passed' ||
          errors.blank? ||
          attention_with_barcode?
        ) && id_type_supported?
      end

      def selfie_status
        if @selfie_required
          return :success if portrait_match_results&.dig(:FaceMatchResult).nil?
          portrait_match_results[:FaceMatchResult] == 'Pass' ? :success : :fail
        else
          :not_processed
        end
      end

      def workflow
        selfie_check_performed? ? 'test_liveness_workflow' : 'test_non_liveness_workflow'
      end

      private

      def parsed_alerts
        parsed_data_from_uploaded_file&.dig('failed_alerts')
      end

      def parsed_pii_from_doc
        if parsed_data_from_uploaded_file.has_key?('document')
          Pii::StateId.new(
            **Idp::Constants::MOCK_IDV_APPLICANT.merge(
              parsed_data_from_uploaded_file['document'].symbolize_keys,
            ).slice(*Pii::StateId.members),
          )
        end
      end

      def parsed_data_from_uploaded_file
        return @parsed_data_from_uploaded_file if defined?(@parsed_data_from_uploaded_file)

        @parsed_data_from_uploaded_file = parse_uri || parse_yaml(uploaded_file)
      end

      def doc_auth_result
        doc_auth_result_from_uploaded_file || doc_auth_result_from_success
      end

      def doc_auth_result_from_uploaded_file
        parsed_data_from_uploaded_file&.[]('doc_auth_result')
      end

      def portrait_match_results
        parsed_data_from_uploaded_file.dig('portrait_match_results')&.
          transform_keys! { |key| key.to_s.camelize }&.
          deep_symbolize_keys
      end

      def classification_info
        info = parsed_data_from_uploaded_file&.[]('classification_info') || {}
        info.to_h.symbolize_keys
      end

      def doc_auth_result_from_success
        if success?
          DocAuth::LexisNexis::ResultCodes::PASSED.name
        else
          DocAuth::LexisNexis::ResultCodes::CAUTION.name
        end
      end

      def all_doc_capture_values_passing?(doc_auth_result, id_type_supported)
        doc_auth_result == 'Passed' &&
          id_type_supported &&
          (selfie_check_performed? ? selfie_passed? : true)
      end

      def selfie_passed?
        selfie_status == :success
      end

      def parse_uri
        uri = URI.parse(uploaded_file.chomp)
        if uri.scheme == 'data'
          {}
        else
          { general: ["parsed URI, but scheme was #{uri.scheme} (expected data)"] }
        end
      rescue URI::InvalidURIError
        # no-op, allows falling through to YAML parsing
      end

      ATTENTION_WITH_BARCODE_ALERT = { 'name' => '2D Barcode Read', 'result' => 'Attention' }.freeze
      DEFAULT_FAILED_ALERTS = [{ name: '2D Barcode Read', result: 'Failed' }].freeze
      DEFAULT_IMAGE_METRICS = {
        front: {
          'VerticalResolution' => 600,
          'HorizontalResolution' => 600,
          'GlareMetric' => 100,
          'SharpnessMetric' => 100,
        },
        back: {
          'VerticalResolution' => 600,
          'HorizontalResolution' => 600,
          'GlareMetric' => 100,
          'SharpnessMetric' => 100,
        },
      }.freeze

      def create_response_info(
        doc_auth_result: 'Failed',
        passed: [],
        failed: DEFAULT_FAILED_ALERTS,
        liveness_enabled: false,
        image_metrics: DEFAULT_IMAGE_METRICS,
        classification_info: nil
      )
        merged_image_metrics = DEFAULT_IMAGE_METRICS.deep_merge(image_metrics)
        {
          vendor: 'Mock',
          doc_auth_result: doc_auth_result,
          processed_alerts: {
            passed: passed,
            failed: failed,
          },
          alert_failure_count: failed&.count.to_i,
          image_metrics: merged_image_metrics,
          liveness_enabled: liveness_enabled,
          classification_info: classification_info,
          portrait_match_results: selfie_check_performed? ? portrait_match_results : nil,
        }
      end
    end
  end
end