Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LG-15216 Handle password reset for in-person enrollments waiting for fraud review #11855

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion app/controllers/concerns/fraud_review_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def handle_fraud_rejection
# bypassing the typical flow of showing the Please Call or Fraud Rejection screens.
def in_person_prevent_fraud_redirection?
IdentityConfig.store.in_person_proofing_enforce_tmx &&
current_user.ipp_enrollment_status_not_passed? &&
current_user.ipp_enrollment_status_not_passed_or_in_fraud_review? &&
(fraud_review_pending? || fraud_rejection?)
end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/concerns/verify_profile_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def pending_profile_policy
# bypassing the typical flow of showing the Please Call or Fraud Rejection screens.
def user_failed_ipp_with_fraud_review_pending?
IdentityConfig.store.in_person_proofing_enforce_tmx &&
current_user.ipp_enrollment_status_not_passed? &&
current_user.ipp_enrollment_status_not_passed_or_in_fraud_review? &&
current_user.fraud_review_pending?
end
end
10 changes: 7 additions & 3 deletions app/controllers/idv/please_call_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ def show
analytics.idv_please_call_visited
pending_at = current_user.fraud_review_pending_profile.fraud_review_pending_at
@call_by_date = pending_at + FRAUD_REVIEW_CONTACT_WITHIN_DAYS
@in_person = ipp_enabled_and_enrollment_passed?
@in_person = ipp_enabled_and_enrollment_passed_or_in_fraud_review?
end

def ipp_enabled_and_enrollment_passed?
def ipp_enabled_and_enrollment_passed_or_in_fraud_review?
return unless in_person_tmx_enabled?
in_person_proofing_enabled? && ipp_enrollment_passed?
in_person_proofing_enabled? && (ipp_enrollment_passed? || ipp_enrollment_in_fraud_review?)
end

private
Expand All @@ -43,5 +43,9 @@ def in_person_tmx_enabled?
def ipp_enrollment_passed?
current_user&.in_person_enrollment_status == 'passed'
end

def ipp_enrollment_in_fraud_review?
current_user&.in_person_enrollment_status == 'in_fraud_review'
end
end
end
5 changes: 3 additions & 2 deletions app/controllers/idv/welcome_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ def create_document_capture_session

def cancel_previous_in_person_enrollments
return unless IdentityConfig.store.in_person_proofing_enabled
UspsInPersonProofing::EnrollmentHelper
Copy link
Contributor

Choose a reason for hiding this comment

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

Is check IdentityConfig.store.in_person_proofing_enabled needed? (I know this code predates you and you did not change it.)

If we disable in_person_proofing_enabled because of an outage and the user begins again, wouldn't we want a clean slate even if they are not able to move through ipp? Can you think of any disadvantages to deleting the check?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good question. I'm not (yet) sure where I fall on this topic, but I am curious to hear more about it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Cc @matthinz because I feel like this is the type of thing we discussed recently. This method is called from the welcome controller's update method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that makes sense, but I feel that this change would be better in a different PR/User Story.

cc: @eileen-nava

Copy link
Contributor

Choose a reason for hiding this comment

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

@shanechesnutt-ft Would you be up for writing a ticket to address this feedback in a separate PR?

.cancel_establishing_and_pending_enrollments(current_user)
UspsInPersonProofing::EnrollmentHelper.cancel_establishing_and_in_progress_enrollments(
current_user,
)
end
end
end
10 changes: 6 additions & 4 deletions app/forms/reset_password_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ def mark_profile_as_password_reset

def password_reset_profile
FeatureManagement.pending_in_person_password_reset_enabled? ?
find_pending_in_person_or_active_profile :
find_in_progress_in_person_or_active_profile :
active_profile
end

def find_pending_in_person_or_active_profile
user.pending_in_person_enrollment&.profile || active_profile
def find_in_progress_in_person_or_active_profile
user.current_in_progress_in_person_enrollment_profile || active_profile
end

# It is possible for an account that is resetting their password to be "invalid".
Expand Down Expand Up @@ -104,7 +104,9 @@ def extra_analytics_attributes

def pending_profile_invalidated?
if FeatureManagement.pending_in_person_password_reset_enabled?
pending_profile.present? && !pending_profile.in_person_verification_pending?
pending_profile.present? &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for updating this.

!pending_profile.in_person_verification_pending? &&
!pending_profile.fraud_deactivation_reason?
else
pending_profile.present?
end
Expand Down
1 change: 1 addition & 0 deletions app/jobs/fraud_rejection_daily_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class FraudRejectionDailyJob < ApplicationJob

def perform(_date)
profiles_eligible_for_fraud_rejection.find_each do |profile|
profile.in_person_enrollment&.failed!
Copy link
Contributor

Choose a reason for hiding this comment

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

💯

profile.reject_for_fraud(notify_user: false)
analytics(user: profile.user).automatic_fraud_rejection(
fraud_rejection_at: profile.fraud_rejection_at,
Expand Down
10 changes: 5 additions & 5 deletions app/jobs/get_usps_proofing_results_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def perform(_now)
enrollments_cancelled: 0,
enrollments_in_progress: 0,
enrollments_passed: 0,
enrollments_in_fraud_review: 0,
enrollments_skipped: 0,
}

Expand Down Expand Up @@ -142,8 +143,7 @@ def check_enrollment(enrollment)

def cancel_enrollment(enrollment)
enrollment_outcomes[:enrollments_cancelled] += 1
enrollment.cancelled!
enrollment.profile.deactivate_due_to_in_person_verification_cancelled
enrollment.cancel
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻

end

def skip_enrollment(enrollment, profile_deactivation_reason)
Expand Down Expand Up @@ -484,7 +484,7 @@ def handle_successful_status_update(enrollment, response)

def handle_passed_with_fraud_review_pending(enrollment, response)
proofed_at = parse_usps_timestamp(response['transactionEndDateTime'])
enrollment_outcomes[:enrollments_passed] += 1
enrollment_outcomes[:enrollments_in_fraud_review] += 1
log_enrollment_updated_analytics(
enrollment: enrollment,
enrollment_passed: true,
Expand All @@ -493,7 +493,7 @@ def handle_passed_with_fraud_review_pending(enrollment, response)
reason: 'Passed with fraud pending',
)
enrollment.update(
status: :passed,
status: :in_fraud_review,
proofed_at: proofed_at,
status_check_completed_at: Time.zone.now,
)
Expand Down Expand Up @@ -641,7 +641,7 @@ def send_enrollment_status_sms_notification(enrollment:)
end

def notification_delivery_params(enrollment)
return {} unless enrollment.passed? || enrollment.failed?
return {} unless enrollment.passed? || enrollment.failed? || enrollment.in_fraud_review?
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like I've gotten rusty in this section of code. Is the intent here to start sending delayed notices of fraud-review status? It's just not obvious to me what the significance of this is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah this isn't very obvious, but in the code before when the user passed at the post office and had been flagged for fraud review we would update the in-person enrollment to have a status of passed and send the please call email. In this PR we are changing things up a little bit by adding the new in_fraud_review status. We wanted to keep the same behavior with sending the email in the same way (with a delay) so I had to add the status check here.


wait_until = enrollment.status_check_completed_at + (
IdentityConfig.store.in_person_results_delay_in_hours || DEFAULT_EMAIL_DELAY_IN_HOURS
Expand Down
4 changes: 4 additions & 0 deletions app/models/in_person_enrollment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ class InPersonEnrollment < ApplicationRecord

has_one :notification_phone_configuration, dependent: :destroy, inverse_of: :in_person_enrollment

IN_PROGRESS_ENROLLMENT_STATUSES = %w[pending in_fraud_review].to_set.freeze

STATUS_ESTABLISHING = 'establishing'
STATUS_PENDING = 'pending'
STATUS_PASSED = 'passed'
STATUS_FAILED = 'failed'
STATUS_EXPIRED = 'expired'
STATUS_CANCELLED = 'cancelled'
STATUS_IN_FRAUD_REVIEW = 'in_fraud_review'

enum :status, {
STATUS_ESTABLISHING.to_sym => 0,
Expand All @@ -27,6 +30,7 @@ class InPersonEnrollment < ApplicationRecord
STATUS_FAILED.to_sym => 3,
STATUS_EXPIRED.to_sym => 4,
STATUS_CANCELLED.to_sym => 5,
STATUS_IN_FRAUD_REVIEW.to_sym => 6,
}

validate :profile_belongs_to_user
Expand Down
24 changes: 18 additions & 6 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,14 @@ def in_person_enrollment_status
pending_profile&.in_person_enrollment&.status
end

def ipp_enrollment_status_not_passed?
# Whether the user's in person enrollment status is not passed or in_fraud_review. Enrollments
# used to go to passed status when profiles were marked as in fraud review. Since LG-15216, this
# will no longer be the case.
def ipp_enrollment_status_not_passed_or_in_fraud_review?
Copy link
Contributor

Choose a reason for hiding this comment

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

This method could benefit from a comment / usage doc since it's public.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in 2edd2c2

!in_person_enrollment_status.blank? &&
in_person_enrollment_status != 'passed'
[InPersonEnrollment::STATUS_PASSED, InPersonEnrollment::STATUS_IN_FRAUD_REVIEW].exclude?(
in_person_enrollment_status,
)
end

def has_in_person_enrollment?
Expand Down Expand Up @@ -533,21 +538,28 @@ def last_sign_in_email_address
email_addresses.confirmed.last_sign_in
end

# Find the user's most recent in-progress enrollment profile.
def current_in_progress_in_person_enrollment_profile
in_person_enrollments
.where(status: InPersonEnrollment::IN_PROGRESS_ENROLLMENT_STATUSES)
.order(created_at: :desc)
.first&.profile
end

private

def find_password_reset_profile
FeatureManagement.pending_in_person_password_reset_enabled? ?
find_pending_in_person_or_active_profile :
find_in_person_in_progress_or_active_profile :
find_active_profile
end

def find_active_profile
profiles.where.not(activated_at: nil).order(activated_at: :desc).first
end

def find_pending_in_person_or_active_profile
pending_in_person_enrollment&.profile ||
profiles.where.not(activated_at: nil).order(activated_at: :desc).first
def find_in_person_in_progress_or_active_profile
current_in_progress_in_person_enrollment_profile || find_active_profile
Copy link
Contributor

@gina-yamada gina-yamada Feb 12, 2025

Choose a reason for hiding this comment

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

Originally, the only behavior was to find an active profile. When we enabled the epic, the default behavior will be to find an in person in progress or active profile. What do you think about changing the order of this so the default is to first look for an active profile because that was the original behavior? I think it would have a smaller affect on pw reset. (If for some reason in person enrollments was not getting cleaned up properly at some point- it would have a small impact because it would only happen if there was not an active profile.) I did not consider this in your previous pr.

What are your thoughts?

find_active_profile || current_in_progress_in_person_enrollment_profile

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So in the last Password Reset PR I prioritized the pending profile first because when the personal key is generated for a profile it will replace the previous personal key. This can occur when the user has an active legacy profile and a pending non-legacy profile. My logic was that since the personal key is replaced, the previous active account is dead when the user performs a password reset. So, rather than having them lose both profiles I would prioritize the latest profile (the pending profile) which would allow the user to use their latest personal key.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jmhooper does this sound reasonable? I know you reviewed the last PR and this didn't come up. I just wanted to make sure the way this is implemented makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

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

@gina-yamada Good question! I agree with Shane's explanation and logic. Please feel free to say if you have more questions about this.

Copy link
Contributor

@gina-yamada gina-yamada Feb 13, 2025

Choose a reason for hiding this comment

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

Shane and I walked through this together prior to his post yesterday. He posted for visibility. I am okay with this now that I have more information. Thank you

end

def lockout_period
Expand Down
3 changes: 3 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3340,6 +3340,7 @@ def idv_in_person_usps_proofing_enrollment_code_email_received(
# @param [Integer] enrollments_failed number of enrollments which failed identity proofing
# @param [Integer] enrollments_in_progress number of enrollments which did not have any change
# @param [Integer] enrollments_passed number of enrollments which passed identity proofing
# @param [Integer] enrollments_in_fraud_review number of enrollments in fraud review
# @param [Integer] enrollments_skipped number of enrollments skipped
# @param [Integer] enrollments_network_error
# @param [Integer] enrollments_cancelled
Expand All @@ -3354,6 +3355,7 @@ def idv_in_person_usps_proofing_results_job_completed(
enrollments_failed:,
enrollments_in_progress:,
enrollments_passed:,
enrollments_in_fraud_review:,
enrollments_skipped:,
enrollments_network_error:,
enrollments_cancelled:,
Expand All @@ -3371,6 +3373,7 @@ def idv_in_person_usps_proofing_results_job_completed(
enrollments_failed:,
enrollments_in_progress:,
enrollments_passed:,
enrollments_in_fraud_review:,
enrollments_skipped:,
enrollments_network_error:,
enrollments_cancelled:,
Expand Down
8 changes: 5 additions & 3 deletions app/services/usps_in_person_proofing/enrollment_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,15 @@ def cancel_stale_establishing_enrollments_for_user(user)
.find_each(&:cancelled!)
end

# Cancel a user's associated establishing and pending in-person enrollments.
# Cancel a user's associated establishing, pending, and in_fraud_review in-person enrollments.
Copy link
Contributor

Choose a reason for hiding this comment

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

Oxford comma FTW!

#
# @param user [User] The user model
def cancel_establishing_and_pending_enrollments(user)
def cancel_establishing_and_in_progress_enrollments(user)
user
.in_person_enrollments
.where(status: [:establishing, :pending])
.where(status:
[InPersonEnrollment::STATUS_ESTABLISHING] +
InPersonEnrollment::IN_PROGRESS_ENROLLMENT_STATUSES.to_a)
.find_each(&:cancel)
end

Expand Down
2 changes: 2 additions & 0 deletions lib/action_account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def run(args:, config:)
elsif FraudReviewChecker.new(user).fraud_review_eligible?
profile = user.fraud_review_pending_profile
profile_fraud_review_pending_at = profile.fraud_review_pending_at
profile.in_person_enrollment&.failed!
profile.reject_for_fraud(notify_user: true)
success = true

Expand Down Expand Up @@ -281,6 +282,7 @@ def run(args:, config:)
elsif FraudReviewChecker.new(user).fraud_review_eligible?
profile = user.fraud_review_pending_profile
profile_fraud_review_pending_at = profile.fraud_review_pending_at
profile.in_person_enrollment&.passed!
Copy link
Contributor

Choose a reason for hiding this comment

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

This is another "Apologies that I don't remember this so well..." one, but I'm trying to make sure I understand what happens here in non-obvious cases.

  1. If I have an IPP enrollment, but don't actually go to the post office because I'm a fraudster, this won't mark me as a pass because I'm not fraud_review_eligible?, right?
  2. If I get in one of the muddled states where I abandon IPP, and continue with doc auth and get myself into fraud review, this won't mark me as passed because I don't have an enrollment, right?

This is a case where I think the logic is good but I just want to make sure we're not accidentally creating a vulnerability.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Yeah that seems to be correct because user has not been to post office for their enrollment/profile to be updated. IPP enrollments profiles are not updated with a fraud_review_pending_at timestamp until the user goes to the post office.

  2. Yeah think that is also the case because the profile associated with the enrollment would be different from the one associated with doc auth. I can do some checks around this today. What exactly to do you mean by "muddled states where I abandon IPP, and continue with doc auth"? I want to make sure I test this case correctly.

profile.activate_after_passing_review
success = true

Expand Down
23 changes: 23 additions & 0 deletions spec/controllers/concerns/idv_step_concern_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,28 @@ def show
expect(response).to redirect_to idv_verify_by_mail_enter_code_url
end
end

context 'with a passed in-person enrollment and a fraudulent profile' do
let(:user) do
profile = create(:profile, :in_person_fraud_review_pending, in_person_enrollment: nil)
create(:in_person_enrollment, :passed, profile:, user: profile.user).user
eileen-nava marked this conversation as resolved.
Show resolved Hide resolved
end

it 'redirects to please call page' do
get :show

expect(response).to redirect_to idv_please_call_url
end
end

context 'with a in_fraud_review in-person enrollment and a fraudulent profile' do
let(:user) { create(:profile, :in_person_fraud_review_pending).user }

it 'redirects to please call page' do
get :show

expect(response).to redirect_to idv_please_call_url
end
end
end
end
62 changes: 62 additions & 0 deletions spec/controllers/idv/please_call_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,66 @@
end
end
end

describe '#ipp_enabled_and_enrollment_passed_or_in_fraud_review?' do
context 'when in person tmx is enabled' do
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx).and_return(true)
end

context 'when ipp is enabled' do
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
end

context 'when user has a passed enrollment' do
let!(:enrollment) { create(:in_person_enrollment, :passed, user: user, profile: profile) }

it 'returns true' do
expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be(true)
end
end

context 'when user has an in_fraud_review enrollment' do
let!(:enrollment) do
create(:in_person_enrollment, :in_fraud_review, user: user, profile: profile)
end

it 'returns true' do
expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be(true)
end
end

context 'when user has a non passed or in_fraud_review enrollment' do
let!(:enrollment) do
create(:in_person_enrollment, :pending, user: user, profile: profile)
end

it 'returns false' do
expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be(false)
end
end
end

context 'when ipp is disabled' do
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(false)
end

it 'returns false' do
expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be(false)
end
end
end

context 'when in person tmx is disabled' do
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx).and_return(false)
end

it 'returns nil' do
Copy link
Contributor

Choose a reason for hiding this comment

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

Tests look clear and thorough.

expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be_nil
end
end
end
end
9 changes: 8 additions & 1 deletion spec/controllers/idv/welcome_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,23 @@
let!(:pending_enrollment) do
create(:in_person_enrollment, :pending, user: user, profile: password_reset_profile)
end
let(:fraud_password_reset_profile) { create(:profile, :password_reset, user: user) }
let!(:fraud_review_enrollment) do
create(
:in_person_enrollment, :in_fraud_review, user: user, profile: fraud_password_reset_profile
)
end

before do
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
end

it 'cancels all previous establishing and pending enrollments' do
it 'cancels all previous establishing, pending, and in_fraud_review enrollments' do
put :update

expect(establishing_enrollment.reload.status).to eq(InPersonEnrollment::STATUS_CANCELLED)
expect(pending_enrollment.reload.status).to eq(InPersonEnrollment::STATUS_CANCELLED)
expect(fraud_review_enrollment.reload.status).to eq(InPersonEnrollment::STATUS_CANCELLED)
expect(user.establishing_in_person_enrollment).to be_blank
expect(user.pending_in_person_enrollment).to be_blank
end
Expand Down
Loading