diff --git a/app/services/acuant/acuant_client.rb b/app/services/acuant/acuant_client.rb index ba93cf64a6e..429e285d0d1 100644 --- a/app/services/acuant/acuant_client.rb +++ b/app/services/acuant/acuant_client.rb @@ -33,7 +33,7 @@ def post_selfie(image:, instance_id:) merge_facial_match_and_liveness_response(facial_match_response, liveness_response) end - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def post_images(front_image:, back_image:, selfie_image:, liveness_checking_enabled: nil, instance_id: nil) document = create_document @@ -49,12 +49,16 @@ def post_images(front_image:, back_image:, selfie_image:, if results.success? && liveness_checking_enabled pii = results.pii_from_doc selfie_response = post_selfie(image: selfie_image, instance_id: instance_id) - Acuant::Responses::ResponseWithPii.new(selfie_response, pii) + Acuant::Responses::ResponseWithPii.new( + acuant_response: selfie_response, + pii: pii, + billed: results.result_code&.billed?, + ) else results end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength def get_results(instance_id:) Requests::GetResultsRequest.new(instance_id: instance_id).fetch diff --git a/app/services/acuant/responses/get_results_response.rb b/app/services/acuant/responses/get_results_response.rb index 0a12b28c138..59a7ccb47f3 100644 --- a/app/services/acuant/responses/get_results_response.rb +++ b/app/services/acuant/responses/get_results_response.rb @@ -1,9 +1,6 @@ module Acuant module Responses class GetResultsResponse < Acuant::Response - GOOD_RESULT = 1 - FYI_RESULT = 2 - def initialize(http_response) @http_response = http_response super( @@ -25,11 +22,17 @@ def pii_from_doc def to_h { success: success?, - erorrs: errors, + errors: errors, exception: exception, + result: result_code.name, } end + # @return [Acuant::ResultCode::ResultCode] + def result_code + Acuant::ResultCodes.from_int(parsed_response_body['Result']) + end + private attr_reader :http_response @@ -57,7 +60,7 @@ def raw_alerts end def successful_result? - parsed_response_body['Result'] == GOOD_RESULT + result_code == Acuant::ResultCodes::PASSED end end end diff --git a/app/services/acuant/responses/liveness_response.rb b/app/services/acuant/responses/liveness_response.rb index 98a89ce9e21..2ae6aba11ef 100644 --- a/app/services/acuant/responses/liveness_response.rb +++ b/app/services/acuant/responses/liveness_response.rb @@ -30,6 +30,7 @@ def extra_attributes { liveness_score: liveness_score, acuant_error: acuant_error, + liveness_assessment: liveness_assessment, } end @@ -42,7 +43,11 @@ def parsed_response_body end def successful_result? - parsed_response_body.dig('LivenessResult', 'LivenessAssessment') == 'Live' + liveness_assessment == 'Live' + end + + def liveness_assessment + parsed_response_body.dig('LivenessResult', 'LivenessAssessment') end end end diff --git a/app/services/acuant/responses/response_with_pii.rb b/app/services/acuant/responses/response_with_pii.rb index bcea99335f4..fc2a6fc6905 100644 --- a/app/services/acuant/responses/response_with_pii.rb +++ b/app/services/acuant/responses/response_with_pii.rb @@ -1,12 +1,12 @@ module Acuant module Responses class ResponseWithPii < Acuant::Response - def initialize(acuant_response, pii) + def initialize(acuant_response:, pii:, billed:) super( success: acuant_response.success?, errors: acuant_response.errors, exception: acuant_response.exception, - extra: acuant_response.extra, + extra: acuant_response.extra.merge(billed: billed), ) @pii = pii end diff --git a/app/services/acuant/result_codes.rb b/app/services/acuant/result_codes.rb new file mode 100644 index 00000000000..5377d0c3386 --- /dev/null +++ b/app/services/acuant/result_codes.rb @@ -0,0 +1,36 @@ +module Acuant + module ResultCodes + ResultCode = Struct.new(:code, :name, :billed) do + alias_method :billed?, :billed + end + + # The authentication test results are unknown. We are not billed for these + UNKNOWN = ResultCode.new(0, 'Unknown', false).freeze + # The authentication test passed. + PASSED = ResultCode.new(1, 'Passed', true).freeze + # The authentication test failed. + FAILED = ResultCode.new(2, 'Failed', true).freeze + # The authentication test was skipped (was not run). + SKIPPED = ResultCode.new(3, 'Skipped', true).freeze + # The authentication test was inconclusive and further investigation is warranted. + CAUTION = ResultCode.new(4, 'Caution', true).freeze + # The authentication test results requires user attention. + ATTENTION = ResultCode.new(5, 'Attention', true).freeze + + ALL = [ + UNKNOWN, + PASSED, + FAILED, + SKIPPED, + CAUTION, + ATTENTION, + ].freeze + + BY_CODE = ALL.map { |r| [r.code, r] }.to_h.freeze + + # @return [ResultCode] + def self.from_int(code) + BY_CODE[code] + end + end +end diff --git a/app/services/db/proofing_cost/add_user_proofing_cost.rb b/app/services/db/proofing_cost/add_user_proofing_cost.rb index 22d0b85fc33..4430e283822 100644 --- a/app/services/db/proofing_cost/add_user_proofing_cost.rb +++ b/app/services/db/proofing_cost/add_user_proofing_cost.rb @@ -4,6 +4,7 @@ class AddUserProofingCost TOKEN_WHITELIST = %i[ acuant_front_image acuant_back_image + acuant_result aamva lexis_nexis_resolution lexis_nexis_address diff --git a/app/services/doc_auth_mock/doc_auth_mock_client.rb b/app/services/doc_auth_mock/doc_auth_mock_client.rb index ce1f0a84222..4a980c735d5 100644 --- a/app/services/doc_auth_mock/doc_auth_mock_client.rb +++ b/app/services/doc_auth_mock/doc_auth_mock_client.rb @@ -1,4 +1,4 @@ -# rubocop:disable Lint/UnusedMethodArgument +# rubocop:disable Lint/UnusedMethodArgument, Metrics/ClassLength module DocAuthMock class DocAuthMockClient class << self @@ -48,7 +48,7 @@ def post_selfie(image:, instance_id:) Acuant::Response.new(success: true) end - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def post_images(front_image:, back_image:, selfie_image:, liveness_checking_enabled: nil, instance_id: nil) return mocked_response_for_method(__method__) if method_mocked?(__method__) @@ -64,12 +64,16 @@ def post_images(front_image:, back_image:, selfie_image:, if results.success? && liveness_checking_enabled pii = results.pii_from_doc selfie_response = post_selfie(image: selfie_image, instance_id: instance_id) - Acuant::Responses::ResponseWithPii.new(selfie_response, pii) + Acuant::Responses::ResponseWithPii.new( + acuant_response: selfie_response, + pii: pii, + billed: true, + ) else results end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength def get_results(instance_id:) return mocked_response_for_method(__method__) if method_mocked?(__method__) @@ -129,4 +133,4 @@ def failure(message, extra = nil) end end end -# rubocop:enable Lint/UnusedMethodArgument +# rubocop:enable Lint/UnusedMethodArgument, Metrics/ClassLength diff --git a/app/services/doc_auth_mock/responses/get_results_response.rb b/app/services/doc_auth_mock/responses/get_results_response.rb index 2136dc05415..d0cfc7b48b3 100644 --- a/app/services/doc_auth_mock/responses/get_results_response.rb +++ b/app/services/doc_auth_mock/responses/get_results_response.rb @@ -3,12 +3,19 @@ module Responses class GetResultsResponse < Acuant::Response attr_reader :pii_from_doc - def initialize(success: true, errors: [], exception: nil, pii_from_doc:) + def initialize( + success: true, + errors: [], + exception: nil, + pii_from_doc:, + billed: true + ) @pii_from_doc = pii_from_doc super( success: success, errors: errors, exception: exception, + extra: { billed: billed }, ) end end diff --git a/app/services/doc_auth_mock/result_response_builder.rb b/app/services/doc_auth_mock/result_response_builder.rb index 0abe81e0356..1085da792b6 100644 --- a/app/services/doc_auth_mock/result_response_builder.rb +++ b/app/services/doc_auth_mock/result_response_builder.rb @@ -27,6 +27,7 @@ def call success: success?, errors: errors, pii_from_doc: pii_from_doc, + billed: true, ) end diff --git a/app/services/idv/steps/back_image_step.rb b/app/services/idv/steps/back_image_step.rb index 33f66a3d47a..bcaa19bb69b 100644 --- a/app/services/idv/steps/back_image_step.rb +++ b/app/services/idv/steps/back_image_step.rb @@ -19,6 +19,7 @@ def fetch_doc_auth_results_or_redirect_to_selfie return if liveness_checking_enabled? get_results_response = doc_auth_client.get_results(instance_id: flow_session[:instance_id]) + add_cost(:acuant_result) if get_results_response.to_h[:billed] if get_results_response.success? mark_step_complete(:selfie) save_proofing_components diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb index 3c74fef5b7b..045d8a930bb 100644 --- a/app/services/idv/steps/doc_auth_base_step.rb +++ b/app/services/idv/steps/doc_auth_base_step.rb @@ -2,9 +2,6 @@ module Idv module Steps class DocAuthBaseStep < Flow::BaseStep - GOOD_RESULT = 1 - FYI_RESULT = 2 - def initialize(flow) super(flow, :doc_auth) end @@ -135,7 +132,7 @@ def post_images liveness_checking_enabled: liveness_checking_enabled?, ) # DP: should these cost recordings happen in the doc_auth_client? - add_costs + add_costs(result) result end @@ -171,10 +168,11 @@ def add_cost(token) Db::ProofingCost::AddUserProofingCost.call(user_id, token) end - def add_costs + def add_costs(result) add_cost(:acuant_front_image) add_cost(:acuant_back_image) add_cost(:acuant_selfie) if liveness_checking_enabled? + add_cost(:acuant_result) if result.to_h[:billed] end def sp_session diff --git a/db/migrate/20200731213827_add_acuant_result_to_proofing_costs.rb b/db/migrate/20200731213827_add_acuant_result_to_proofing_costs.rb new file mode 100644 index 00000000000..fbc9e53b143 --- /dev/null +++ b/db/migrate/20200731213827_add_acuant_result_to_proofing_costs.rb @@ -0,0 +1,5 @@ +class AddAcuantResultToProofingCosts < ActiveRecord::Migration[5.2] + def change + add_column :proofing_costs, :acuant_result_count, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index b423803ba02..20591c48235 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.define(version: 2020_07_23_214611) do +ActiveRecord::Schema.define(version: 2020_07_31_213827) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -380,6 +380,7 @@ t.integer "phone_otp_count", default: 0 t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "acuant_result_count", default: 0 t.index ["user_id"], name: "index_proofing_costs_on_user_id", unique: true end diff --git a/spec/features/reports/proofing_costs_report_spec.rb b/spec/features/reports/proofing_costs_report_spec.rb index a971fa403c7..1fdb2b913fb 100644 --- a/spec/features/reports/proofing_costs_report_spec.rb +++ b/spec/features/reports/proofing_costs_report_spec.rb @@ -4,7 +4,7 @@ include IdvStepHelper include DocAuthHelper - let(:subject) { Reports::ProofingCostsReport } + let(:report) { JSON.parse(Reports::ProofingCostsReport.new.call) } let(:user) { create(:user, :signed_up) } let(:user2) { create(:user, :signed_up) } let(:summary1) do @@ -21,6 +21,7 @@ { 'acuant_front_image_count_average' => 1.0, 'acuant_back_image_count_average' => 1.0, + 'acuant_result_count_average' => 1.0, 'aamva_count_average' => 1.0, 'lexis_nexis_resolution_count_average' => 1.0, 'gpo_letter_count_average' => 0.0, @@ -30,14 +31,14 @@ end it 'works for no records' do - expect(JSON.parse(subject.new.call)).to eq({}) + expect(report).to eq({}) end it 'works for one flow' do sign_in_and_2fa_user(user) complete_doc_auth_steps_before_doc_success_step - expect(JSON.parse(subject.new.call)).to eq(doc_success_funnel.merge(summary1)) + expect(report).to eq(doc_success_funnel.merge(summary1)) end it 'works for two flows' do @@ -46,6 +47,6 @@ sign_in_and_2fa_user(user2) complete_doc_auth_steps_before_doc_success_step - expect(JSON.parse(subject.new.call)).to eq(doc_success_funnel.merge(summary2)) + expect(report).to eq(doc_success_funnel.merge(summary2)) end end diff --git a/spec/services/acuant/responses/get_results_response_spec.rb b/spec/services/acuant/responses/get_results_response_spec.rb index 89e1df14e03..bd444395d57 100644 --- a/spec/services/acuant/responses/get_results_response_spec.rb +++ b/spec/services/acuant/responses/get_results_response_spec.rb @@ -15,6 +15,14 @@ expect(response.success?).to eq(true) expect(response.errors).to eq([]) expect(response.exception).to be_nil + expect(response.to_h).to eq( + success: true, + errors: [], + exception: nil, + result: 'Passed', + ) + expect(response.result_code).to eq(Acuant::ResultCodes::PASSED) + expect(response.result_code.billed?).to eq(true) end it 'parsed PII from the doc' do @@ -52,6 +60,8 @@ [I18n.t('friendly_errors.doc_auth.document_type_could_not_be_determined')], ) expect(response.exception).to be_nil + expect(response.result_code).to eq(Acuant::ResultCodes::UNKNOWN) + expect(response.result_code.billed?).to eq(false) end context 'when a friendly error does not exist for the acuant error message' do diff --git a/spec/services/acuant/responses/liveness_response_spec.rb b/spec/services/acuant/responses/liveness_response_spec.rb index 9a82ced4200..2bea2331814 100644 --- a/spec/services/acuant/responses/liveness_response_spec.rb +++ b/spec/services/acuant/responses/liveness_response_spec.rb @@ -17,6 +17,7 @@ success: true, errors: [], exception: nil, + liveness_assessment: 'Live', liveness_score: 99, acuant_error: { message: nil, code: nil }, ) @@ -39,6 +40,7 @@ success: false, errors: [I18n.t('errors.doc_auth.selfie')], exception: nil, + liveness_assessment: nil, liveness_score: nil, acuant_error: { message: 'Face is too small. Move the camera closer to the face and retake the picture.', diff --git a/spec/services/acuant/result_codes_spec.rb b/spec/services/acuant/result_codes_spec.rb new file mode 100644 index 00000000000..9af0fcc207f --- /dev/null +++ b/spec/services/acuant/result_codes_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe Acuant::ResultCodes do + describe '.from_int' do + it 'is a result code for the int' do + result_code = Acuant::ResultCodes.from_int(1) + expect(result_code).to be_a(Acuant::ResultCodes::ResultCode) + expect(result_code.billed?).to eq(true) + end + + it 'is nil when there is no matching code' do + result_code = Acuant::ResultCodes.from_int(999) + expect(result_code).to be_nil + end + end +end