Skip to content

Commit 50afb6c

Browse files
Add TIN reverification workflow for 1099-DIV (#1591)
Issue antiwork/accounting#77 # Description The IRS sent a CP 2100A notice indicating TIN/name mismatches for certain users on 1099-DIV. We need to: 1. Flag affected users for TIN re-verification 2. Notify them via email to update their tax information 3. Prevent them from receiving payments until they re-verify # Screenshots ## Tax Setting Page <img width="1470" height="791" alt="image" src="https://github.com/user-attachments/assets/2db83690-ea0a-4a02-aaae-424ed6406e51" /> ## Email <img width="756" height="619" alt="image" src="https://github.com/user-attachments/assets/066065c2-2636-4f39-ba1e-9ad825f6e46b" /> ## One Time Script execution ### Dry run <img width="1131" height="470" alt="image" src="https://github.com/user-attachments/assets/c2893e49-99f6-470a-a10b-9a19dbea9dd1" /> ### Live <img width="1136" height="588" alt="image" src="https://github.com/user-attachments/assets/1e646a98-be85-4e12-89f6-e26fbf3d34d7" /> # AI disclosure Claude Sonnet 4.5 used for code generation. All code self reviewed.
1 parent efaebfe commit 50afb6c

File tree

14 files changed

+188
-5
lines changed

14 files changed

+188
-5
lines changed

backend/app/mailers/user_mailer.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ def tax_id_validation_success(user_id)
2626
mail(to: @user.email, subject: "✅ Thanks for updating your tax information")
2727
end
2828

29+
def tin_reverification_required(user_id)
30+
@user = User.find(user_id)
31+
@settings_url = "#{PROTOCOL}://#{DOMAIN}/settings/tax"
32+
33+
mail(to: @user.email, subject: "Action required: Update your tax information")
34+
end
35+
2936
def tax_form_review_reminder(user_compliance_info_id, company_id, tax_year)
3037
@user_compliance_info = UserComplianceInfo.find(user_compliance_info_id)
3138
@company = Company.find(company_id)

backend/app/models/user.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class User < ApplicationRecord
1111

1212
NON_TAX_COMPLIANCE_ATTRIBUTES = %i[legal_name birth_date country_code citizenship_country_code street_address city state zip_code]
1313
USER_PROVIDED_TAX_ATTRIBUTES = %i[tax_id business_entity business_name business_type tax_classification]
14-
TAX_ATTRIBUTES = USER_PROVIDED_TAX_ATTRIBUTES + %i[tax_id_status tax_information_confirmed_at]
14+
TAX_ATTRIBUTES = USER_PROVIDED_TAX_ATTRIBUTES + %i[tax_id_status tax_information_confirmed_at requires_tin_reverification]
1515
COMPLIANCE_ATTRIBUTES = NON_TAX_COMPLIANCE_ATTRIBUTES + USER_PROVIDED_TAX_ATTRIBUTES
1616
CONSULTING_CONTRACT_ATTRIBUTES = %i[email legal_name business_entity business_name street_address city state zip_code country_code citizenship_country_code] # should include all attributes that are referenced in the consulting contract
1717
MAX_MINIMUM_DIVIDEND_PAYMENT_IN_CENTS = 1_000_00
@@ -110,7 +110,7 @@ def compliance_attributes
110110
end
111111

112112
def has_verified_tax_id?
113-
tax_id.present? && (!requires_w9? || tax_id_status == UserComplianceInfo::TAX_ID_STATUS_VERIFIED)
113+
tax_id.present? && (!requires_w9? || tax_id_status == UserComplianceInfo::TAX_ID_STATUS_VERIFIED) && !requires_tin_reverification
114114
end
115115

116116
def company_administrator_for(company)

backend/app/models/user_compliance_info.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ def update_tax_id_status
8686
tax_status_related_attributes = %w[legal_name business_name business_entity tax_id]
8787

8888
if persisted?
89-
self.tax_id_status = nil if tax_status_related_attributes.any? { send("#{_1}_changed?") }
89+
if tax_status_related_attributes.any? { send("#{_1}_changed?") }
90+
self.tax_id_status = nil
91+
self.requires_tin_reverification = false if requires_tin_reverification?
92+
end
9093
elsif prior_compliance_info.present? && prior_compliance_info.attributes.values_at(*tax_status_related_attributes) == attributes.values_at(*tax_status_related_attributes)
9194
self.tax_id_status = prior_compliance_info.tax_id_status
9295
end

backend/app/presenters/user_presenter.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class UserPresenter
66
:legal_name, :preferred_name, :display_name, :billing_entity_name, :unconfirmed_email,
77
:created_at, :state, :city, :zip_code, :street_address, :bank_account, :contracts, :tax_id, :birth_date,
88
:requires_w9?, :tax_information_confirmed_at, :minimum_dividend_payment_in_cents, :bank_accounts,
9-
:tax_id_status, private: true, to: :user, allow_nil: true
9+
:tax_id_status, :requires_tin_reverification, private: true, to: :user, allow_nil: true
1010

1111
def initialize(current_context:)
1212
@current_context = current_context
@@ -119,6 +119,7 @@ def logged_in_user
119119
email: user.display_email,
120120
onboardingPath: worker && worker.role.nil? ? "/documents" : nil,
121121
taxInformationConfirmedAt: tax_information_confirmed_at&.iso8601,
122+
requiresTinReverification: !!requires_tin_reverification,
122123
isImpersonating: Current.impersonated_user.present?,
123124
githubUsername: user.github_username,
124125
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
# Onetime script to flag users from IRS CP 2100A notice for TIN re-verification
4+
# Usage:
5+
# tins = %w[111111111 222222222]
6+
# Onetime::FlagCp2100aTinReverification.perform(tins:, dry_run: true) # Preview only
7+
# Onetime::FlagCp2100aTinReverification.perform(tins:, dry_run: false) # Actually flag users
8+
class Onetime::FlagCp2100aTinReverification
9+
def self.perform(tins:, dry_run: true)
10+
if dry_run
11+
puts "DRY RUN MODE - No changes will be made"
12+
puts ""
13+
else
14+
puts "LIVE MODE - Users will be flagged for TIN re-verification"
15+
puts ""
16+
end
17+
18+
puts "Processing #{tins.count} TINs from IRS CP 2100A notice..."
19+
puts "================================================================================"
20+
21+
found_users = []
22+
not_found_tins = []
23+
24+
all_compliance_infos = UserComplianceInfo.alive.where.not(tax_id: nil).includes(:user)
25+
26+
tins.each do |tin|
27+
compliance_info = all_compliance_infos.find { |ci| ci.tax_id == tin }
28+
29+
if compliance_info.nil?
30+
not_found_tins << tin
31+
next
32+
end
33+
34+
user = compliance_info.user
35+
found_users << {
36+
user:,
37+
compliance_info:,
38+
tin:,
39+
}
40+
end
41+
42+
puts "\n=== FOUND USERS (#{found_users.count}) ==="
43+
puts "================================================================================"
44+
45+
if found_users.any?
46+
found_users.each_with_index do |data, index|
47+
user = data[:user]
48+
tin = data[:tin]
49+
50+
puts "\n#{index + 1}. User ID: #{user.id}"
51+
puts " Email: #{user.email}"
52+
puts " Name: #{user.name}"
53+
puts " Legal Name: #{user.legal_name}"
54+
puts " TIN: ***#{tin[-4..]}"
55+
puts " Current Status: #{data[:compliance_info].requires_tin_reverification ? 'Already flagged' : 'Not flagged'}"
56+
57+
if dry_run
58+
puts " [DRY RUN] Would flag this user for TIN re-verification and send email"
59+
else
60+
begin
61+
data[:compliance_info].update!(
62+
requires_tin_reverification: true,
63+
tax_id_status: nil
64+
)
65+
UserMailer.tin_reverification_required(user.id).deliver_later
66+
puts " ✓ Flagged for TIN re-verification and email sent"
67+
rescue => e
68+
puts " ✗ Error: #{e.message}"
69+
end
70+
end
71+
end
72+
else
73+
puts "No users found with the provided TINs"
74+
end
75+
76+
if not_found_tins.any?
77+
puts "\n=== NOT FOUND TINS (#{not_found_tins.count}) ==="
78+
puts "================================================================================"
79+
not_found_tins.each do |tin|
80+
puts " #{tin}"
81+
end
82+
end
83+
84+
puts "\n================================================================================"
85+
puts "SUMMARY:"
86+
puts "================================================================================"
87+
puts " Total TINs processed: #{tins.count}"
88+
puts " Users found: #{found_users.count}"
89+
puts " TINs not found: #{not_found_tins.count}"
90+
91+
if dry_run
92+
puts "\n✓ Dry run complete! No changes were made."
93+
puts " To actually flag users and send emails, run with dry_run: false"
94+
else
95+
puts "\n✓ Flagging complete!"
96+
puts " Users have been flagged for TIN re-verification and emails have been sent."
97+
end
98+
end
99+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<h1>IRS Notice: Action Required</h1>
2+
<p>We received a <strong>CP 2100A notice from the IRS</strong> regarding a mismatch between your <%= @user.business_entity? ? "business name" : "legal name" %> and <%= @user.tax_id_name %> on file.</p>
3+
4+
<p>To continue receiving payments without tax withholding, please log in to Flexile and re-enter your tax information. <strong>Your legal name must match your IRS records exactly.</strong></p>
5+
6+
<p><strong>Important:</strong> Until you update your information, you will not be able to receive payments.</p>
7+
8+
<%= link_to "Update your tax info", @settings_url, class: "button" %>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddTinReverificationFieldsToUserComplianceInfos < ActiveRecord::Migration[8.0]
2+
def change
3+
add_column :user_compliance_infos, :requires_tin_reverification, :boolean, default: false, null: false
4+
end
5+
end

backend/db/schema.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[8.0].define(version: 2026_01_28_070657) do
13+
ActiveRecord::Schema[8.0].define(version: 2026_02_02_034515) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "citext"
1616
enable_extension "pg_catalog.plpgsql"
@@ -836,6 +836,7 @@
836836
t.boolean "business_entity", default: false
837837
t.integer "business_type"
838838
t.integer "tax_classification"
839+
t.boolean "requires_tin_reverification", default: false, null: false
839840
t.index ["user_id"], name: "index_user_compliance_infos_on_user_id"
840841
end
841842

backend/spec/models/user_compliance_info_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,24 @@
303303
user_compliance_info.update!(legal_name: "Elmer Fudd")
304304
end.to change { user_compliance_info.reload.tax_id_status }.from(UserComplianceInfo::TAX_ID_STATUS_INVALID).to(nil)
305305
end
306+
307+
context "when user is flagged for TIN re-verification" do
308+
it "clears the re-verification flag when tax-related attributes are updated" do
309+
user_compliance_info.update!(requires_tin_reverification: true, tax_id_status: nil)
310+
311+
expect do
312+
user_compliance_info.update!(legal_name: "New Name")
313+
end.to change { user_compliance_info.reload.requires_tin_reverification }.from(true).to(false)
314+
end
315+
316+
it "does not clear the flag if tax-related attributes are unchanged" do
317+
user_compliance_info.update!(requires_tin_reverification: true, tax_id_status: nil)
318+
319+
expect do
320+
user_compliance_info.update!(street_address: "456 New St")
321+
end.not_to change { user_compliance_info.reload.requires_tin_reverification }
322+
end
323+
end
306324
end
307325
end
308326
end

backend/spec/models/user_spec.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,12 @@
365365
user.compliance_info.save(validate: false) # bypass validation
366366
expect(user.reload.has_verified_tax_id?).to eq false
367367
end
368+
369+
it "returns false if the user is flagged for TIN re-verification" do
370+
user.compliance_info.update!(tax_id_status: UserComplianceInfo::TAX_ID_STATUS_VERIFIED)
371+
user.compliance_info.update!(requires_tin_reverification: true, tax_id_status: nil)
372+
expect(user.reload.has_verified_tax_id?).to eq false
373+
end
368374
end
369375

370376
context "for users outside of the US" do
@@ -383,6 +389,11 @@
383389
user.compliance_info.save(validate: false) # bypass validation
384390
expect(user.reload.has_verified_tax_id?).to eq false
385391
end
392+
393+
it "returns false if the user is flagged for TIN re-verification" do
394+
user.compliance_info.update!(requires_tin_reverification: true, tax_id_status: nil)
395+
expect(user.reload.has_verified_tax_id?).to eq false
396+
end
386397
end
387398
end
388399

0 commit comments

Comments
 (0)