diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index afe4b702609..db9eab7cdb4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -63,7 +63,7 @@ stages: - after_test - review - scan - - deploy_production + - deploy_eks workflow: rules: @@ -604,21 +604,6 @@ stop-review-app: - if: $CI_PIPELINE_SOURCE != "merge_request_event" when: never -deploy_production: - stage: deploy_production - allow_failure: true - needs: - - job: build-review-image - resource_group: $CI_ENVIRONMENT_SLUG.reviewapps.identitysandbox.gov - extends: .deploy - environment: - name: production - deployment_tier: production - url: https://$CI_ENVIRONMENT_SLUG.reviewapps.identitysandbox.gov - rules: - - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push" - - include: - template: Jobs/SAST.gitlab-ci.yml - template: Jobs/Dependency-Scanning.gitlab-ci.yml @@ -867,3 +852,15 @@ audit_packages_scheduled: fi rules: - if: $CI_PIPELINE_SOURCE == "schedule" + +# EKS deployment +deploy_eks: + trigger: + project: lg-public/identity-eks-control + branch: main + stage: deploy_eks + variables: + APP: idp + IMAGE_TAG: $CI_COMMIT_SHA + rules: + - if: $CI_COMMIT_BRANCH == "main" diff --git a/app/helpers/ipp_helper.rb b/app/helpers/ipp_helper.rb index a08e3394e5e..24c3533d2e1 100644 --- a/app/helpers/ipp_helper.rb +++ b/app/helpers/ipp_helper.rb @@ -8,8 +8,14 @@ def scrub_message(message) def scrub_body(body) return nil if body.nil? - body = body.with_indifferent_access - body[:responseMessage] = scrub_message(body[:responseMessage]) - body + if body.is_a?(String) + scrub_message(body) + else + body = body.with_indifferent_access + if body[:responseMessage].present? + body[:responseMessage] = scrub_message(body[:responseMessage]) + end + body + end end end diff --git a/app/jobs/reports/duplicate_ssn_report.rb b/app/jobs/reports/duplicate_ssn_report.rb index 2e727d77e16..bbb1da3be1b 100644 --- a/app/jobs/reports/duplicate_ssn_report.rb +++ b/app/jobs/reports/duplicate_ssn_report.rb @@ -38,10 +38,12 @@ def report_body ssn_signatures = todays_profiles.map(&:ssn_signature).uniq - profiles_connected_by_ssn = Profile. - includes(:user). - where(ssn_signature: ssn_signatures). - to_a + profiles_connected_by_ssn = ssn_signatures.each_slice(1000).flat_map do |ssn_signature_slice| + Profile. + includes(:user). + where(ssn_signature: ssn_signature_slice). + to_a + end profiles_connected_by_ssn.sort_by!(&:id).reverse! diff --git a/app/jobs/reports/idv_legacy_conversion_supplement_report.rb b/app/jobs/reports/idv_legacy_conversion_supplement_report.rb new file mode 100644 index 00000000000..dbc8c95dab0 --- /dev/null +++ b/app/jobs/reports/idv_legacy_conversion_supplement_report.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'csv' + +module Reports + class IdvLegacyConversionSupplementReport < BaseReport + REPORT_NAME = 'idv-legacy-conversion-supplement-report' + + def perform(_date) + csv = build_csv + save_report(REPORT_NAME, csv, extension: 'csv') + end + + # @return [String] CSV report + def build_csv + sql = <<~SQL + SELECT + iaa_orders.start_date + , iaa_orders.end_date + , iaa_orders.order_number + , iaa_gtcs.gtc_number AS gtc_number + , upgrade.issuer AS issuer + , sp.friendly_name AS friendly_name + , DATE_TRUNC('month', upgrade.upgraded_at) AS year_month + , COUNT(DISTINCT upgrade.user_id) AS user_count + FROM iaa_orders + INNER JOIN integration_usages iu ON iu.iaa_order_id = iaa_orders.id + INNER JOIN integrations ON integrations.id = iu.integration_id + INNER JOIN iaa_gtcs ON iaa_gtcs.id = iaa_orders.iaa_gtc_id + INNER JOIN service_providers sp ON sp.issuer = integrations.issuer + INNER JOIN ( + SELECT DISTINCT ON (user_id) * + FROM sp_upgraded_biometric_profiles + ) upgrade ON upgrade.issuer = integrations.issuer + WHERE upgrade.upgraded_at BETWEEN iaa_orders.start_date AND iaa_orders.end_date + GROUP BY iaa_orders.id, upgrade.issuer, year_month, iaa_gtcs.gtc_number, sp.friendly_name + ORDER BY iaa_orders.id, year_month + SQL + + results = transaction_with_timeout do + ActiveRecord::Base.connection.select_all(sql) + end + + CSV.generate do |csv| + csv << [ + 'iaa_order_number', + 'iaa_start_date', + 'iaa_end_date', + 'issuer', + 'friendly_name', + 'year_month', + 'year_month_readable', + 'user_count', + ] + + results.each do |iaa| + csv << [ + IaaReportingHelper.key( + gtc_number: iaa['gtc_number'], + order_number: iaa['order_number'], + ), + iaa['start_date'], + iaa['end_date'], + iaa['issuer'], + iaa['friendly_name'], + iaa['year_month'].strftime('%Y%m'), + iaa['year_month'].strftime('%B %Y'), + iaa['user_count'], + ] + end + end + end + end +end diff --git a/app/models/profile.rb b/app/models/profile.rb index e5c9671d4bf..12edabbee1e 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -92,7 +92,9 @@ def activate(reason_deactivated: nil) confirm_that_profile_can_be_activated! now = Time.zone.now - is_reproof = Profile.find_by(user_id: user_id, active: true) + profile_to_deactivate = Profile.find_by(user_id: user_id, active: true) + is_reproof = profile_to_deactivate.present? + is_biometric_upgrade = is_reproof && biometric? && !profile_to_deactivate.biometric? attrs = { active: true, @@ -105,6 +107,7 @@ def activate(reason_deactivated: nil) Profile.where(user_id: user_id).update_all(active: false) update!(attrs) end + track_biometric_reproof if is_biometric_upgrade send_push_notifications if is_reproof end # rubocop:enable Rails/SkipsModelValidations @@ -199,8 +202,8 @@ def deactivate_due_to_gpo_expiration def deactivate_due_to_in_person_verification_cancelled update!( active: false, - deactivation_reason: :verification_cancelled, in_person_verification_pending_at: nil, + deactivation_reason: deactivation_reason.presence || :verification_cancelled, ) end @@ -306,6 +309,10 @@ def profile_age_in_seconds (Time.zone.now - created_at).round end + def biometric? + ::User::BIOMETRIC_COMPARISON_IDV_LEVELS.include?(idv_level) + end + private def confirm_that_profile_can_be_activated! @@ -333,4 +340,13 @@ def send_push_notifications event = PushNotification::ReproofCompletedEvent.new(user: user) PushNotification::HttpPush.deliver(event) end + + def track_biometric_reproof + SpUpgradedBiometricProfile.create( + user: user, + upgraded_at: Time.zone.now, + idv_level: idv_level, + issuer: initiating_service_provider_issuer, + ) + end end diff --git a/app/models/sp_upgraded_biometric_profile.rb b/app/models/sp_upgraded_biometric_profile.rb new file mode 100644 index 00000000000..d3a483d8bfd --- /dev/null +++ b/app/models/sp_upgraded_biometric_profile.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class SpUpgradedBiometricProfile < ApplicationRecord + belongs_to :user +end diff --git a/app/services/iaa_reporting_helper.rb b/app/services/iaa_reporting_helper.rb index 8c25a892c7d..2422c1d71df 100644 --- a/app/services/iaa_reporting_helper.rb +++ b/app/services/iaa_reporting_helper.rb @@ -13,7 +13,7 @@ module IaaReportingHelper ) do # ex LG123567-0001 def key - "#{gtc_number}-#{format('%04d', order_number)}" + IaaReportingHelper.key(gtc_number:, order_number:) end end @@ -74,4 +74,8 @@ def partner_accounts end end.compact end + + def key(gtc_number:, order_number:) + "#{gtc_number}-#{format('%04d', order_number)}" + end end diff --git a/app/services/proofing/aamva/proofer.rb b/app/services/proofing/aamva/proofer.rb index 374a83572f1..b9b3bf1aee1 100644 --- a/app/services/proofing/aamva/proofer.rb +++ b/app/services/proofing/aamva/proofer.rb @@ -21,6 +21,13 @@ class Proofer ], ).freeze + REQUIRED_VERIFICATION_ATTRIBUTES = %i[ + state_id_number + dob + last_name + first_name + ].freeze + ADDRESS_ATTRIBUTES = [ :address1, :address2, @@ -63,7 +70,7 @@ def proof(applicant) def build_result_from_response(verification_response, jurisdiction) Proofing::StateIdResult.new( - success: verification_response.success?, + success: successful?(verification_response), errors: parse_verification_errors(verification_response), exception: nil, vendor_name: 'aamva:state_id', @@ -77,7 +84,7 @@ def build_result_from_response(verification_response, jurisdiction) def parse_verification_errors(verification_response) errors = Hash.new { |h, k| h[k] = [] } - return errors if verification_response.success? + return errors if successful?(verification_response) verification_response.verification_results.each do |attribute, v_result| attribute_key = attribute.to_sym @@ -121,6 +128,14 @@ def send_to_new_relic(result) NewRelic::Agent.notice_error(result.exception) end + def successful?(verification_response) + REQUIRED_VERIFICATION_ATTRIBUTES.each do |verification_attribute| + return false unless verification_response.verification_results[verification_attribute] + end + + true + end + def jurisdiction_in_maintenance_window?(state) Idv::AamvaStateMaintenanceWindow.in_maintenance_window?(state) end diff --git a/app/services/proofing/aamva/response/verification_response.rb b/app/services/proofing/aamva/response/verification_response.rb index adcbdadfb7b..c0fc81d40d9 100644 --- a/app/services/proofing/aamva/response/verification_response.rb +++ b/app/services/proofing/aamva/response/verification_response.rb @@ -22,13 +22,6 @@ class VerificationResponse 'AddressZIP5MatchIndicator' => :zipcode, }.freeze - REQUIRED_VERIFICATION_ATTRIBUTES = %i[ - state_id_number - dob - last_name - first_name - ].freeze - attr_reader :verification_results, :transaction_locator_id def initialize(http_response) @@ -48,26 +41,6 @@ def initialize(http_response) raise VerificationError.new(error_message) end - def reasons - REQUIRED_VERIFICATION_ATTRIBUTES.map do |verification_attribute| - verification_result = verification_results[verification_attribute] - case verification_result - when false - "Failed to verify #{verification_attribute}" - when nil - "Response was missing #{verification_attribute}" - end - end.compact - end - - def success? - REQUIRED_VERIFICATION_ATTRIBUTES.each do |verification_attribute| - return false unless verification_results[verification_attribute] - end - - true - end - private attr_reader :http_response, :missing_attributes diff --git a/config/application.yml.default b/config/application.yml.default index 63d0fd90c9b..92057c61299 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -62,10 +62,12 @@ component_previews_enabled: false compromised_password_randomizer_threshold: 900 compromised_password_randomizer_value: 1000 country_phone_number_overrides: '{}' +database_advisory_locks_enabled: false database_host: '' database_name: '' database_password: '' database_pool_idp: 5 +database_prepared_statements_enabled: false database_read_replica_host: '' database_readonly_password: '' database_readonly_username: '' diff --git a/config/database.yml b/config/database.yml index 0b623f08000..05c4561eb15 100644 --- a/config/database.yml +++ b/config/database.yml @@ -86,8 +86,8 @@ production: host: <%= IdentityConfig.store.database_socket.present? ? IdentityConfig.store.database_socket : IdentityConfig.store.database_host %> password: <%= IdentityConfig.store.database_password %> pool: <%= primary_pool %> - advisory_locks: <%= !IdentityConfig.store.database_socket.present? %> - prepared_statements: <%= !IdentityConfig.store.database_socket.present? %> + advisory_locks: <%= IdentityConfig.store.database_advisory_locks_enabled %> + prepared_statements: <%= IdentityConfig.store.database_prepared_statements_enabled %> sslmode: <%= IdentityConfig.store.database_sslmode %> sslrootcert: '/usr/local/share/aws/rds-combined-ca-bundle.pem' migrations_paths: db/primary_migrate diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index ac97907c86e..5559da9bb78 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -59,6 +59,12 @@ cron: cron_24h, args: -> { [Time.zone.today] }, }, + # Idv Legacy Conversion Supplement Report to S3 + idv_legacy_conversion_supplement_report: { + class: 'Reports::IdvLegacyConversionSupplementReport', + cron: cron_24h, + args: -> { [Time.zone.today] }, + }, agreement_summary_report: { class: 'Reports::AgreementSummaryReport', cron: cron_24h, diff --git a/db/primary_migrate/20240916202940_create_sp_upgraded_biometric_profiles.rb b/db/primary_migrate/20240916202940_create_sp_upgraded_biometric_profiles.rb new file mode 100644 index 00000000000..0c6c9699d0c --- /dev/null +++ b/db/primary_migrate/20240916202940_create_sp_upgraded_biometric_profiles.rb @@ -0,0 +1,14 @@ +class CreateSpUpgradedBiometricProfiles < ActiveRecord::Migration[7.1] + def change + create_table :sp_upgraded_biometric_profiles do |t| + t.datetime :upgraded_at, null: false + t.references :user, null: false + t.string :idv_level, null: false + t.string :issuer, null: false + + t.timestamps + + t.index [:issuer, :upgraded_at] + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 385018287e3..cd2309f566f 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[7.1].define(version: 2024_08_28_182041) do +ActiveRecord::Schema[7.1].define(version: 2024_09_16_202940) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_stat_statements" @@ -585,6 +585,17 @@ t.index ["request_id"], name: "index_sp_return_logs_on_request_id", unique: true end + create_table "sp_upgraded_biometric_profiles", force: :cascade do |t| + t.datetime "upgraded_at", null: false + t.bigint "user_id", null: false + t.string "idv_level", null: false + t.string "issuer", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["issuer", "upgraded_at"], name: "index_sp_upgraded_biometric_profiles_on_issuer_and_upgraded_at" + t.index ["user_id"], name: "index_sp_upgraded_biometric_profiles_on_user_id" + end + create_table "suspended_emails", force: :cascade do |t| t.bigint "email_address_id", null: false t.string "digested_base_email", null: false diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 3dd9c8f1f98..67388dead1d 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -81,10 +81,12 @@ def self.store config.add(:country_phone_number_overrides, type: :json) config.add(:dashboard_api_token, type: :string) config.add(:dashboard_url, type: :string) + config.add(:database_advisory_locks_enabled, type: :boolean) config.add(:database_host, type: :string) config.add(:database_name, type: :string) config.add(:database_password, type: :string) config.add(:database_pool_idp, type: :integer) + config.add(:database_prepared_statements_enabled, type: :boolean) config.add(:database_read_replica_host, type: :string) config.add(:database_readonly_password, type: :string) config.add(:database_readonly_username, type: :string) diff --git a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb index 8f0672fe808..10572bc74f8 100644 --- a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb +++ b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb @@ -324,6 +324,35 @@ expect(status).to eq 422 end end + + context 'with 400 error without a response message' do + let(:response_message) { 'SponsorID 57 is not registered as an IPP client' } + let(:response_body) { { differentMessage: 'Something else is wrong' } } + let(:error_response) { { body: response_body, status: 400 } } + let(:sponsor_id_error) { Faraday::BadRequestError.new(response_message, error_response) } + let(:filtered_message) { 'sponsorID [FILTERED] is not registered as an IPP client' } + + before do + allow(proofer).to receive(:request_facilities).and_raise(sponsor_id_error) + end + + it 'returns an unprocessible entity client error with scrubbed analytics event' do + subject + + expect(@analytics).to have_logged_event( + 'Request USPS IPP locations: request failed', + api_status_code: 422, + exception_class: sponsor_id_error.class, + exception_message: filtered_message, + response_body_present: true, + response_body: response_body, + response_status_code: 400, + ) + + status = response.status + expect(status).to eq 422 + end + end end describe '#update' do diff --git a/spec/factories/profiles.rb b/spec/factories/profiles.rb index c79e6d0d124..f39daca07a9 100644 --- a/spec/factories/profiles.rb +++ b/spec/factories/profiles.rb @@ -79,6 +79,11 @@ end end + trait :biometric_proof do + idv_level { :in_person } + initiating_service_provider_issuer { 'urn:gov:gsa:openidconnect:inactive:sp:test' } + end + after(:build) do |profile, evaluator| if evaluator.pii pii_attrs = Pii::Attributes.new_from_hash(evaluator.pii) diff --git a/spec/factories/sp_upgraded_biometric_profiles.rb b/spec/factories/sp_upgraded_biometric_profiles.rb new file mode 100644 index 00000000000..faa18072397 --- /dev/null +++ b/spec/factories/sp_upgraded_biometric_profiles.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :sp_upgraded_biometric_profile do + idv_level { :in_person } + end +end diff --git a/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml b/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml index 19d08f8624e..d875f2d12a4 100644 --- a/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml +++ b/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml @@ -16,6 +16,7 @@ true + true true true true diff --git a/spec/helpers/ipp_helper_spec.rb b/spec/helpers/ipp_helper_spec.rb new file mode 100644 index 00000000000..770690f653a --- /dev/null +++ b/spec/helpers/ipp_helper_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe IppHelper do + include IppHelper + + describe '#scrub_body' do + let(:message) { "This is a test message with sponsorID #{sponsor_id}" } + let(:sponsor_id) { 1111111 } + + context 'when body is a String' do + it 'scrubs the sponsorID from the message' do + expect(scrub_body(message)).to eq('This is a test message with sponsorID [FILTERED]') + end + end + + context 'when body is a Hash' do + it 'scrubs the responseMessage' do + body = { responseMessage: message } + expect(scrub_body(body)).to eq( + 'responseMessage' => 'This is a test message with sponsorID [FILTERED]', + ) + end + end + + context 'when body is nil' do + it 'returns nil' do + expect(scrub_body(nil)).to be_nil + end + end + end +end diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index d911935005d..537908ea396 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -758,6 +758,16 @@ job_name: 'GetUspsProofingResultsJob', ) end + + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.deactivation_reason).to be_nil + job.perform(Time.zone.now) + pending_enrollment.reload + + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled') + end end context 'when an enrollment fails and fraud is suspected' do @@ -820,6 +830,7 @@ ) it 'deactivates the associated profile' do + expect(pending_enrollment.profile.deactivation_reason).to be_nil expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil job.perform Time.zone.now pending_enrollment.reload @@ -889,6 +900,7 @@ end it 'deactivates the associated profile' do + expect(pending_enrollment.profile.deactivation_reason).to be_nil expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil job.perform(Time.zone.now) @@ -1529,6 +1541,7 @@ ) it 'deactivates the associated profile' do + expect(pending_enrollment.profile.deactivation_reason).to be_nil expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil job.perform(Time.zone.now) pending_enrollment.reload @@ -1657,6 +1670,54 @@ end end end + + describe 'the profile has deactivation_reason set to encryption_error' do + context 'the enrollment passes proofing with USPS' do + let(:pending_enrollment) do + create( + :in_person_enrollment, :pending, :with_notification_phone_configuration + ) + end + + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + stub_request_passed_proofing_results + end + + it 'overwrites the deactivation_reason' do + expect(pending_enrollment.profile.deactivation_reason).to be_nil + # to mimic pw reset + pending_enrollment.profile.update(deactivation_reason: 'encryption_error') + job.perform(Time.zone.now) + pending_enrollment.reload + + expect(pending_enrollment.profile.deactivation_reason).to be_nil + end + end + + context 'the enrollment fails proofing with USPS' do + let(:pending_enrollment) do + create( + :in_person_enrollment, :pending + ) + end + + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + stub_request_failed_proofing_results + end + + it 'does not overwrite the deactivation_reason' do + expect(pending_enrollment.profile.deactivation_reason).to be_nil + # to mimic pw reset + pending_enrollment.profile.update(deactivation_reason: 'encryption_error') + job.perform(Time.zone.now) + pending_enrollment.reload + + expect(pending_enrollment.profile.deactivation_reason).to eq('encryption_error') + end + end + end end describe 'Enhanced In-Person Proofing' do @@ -1775,6 +1836,16 @@ ) end + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.deactivation_reason).to be_nil + job.perform(Time.zone.now) + pending_enrollment.reload + + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled') + end + context 'when the in_person_stop_expiring_enrollments flag is true' do before do allow(IdentityConfig.store).to( diff --git a/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb b/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb new file mode 100644 index 00000000000..dad90904785 --- /dev/null +++ b/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb @@ -0,0 +1,324 @@ +require 'rails_helper' + +RSpec.describe Reports::IdvLegacyConversionSupplementReport do + subject(:report) { Reports::IdvLegacyConversionSupplementReport.new } + + before do + clear_agreements_data + ServiceProvider.delete_all + end + + describe '#perform' do + it 'is empty with no data' do + csv = CSV.parse(report.perform(Time.zone.today), headers: true) + expect(csv).to be_empty + end + + context 'with data generates reports by iaa + order number, issuer and year_month' do + context 'when there are converted profiles in April 2020' do + let(:partner_account1) { create(:partner_account) } + let(:iaa1_range) { DateTime.new(2020, 4, 15).utc..DateTime.new(2021, 4, 14).utc } + let(:gtc1) do + create( + :iaa_gtc, + gtc_number: 'gtc1234', + partner_account: partner_account1, + start_date: iaa1_range.begin, + end_date: iaa1_range.end, + ) + end + + let(:iaa1) { 'iaa1' } + + let(:iaa1_sp) do + create( + :service_provider, + iaa: iaa1, + iaa_start_date: iaa1_range.begin, + iaa_end_date: iaa1_range.end, + ) + end + + let(:iaa_order1) do + build_iaa_order(order_number: 1, date_range: iaa1_range, iaa_gtc: gtc1) + end + + let(:inside_iaa1) { iaa1_range.begin + 1.day } + + let(:user1) { create(:user) } + + let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) } + + before do + iaa_order1.integrations << build_integration( + issuer: iaa1_sp.issuer, + partner_account: partner_account1, + ) + iaa_order1.save + create( + :sp_upgraded_biometric_profile, + issuer: iaa1_sp.issuer, user_id: user1.id, upgraded_at: inside_iaa1, + ) + end + + it 'finds the iaa related to the upgraded profile' do + expect(csv.length).to eq(1) + aggregate_failures do + row = csv.find { |r| r['issuer'] == iaa1_sp.issuer } + + expect(row.to_h.symbolize_keys).to eq( + { + iaa_order_number: 'gtc1234-0001', + iaa_start_date: '2020-04-15', + iaa_end_date: '2021-04-14', + issuer: iaa1_sp.issuer, + friendly_name: iaa1_sp.friendly_name, + year_month: '202004', + year_month_readable: 'April 2020', + user_count: '1', + }, + ) + end + end + end + + context 'when there are converted profiles with two issuers in September 2020' do + let(:partner_account2) { create(:partner_account) } + let(:iaa2_range) { DateTime.new(2020, 9, 1).utc..DateTime.new(2021, 8, 30).utc } + + let(:gtc2) do + create( + :iaa_gtc, + gtc_number: 'gtc5678', + partner_account: partner_account2, + start_date: iaa2_range.begin, + end_date: iaa2_range.end, + ) + end + + let(:iaa2) { 'iaa2' } + + let(:iaa2_sp1) do + create( + :service_provider, + iaa: iaa2, + iaa_start_date: iaa2_range.begin, + iaa_end_date: iaa2_range.end, + ) + end + + let(:iaa2_sp2) do + create( + :service_provider, + iaa: iaa2, + iaa_start_date: iaa2_range.begin, + iaa_end_date: iaa2_range.end, + ) + end + + let(:iaa_order2) do + build_iaa_order(order_number: 2, date_range: iaa2_range, iaa_gtc: gtc2) + end + + let(:inside_iaa2) { iaa2_range.begin + 1.day } + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) } + + before do + iaa_order2.integrations << build_integration( + issuer: iaa2_sp1.issuer, + partner_account: partner_account2, + ) + iaa_order2.integrations << build_integration( + issuer: iaa2_sp2.issuer, + partner_account: partner_account2, + ) + + create( + :sp_upgraded_biometric_profile, + issuer: iaa2_sp1.issuer, user_id: user1.id, upgraded_at: inside_iaa2, + ) + + create( + :sp_upgraded_biometric_profile, + issuer: iaa2_sp2.issuer, user_id: user2.id, upgraded_at: inside_iaa2, + ) + end + + it 'checks values for all profile upgrades and multiple issuers for single partner' do + aggregate_failures do + row = csv.find { |r| r['issuer'] == iaa2_sp1.issuer } + + expect(row.to_h.symbolize_keys).to eq( + { + iaa_order_number: 'gtc5678-0002', + iaa_start_date: '2020-09-01', + iaa_end_date: '2021-08-30', + issuer: iaa2_sp1.issuer, + friendly_name: iaa2_sp1.friendly_name, + year_month: '202009', + year_month_readable: 'September 2020', + user_count: '1', + }, + ) + end + + aggregate_failures do + row = csv.find { |r| r['issuer'] == iaa2_sp2.issuer } + + expect(row.to_h.symbolize_keys).to eq( + { + iaa_order_number: 'gtc5678-0002', + iaa_start_date: '2020-09-01', + iaa_end_date: '2021-08-30', + issuer: iaa2_sp2.issuer, + friendly_name: iaa2_sp2.friendly_name, + year_month: '202009', + year_month_readable: 'September 2020', + user_count: '1', + }, + ) + end + expect(csv.length).to eq(2) + end + end + + context 'with multiple upgraded profiles for an issuer from September-October 2020' do + let(:partner_account3) { create(:partner_account) } + + let(:iaa3_range) { DateTime.new(2020, 9, 1).utc..DateTime.new(2021, 8, 30).utc } + let(:expired_iaa_range) { DateTime.new(2019, 9, 1).utc..DateTime.new(2020, 8, 31).utc } + + let(:gtc3) do + create( + :iaa_gtc, + gtc_number: 'gtc9101', + partner_account: partner_account3, + start_date: iaa3_range.begin, + end_date: iaa3_range.end, + ) + end + + let(:iaa3) { 'iaa3' } + + let(:iaa3_sp1) do + create( + :service_provider, + iaa: iaa3, + iaa_start_date: iaa3_range.begin, + iaa_end_date: iaa3_range.end, + ) + end + + let(:iaa_order3) do + build_iaa_order(order_number: 3, date_range: iaa3_range, iaa_gtc: gtc3) + end + + let(:iaa_order_expired) do + build_iaa_order(order_number: 2, date_range: expired_iaa_range, iaa_gtc: gtc3) + end + + let(:integration_1) do + build_integration( + issuer: iaa3_sp1.issuer, + partner_account: partner_account3, + ) + end + + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) } + + before do + iaa_order3.integrations << integration_1 + + create( + :sp_upgraded_biometric_profile, + issuer: iaa3_sp1.issuer, user_id: user1.id, upgraded_at: iaa3_range.begin + 1.day, + ) + create( + :sp_upgraded_biometric_profile, + issuer: iaa3_sp1.issuer, user_id: user2.id, upgraded_at: iaa3_range.begin + 1.month, + ) + end + + it 'finds data for the same issuer with different year_months' do + expect(csv.length).to eq(2) + aggregate_failures do + row = csv.find { |r| r['issuer'] == iaa3_sp1.issuer && r['year_month'] == '202009' } + + expect(row['iaa_order_number']).to eq('gtc9101-0003') + expect(row['iaa_start_date']).to eq('2020-09-01') + expect(row['iaa_end_date']).to eq('2021-08-30') + expect(row['issuer']).to eq(iaa3_sp1.issuer) + expect(row['friendly_name']).to eq(iaa3_sp1.friendly_name) + expect(row['year_month']).to eq('202009') + expect(row['year_month_readable']).to eq('September 2020') + expect(row['user_count']).to eq('1') + end + + aggregate_failures do + row = csv.find { |r| r['issuer'] == iaa3_sp1.issuer && r['year_month'] == '202010' } + + expect(row.to_h.symbolize_keys).to eq( + { + iaa_order_number: 'gtc9101-0003', + iaa_start_date: '2020-09-01', + iaa_end_date: '2021-08-30', + issuer: iaa3_sp1.issuer, + friendly_name: iaa3_sp1.friendly_name, + year_month: '202010', + year_month_readable: 'October 2020', + user_count: '1', + }, + ) + end + end + + context 'when there is an expired iaa with the same issuer' do + before do + iaa_order_expired.integrations << integration_1 + end + + it 'does not include the expired iaa in the report' do + expect(csv.length).to eq(2) + + row = csv.find { |r| r['issuer'] == iaa3_sp1.issuer } + + expect(row.to_h.symbolize_keys).not_to eq( + { + iaa_order_number: 'gtc9101-0002', + iaa_start_date: '2019-09-01', + iaa_end_date: '2020-08-31', + issuer: iaa3_sp1.issuer, + friendly_name: iaa3_sp1.friendly_name, + year_month: '202010', + year_month_readable: 'October 2020', + user_count: '1', + }, + ) + end + end + end + end + end + + def build_iaa_order(order_number:, date_range:, iaa_gtc:) + create( + :iaa_order, + order_number: order_number, + start_date: date_range.begin, + end_date: date_range.end, + iaa_gtc: iaa_gtc, + ) + end + + def build_integration(issuer:, partner_account:) + create( + :integration, + issuer: issuer, + partner_account: partner_account, + ) + end +end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 7edffff6cd3..cf4b1aa3004 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -347,6 +347,46 @@ expect(active_profile.verified_at).to be_present end + context 'when a user creates a biometric comparision profile' do + context 'when the user has an active profile' do + it 'creates a biometric upgrade record' do + profile.activate + biometric_profile = create( + :profile, + :biometric_proof, + user: user, + ) + + expect { biometric_profile.activate }.to( + change do + SpUpgradedBiometricProfile.count + end.by(1), + ) + end + end + + context 'when the user has an active biometric profile' do + it 'does not create a biometric conversion record' do + create(:profile, :active, :biometric_proof, user: user) + + biometric_reproof = create(:profile, :biometric_proof, user: user) + expect { biometric_reproof.activate }.to_not(change { SpUpgradedBiometricProfile.count }) + end + end + + context 'when the user does not have an active profile' do + it 'does not create a biometric conversion record' do + profile = create(:profile, :biometric_proof, user: user) + + expect { profile.activate }.to_not(change { SpUpgradedBiometricProfile.count }) + end + end + end + + it 'does not create a biometric upgrade record for a non-biometric profile' do + expect { profile.activate }.to_not(change { SpUpgradedBiometricProfile.count }) + end + it 'sends a reproof completed push event' do profile = create(:profile, :active, user: user) expect(PushNotification::HttpPush).to receive(:deliver). @@ -1003,12 +1043,39 @@ describe '#deactivate_due_to_in_person_verification_cancelled' do let(:profile) { create(:profile, :in_person_verification_pending) } - it 'updates the profile' do - profile.deactivate_due_to_in_person_verification_cancelled + context 'when the profile does not have a deactivation reason' do + it 'updates the profile and sets the deactivation reason to "verification_cancelled"' do + expect(profile.deactivation_reason).to be_nil + profile.deactivate_due_to_in_person_verification_cancelled + + expect(profile.active).to be false + expect(profile.deactivation_reason).to eq('verification_cancelled') + expect(profile.in_person_verification_pending_at).to be nil + end + end + + context 'when the profile has a deactivation reason' do + it 'updates the profile without overwriting the deactivation reason (encryption_error)' do + profile.deactivation_reason = 'encryption_error' + expect(profile.deactivation_reason).to_not be_nil - expect(profile.active).to be false - expect(profile.deactivation_reason).to eq('verification_cancelled') - expect(profile.in_person_verification_pending_at).to be nil + profile.deactivate_due_to_in_person_verification_cancelled + + expect(profile.active).to be false + expect(profile.deactivation_reason).to eq('encryption_error') + expect(profile.in_person_verification_pending_at).to be nil + end + + it 'updates the profile without overwriting the deactivation reason (password_reset)' do + profile.deactivation_reason = 'password_reset' + expect(profile.deactivation_reason).to_not be_nil + + profile.deactivate_due_to_in_person_verification_cancelled + + expect(profile.active).to be false + expect(profile.deactivation_reason).to eq('password_reset') + expect(profile.in_person_verification_pending_at).to be nil + end end end diff --git a/spec/services/proofing/aamva/request/verification_request_spec.rb b/spec/services/proofing/aamva/request/verification_request_spec.rb index 14de5da2a5d..190d9edf374 100644 --- a/spec/services/proofing/aamva/request/verification_request_spec.rb +++ b/spec/services/proofing/aamva/request/verification_request_spec.rb @@ -113,9 +113,9 @@ stub_request(:post, config.verification_url). to_return(body: AamvaFixtures.verification_response, status: 200) - result = subject.send + response = subject.send - expect(result.success?).to eq(true) + expect(response).to be_an_instance_of(Proofing::Aamva::Response::VerificationResponse) end it 'sends state id jurisdiction to AAMVA' do diff --git a/spec/services/proofing/aamva/response/verification_response_spec.rb b/spec/services/proofing/aamva/response/verification_response_spec.rb index 2bd0287eb63..d52f1165bb0 100644 --- a/spec/services/proofing/aamva/response/verification_response_spec.rb +++ b/spec/services/proofing/aamva/response/verification_response_spec.rb @@ -68,119 +68,17 @@ end end - describe '#reasons' do - context 'when all attributes are verified' do - it 'returns an empty array' do - expect(subject.reasons).to eq([]) - end - - context 'with a namespaced XML body' do - let(:response_body) { AamvaFixtures.verification_response_namespaced_success } - - it 'returns an empty array' do - expect(subject.reasons).to eq([]) - end - end - end - - context 'when required attributes are verified' do - let(:response_body) do - modify_match_indicator( - AamvaFixtures.verification_response, - 'PersonLastNameFuzzyPrimaryMatchIndicator', - 'false', - ) - end - - it 'returns an empty array' do - expect(subject.reasons).to eq([]) - end - end - - context 'when required attributes are not verified' do - let(:response_body) do - body = modify_match_indicator( - AamvaFixtures.verification_response, - 'PersonBirthDateMatchIndicator', - 'false', - ) - delete_match_indicator( - body, - 'PersonFirstNameExactMatchIndicator', - ) - end - - it 'returns an array with the reasons verification failed' do - expect(subject.reasons).to eq(['Failed to verify dob', 'Response was missing first_name']) - end - - context 'with a namespaced XML response' do - let(:response_body) { AamvaFixtures.verification_response_namespaced_failure } - - it 'returns an array with the reasons verification failed' do - expect(subject.reasons).to eq( - [ - 'Failed to verify state_id_number', - 'Response was missing dob', - 'Response was missing last_name', - 'Response was missing first_name', - ], - ) - end - end - end - end - - describe '#success?' do + describe '#verification_results' do context 'when all attributes are verified' do - it { expect(subject.success?).to eq(true) } - end - - context 'when required attributes are verified' do - let(:response_body) do - modify_match_indicator( - AamvaFixtures.verification_response, - 'PersonLastNameFuzzyPrimaryMatchIndicator', - 'false', - ) + it 'returns a hash of values that were verified' do + expect(subject.verification_results).to eq(verification_results) end - it { expect(subject.success?).to eq(true) } - context 'with a namespaced XML response' do let(:response_body) { AamvaFixtures.verification_response_namespaced_success } - it { expect(subject.success?).to eq(true) } - end - end - - context 'when required attributes are not verified' do - let(:response_body) do - modify_match_indicator( - AamvaFixtures.verification_response, - 'PersonBirthDateMatchIndicator', - 'false', - ) - end - - it { expect(subject.success?).to eq(false) } - end - - context 'when required attributes are missing' do - let(:response_body) do - delete_match_indicator( - AamvaFixtures.verification_response, - 'PersonBirthDateMatchIndicator', - ) - end - - it { expect(subject.success?).to eq(false) } - end - end - - describe '#verification_results' do - context 'when all attributes are verified' do - it 'returns a hash of values that were verified' do - expect(subject.verification_results).to eq(verification_results) + it 'returns a hash of values that were verified' do + expect(subject.verification_results).to eq(verification_results) + end end end diff --git a/spec/services/proofing/aamva/verification_client_spec.rb b/spec/services/proofing/aamva/verification_client_spec.rb index 47f7df59d00..8f298374090 100644 --- a/spec/services/proofing/aamva/verification_client_spec.rb +++ b/spec/services/proofing/aamva/verification_client_spec.rb @@ -59,7 +59,22 @@ context 'when verification is successful' do it 'returns a successful response' do expect(response).to be_a Proofing::Aamva::Response::VerificationResponse - expect(response.success?).to eq(true) + expect(response.verification_results).to eq( + { + address1: true, + address2: nil, + city: true, + dob: true, + first_name: true, + last_name: true, + state: true, + state_id_expiration: nil, + state_id_issued: nil, + state_id_number: true, + state_id_type: true, + zipcode: true, + }, + ) end end @@ -75,7 +90,22 @@ it 'returns an unsuccessful response with errors' do expect(response).to be_a Proofing::Aamva::Response::VerificationResponse - expect(response.success?).to eq(false) + expect(response.verification_results).to eq( + { + address1: true, + address2: nil, + city: true, + dob: false, + first_name: true, + last_name: true, + state: true, + state_id_expiration: nil, + state_id_issued: nil, + state_id_number: true, + state_id_type: true, + zipcode: true, + }, + ) end end