Repository navigation
Conversation
| def enable_accounts_with_same_stripe_fingerprint | ||
| fingerprints = bank_accounts.where.not(stripe_fingerprint: [nil, ""]).pluck(:stripe_fingerprint).uniq | ||
| return if fingerprints.empty? |
There was a problem hiding this comment.
Same pattern as the suspend worker - checking all bank accounts (including deleted) to find all fingerprints this user has ever used.
| matching_fingerprint = (fingerprints & user.alive_bank_accounts.pluck(:stripe_fingerprint)).first | ||
| user.mark_compliant!(author_name: "enable_sellers_other_accounts", content: "Marked compliant automatically on #{Time.current.to_fs(:formatted_date_full_month)} as bank account fingerprint #{matching_fingerprint} is now unblocked") |
There was a problem hiding this comment.
Finding the specific matching fingerprint for an accurate admin comment.
| bank_account.stripe_fingerprint = stripe_external_account.fingerprint | ||
| bank_account.save! | ||
|
|
||
| CheckPaymentAddressWorker.perform_async(bank_account.user_id) |
There was a problem hiding this comment.
This triggers the fraud check right after Stripe gives us the fingerprint. Mirrors how PayPal triggers the check in UpdatePayoutMethod when a payment address is set.
There was a problem hiding this comment.
Adding an index since we're now querying by fingerprint. Should help with performance when we have many bank accounts.
|
|
||
| return if suspended_user.payment_address.blank? | ||
| suspend_users_with_same_payment_address(suspended_user) | ||
| suspend_users_with_same_stripe_fingerprint(suspended_user) |
There was a problem hiding this comment.
After checking PayPal addresses, we now also check for matching Stripe fingerprints.
| User.where(id: user_ids_with_same_fingerprint).find_each do |user| | ||
| next if user.suspended? | ||
|
|
||
| matching_fingerprint = (fingerprints & user.alive_bank_accounts.pluck(:stripe_fingerprint)).first |
There was a problem hiding this comment.
When the suspended user has multiple fingerprints, we find which specific one matched so the admin comment is accurate.
| include Sidekiq::Job | ||
| sidekiq_options retry: 0, queue: :default | ||
|
|
||
| SUSPENDED_STATES = %w[suspended_for_tos_violation suspended_for_fraud].freeze |
There was a problem hiding this comment.
Extracted this constant to avoid repetition and make it clearer what states we're checking for.
There was a problem hiding this comment.
This constant would be better off being defined in the User::Risk module and invoked here
| end | ||
|
|
||
| def stripe_fingerprint_matches_suspended_account?(user) | ||
| fingerprints = user.alive_bank_accounts.where.not(stripe_fingerprint: [nil, ""]).pluck(:stripe_fingerprint).uniq |
There was a problem hiding this comment.
Checking all of the new user's fingerprints, not just their active bank account. A user might have multiple bank accounts.
| suspended_accounts_with_same_fingerprint = BankAccount | ||
| .joins(:user) | ||
| .where(stripe_fingerprint: fingerprints) | ||
| .where.not(user_id: user.id) | ||
| .where(users: { user_risk_state: SUSPENDED_STATES }) | ||
|
|
There was a problem hiding this comment.
Not filtering by .alive here - we intentionally check against ALL bank accounts (including deleted) from suspended users. This preserves fraud history even if fraudsters delete their accounts.
| .where.not(user_id: user.id) | ||
| .where(users: { user_risk_state: SUSPENDED_STATES }) | ||
|
|
||
| blocked_fingerprints = fingerprints.any? { |fp| BlockedObject.find_active_object(fp).present? } |
There was a problem hiding this comment.
Also checking BlockedObject for explicitly blocked fingerprints, same pattern as the PayPal email check.
There was a problem hiding this comment.
You can query MongoDB once instead of one time per fingerprint
| blocked_fingerprints = fingerprints.any? { |fp| BlockedObject.find_active_object(fp).present? } | |
| blocked_fingerprints = BlockedObject.find_active_objects(fingerprints) |
There was a problem hiding this comment.
Can you verify this does not evaluate immediately? It would be better to not send the query to MongoDB if suspended_accounts_with_same_fingerprint.exists? is already true
Resolved conflicts in: - app/sidekiq/suspend_accounts_with_payment_address_worker.rb - app/modules/user/risk.rb Combined Stripe fingerprint detection with skip_transition_callback fix.
- Keep upstream's schema version (2026_11_19_011937) - Preserve stripe_fingerprint index on bank_accounts - Use upstream's integer type for default_offer_code_id
EmCousin
left a comment
There was a problem hiding this comment.
The implementation looks sound and well tested, I added a few comments addressing performance. I will also run CI now. Can you address them, and fix failing tests if we find some. Thanks!
| end | ||
|
|
||
| def enable_accounts_with_same_stripe_fingerprint | ||
| fingerprints = bank_accounts.where.not(stripe_fingerprint: [nil, ""]).pluck(:stripe_fingerprint).uniq |
There was a problem hiding this comment.
You can uniq the fingerprints in the database rather than in memory:
| fingerprints = bank_accounts.where.not(stripe_fingerprint: [nil, ""]).pluck(:stripe_fingerprint).uniq | |
| fingerprints = bank_accounts.where.not(stripe_fingerprint: [nil, ""]).distinct.pluck(:stripe_fingerprint) |
| user_ids_with_same_fingerprint = BankAccount.alive | ||
| .where(stripe_fingerprint: fingerprints) | ||
| .where.not(user_id: id) | ||
| .pluck(:user_id) | ||
| .uniq |
There was a problem hiding this comment.
| user_ids_with_same_fingerprint = BankAccount.alive | |
| .where(stripe_fingerprint: fingerprints) | |
| .where.not(user_id: id) | |
| .pluck(:user_id) | |
| .uniq | |
| user_ids_with_same_fingerprint = BankAccount.alive | |
| .where(stripe_fingerprint: fingerprints) | |
| .where.not(user_id: id) | |
| .distinct | |
| .pluck(:user_id) |
| include Sidekiq::Job | ||
| sidekiq_options retry: 0, queue: :default | ||
|
|
||
| SUSPENDED_STATES = %w[suspended_for_tos_violation suspended_for_fraud].freeze |
There was a problem hiding this comment.
This constant would be better off being defined in the User::Risk module and invoked here
| end | ||
|
|
||
| def stripe_fingerprint_matches_suspended_account?(user) | ||
| fingerprints = user.alive_bank_accounts.where.not(stripe_fingerprint: [nil, ""]).pluck(:stripe_fingerprint).uniq |
There was a problem hiding this comment.
| fingerprints = user.alive_bank_accounts.where.not(stripe_fingerprint: [nil, ""]).pluck(:stripe_fingerprint).uniq | |
| fingerprints = user.alive_bank_accounts.where.not(stripe_fingerprint: [nil, ""]).distinct.pluck(:stripe_fingerprint) |
| .where.not(user_id: user.id) | ||
| .where(users: { user_risk_state: SUSPENDED_STATES }) | ||
|
|
||
| blocked_fingerprints = fingerprints.any? { |fp| BlockedObject.find_active_object(fp).present? } |
There was a problem hiding this comment.
You can query MongoDB once instead of one time per fingerprint
| blocked_fingerprints = fingerprints.any? { |fp| BlockedObject.find_active_object(fp).present? } | |
| blocked_fingerprints = BlockedObject.find_active_objects(fingerprints) |
| .where.not(user_id: user.id) | ||
| .where(users: { user_risk_state: SUSPENDED_STATES }) | ||
|
|
||
| blocked_fingerprints = fingerprints.any? { |fp| BlockedObject.find_active_object(fp).present? } |
There was a problem hiding this comment.
Can you verify this does not evaluate immediately? It would be better to not send the query to MongoDB if suspended_accounts_with_same_fingerprint.exists? is already true
| end | ||
|
|
||
| def suspend_users_with_same_stripe_fingerprint(suspended_user) | ||
| fingerprints = suspended_user.bank_accounts.where.not(stripe_fingerprint: [nil, ""]).pluck(:stripe_fingerprint).uniq |
There was a problem hiding this comment.
| fingerprints = suspended_user.bank_accounts.where.not(stripe_fingerprint: [nil, ""]).pluck(:stripe_fingerprint).uniq | |
| fingerprints = suspended_user.bank_accounts.where.not(stripe_fingerprint: [nil, ""]).distinct.pluck(:stripe_fingerprint) |
| .pluck(:user_id) | ||
| .uniq |
There was a problem hiding this comment.
| .pluck(:user_id) | |
| .uniq | |
| .distinct | |
| .pluck(:user_id) |
…nts) (antiwork#2810) Fixes : #1158 # Description ## Problem We currently auto-suspend accounts that share the same PayPal payout address as another suspended account. However, fraudsters are increasingly using shared ACH bank accounts (identified by Stripe fingerprints) across multiple accounts. When one account gets suspended, the related accounts using the same bank account slip through undetected, leading to more chargebacks and time spent hunting down fraudsters manually. ## Solution Extended the existing PayPal-based auto-suspension system to also handle Stripe bank account fingerprints: **When an admin suspends an account:** - Find all other accounts using the same bank account fingerprint - Flag them for fraud and suspend them automatically - Add comments explaining why they were suspended and which account triggered it **When a new account sets up a bank account:** - Check if the fingerprint matches any suspended account's bank account - Also check if the fingerprint is explicitly blocked - Flag the account for fraud if there's a match **When an account is marked compliant:** - Related accounts with the same fingerprint are also marked compliant I also addressed several edge cases that improve on the original PayPal implementation: - Checks ALL bank account fingerprints (not just the active one) since a fraudster might have multiple accounts - Preserves fraud history even if the suspended user deletes their bank account - Skips already suspended users to avoid duplicate processing - Added a database index on `stripe_fingerprint` for performance # Test Results <img width="781" height="237" alt="image" src="https://github.com/user-attachments/assets/8ac8199d-83ca-4c8d-9ea8-41cd1c438c81" /> --- # Checklist - [x] I have read the [contributing guidelines](https://github.com/antiwork/gumroad/blob/main/CONTRIBUTING.md) - [x] I have watched [Gumroad PR review livestreams](https://www.youtube.com/@anti-work) - [x] I have performed a self-review and left review comments on my PR - [x] I have added/updated tests for my changes --- # AI Disclosure Claude Opus 4.5 was used to assist with implementing this feature via Claude Code. --------- Co-authored-by: Emmanuel Cousin <[email protected]>
…nts) (antiwork#2810) Fixes : #1158 # Description ## Problem We currently auto-suspend accounts that share the same PayPal payout address as another suspended account. However, fraudsters are increasingly using shared ACH bank accounts (identified by Stripe fingerprints) across multiple accounts. When one account gets suspended, the related accounts using the same bank account slip through undetected, leading to more chargebacks and time spent hunting down fraudsters manually. ## Solution Extended the existing PayPal-based auto-suspension system to also handle Stripe bank account fingerprints: **When an admin suspends an account:** - Find all other accounts using the same bank account fingerprint - Flag them for fraud and suspend them automatically - Add comments explaining why they were suspended and which account triggered it **When a new account sets up a bank account:** - Check if the fingerprint matches any suspended account's bank account - Also check if the fingerprint is explicitly blocked - Flag the account for fraud if there's a match **When an account is marked compliant:** - Related accounts with the same fingerprint are also marked compliant I also addressed several edge cases that improve on the original PayPal implementation: - Checks ALL bank account fingerprints (not just the active one) since a fraudster might have multiple accounts - Preserves fraud history even if the suspended user deletes their bank account - Skips already suspended users to avoid duplicate processing - Added a database index on `stripe_fingerprint` for performance # Test Results <img width="781" height="237" alt="image" src="https://github.com/user-attachments/assets/8ac8199d-83ca-4c8d-9ea8-41cd1c438c81" /> --- # Checklist - [x] I have read the [contributing guidelines](https://github.com/antiwork/gumroad/blob/main/CONTRIBUTING.md) - [x] I have watched [Gumroad PR review livestreams](https://www.youtube.com/@anti-work) - [x] I have performed a self-review and left review comments on my PR - [x] I have added/updated tests for my changes --- # AI Disclosure Claude Opus 4.5 was used to assist with implementing this feature via Claude Code. --------- Co-authored-by: Emmanuel Cousin <[email protected]>
Fixes : #1158
Description
Problem
We currently auto-suspend accounts that share the same PayPal payout address as another suspended account. However, fraudsters are increasingly using shared ACH bank accounts (identified by Stripe fingerprints) across multiple accounts. When one account gets suspended, the related accounts using the same bank account slip through undetected, leading to more chargebacks and time spent hunting down fraudsters manually.
Solution
Extended the existing PayPal-based auto-suspension system to also handle Stripe bank account fingerprints:
When an admin suspends an account:
When a new account sets up a bank account:
When an account is marked compliant:
I also addressed several edge cases that improve on the original PayPal implementation:
stripe_fingerprintfor performanceTest Results
Checklist
AI Disclosure
Claude Opus 4.5 was used to assist with implementing this feature via Claude Code.