diff --git a/backend/app/mailers/user_mailer.rb b/backend/app/mailers/user_mailer.rb index dd366ffaa4..93c8fe42f9 100644 --- a/backend/app/mailers/user_mailer.rb +++ b/backend/app/mailers/user_mailer.rb @@ -26,6 +26,13 @@ def tax_id_validation_success(user_id) mail(to: @user.email, subject: "✅ Thanks for updating your tax information") end + def tin_reverification_required(user_id) + @user = User.find(user_id) + @settings_url = "#{PROTOCOL}://#{DOMAIN}/settings/tax" + + mail(to: @user.email, subject: "Action required: Update your tax information") + end + def tax_form_review_reminder(user_compliance_info_id, company_id, tax_year) @user_compliance_info = UserComplianceInfo.find(user_compliance_info_id) @company = Company.find(company_id) diff --git a/backend/app/models/user.rb b/backend/app/models/user.rb index 9c5c314b99..f0c13682da 100644 --- a/backend/app/models/user.rb +++ b/backend/app/models/user.rb @@ -11,7 +11,7 @@ class User < ApplicationRecord NON_TAX_COMPLIANCE_ATTRIBUTES = %i[legal_name birth_date country_code citizenship_country_code street_address city state zip_code] USER_PROVIDED_TAX_ATTRIBUTES = %i[tax_id business_entity business_name business_type tax_classification] - TAX_ATTRIBUTES = USER_PROVIDED_TAX_ATTRIBUTES + %i[tax_id_status tax_information_confirmed_at] + TAX_ATTRIBUTES = USER_PROVIDED_TAX_ATTRIBUTES + %i[tax_id_status tax_information_confirmed_at requires_tin_reverification] COMPLIANCE_ATTRIBUTES = NON_TAX_COMPLIANCE_ATTRIBUTES + USER_PROVIDED_TAX_ATTRIBUTES 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 MAX_MINIMUM_DIVIDEND_PAYMENT_IN_CENTS = 1_000_00 @@ -110,7 +110,7 @@ def compliance_attributes end def has_verified_tax_id? - tax_id.present? && (!requires_w9? || tax_id_status == UserComplianceInfo::TAX_ID_STATUS_VERIFIED) + tax_id.present? && (!requires_w9? || tax_id_status == UserComplianceInfo::TAX_ID_STATUS_VERIFIED) && !requires_tin_reverification end def company_administrator_for(company) diff --git a/backend/app/models/user_compliance_info.rb b/backend/app/models/user_compliance_info.rb index 36e3b666d1..e190c035cd 100644 --- a/backend/app/models/user_compliance_info.rb +++ b/backend/app/models/user_compliance_info.rb @@ -86,7 +86,10 @@ def update_tax_id_status tax_status_related_attributes = %w[legal_name business_name business_entity tax_id] if persisted? - self.tax_id_status = nil if tax_status_related_attributes.any? { send("#{_1}_changed?") } + if tax_status_related_attributes.any? { send("#{_1}_changed?") } + self.tax_id_status = nil + self.requires_tin_reverification = false if requires_tin_reverification? + end elsif prior_compliance_info.present? && prior_compliance_info.attributes.values_at(*tax_status_related_attributes) == attributes.values_at(*tax_status_related_attributes) self.tax_id_status = prior_compliance_info.tax_id_status end diff --git a/backend/app/presenters/user_presenter.rb b/backend/app/presenters/user_presenter.rb index 5fc07a0156..d54c8b08b4 100644 --- a/backend/app/presenters/user_presenter.rb +++ b/backend/app/presenters/user_presenter.rb @@ -6,7 +6,7 @@ class UserPresenter :legal_name, :preferred_name, :display_name, :billing_entity_name, :unconfirmed_email, :created_at, :state, :city, :zip_code, :street_address, :bank_account, :contracts, :tax_id, :birth_date, :requires_w9?, :tax_information_confirmed_at, :minimum_dividend_payment_in_cents, :bank_accounts, - :tax_id_status, private: true, to: :user, allow_nil: true + :tax_id_status, :requires_tin_reverification, private: true, to: :user, allow_nil: true def initialize(current_context:) @current_context = current_context @@ -118,6 +118,7 @@ def logged_in_user email: user.display_email, onboardingPath: worker && worker.role.nil? ? "/documents" : nil, taxInformationConfirmedAt: tax_information_confirmed_at&.iso8601, + requiresTinReverification: !!requires_tin_reverification, isImpersonating: Current.impersonated_user.present?, githubUsername: user.github_username, } diff --git a/backend/app/services/onetime/flag_cp2100a_tin_reverification.rb b/backend/app/services/onetime/flag_cp2100a_tin_reverification.rb new file mode 100644 index 0000000000..97fba52cb2 --- /dev/null +++ b/backend/app/services/onetime/flag_cp2100a_tin_reverification.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# Onetime script to flag users from IRS CP 2100A notice for TIN re-verification +# Usage: +# tins = %w[111111111 222222222] +# Onetime::FlagCp2100aTinReverification.perform(tins:, dry_run: true) # Preview only +# Onetime::FlagCp2100aTinReverification.perform(tins:, dry_run: false) # Actually flag users +class Onetime::FlagCp2100aTinReverification + def self.perform(tins:, dry_run: true) + if dry_run + puts "DRY RUN MODE - No changes will be made" + puts "" + else + puts "LIVE MODE - Users will be flagged for TIN re-verification" + puts "" + end + + puts "Processing #{tins.count} TINs from IRS CP 2100A notice..." + puts "================================================================================" + + found_users = [] + not_found_tins = [] + + all_compliance_infos = UserComplianceInfo.alive.where.not(tax_id: nil).includes(:user) + + tins.each do |tin| + compliance_info = all_compliance_infos.find { |ci| ci.tax_id == tin } + + if compliance_info.nil? + not_found_tins << tin + next + end + + user = compliance_info.user + found_users << { + user:, + compliance_info:, + tin:, + } + end + + puts "\n=== FOUND USERS (#{found_users.count}) ===" + puts "================================================================================" + + if found_users.any? + found_users.each_with_index do |data, index| + user = data[:user] + tin = data[:tin] + + puts "\n#{index + 1}. User ID: #{user.id}" + puts " Email: #{user.email}" + puts " Name: #{user.name}" + puts " Legal Name: #{user.legal_name}" + puts " TIN: ***#{tin[-4..]}" + puts " Current Status: #{data[:compliance_info].requires_tin_reverification ? 'Already flagged' : 'Not flagged'}" + + if dry_run + puts " [DRY RUN] Would flag this user for TIN re-verification and send email" + else + begin + data[:compliance_info].update!( + requires_tin_reverification: true, + tax_id_status: nil + ) + UserMailer.tin_reverification_required(user.id).deliver_later + puts " ✓ Flagged for TIN re-verification and email sent" + rescue => e + puts " ✗ Error: #{e.message}" + end + end + end + else + puts "No users found with the provided TINs" + end + + if not_found_tins.any? + puts "\n=== NOT FOUND TINS (#{not_found_tins.count}) ===" + puts "================================================================================" + not_found_tins.each do |tin| + puts " #{tin}" + end + end + + puts "\n================================================================================" + puts "SUMMARY:" + puts "================================================================================" + puts " Total TINs processed: #{tins.count}" + puts " Users found: #{found_users.count}" + puts " TINs not found: #{not_found_tins.count}" + + if dry_run + puts "\n✓ Dry run complete! No changes were made." + puts " To actually flag users and send emails, run with dry_run: false" + else + puts "\n✓ Flagging complete!" + puts " Users have been flagged for TIN re-verification and emails have been sent." + end + end +end diff --git a/backend/app/views/user_mailer/tin_reverification_required.html.erb b/backend/app/views/user_mailer/tin_reverification_required.html.erb new file mode 100644 index 0000000000..ba1e462bb7 --- /dev/null +++ b/backend/app/views/user_mailer/tin_reverification_required.html.erb @@ -0,0 +1,8 @@ +
We received a CP 2100A notice from the IRS regarding a mismatch between your <%= @user.business_entity? ? "business name" : "legal name" %> and <%= @user.tax_id_name %> on file.
+ +To continue receiving payments without tax withholding, please log in to Flexile and re-enter your tax information. Your legal name must match your IRS records exactly.
+ +Important: Until you update your information, you will not be able to receive payments.
+ +<%= link_to "Update your tax info", @settings_url, class: "button" %> diff --git a/backend/db/migrate/20260202034515_add_tin_reverification_fields_to_user_compliance_infos.rb b/backend/db/migrate/20260202034515_add_tin_reverification_fields_to_user_compliance_infos.rb new file mode 100644 index 0000000000..c26a39d8bc --- /dev/null +++ b/backend/db/migrate/20260202034515_add_tin_reverification_fields_to_user_compliance_infos.rb @@ -0,0 +1,5 @@ +class AddTinReverificationFieldsToUserComplianceInfos < ActiveRecord::Migration[8.0] + def change + add_column :user_compliance_infos, :requires_tin_reverification, :boolean, default: false, null: false + end +end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index ebdc0f3193..3840ce08b8 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_01_28_070657) do +ActiveRecord::Schema[8.0].define(version: 2026_02_02_034515) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_catalog.plpgsql" @@ -836,6 +836,7 @@ t.boolean "business_entity", default: false t.integer "business_type" t.integer "tax_classification" + t.boolean "requires_tin_reverification", default: false, null: false t.index ["user_id"], name: "index_user_compliance_infos_on_user_id" end diff --git a/backend/spec/models/user_compliance_info_spec.rb b/backend/spec/models/user_compliance_info_spec.rb index 6d5f10df52..3d7bb037e4 100644 --- a/backend/spec/models/user_compliance_info_spec.rb +++ b/backend/spec/models/user_compliance_info_spec.rb @@ -303,6 +303,24 @@ user_compliance_info.update!(legal_name: "Elmer Fudd") end.to change { user_compliance_info.reload.tax_id_status }.from(UserComplianceInfo::TAX_ID_STATUS_INVALID).to(nil) end + + context "when user is flagged for TIN re-verification" do + it "clears the re-verification flag when tax-related attributes are updated" do + user_compliance_info.update!(requires_tin_reverification: true, tax_id_status: nil) + + expect do + user_compliance_info.update!(legal_name: "New Name") + end.to change { user_compliance_info.reload.requires_tin_reverification }.from(true).to(false) + end + + it "does not clear the flag if tax-related attributes are unchanged" do + user_compliance_info.update!(requires_tin_reverification: true, tax_id_status: nil) + + expect do + user_compliance_info.update!(street_address: "456 New St") + end.not_to change { user_compliance_info.reload.requires_tin_reverification } + end + end end end end diff --git a/backend/spec/models/user_spec.rb b/backend/spec/models/user_spec.rb index ec86ced64a..6ab4b91cfc 100644 --- a/backend/spec/models/user_spec.rb +++ b/backend/spec/models/user_spec.rb @@ -365,6 +365,12 @@ user.compliance_info.save(validate: false) # bypass validation expect(user.reload.has_verified_tax_id?).to eq false end + + it "returns false if the user is flagged for TIN re-verification" do + user.compliance_info.update!(tax_id_status: UserComplianceInfo::TAX_ID_STATUS_VERIFIED) + user.compliance_info.update!(requires_tin_reverification: true, tax_id_status: nil) + expect(user.reload.has_verified_tax_id?).to eq false + end end context "for users outside of the US" do @@ -383,6 +389,11 @@ user.compliance_info.save(validate: false) # bypass validation expect(user.reload.has_verified_tax_id?).to eq false end + + it "returns false if the user is flagged for TIN re-verification" do + user.compliance_info.update!(requires_tin_reverification: true, tax_id_status: nil) + expect(user.reload.has_verified_tax_id?).to eq false + end end end diff --git a/frontend/app/(dashboard)/layout.tsx b/frontend/app/(dashboard)/layout.tsx index a6cc7265ba..963966e897 100644 --- a/frontend/app/(dashboard)/layout.tsx +++ b/frontend/app/(dashboard)/layout.tsx @@ -80,6 +80,20 @@ function DashboardLayout({ children }: { children: React.ReactNode }) { logout(); }; + React.useEffect(() => { + if (user.requiresTinReverification && pathname !== "/settings/tax") { + router.replace("/settings/tax"); + } + }, [user.requiresTinReverification, pathname, router]); + + if (user.requiresTinReverification && pathname !== "/settings/tax") { + return ( +Redirecting...
+