Add tax treatment classification for investment accounts with international support (#693)

* Add tax treatment support for accounts, investments, and cryptos

* Replace hardcoded region labels with I18n translations

* Add I18n support for subtype labels with fallback to hardcoded values

* fixed schema

---------

Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
This commit is contained in:
LPW
2026-01-18 11:29:02 -05:00
committed by GitHub
parent 6ec03e93f4
commit 64dc5c2fb8
14 changed files with 488 additions and 23 deletions

View File

@@ -1,5 +1,5 @@
class Account < ApplicationRecord
include AASM, Syncable, Monetizable, Chartable, Linkable, Enrichable, Anchorable, Reconcileable
include AASM, Syncable, Monetizable, Chartable, Linkable, Enrichable, Anchorable, Reconcileable, TaxTreatable
validates :name, :balance, :currency, presence: true

View File

@@ -31,11 +31,17 @@ module Accountable
end
# Given a subtype, look up the label for this accountable type
# Uses i18n with fallback to hardcoded SUBTYPES values
def subtype_label_for(subtype, format: :short)
return nil if subtype.nil?
label_type = format == :long ? :long : :short
self::SUBTYPES[subtype]&.fetch(label_type, nil)
fallback = self::SUBTYPES.dig(subtype, label_type)
I18n.t(
"#{name.underscore.pluralize}.subtypes.#{subtype}.#{label_type}",
default: fallback
)
end
# Convenience method for getting the short label

View File

@@ -0,0 +1,26 @@
module TaxTreatable
extend ActiveSupport::Concern
# Delegates tax treatment to the accountable (Investment or Crypto)
# Returns nil for account types that don't support tax treatment
def tax_treatment
return nil unless accountable.respond_to?(:tax_treatment)
accountable.tax_treatment&.to_sym
end
# Returns the i18n label for the tax treatment
def tax_treatment_label
return nil unless tax_treatment
I18n.t("accounts.tax_treatments.#{tax_treatment}")
end
# Returns true if the account has tax advantages (deferred, exempt, or advantaged)
def tax_advantaged?
tax_treatment.in?(%i[tax_deferred tax_exempt tax_advantaged])
end
# Returns true if gains in this account are taxable
def taxable?
tax_treatment == :taxable || tax_treatment.nil?
end
end

View File

@@ -1,6 +1,14 @@
class Crypto < ApplicationRecord
include Accountable
# Crypto is taxable by default, but can be held in tax-advantaged accounts
# (e.g., self-directed IRA, though rare)
enum :tax_treatment, {
taxable: "taxable",
tax_deferred: "tax_deferred",
tax_exempt: "tax_exempt"
}, default: :taxable
class << self
def color
"#737373"

View File

@@ -1,29 +1,63 @@
class Investment < ApplicationRecord
include Accountable
# Tax treatment categories:
# - taxable: Gains taxed when realized
# - tax_deferred: Taxes deferred until withdrawal
# - tax_exempt: Qualified gains are tax-free
# - tax_advantaged: Special tax benefits with conditions
SUBTYPES = {
"brokerage" => { short: "Brokerage", long: "Brokerage" },
"pension" => { short: "Pension", long: "Pension" },
"retirement" => { short: "Retirement", long: "Retirement" },
"401k" => { short: "401(k)", long: "401(k)" },
"roth_401k" => { short: "Roth 401(k)", long: "Roth 401(k)" },
"403b" => { short: "403(b)", long: "403(b)" },
"457b" => { short: "457(b)", long: "457(b)" },
"tsp" => { short: "TSP", long: "Thrift Savings Plan" },
"529_plan" => { short: "529 Plan", long: "529 Plan" },
"hsa" => { short: "HSA", long: "Health Savings Account" },
"mutual_fund" => { short: "Mutual Fund", long: "Mutual Fund" },
"ira" => { short: "IRA", long: "Traditional IRA" },
"roth_ira" => { short: "Roth IRA", long: "Roth IRA" },
"sep_ira" => { short: "SEP IRA", long: "SEP IRA" },
"simple_ira" => { short: "SIMPLE IRA", long: "SIMPLE IRA" },
"angel" => { short: "Angel", long: "Angel" },
"trust" => { short: "Trust", long: "Trust" },
"ugma" => { short: "UGMA", long: "UGMA" },
"utma" => { short: "UTMA", long: "UTMA" },
"other" => { short: "Other", long: "Other Investment" }
# === United States ===
"brokerage" => { short: "Brokerage", long: "Brokerage", region: "us", tax_treatment: :taxable },
"401k" => { short: "401(k)", long: "401(k)", region: "us", tax_treatment: :tax_deferred },
"roth_401k" => { short: "Roth 401(k)", long: "Roth 401(k)", region: "us", tax_treatment: :tax_exempt },
"403b" => { short: "403(b)", long: "403(b)", region: "us", tax_treatment: :tax_deferred },
"457b" => { short: "457(b)", long: "457(b)", region: "us", tax_treatment: :tax_deferred },
"tsp" => { short: "TSP", long: "Thrift Savings Plan", region: "us", tax_treatment: :tax_deferred },
"ira" => { short: "IRA", long: "Traditional IRA", region: "us", tax_treatment: :tax_deferred },
"roth_ira" => { short: "Roth IRA", long: "Roth IRA", region: "us", tax_treatment: :tax_exempt },
"sep_ira" => { short: "SEP IRA", long: "SEP IRA", region: "us", tax_treatment: :tax_deferred },
"simple_ira" => { short: "SIMPLE IRA", long: "SIMPLE IRA", region: "us", tax_treatment: :tax_deferred },
"529_plan" => { short: "529 Plan", long: "529 Education Savings Plan", region: "us", tax_treatment: :tax_advantaged },
"hsa" => { short: "HSA", long: "Health Savings Account", region: "us", tax_treatment: :tax_advantaged },
"ugma" => { short: "UGMA", long: "UGMA Custodial Account", region: "us", tax_treatment: :taxable },
"utma" => { short: "UTMA", long: "UTMA Custodial Account", region: "us", tax_treatment: :taxable },
# === United Kingdom ===
"isa" => { short: "ISA", long: "Individual Savings Account", region: "uk", tax_treatment: :tax_exempt },
"lisa" => { short: "LISA", long: "Lifetime ISA", region: "uk", tax_treatment: :tax_exempt },
"sipp" => { short: "SIPP", long: "Self-Invested Personal Pension", region: "uk", tax_treatment: :tax_deferred },
"workplace_pension_uk" => { short: "Pension", long: "Workplace Pension", region: "uk", tax_treatment: :tax_deferred },
# === Canada ===
"rrsp" => { short: "RRSP", long: "Registered Retirement Savings Plan", region: "ca", tax_treatment: :tax_deferred },
"tfsa" => { short: "TFSA", long: "Tax-Free Savings Account", region: "ca", tax_treatment: :tax_exempt },
"resp" => { short: "RESP", long: "Registered Education Savings Plan", region: "ca", tax_treatment: :tax_advantaged },
"lira" => { short: "LIRA", long: "Locked-In Retirement Account", region: "ca", tax_treatment: :tax_deferred },
"rrif" => { short: "RRIF", long: "Registered Retirement Income Fund", region: "ca", tax_treatment: :tax_deferred },
# === Australia ===
"super" => { short: "Super", long: "Superannuation", region: "au", tax_treatment: :tax_deferred },
"smsf" => { short: "SMSF", long: "Self-Managed Super Fund", region: "au", tax_treatment: :tax_deferred },
# === Europe ===
"pea" => { short: "PEA", long: "Plan d'Épargne en Actions", region: "eu", tax_treatment: :tax_advantaged },
"pillar_3a" => { short: "Pillar 3a", long: "Private Pension (Pillar 3a)", region: "eu", tax_treatment: :tax_deferred },
"riester" => { short: "Riester", long: "Riester-Rente", region: "eu", tax_treatment: :tax_deferred },
# === Generic (available everywhere) ===
"pension" => { short: "Pension", long: "Pension", region: nil, tax_treatment: :tax_deferred },
"retirement" => { short: "Retirement", long: "Retirement Account", region: nil, tax_treatment: :tax_deferred },
"mutual_fund" => { short: "Mutual Fund", long: "Mutual Fund", region: nil, tax_treatment: :taxable },
"angel" => { short: "Angel", long: "Angel Investment", region: nil, tax_treatment: :taxable },
"trust" => { short: "Trust", long: "Trust", region: nil, tax_treatment: :taxable },
"other" => { short: "Other", long: "Other Investment", region: nil, tax_treatment: :taxable }
}.freeze
def tax_treatment
SUBTYPES.dig(subtype, :tax_treatment) || :taxable
end
class << self
def color
"#1570EF"
@@ -36,5 +70,9 @@ class Investment < ApplicationRecord
def icon
"chart-line"
end
def region_label_for(region)
I18n.t("accounts.subtype_regions.#{region || 'generic'}")
end
end
end