diff --git a/app/jobs/fraud_rejection_daily_job.rb b/app/jobs/fraud_rejection_daily_job.rb new file mode 100644 index 00000000000..67c2840f818 --- /dev/null +++ b/app/jobs/fraud_rejection_daily_job.rb @@ -0,0 +1,19 @@ +class FraudRejectionDailyJob < ApplicationJob + queue_as :low + + def perform(_date) + profiles_eligible_for_fraud_rejection.find_each do |profile| + analytics.automatic_fraud_rejection(verified_at: profile.verified_at) + profile.reject_for_fraud(notify_user: false) + end + end + + private + + def profiles_eligible_for_fraud_rejection + Profile.where( + fraud_review_pending: true, + verified_at: ..30.days.ago, + ) + end +end diff --git a/app/models/profile.rb b/app/models/profile.rb index 024abbbe2fa..09c5b62a085 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -60,9 +60,9 @@ def deactivate_for_fraud_review update!(active: false, fraud_review_pending: true, fraud_rejection: false) end - def reject_for_fraud + def reject_for_fraud(notify_user:) update!(active: false, fraud_review_pending: false, fraud_rejection: true) - UserAlerts::AlertUserAboutAccountRejected.call(user) + UserAlerts::AlertUserAboutAccountRejected.call(user) if notify_user end def decrypt_pii(password) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 83fff0f245f..88992adc829 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -163,6 +163,16 @@ def authentication_confirmation_reset track_event('Authentication Confirmation: Reset selected') end + # @param [Date] verified_at + # Tracks when a profile is automatically rejected due to being under review for 30 days + def automatic_fraud_rejection(verified_at:, **extra) + track_event( + 'Fraud: Automatic Fraud Rejection', + verified_at: verified_at, + **extra, + ) + end + # Tracks when the user creates a set of backup mfa codes. # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user def backup_code_created(enabled_mfa_methods_count:, **extra) diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index b6b611de704..3685875a79e 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -140,6 +140,12 @@ cron: cron_1w, args: -> { [Time.zone.now] }, }, + # Reject profiles that have been in fraud_review_pending for 30 days + fraud_rejection: { + class: 'FraudRejectionDailyJob', + cron: cron_24h, + args: -> { [Time.zone.today] }, + }, } end # rubocop:enable Metrics/BlockLength diff --git a/db/primary_migrate/20230307203559_add_index_on_fraud_review_pending_to_profiles.rb b/db/primary_migrate/20230307203559_add_index_on_fraud_review_pending_to_profiles.rb new file mode 100644 index 00000000000..4635dc24d5b --- /dev/null +++ b/db/primary_migrate/20230307203559_add_index_on_fraud_review_pending_to_profiles.rb @@ -0,0 +1,7 @@ +class AddIndexOnFraudReviewPendingToProfiles < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + add_index :profiles, [:fraud_review_pending], algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 8e240675cd5..7a161d48c19 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.0].define(version: 2023_02_14_213731) do +ActiveRecord::Schema[7.0].define(version: 2023_03_07_203559) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pgcrypto" @@ -433,6 +433,7 @@ t.string "initiating_service_provider_issuer" t.boolean "fraud_review_pending", default: false t.boolean "fraud_rejection", default: false + t.index ["fraud_review_pending"], name: "index_profiles_on_fraud_review_pending" t.index ["name_zip_birth_year_signature"], name: "index_profiles_on_name_zip_birth_year_signature" t.index ["reproof_at"], name: "index_profiles_on_reproof_at" t.index ["ssn_signature"], name: "index_profiles_on_ssn_signature" diff --git a/lib/tasks/review_profile.rake b/lib/tasks/review_profile.rake index e5a14b85857..11dd01f2c72 100644 --- a/lib/tasks/review_profile.rake +++ b/lib/tasks/review_profile.rake @@ -41,7 +41,7 @@ namespace :users do if user.fraud_review_pending? && user.proofing_component.review_eligible? profile = user.fraud_review_pending_profile - profile.reject_for_fraud + profile.reject_for_fraud(notify_user: true) puts "User's profile has been deactivated due to fraud rejection." elsif !user.proofing_component.review_eligible? diff --git a/spec/jobs/fraud_rejection_daily_job_spec.rb b/spec/jobs/fraud_rejection_daily_job_spec.rb new file mode 100644 index 00000000000..9a1022acef3 --- /dev/null +++ b/spec/jobs/fraud_rejection_daily_job_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe FraudRejectionDailyJob do + subject(:job) { FraudRejectionDailyJob.new } + let(:job_analytics) { FakeAnalytics.new } + + before do + allow(job).to receive(:analytics).and_return(job_analytics) + end + + describe '#perform' do + it 'rejects profiles which have been review pending for more than 30 days' do + create(:profile, fraud_review_pending: true, verified_at: 31.days.ago) + create(:profile, fraud_review_pending: true, verified_at: 20.days.ago) + + rejected_profiles = Profile.where(fraud_rejection: true) + + expect { job.perform(Time.zone.today) }.to change { rejected_profiles.count }.by(1) + expect(job_analytics).to have_logged_event( + 'Fraud: Automatic Fraud Rejection', + verified_at: rejected_profiles.first.verified_at, + ) + end + end +end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index e0a6eb6fb96..b79ceaf2990 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -313,17 +313,7 @@ end describe '#reject_for_fraud' do - let(:profile) do - profile = create(:profile, user: user) - profile.reject_for_fraud - profile - end - - it 'sets fraud_rejection to true' do - expect(profile).to_not be_active - end - - it 'sends an email' do + before do # This is necessary because UserMailer reaches into the # controller's params. As this is a model spec, we have to fake # the params object. @@ -332,8 +322,36 @@ email_address: OpenStruct.new(user_id: 'fake_user_id', email: 'fake_user@test.com'), ) allow_any_instance_of(UserMailer).to receive(:params).and_return(fake_params) + end + + context 'it notifies the user' do + let(:profile) do + profile = create(:profile, user: user, fraud_review_pending: true) + profile.reject_for_fraud(notify_user: true) + profile + end + + it 'sets fraud_rejection to true' do + expect(profile).to_not be_active + end + + it 'sends an email' do + expect { profile }.to change(ActionMailer::Base.deliveries, :count).by(1) + end + end + + context 'it does not notify the user' do + let(:profile) do + profile = create(:profile, user: user, fraud_review_pending: true) + profile.reject_for_fraud(notify_user: false) + profile + end - expect { profile }.to change(ActionMailer::Base.deliveries, :count).by(1) + it 'does not send an email' do + expect(profile).to_not be_active + + expect { profile }.to change(ActionMailer::Base.deliveries, :count).by(0) + end end end