From c060df4ca054d330920a6714f92e3e67cc29cc61 Mon Sep 17 00:00:00 2001 From: Vraj Mohan Date: Mon, 18 Aug 2025 20:41:58 -0700 Subject: [PATCH] Add action-account tasks for resolving duplicates changelog: Upcoming Features, One Account, Add action-account tasks for resolving duplicates --- app/models/profile.rb | 87 ++++++++ app/services/analytics_events.rb | 36 ++++ lib/action_account.rb | 286 +++++++++++++++++++++++++-- spec/factories/duplicate_profiles.rb | 2 +- spec/lib/action_account_spec.rb | 282 ++++++++++++++++++++++++++ spec/models/profile_spec.rb | 204 +++++++++++++++++++ 6 files changed, 885 insertions(+), 12 deletions(-) diff --git a/app/models/profile.rb b/app/models/profile.rb index b6dc89b7d37..beab13a3555 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -256,6 +256,93 @@ def deactivate_due_to_ipp_expiration_during_fraud_review ) end + def deactivate_duplicate + raise 'Profile not active' unless active + raise 'Profile not a duplicate' unless DuplicateProfile.exists?(['? = ANY(profile_ids)', id]) + + transaction do + update!( + active: false, + fraud_review_pending_at: nil, + fraud_rejection_at: Time.zone.now, + ) + DuplicateProfile.where(['? = ANY(profile_ids)', id]).find_each do |duplicate_profile| + if duplicate_profile.profile_ids.length > 1 + duplicate_profile.profile_ids.delete(id) + duplicate_profile.save + else + duplicate_profile.update!( + closed_at: Time.zone.now, + self_serviced: false, + fraud_investigation_conclusive: true, + ) + end + + service_provider = ServiceProvider.find_sole_by(issuer: duplicate_profile.service_provider) + user.confirmed_email_addresses.each do |email_address| + mailer = UserMailer.with(user: user, email_address: email_address) + mailer.dupe_profile_account_review_complete_locked( + agency_name: service_provider.friendly_name, + ).deliver_now_or_later + end + end + end + end + + def clear_duplicate + raise 'Profile not active' unless active + raise 'Profile not a duplicate' unless DuplicateProfile.exists?(['? = ANY(profile_ids)', id]) + raise 'Profile has other duplicates' if DuplicateProfile.exists?( + ['? = ANY(profile_ids) AND cardinality(profile_ids) > 1', id], + ) + + transaction do + DuplicateProfile.where(['? = ANY(profile_ids)', id]).find_each do |duplicate_profile| + duplicate_profile.update!( + closed_at: Time.zone.now, + self_serviced: false, + fraud_investigation_conclusive: true, + ) + + service_provider = ServiceProvider.find_sole_by(issuer: duplicate_profile.service_provider) + user.confirmed_email_addresses.each do |email_address| + mailer = UserMailer.with(user: user, email_address: email_address) + mailer.dupe_profile_account_review_complete_success( + agency_name: service_provider.friendly_name, + ).deliver_now_or_later + end + end + end + end + + def close_inconclusive_duplicate + raise 'Profile not active' unless active + raise 'Profile not a duplicate' unless DuplicateProfile.exists?(['? = ANY(profile_ids)', id]) + + transaction do + DuplicateProfile.where(['? = ANY(profile_ids)', id]).find_each do |duplicate_profile| + if duplicate_profile.profile_ids.length > 1 + duplicate_profile.profile_ids.delete(id) + duplicate_profile.save + else + duplicate_profile.update!( + closed_at: Time.zone.now, + self_serviced: false, + fraud_investigation_conclusive: false, + ) + end + + service_provider = ServiceProvider.find_sole_by(issuer: duplicate_profile.service_provider) + user.confirmed_email_addresses.each do |email_address| + mailer = UserMailer.with(user: user, email_address: email_address) + mailer.dupe_profile_account_review_complete_unable( + agency_name: service_provider.friendly_name, + ).deliver_now_or_later + end + end + end + end + def reject_for_fraud(notify_user:) update!( active: false, diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index d08374fd678..d778e31b858 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -6523,6 +6523,42 @@ def oidc_logout_visited( ) end + # Tracks when fraud clears duplicate profile + # @param [Boolean] success Whether the profile was successfully cleared + # @param [Hash] errors Errors resulting from clearing + def one_account_clear_duplicate_profile(success:, errors:, **extra) + track_event( + :one_account_clear_duplicate_profile, + success: success, + errors: errors, + **extra, + ) + end + + # Tracks when fraud clears duplicate profile + # @param [Boolean] success Whether the profile was successfully cleared + # @param [Hash] errors Errors resulting from clearing + def one_account_close_inconclusive_duplicate(success:, errors:, **extra) + track_event( + :one_account_close_inconclusive_duplicate, + success: success, + errors: errors, + **extra, + ) + end + + # Tracks when fraud deactivates duplicate profile + # @param [Boolean] success Whether the profile was successfully deactivated + # @param [Hash] errors Errors resulting from deactivation + def one_account_deactivate_duplicate_profile(success:, errors:, **extra) + track_event( + :one_account_deactivate_duplicate_profile, + success: success, + errors: errors, + **extra, + ) + end + # Tracks when user lands on page notifying them multiple profiles contain same information def one_account_duplicate_profiles_detected track_event(:one_account_duplicate_profiles_detected) diff --git a/lib/action_account.rb b/lib/action_account.rb index e756ae731ca..7cca6466a7c 100644 --- a/lib/action_account.rb +++ b/lib/action_account.rb @@ -46,6 +46,10 @@ def banner * #{basename} confirm-suspend-user uuid1 uuid2 * #{basename} clear-device-profiling-failure uuid1 uuid2 + + * #{basename} deactivate-duplicate uuid1 uuid2 + + * #{basename} close-inconclusive-duplicate uuid1 uuid2 Options: EOS end @@ -62,6 +66,9 @@ def subtask(name) 'reinstate-user' => ReinstateUser, 'confirm-suspend-user' => ConfirmSuspendUser, 'clear-device-profiling-failure' => ClearDeviceProfilingFailure, + 'deactivate-duplicate' => DeactivateDuplicate, + 'clear-duplicate' => ClearDuplicate, + 'close-inconclusive-duplicate' => CloseInconclusiveDuplicate, }[name] end @@ -74,21 +81,26 @@ def log_message(uuid:, log:, reason:, table:, messages:) def log_text { - no_pending: 'Error: User does not have a pending fraud review', - rejected_for_fraud: "User's profile has been deactivated due to fraud rejection.", - profile_activated: "User's profile has been activated and the user has been emailed.", + cleared_duplicate: "User's profile has been cleared and the user has been notified", + closed_inconclusive_duplicate: + 'User has been notified that the fraud investigation is inconclusive', + deactivated_duplicate: "User's profile has been deactivated and the user has been notified", + device_profiling_approved: 'Device profiling result has been updated to pass', + device_profiling_already_passed: 'Device profiling result already passed', + device_profiling_no_results_found: 'No device profiling results found for this user', error_activating: "There was an error activating the user's profile. Please try again.", - past_eligibility: 'User is past the 30 day review eligibility.', missing_uuid: 'Error: Could not find user with that UUID', - user_emailed: 'User has been emailed', - user_suspended: 'User has been suspended', - user_reinstated: 'User has been reinstated and the user has been emailed', + no_pending: 'Error: User does not have a pending fraud review', + past_eligibility: 'User is past the 30 day review eligibility.', + profile_activated: "User's profile has been activated and the user has been emailed.", + profile_not_active: "Error: User's profile is not active", + rejected_for_fraud: "User's profile has been deactivated due to fraud rejection.", + user_already_reinstated: 'User has already been reinstated', user_already_suspended: 'User has already been suspended', + user_emailed: 'User has been emailed', user_is_not_suspended: 'User is not suspended', - user_already_reinstated: 'User has already been reinstated', - device_profiling_approved: 'Device profiling result has been updated to pass', - device_profiling_already_passed: 'Device profiling result already passed', - device_profiling_no_results_found: 'No device profiling results found for this user', + user_reinstated: 'User has been reinstated and the user has been emailed', + user_suspended: 'User has been suspended', } end end @@ -391,6 +403,258 @@ def attempts_api_tracker(profile:) end end + class DeactivateDuplicate + include LogBase + def run(args:, config:) + uuids = args + + users = User.where(uuid: uuids).order(:uuid) + + table = [] + table << %w[uuid status reason] + + messages = [] + + users.each do |user| + profile = user.active_profile + success = false + + log_texts = [] + + if !profile + log_texts << log_text[:profile_not_active] + else + begin + profile.deactivate_duplicate + success = true + log_texts << log_text[:deactivated_duplicate] + rescue RuntimeError => error + log_texts << "Error: #{error.message}" + end + end + + log_texts.each do |text| + table, messages = log_message( + uuid: user.uuid, + log: text, + reason: config.reason, + table:, + messages:, + ) + end + ensure + if !success + analytics_error_hash = { message: log_texts.last } + end + + Analytics.new( + user: user, + request: nil, + session: {}, + sp: nil, + ).one_account_deactivate_duplicate_profile( + success:, + errors: analytics_error_hash, + ) + end + + missing_uuids = (uuids - users.map(&:uuid)) + + if config.include_missing? && !missing_uuids.empty? + missing_uuids.each do |missing_uuid| + table, messages = log_message( + uuid: missing_uuid, + log: log_text[:missing_uuid], + reason: config.reason, + table:, + messages:, + ) + end + Analytics.new( + user: AnonymousUser.new, request: nil, session: {}, sp: nil, + ).one_account_deactivate_duplicate_profile( + success: false, + errors: { message: log_text[:missing_uuid] }, + ) + end + + ScriptBase::Result.new( + subtask: 'deactivate-duplicate', + uuids: users.map(&:uuid), + messages:, + table:, + ) + end + end + + class ClearDuplicate + include LogBase + def run(args:, config:) + uuids = args + + users = User.where(uuid: uuids).order(:uuid) + + table = [] + table << %w[uuid status reason] + + messages = [] + + users.each do |user| + profile = user.active_profile + success = false + + log_texts = [] + + if !profile + log_texts << log_text[:profile_not_active] + else + begin + profile.clear_duplicate + success = true + log_texts << log_text[:cleared_duplicate] + rescue RuntimeError => error + log_texts << "Error: #{error.message}" + end + end + + log_texts.each do |text| + table, messages = log_message( + uuid: user.uuid, + log: text, + reason: config.reason, + table:, + messages:, + ) + end + ensure + if !success + analytics_error_hash = { message: log_texts.last } + end + + Analytics.new( + user: user, + request: nil, + session: {}, + sp: nil, + ).one_account_clear_duplicate_profile( + success:, + errors: analytics_error_hash, + ) + end + + missing_uuids = (uuids - users.map(&:uuid)) + + if config.include_missing? && !missing_uuids.empty? + missing_uuids.each do |missing_uuid| + table, messages = log_message( + uuid: missing_uuid, + log: log_text[:missing_uuid], + reason: config.reason, + table:, + messages:, + ) + end + Analytics.new( + user: AnonymousUser.new, request: nil, session: {}, sp: nil, + ).one_account_clear_duplicate_profile( + success: false, + errors: { message: log_text[:missing_uuid] }, + ) + end + + ScriptBase::Result.new( + subtask: 'clear-duplicate', + uuids: users.map(&:uuid), + messages:, + table:, + ) + end + end + + class CloseInconclusiveDuplicate + include LogBase + def run(args:, config:) + uuids = args + + users = User.where(uuid: uuids).order(:uuid) + + table = [] + table << %w[uuid status reason] + + messages = [] + + users.each do |user| + profile = user.active_profile + success = false + + log_texts = [] + + if !profile + log_texts << log_text[:profile_not_active] + else + begin + profile.close_inconclusive_duplicate + success = true + log_texts << log_text[:closed_inconclusive_duplicate] + rescue RuntimeError => error + log_texts << "Error: #{error.message}" + end + end + + log_texts.each do |text| + table, messages = log_message( + uuid: user.uuid, + log: text, + reason: config.reason, + table:, + messages:, + ) + end + ensure + if !success + analytics_error_hash = { message: log_texts.last } + end + + Analytics.new( + user: user, + request: nil, + session: {}, + sp: nil, + ).one_account_close_inconclusive_duplicate( + success:, + errors: analytics_error_hash, + ) + end + + missing_uuids = (uuids - users.map(&:uuid)) + + if config.include_missing? && !missing_uuids.empty? + missing_uuids.each do |missing_uuid| + table, messages = log_message( + uuid: missing_uuid, + log: log_text[:missing_uuid], + reason: config.reason, + table:, + messages:, + ) + end + Analytics.new( + user: AnonymousUser.new, request: nil, session: {}, sp: nil, + ).one_account_close_inconclusive_duplicate( + success: false, + errors: { message: log_text[:missing_uuid] }, + ) + end + + ScriptBase::Result.new( + subtask: 'close-inconclusive-duplicate', + uuids: users.map(&:uuid), + messages:, + table:, + ) + end + end + class SuspendUser include UserActions diff --git a/spec/factories/duplicate_profiles.rb b/spec/factories/duplicate_profiles.rb index e4606f7f0e2..7554495081c 100644 --- a/spec/factories/duplicate_profiles.rb +++ b/spec/factories/duplicate_profiles.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :duplicate_profile do profile_ids { [] } - service_provider { nil } + service_provider { OidcAuthHelper::OIDC_FACIAL_MATCH_ISSUER } created_at { Time.zone.now } updated_at { Time.zone.now } end diff --git a/spec/lib/action_account_spec.rb b/spec/lib/action_account_spec.rb index c11e8b52c33..7dd62cf6c9a 100644 --- a/spec/lib/action_account_spec.rb +++ b/spec/lib/action_account_spec.rb @@ -491,4 +491,286 @@ end end end + + describe ActionAccount::DeactivateDuplicate do + subject(:subtask) { ActionAccount::DeactivateDuplicate.new } + + describe '#run' do + let(:analytics) { FakeAnalytics.new } + + before do + allow(Analytics).to receive(:new).and_return(analytics) + end + + let(:include_missing) { true } + let(:config) { ScriptBase::Config.new(include_missing:, reason: 'INV1234') } + let(:args) { [user.uuid] } + subject(:result) { subtask.run(args:, config:) } + + context 'when the user has no active profile' do + let(:user) do + create( + :profile, + :deactivated, + ).user + end + + it 'reports that the profile is not active' do + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, "Error: User's profile is not active", 'INV1234'], + ], + ) + expect(analytics).to have_logged_event( + :one_account_deactivate_duplicate_profile, + success: false, + errors: { message: "Error: User's profile is not active" }, + ) + end + end + + context 'when the profile has not been flagged as a duplicate' do + let(:user) do + create( + :profile, + :active, + ).user + end + + it 'reports that the profile has not been flagged as a duplicate' do + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, 'Error: Profile not a duplicate', 'INV1234'], + ], + ) + expect(analytics).to have_logged_event( + :one_account_deactivate_duplicate_profile, + success: false, + errors: { message: 'Error: Profile not a duplicate' }, + ) + end + end + + context 'when the profile has been flagged as a duplicate' do + let(:profile) do + create( + :profile, + :active, + ) + end + let(:user) { profile.user } + let!(:duplicate_profile) do + create( + :duplicate_profile, + profile_ids: [user.profiles.active.sole.id], + ) + end + + it 'deactivates the profile for duplicate' do + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, "User's profile has been deactivated and the user has been notified", + 'INV1234'], + ], + ) + expect(analytics).to have_logged_event( + :one_account_deactivate_duplicate_profile, + success: true, + ) + expect(profile.reload).not_to be_active + end + end + end + end + + describe ActionAccount::ClearDuplicate do + subject(:subtask) { ActionAccount::ClearDuplicate.new } + + describe '#run' do + let(:analytics) { FakeAnalytics.new } + + before do + allow(Analytics).to receive(:new).and_return(analytics) + end + + let(:include_missing) { true } + let(:config) { ScriptBase::Config.new(include_missing:, reason: 'INV1234') } + let(:args) { [user.uuid] } + subject(:result) { subtask.run(args:, config:) } + + context 'when the user has no active profile' do + let(:user) do + create( + :profile, + :deactivated, + ).user + end + + it 'reports that the profile is not active' do + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, "Error: User's profile is not active", 'INV1234'], + ], + ) + expect(analytics).to have_logged_event( + :one_account_clear_duplicate_profile, + success: false, + errors: { message: "Error: User's profile is not active" }, + ) + end + end + + context 'when the profile has not been flagged as a duplicate' do + let(:user) do + create( + :profile, + :active, + ).user + end + + it 'reports that the profile has not been flagged as a duplicate' do + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, 'Error: Profile not a duplicate', 'INV1234'], + ], + ) + expect(analytics).to have_logged_event( + :one_account_clear_duplicate_profile, + success: false, + errors: { message: 'Error: Profile not a duplicate' }, + ) + end + end + + context 'when the profile has been flagged as a duplicate' do + let(:profile) do + create( + :profile, + :active, + ) + end + let(:user) { profile.user } + let!(:duplicate_profile) do + create( + :duplicate_profile, + profile_ids: [user.profiles.active.sole.id], + ) + end + + it 'clears the profile for duplicate' do + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, "User's profile has been cleared and the user has been notified", + 'INV1234'], + ], + ) + expect(analytics).to have_logged_event( + :one_account_clear_duplicate_profile, + success: true, + ) + expect(profile.reload).to be_active + end + end + end + end + + describe ActionAccount::CloseInconclusiveDuplicate do + subject(:subtask) { ActionAccount::CloseInconclusiveDuplicate.new } + + describe '#run' do + let(:analytics) { FakeAnalytics.new } + + before do + allow(Analytics).to receive(:new).and_return(analytics) + end + + let(:include_missing) { true } + let(:config) { ScriptBase::Config.new(include_missing:, reason: 'INV1234') } + let(:args) { [user.uuid] } + subject(:result) { subtask.run(args:, config:) } + + context 'when the user has no active profile' do + let(:user) do + create( + :profile, + :deactivated, + ).user + end + + it 'reports that the profile is not active' do + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, "Error: User's profile is not active", 'INV1234'], + ], + ) + expect(analytics).to have_logged_event( + :one_account_close_inconclusive_duplicate, + success: false, + errors: { message: "Error: User's profile is not active" }, + ) + end + end + + context 'when the profile has not been flagged as a duplicate' do + let(:user) do + create( + :profile, + :active, + ).user + end + + it 'reports that the profile has not been flagged as a duplicate' do + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, 'Error: Profile not a duplicate', 'INV1234'], + ], + ) + expect(analytics).to have_logged_event( + :one_account_close_inconclusive_duplicate, + success: false, + errors: { message: 'Error: Profile not a duplicate' }, + ) + end + end + + context 'when the profile has been flagged as a duplicate' do + let(:profile) do + create( + :profile, + :active, + ) + end + let(:user) { profile.user } + let!(:duplicate_profile) do + create( + :duplicate_profile, + profile_ids: [user.profiles.active.sole.id], + ) + end + + it 'logs an event and leaves the profile active' do + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, 'User has been notified that the fraud investigation is inconclusive', + 'INV1234'], + ], + ) + expect(analytics).to have_logged_event( + :one_account_close_inconclusive_duplicate, + success: true, + ) + expect(profile.reload).to be_active + end + end + end + end end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index d1555ddd05a..4bb12ec6b0b 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -1177,6 +1177,210 @@ end end + describe '#deactivate_duplicate' do + context 'when the profile is not active' do + let(:profile) { create(:profile, :deactivated) } + it 'raises an exception' do + expect { profile.deactivate_duplicate }.to raise_error('Profile not active') + end + end + + context 'when the profile is active' do + let(:profile) { create(:profile, :active) } + context 'when the profile is not identified as a duplicate' do + it 'raises an exception' do + expect { profile.deactivate_duplicate }.to raise_error('Profile not a duplicate') + end + end + + context 'when the profile is identified as a duplicate' do + let!(:duplicate_profile) do + create( + :duplicate_profile, + profile_ids: profile_ids, + ) + end + + context 'when the profile is the only one in its duplicate set' do + let(:profile_ids) { [profile.id] } + + it 'deactivates the profile', :freeze_time do + profile.deactivate_duplicate + expect(profile).to_not be_active + expect(profile.fraud_rejection_at).to eq(Time.zone.now) + end + + it 'closes the case as resolved by fraud', :freeze_time do + profile.deactivate_duplicate + duplicate_profile.reload + expect(duplicate_profile.profile_ids).to include(profile.id) + expect(duplicate_profile.closed_at).to eq(Time.zone.now) + expect(duplicate_profile.self_serviced).to be(false) + expect(duplicate_profile.fraud_investigation_conclusive).to be(true) + end + + it 'notifies the user' do + expect { profile.deactivate_duplicate } + .to(change { ActionMailer::Base.deliveries.count }.by(1)) + end + end + + context 'when there are other profiles in the duplicate set' do + let(:second_profile) { create(:profile, :active) } + let(:profile_ids) { [profile.id, second_profile.id] } + + it 'deactivates the profile', :freeze_time do + profile.deactivate_duplicate + expect(profile).to_not be_active + expect(profile.fraud_rejection_at).to eq(Time.zone.now) + end + + it 'does not close the case', :freeze_time do + profile.deactivate_duplicate + duplicate_profile.reload + expect(duplicate_profile.profile_ids).not_to include(profile.id) + expect(duplicate_profile.closed_at).to be(nil) + expect(duplicate_profile.self_serviced).to be(nil) + expect(duplicate_profile.fraud_investigation_conclusive).to be(nil) + end + + it 'notifies the user' do + expect { profile.deactivate_duplicate } + .to(change { ActionMailer::Base.deliveries.count }.by(1)) + end + end + end + end + end + + describe '#clear_duplicate' do + context 'when the profile is not active' do + let(:profile) { create(:profile, :deactivated) } + it 'raises an exception' do + expect { profile.clear_duplicate }.to raise_error('Profile not active') + end + end + + context 'when the profile is active' do + let(:profile) { create(:profile, :active) } + context 'when the profile is not identified as a duplicate' do + it 'raises an exception' do + expect { profile.clear_duplicate }.to raise_error('Profile not a duplicate') + end + end + + context 'when the profile is identified as a duplicate' do + let!(:duplicate_profile) do + create( + :duplicate_profile, + profile_ids: profile_ids, + ) + end + + context 'when the profile is the only one in its duplicate set' do + let(:profile_ids) { [profile.id] } + + it 'leaves the profile as active', :freeze_time do + profile.clear_duplicate + expect(profile).to be_active + end + + it 'closes the case as resolved by fraud', :freeze_time do + profile.clear_duplicate + duplicate_profile.reload + expect(duplicate_profile.profile_ids).to include(profile.id) + expect(duplicate_profile.closed_at).to eq(Time.zone.now) + expect(duplicate_profile.self_serviced).to be(false) + expect(duplicate_profile.fraud_investigation_conclusive).to be(true) + end + + it 'notifies the user' do + expect { profile.clear_duplicate } + .to(change { ActionMailer::Base.deliveries.count }.by(1)) + end + end + + context 'when there are other profiles in the duplicate set' do + let(:second_profile) { create(:profile, :active) } + let(:profile_ids) { [profile.id, second_profile.id] } + + it 'raises an exception' do + expect { profile.clear_duplicate }.to raise_error('Profile has other duplicates') + end + end + end + end + end + + describe '#close_inconclusive_duplicate' do + context 'when the profile is not active' do + let(:profile) { create(:profile, :deactivated) } + it 'raises an exception' do + expect { profile.close_inconclusive_duplicate }.to raise_error('Profile not active') + end + end + + context 'when the profile is active' do + let(:profile) { create(:profile, :active) } + context 'when the profile is not identified as a duplicate' do + it 'raises an exception' do + expect { profile.close_inconclusive_duplicate } + .to raise_error('Profile not a duplicate') + end + end + + context 'when the profile is identified as a duplicate' do + let!(:duplicate_profile) do + create( + :duplicate_profile, + profile_ids: profile_ids, + ) + end + + context 'when the profile is the only one in its duplicate set' do + let(:profile_ids) { [profile.id] } + + it 'leaves the profile as active', :freeze_time do + profile.close_inconclusive_duplicate + expect(profile).to be_active + end + + it 'closes the case as inconclusive', :freeze_time do + profile.close_inconclusive_duplicate + duplicate_profile.reload + expect(duplicate_profile.closed_at).to eq(Time.zone.now) + expect(duplicate_profile.self_serviced).to be(false) + expect(duplicate_profile.fraud_investigation_conclusive).to be(false) + end + end + + context 'when there are other profiles in the duplicate set' do + let(:second_profile) { create(:profile, :active) } + let(:profile_ids) { [profile.id, second_profile.id] } + + it 'leaves the profile as active', :freeze_time do + profile.close_inconclusive_duplicate + expect(profile).to be_active + end + + it 'does not close the case', :freeze_time do + profile.close_inconclusive_duplicate + duplicate_profile.reload + expect(duplicate_profile.profile_ids).not_to include(profile.id) + expect(duplicate_profile.closed_at).to be(nil) + expect(duplicate_profile.self_serviced).to be(nil) + expect(duplicate_profile.fraud_investigation_conclusive).to be(nil) + end + + it 'notifies the user' do + expect { profile.close_inconclusive_duplicate } + .to(change { ActionMailer::Base.deliveries.count }.by(1)) + end + end + end + end + end + describe '#profile_age_in_seconds' do it 'logs the time since the created_at timestamp', :freeze_time do profile = create(:profile, created_at: 5.minutes.ago)