Skip to content

Commit fa38a1c

Browse files
gumclawclaude
andauthored
Allow direct suspension without requiring flagging first (#4364)
## Summary - Expand the user risk state machine to allow suspending users directly from `not_reviewed`, `compliant`, and cross-flag states (e.g., `flagged_for_tos_violation` → `suspended_for_fraud`) - Simplify callers that previously did flag-then-suspend: `suspend_due_to_stripe_risk`, `Iffy::User::BanService`, `SuspendUsersWorker`, and `SuspendAccountsWithPaymentAddressWorker` - Flag events (`flag_for_fraud`, `flag_for_tos_violation`) are preserved for graduated response — they're now optional, not eliminated Closes #4290 ## Test plan - [ ] Verify direct suspension from `not_reviewed` and `compliant` states works - [ ] Verify cross-flag suspension (e.g., `flagged_for_tos_violation` → `suspended_for_fraud`) works - [ ] Verify existing flag-then-suspend paths still work - [ ] Verify mass suspension via admin still works - [ ] Verify cascade suspension of related accounts (same payment address/fingerprint) still works 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 395dc90 commit fa38a1c

File tree

9 files changed

+39
-31
lines changed

9 files changed

+39
-31
lines changed

app/models/user.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,11 @@ class User < ApplicationRecord
366366
end
367367

368368
event :suspend_for_fraud do
369-
transition %i[on_probation flagged_for_fraud] => :suspended_for_fraud
369+
transition %i[not_reviewed compliant on_probation flagged_for_fraud flagged_for_tos_violation] => :suspended_for_fraud
370370
end
371371

372372
event :suspend_for_tos_violation do
373-
transition %i[on_probation flagged_for_tos_violation] => :suspended_for_tos_violation
373+
transition %i[not_reviewed compliant on_probation flagged_for_tos_violation flagged_for_fraud] => :suspended_for_tos_violation
374374
end
375375

376376
event :put_on_probation do

app/modules/user/risk.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ def flag_for_explicit_nsfw_tos_violation!(options)
8787
def suspend_due_to_stripe_risk
8888
transaction do
8989
update!(tos_violation_reason: "Stripe reported high risk")
90-
flag_for_tos_violation!(author_name: "stripe_risk", bulk: true) unless flagged_for_tos_violation? || on_probation? || suspended?
9190
suspend_for_tos_violation!(author_name: "stripe_risk", bulk: true) unless suspended?
9291
links.alive.find_each do |product|
9392
product.unpublish!(is_unpublished_by_admin: true)

app/services/iffy/user/ban_service.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ def perform
1212
reason = "General non-compliance"
1313
user.update!(tos_violation_reason: reason)
1414
comment_content = "Banned for a policy violation on #{Time.current.to_fs(:formatted_date_full_month)} (#{reason})"
15-
user.flag_for_tos_violation!(author_name: "Iffy", content: comment_content, bulk: true) unless user.flagged_for_tos_violation? || user.on_probation? || user.suspended?
1615
user.suspend_for_tos_violation!(author_name: "Iffy", content: comment_content, bulk: true) unless user.suspended?
1716
end
1817
end

app/sidekiq/suspend_accounts_with_payment_address_worker.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def suspend_users_with_same_payment_address(suspended_user)
1919
.where(payment_address: suspended_user.payment_address)
2020
.where.not(id: suspended_user.id)
2121
.find_each do |user|
22-
flag_and_suspend_user(user, suspended_user, "payment address", suspended_user.payment_address)
22+
suspend_user(user, suspended_user, "payment address", suspended_user.payment_address)
2323
end
2424
end
2525

@@ -35,15 +35,11 @@ def suspend_users_with_same_stripe_fingerprint(suspended_user)
3535

3636
User.not_suspended.where(id: user_ids_with_same_fingerprint).find_each do |user|
3737
matching_fingerprint = (fingerprints & user.alive_bank_accounts.pluck(:stripe_fingerprint)).first
38-
flag_and_suspend_user(user, suspended_user, "bank account fingerprint", matching_fingerprint)
38+
suspend_user(user, suspended_user, "bank account fingerprint", matching_fingerprint)
3939
end
4040
end
4141

42-
def flag_and_suspend_user(user, suspended_user, identifier_type, identifier_value)
43-
user.flag_for_fraud(
44-
author_name: "suspend_sellers_other_accounts",
45-
content: "Flagged for fraud automatically on #{Time.current.to_fs(:formatted_date_full_month)} because of usage of #{identifier_type} #{identifier_value} (from User##{suspended_user.id})"
46-
)
42+
def suspend_user(user, suspended_user, identifier_type, identifier_value)
4743
user.suspend_for_fraud(
4844
author_name: "suspend_sellers_other_accounts",
4945
content: "Suspended for fraud automatically on #{Time.current.to_fs(:formatted_date_full_month)} because of usage of #{identifier_type} #{identifier_value} (from User##{suspended_user.id})",

app/sidekiq/suspend_users_worker.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ class SuspendUsersWorker
66

77
def perform(author_id, user_ids, reason, additional_notes)
88
User.where(id: user_ids).or(User.where(external_id: user_ids)).find_each(batch_size: 100) do |user|
9-
user.flag_for_tos_violation(author_id:, bulk: true)
109
author_name = User.find(author_id).name_or_username
1110
content = "Suspended for a policy violation by #{author_name} on #{Time.current.to_fs(:formatted_date_full_month)} as part of mass suspension. Reason: #{reason}."
1211
content += "\nAdditional notes: #{additional_notes}" if additional_notes.present?

spec/models/user_spec.rb

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,6 +1734,30 @@ def sample_string_of_length(n)
17341734
expect(@user.suspend_for_fraud!(author_id: @admin_user.id)).to be(true)
17351735
end
17361736

1737+
it "suspends the user directly from not_reviewed state" do
1738+
expect(@user.user_risk_state).to eq("not_reviewed")
1739+
expect(@user.suspend_for_fraud!(author_id: @admin_user.id)).to be(true)
1740+
expect(@user.reload.suspended_for_fraud?).to be(true)
1741+
end
1742+
1743+
it "suspends the user directly from compliant state" do
1744+
@user.update!(user_risk_state: "compliant")
1745+
expect(@user.suspend_for_tos_violation!(author_id: @admin_user.id)).to be(true)
1746+
expect(@user.reload.suspended_for_tos_violation?).to be(true)
1747+
end
1748+
1749+
it "suspends for fraud from flagged_for_tos_violation state" do
1750+
@user.flag_for_tos_violation!(author_id: @admin_user.id, product_id: @product_1.id)
1751+
expect(@user.suspend_for_fraud!(author_id: @admin_user.id)).to be(true)
1752+
expect(@user.reload.suspended_for_fraud?).to be(true)
1753+
end
1754+
1755+
it "suspends for tos_violation from flagged_for_fraud state" do
1756+
@user.flag_for_fraud!(author_id: @admin_user.id)
1757+
expect(@user.suspend_for_tos_violation!(author_id: @admin_user.id)).to be(true)
1758+
expect(@user.reload.suspended_for_tos_violation?).to be(true)
1759+
end
1760+
17371761
it "is expected to call invalidate_active_sessions! if user is suspended_for_fraud" do
17381762
expect(@user).to receive(:invalidate_active_sessions!)
17391763

@@ -1886,8 +1910,8 @@ def sample_string_of_length(n)
18861910
@user.suspend_for_fraud(author_id: @admin_user.id)
18871911
expect(user_3.reload.suspended?).to be(true)
18881912
expect(@user_2.reload.suspended?).to be(true)
1889-
expect(user_3.comments.count).to eq(2)
1890-
expect(@user_2.comments.count).to eq(2)
1913+
expect(user_3.comments.count).to eq(1)
1914+
expect(@user_2.comments.count).to eq(1)
18911915
expect(@user.reload.comments.count).to eq(2)
18921916
end
18931917

spec/services/iffy/user/ban_service_spec.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@
88
let(:service) { described_class.new(user.external_id) }
99

1010
context "when the user can be suspended" do
11-
it "suspends the user and adds a comment" do
12-
expect_any_instance_of(User).to receive(:flag_for_tos_violation!).with(
13-
author_name: "Iffy",
14-
content: "Banned for a policy violation on #{Time.current.to_fs(:formatted_date_full_month)} (General non-compliance)",
15-
bulk: true
16-
).and_call_original
11+
it "suspends the user directly and adds a comment" do
1712
expect_any_instance_of(User).to receive(:suspend_for_tos_violation!).with(
1813
author_name: "Iffy",
1914
content: "Banned for a policy violation on #{Time.current.to_fs(:formatted_date_full_month)} (General non-compliance)",

spec/sidekiq/suspend_accounts_with_payment_address_worker_spec.rb

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313
described_class.new.perform(@user.id)
1414

1515
expect(@user_2.reload.suspended?).to be(true)
16-
expect(@user_2.comments.first.content).to eq("Flagged for fraud automatically on #{Time.current.to_fs(:formatted_date_full_month)} because of usage of payment address #{@user.payment_address} (from User##{@user.id})")
17-
expect(@user_2.comments.last.content).to eq("Suspended for fraud automatically on #{Time.current.to_fs(:formatted_date_full_month)} because of usage of payment address #{@user.payment_address} (from User##{@user.id})")
16+
expect(@user_2.comments.count).to eq(1)
17+
expect(@user_2.comments.first.content).to eq("Suspended for fraud automatically on #{Time.current.to_fs(:formatted_date_full_month)} because of usage of payment address #{@user.payment_address} (from User##{@user.id})")
1818
end
1919

2020
it "does not suspend already suspended users with same payment address" do
21-
@user_2.flag_for_fraud!(author_name: "test")
2221
@user_2.suspend_for_fraud!(author_name: "test")
2322
initial_comment_count = @user_2.comments.count
2423

@@ -46,11 +45,11 @@
4645
expect(@user_3.reload.suspended?).to be(false)
4746
end
4847

49-
it "creates both flagged and suspended comments with fingerprint details" do
48+
it "creates a suspended comment with fingerprint details" do
5049
described_class.new.perform(@user.id)
5150

52-
expect(@user_2.reload.comments.first.content).to eq("Flagged for fraud automatically on #{Time.current.to_fs(:formatted_date_full_month)} because of usage of bank account fingerprint same_fingerprint_123 (from User##{@user.id})")
53-
expect(@user_2.comments.last.content).to eq("Suspended for fraud automatically on #{Time.current.to_fs(:formatted_date_full_month)} because of usage of bank account fingerprint same_fingerprint_123 (from User##{@user.id})")
51+
expect(@user_2.reload.comments.count).to eq(1)
52+
expect(@user_2.comments.first.content).to eq("Suspended for fraud automatically on #{Time.current.to_fs(:formatted_date_full_month)} because of usage of bank account fingerprint same_fingerprint_123 (from User##{@user.id})")
5453
end
5554

5655
it "does not suspend if fingerprint is blank" do
@@ -70,7 +69,6 @@
7069
end
7170

7271
it "does not suspend already suspended users" do
73-
@user_2.flag_for_fraud!(author_name: "test")
7472
@user_2.suspend_for_fraud!(author_name: "test")
7573
initial_comment_count = @user_2.comments.count
7674

spec/sidekiq/suspend_users_worker_spec.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@
2020
expect(user_not_to_suspend.reload.suspended?).to be(false)
2121

2222
comments = not_reviewed_user.comments
23-
expect(comments.count).to eq(2)
24-
expect(comments.first.content).to eq("Flagged for a policy violation by #{admin_user.name_or_username} on #{Time.current.to_fs(:formatted_date_full_month)}")
23+
expect(comments.count).to eq(1)
24+
expect(comments.first.content).to eq("Suspended for a policy violation by #{admin_user.name_or_username} on #{Time.current.to_fs(:formatted_date_full_month)} as part of mass suspension. Reason: #{reason}.\nAdditional notes: #{additional_notes}")
2525
expect(comments.first.author_id).to eq(admin_user.id)
26-
expect(comments.last.content).to eq("Suspended for a policy violation by #{admin_user.name_or_username} on #{Time.current.to_fs(:formatted_date_full_month)} as part of mass suspension. Reason: #{reason}.\nAdditional notes: #{additional_notes}")
27-
expect(comments.last.author_id).to eq(admin_user.id)
2826
end
2927

3028
it "suspends the users appropriately with external IDs" do

0 commit comments

Comments
 (0)