diff --git a/app/models/user.rb b/app/models/user.rb index 1f1a18c440..844efef05f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -106,6 +106,32 @@ def gpo_verification_pending_profile? gpo_verification_pending_profile.present? end + def suspended? + suspended_at.to_s > reinstated_at.to_s + end + + def reinstated? + reinstated_at.to_s > suspended_at.to_s + end + + def suspend! + if suspended? + analytics.user_suspended(success: false, error_message: :user_already_suspended) + raise 'user_already_suspended' + end + update!(suspended_at: Time.zone.now) + analytics.user_suspended(success: true) + end + + def reinstate! + if !suspended? + analytics.user_reinstated(success: false, error_message: :user_is_not_suspended) + raise 'user_is_not_suspended' + end + update!(reinstated_at: Time.zone.now) + analytics.user_reinstated(success: true) + end + def pending_profile return @pending_profile if defined?(@pending_profile) @@ -403,6 +429,10 @@ def send_confirmation_instructions add_method_tracer :send_devise_notification, "Custom/#{name}/send_devise_notification" + def analytics + @analytics ||= Analytics.new(user: self, request: nil, session: {}, sp: nil) + end + def send_email_to_all_addresses(user_mailer_template) confirmed_email_addresses.each do |email_address| UserMailer.with( diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index f4e23b1d28..539689d354 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -3876,6 +3876,42 @@ def user_registration_user_fully_registered( ) end + # Tracks when user reinstated + # @param [Boolean] success + # @param [String] error_message + def user_reinstated( + success:, + error_message: nil, + **extra + ) + track_event( + 'User Suspension: Reinstated', + { + success: success, + error_message: error_message, + **extra, + }.compact, + ) + end + + # Tracks when user suspended + # @param [Boolean] success + # @param [String] error_message + def user_suspended( + success:, + error_message: nil, + **extra + ) + track_event( + 'User Suspension: Suspended', + { + success: success, + error_message: error_message, + **extra, + }.compact, + ) + end + # Tracks when USPS in-person proofing enrollment is created # @param [String] enrollment_code # @param [Integer] enrollment_id diff --git a/spec/factories/users.rb b/spec/factories/users.rb index afa8e08cad..09eb35930b 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -259,5 +259,15 @@ create(:profile, :password_reset, :with_pii, user: user) end end + + trait :suspended do + suspended_at { Time.zone.now } + reinstated_at { nil } + end + + trait :reinstated do + suspended_at { Time.zone.now } + reinstated_at { Time.zone.now + 1.hour } + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b2f19cd00b..2c7988b785 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -699,6 +699,132 @@ end end + describe 'user suspension' do + let(:user) { User.new } + let(:cannot_reinstate_message) { :user_is_not_suspended } + let(:cannot_suspend_message) { :user_already_suspended } + + describe '#suspended?' do + context 'when suspended_at is after reinstated_at' do + before do + user.suspended_at = Time.zone.now + user.reinstated_at = Time.zone.now - 1.day + end + it 'returns true' do + expect(user.suspended?).to be true + end + end + + context 'when suspended_at is before reinstated_at' do + before do + user.suspended_at = Time.zone.now - 1.day + user.reinstated_at = Time.zone.now + end + + it 'returns false' do + expect(user.suspended?).to be false + end + end + + context 'when suspended_at is nil' do + before do + user.suspended_at = nil + user.reinstated_at = nil + end + + it 'returns false' do + expect(user.suspended?).to be false + end + end + end + + describe '#reinstated?' do + context 'when reinstated_at is after suspended_at' do + before do + user.suspended_at = Time.zone.now - 1.day + user.reinstated_at = Time.zone.now + end + + it 'returns true' do + expect(user.reinstated?).to be true + end + end + + context 'when reinstated_at is before suspended_at' do + before do + user.suspended_at = Time.zone.now + user.reinstated_at = Time.zone.now - 1.day + end + + it 'returns false' do + expect(user.reinstated?).to be false + end + end + + context 'when reinstated_at is nil' do + before do + user.suspended_at = nil + user.reinstated_at = nil + end + it 'returns false' do + expect(user.reinstated?).to be false + end + end + end + + describe '#suspend!' do + it 'updates the suspended_at attribute with the current time' do + expect do + user.suspend! + end.to change(user, :suspended_at).from(nil).to(be_within(1.second).of(Time.zone.now)) + end + + it 'tracks the user suspension' do + expect(user.analytics).to receive(:user_suspended).with(success: true) + user.suspend! + end + + it 'raises an error if the user is already suspended' do + user.suspended_at = Time.zone.now + expect(user.analytics).to receive(:user_suspended).with( + success: false, + error_message: cannot_suspend_message, + ) + expect do + user.suspend! + end.to raise_error(cannot_suspend_message.to_s) + end + end + + describe '#reinstate!' do + before do + user.suspended_at = Time.zone.now + user.reinstated_at = nil + end + it 'updates the reinstated_at attribute with the current time' do + expect do + user.reinstate! + end.to change(user, :reinstated_at).from(nil).to(be_within(1.second).of(Time.zone.now)) + end + + it 'tracks the user reinstatement' do + expect(user.analytics).to receive(:user_reinstated).with(success: true) + user.reinstate! + end + + it 'raises an error if the user is not currently suspended' do + user.suspended_at = nil + expect(user.analytics).to receive(:user_reinstated).with( + success: false, + error_message: cannot_reinstate_message, + ) + expect do + user.reinstate! + end.to raise_error(cannot_reinstate_message.to_s) + end + end + end + describe '#should_receive_in_person_completion_survey?' do let!(:user) { create(:user) } let(:service_provider) { create(:service_provider) }