Skip to content
19 changes: 19 additions & 0 deletions app/jobs/fraud_rejection_daily_job.rb
Original file line number Diff line number Diff line change
@@ -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,
)
Comment on lines +14 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have an index on verified_at , can we add one? otherwise this will be a table scan

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or on fraud_review_pending just something so that we don't scan the whole table

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I'll place it on fraud_review_pending we are looking to replace the boolean with a timestamp further down the line.

end
end
4 changes: 2 additions & 2 deletions app/models/profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions config/initializers/job_configurations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion lib/tasks/review_profile.rake
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
25 changes: 25 additions & 0 deletions spec/jobs/fraud_rejection_daily_job_spec.rb
Original file line number Diff line number Diff line change
@@ -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
42 changes: 30 additions & 12 deletions spec/models/profile_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down