mirror of
https://github.com/we-promise/sure.git
synced 2026-04-18 03:24:09 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
26
app/models/concerns/tax_treatable.rb
Normal file
26
app/models/concerns/tax_treatable.rb
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
17
app/views/accounts/_tax_treatment_badge.html.erb
Normal file
17
app/views/accounts/_tax_treatment_badge.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<%# locals: (account:) %>
|
||||
<%
|
||||
treatment = account.tax_treatment
|
||||
badge_classes = case treatment
|
||||
when :tax_exempt
|
||||
"bg-green-500/10 text-green-600 theme-dark:text-green-400"
|
||||
when :tax_deferred
|
||||
"bg-blue-500/10 text-blue-600 theme-dark:text-blue-400"
|
||||
when :tax_advantaged
|
||||
"bg-purple-500/10 text-purple-600 theme-dark:text-purple-400"
|
||||
else
|
||||
"bg-gray-500/10 text-secondary"
|
||||
end
|
||||
%>
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium <%= badge_classes %>" title="<%= t("accounts.tax_treatment_descriptions.#{treatment}") %>">
|
||||
<%= account.tax_treatment_label %>
|
||||
</span>
|
||||
@@ -9,6 +9,9 @@
|
||||
<div class="truncate">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="font-medium text-xl truncate <%= "animate-pulse" if account.syncing? %>"><%= title %></h2>
|
||||
<% if account.tax_treatment.present? %>
|
||||
<%= render partial: "accounts/tax_treatment_badge", locals: { account: account } %>
|
||||
<% end %>
|
||||
<% if account.draft? %>
|
||||
<%= render DS::Link.new(
|
||||
text: "Complete setup",
|
||||
|
||||
@@ -103,6 +103,23 @@ en:
|
||||
credit_card: Credit Card
|
||||
loan: Loan
|
||||
other_liability: Other Liability
|
||||
tax_treatments:
|
||||
taxable: Taxable
|
||||
tax_deferred: Tax-Deferred
|
||||
tax_exempt: Tax-Exempt
|
||||
tax_advantaged: Tax-Advantaged
|
||||
tax_treatment_descriptions:
|
||||
taxable: Gains taxed when realized
|
||||
tax_deferred: Contributions deductible, taxed on withdrawal
|
||||
tax_exempt: Contributions after-tax, gains not taxed
|
||||
tax_advantaged: Special tax benefits with conditions
|
||||
subtype_regions:
|
||||
us: United States
|
||||
uk: United Kingdom
|
||||
ca: Canada
|
||||
au: Australia
|
||||
eu: Europe
|
||||
generic: General
|
||||
confirm_unlink:
|
||||
title: Unlink account from provider?
|
||||
description_html: "You are about to unlink <strong>%{account_name}</strong> from <strong>%{provider_name}</strong>. This will convert it to a manual account."
|
||||
|
||||
@@ -10,6 +10,115 @@ en:
|
||||
title: Enter account balance
|
||||
show:
|
||||
chart_title: Total value
|
||||
subtypes:
|
||||
# United States
|
||||
brokerage:
|
||||
short: Brokerage
|
||||
long: Brokerage
|
||||
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
|
||||
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
|
||||
529_plan:
|
||||
short: 529 Plan
|
||||
long: 529 Education Savings Plan
|
||||
hsa:
|
||||
short: HSA
|
||||
long: Health Savings Account
|
||||
ugma:
|
||||
short: UGMA
|
||||
long: UGMA Custodial Account
|
||||
utma:
|
||||
short: UTMA
|
||||
long: UTMA Custodial Account
|
||||
# United Kingdom
|
||||
isa:
|
||||
short: ISA
|
||||
long: Individual Savings Account
|
||||
lisa:
|
||||
short: LISA
|
||||
long: Lifetime ISA
|
||||
sipp:
|
||||
short: SIPP
|
||||
long: Self-Invested Personal Pension
|
||||
workplace_pension_uk:
|
||||
short: Pension
|
||||
long: Workplace Pension
|
||||
# Canada
|
||||
rrsp:
|
||||
short: RRSP
|
||||
long: Registered Retirement Savings Plan
|
||||
tfsa:
|
||||
short: TFSA
|
||||
long: Tax-Free Savings Account
|
||||
resp:
|
||||
short: RESP
|
||||
long: Registered Education Savings Plan
|
||||
lira:
|
||||
short: LIRA
|
||||
long: Locked-In Retirement Account
|
||||
rrif:
|
||||
short: RRIF
|
||||
long: Registered Retirement Income Fund
|
||||
# Australia
|
||||
super:
|
||||
short: Super
|
||||
long: Superannuation
|
||||
smsf:
|
||||
short: SMSF
|
||||
long: Self-Managed Super Fund
|
||||
# Europe
|
||||
pea:
|
||||
short: PEA
|
||||
long: Plan d'Épargne en Actions
|
||||
pillar_3a:
|
||||
short: Pillar 3a
|
||||
long: Private Pension (Pillar 3a)
|
||||
riester:
|
||||
short: Riester
|
||||
long: Riester-Rente
|
||||
# Generic
|
||||
pension:
|
||||
short: Pension
|
||||
long: Pension
|
||||
retirement:
|
||||
short: Retirement
|
||||
long: Retirement Account
|
||||
mutual_fund:
|
||||
short: Mutual Fund
|
||||
long: Mutual Fund
|
||||
angel:
|
||||
short: Angel
|
||||
long: Angel Investment
|
||||
trust:
|
||||
short: Trust
|
||||
long: Trust
|
||||
other:
|
||||
short: Other
|
||||
long: Other Investment
|
||||
value_tooltip:
|
||||
cash: Cash
|
||||
holdings: Holdings
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddTaxTreatmentToCryptos < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :cryptos, :tax_treatment, :string, default: "taxable", null: false
|
||||
end
|
||||
end
|
||||
3
db/schema.rb
generated
3
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2026_01_16_090336) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2026_01_17_200000) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
@@ -256,6 +256,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_01_16_090336) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.jsonb "locked_attributes", default: {}
|
||||
t.string "subtype"
|
||||
t.string "tax_treatment", default: "taxable", null: false
|
||||
end
|
||||
|
||||
create_table "data_enrichments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
|
||||
@@ -89,4 +89,70 @@ class AccountTest < ActiveSupport::TestCase
|
||||
assert_equal "Investments", account.short_subtype_label
|
||||
assert_equal "Investments", account.long_subtype_label
|
||||
end
|
||||
|
||||
# Tax treatment tests (TaxTreatable concern)
|
||||
|
||||
test "tax_treatment delegates to accountable for Investment" do
|
||||
investment = Investment.new(subtype: "401k")
|
||||
account = @family.accounts.create!(
|
||||
name: "Test 401k",
|
||||
balance: 1000,
|
||||
currency: "USD",
|
||||
accountable: investment
|
||||
)
|
||||
|
||||
assert_equal :tax_deferred, account.tax_treatment
|
||||
assert_equal I18n.t("accounts.tax_treatments.tax_deferred"), account.tax_treatment_label
|
||||
end
|
||||
|
||||
test "tax_treatment delegates to accountable for Crypto" do
|
||||
crypto = Crypto.new(tax_treatment: :taxable)
|
||||
account = @family.accounts.create!(
|
||||
name: "Test Crypto",
|
||||
balance: 500,
|
||||
currency: "USD",
|
||||
accountable: crypto
|
||||
)
|
||||
|
||||
assert_equal :taxable, account.tax_treatment
|
||||
assert_equal I18n.t("accounts.tax_treatments.taxable"), account.tax_treatment_label
|
||||
end
|
||||
|
||||
test "tax_treatment returns nil for non-investment accounts" do
|
||||
# Depository accounts don't have tax_treatment
|
||||
assert_nil @account.tax_treatment
|
||||
assert_nil @account.tax_treatment_label
|
||||
end
|
||||
|
||||
test "tax_advantaged? returns true for tax-advantaged accounts" do
|
||||
investment = Investment.new(subtype: "401k")
|
||||
account = @family.accounts.create!(
|
||||
name: "Test 401k",
|
||||
balance: 1000,
|
||||
currency: "USD",
|
||||
accountable: investment
|
||||
)
|
||||
|
||||
assert account.tax_advantaged?
|
||||
assert_not account.taxable?
|
||||
end
|
||||
|
||||
test "tax_advantaged? returns false for taxable accounts" do
|
||||
investment = Investment.new(subtype: "brokerage")
|
||||
account = @family.accounts.create!(
|
||||
name: "Test Brokerage",
|
||||
balance: 1000,
|
||||
currency: "USD",
|
||||
accountable: investment
|
||||
)
|
||||
|
||||
assert_not account.tax_advantaged?
|
||||
assert account.taxable?
|
||||
end
|
||||
|
||||
test "taxable? returns true for accounts without tax_treatment" do
|
||||
# Depository accounts
|
||||
assert @account.taxable?
|
||||
assert_not @account.tax_advantaged?
|
||||
end
|
||||
end
|
||||
|
||||
30
test/models/crypto_test.rb
Normal file
30
test/models/crypto_test.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
require "test_helper"
|
||||
|
||||
class CryptoTest < ActiveSupport::TestCase
|
||||
test "tax_treatment defaults to taxable" do
|
||||
crypto = Crypto.new
|
||||
assert_equal "taxable", crypto.tax_treatment
|
||||
end
|
||||
|
||||
test "tax_treatment can be set to tax_deferred" do
|
||||
crypto = Crypto.new(tax_treatment: :tax_deferred)
|
||||
assert_equal "tax_deferred", crypto.tax_treatment
|
||||
end
|
||||
|
||||
test "tax_treatment can be set to tax_exempt" do
|
||||
crypto = Crypto.new(tax_treatment: :tax_exempt)
|
||||
assert_equal "tax_exempt", crypto.tax_treatment
|
||||
end
|
||||
|
||||
test "tax_treatment enum provides predicate methods" do
|
||||
crypto = Crypto.new(tax_treatment: :taxable)
|
||||
assert crypto.taxable?
|
||||
assert_not crypto.tax_deferred?
|
||||
assert_not crypto.tax_exempt?
|
||||
|
||||
crypto.tax_treatment = :tax_deferred
|
||||
assert_not crypto.taxable?
|
||||
assert crypto.tax_deferred?
|
||||
assert_not crypto.tax_exempt?
|
||||
end
|
||||
end
|
||||
139
test/models/investment_test.rb
Normal file
139
test/models/investment_test.rb
Normal file
@@ -0,0 +1,139 @@
|
||||
require "test_helper"
|
||||
|
||||
class InvestmentTest < ActiveSupport::TestCase
|
||||
# Tax treatment derivation tests
|
||||
|
||||
test "tax_treatment returns tax_deferred for US retirement accounts" do
|
||||
%w[401k 403b 457b tsp ira sep_ira simple_ira].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :tax_deferred, investment.tax_treatment, "Expected #{subtype} to be tax_deferred"
|
||||
end
|
||||
end
|
||||
|
||||
test "tax_treatment returns tax_exempt for Roth accounts" do
|
||||
%w[roth_401k roth_ira].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :tax_exempt, investment.tax_treatment, "Expected #{subtype} to be tax_exempt"
|
||||
end
|
||||
end
|
||||
|
||||
test "tax_treatment returns tax_advantaged for special accounts" do
|
||||
%w[529_plan hsa].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :tax_advantaged, investment.tax_treatment, "Expected #{subtype} to be tax_advantaged"
|
||||
end
|
||||
end
|
||||
|
||||
test "tax_treatment returns taxable for standard accounts" do
|
||||
%w[brokerage mutual_fund angel trust ugma utma other].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :taxable, investment.tax_treatment, "Expected #{subtype} to be taxable"
|
||||
end
|
||||
end
|
||||
|
||||
test "tax_treatment returns taxable for nil subtype" do
|
||||
investment = Investment.new(subtype: nil)
|
||||
assert_equal :taxable, investment.tax_treatment
|
||||
end
|
||||
|
||||
test "tax_treatment returns taxable for unknown subtype" do
|
||||
investment = Investment.new(subtype: "unknown_type")
|
||||
assert_equal :taxable, investment.tax_treatment
|
||||
end
|
||||
|
||||
# UK account types
|
||||
|
||||
test "tax_treatment returns tax_exempt for UK ISA accounts" do
|
||||
%w[isa lisa].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :tax_exempt, investment.tax_treatment, "Expected #{subtype} to be tax_exempt"
|
||||
end
|
||||
end
|
||||
|
||||
test "tax_treatment returns tax_deferred for UK pension accounts" do
|
||||
%w[sipp workplace_pension_uk].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :tax_deferred, investment.tax_treatment, "Expected #{subtype} to be tax_deferred"
|
||||
end
|
||||
end
|
||||
|
||||
# Canadian account types
|
||||
|
||||
test "tax_treatment returns tax_deferred for Canadian retirement accounts" do
|
||||
%w[rrsp lira rrif].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :tax_deferred, investment.tax_treatment, "Expected #{subtype} to be tax_deferred"
|
||||
end
|
||||
end
|
||||
|
||||
test "tax_treatment returns tax_exempt for Canadian TFSA" do
|
||||
investment = Investment.new(subtype: "tfsa")
|
||||
assert_equal :tax_exempt, investment.tax_treatment
|
||||
end
|
||||
|
||||
test "tax_treatment returns tax_advantaged for Canadian RESP" do
|
||||
investment = Investment.new(subtype: "resp")
|
||||
assert_equal :tax_advantaged, investment.tax_treatment
|
||||
end
|
||||
|
||||
# Australian account types
|
||||
|
||||
test "tax_treatment returns tax_deferred for Australian super accounts" do
|
||||
%w[super smsf].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :tax_deferred, investment.tax_treatment, "Expected #{subtype} to be tax_deferred"
|
||||
end
|
||||
end
|
||||
|
||||
# European account types
|
||||
|
||||
test "tax_treatment returns tax_deferred for European pension accounts" do
|
||||
%w[pillar_3a riester].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :tax_deferred, investment.tax_treatment, "Expected #{subtype} to be tax_deferred"
|
||||
end
|
||||
end
|
||||
|
||||
test "tax_treatment returns tax_advantaged for French PEA" do
|
||||
investment = Investment.new(subtype: "pea")
|
||||
assert_equal :tax_advantaged, investment.tax_treatment
|
||||
end
|
||||
|
||||
# Generic account types
|
||||
|
||||
test "tax_treatment returns tax_deferred for generic pension and retirement" do
|
||||
%w[pension retirement].each do |subtype|
|
||||
investment = Investment.new(subtype: subtype)
|
||||
assert_equal :tax_deferred, investment.tax_treatment, "Expected #{subtype} to be tax_deferred"
|
||||
end
|
||||
end
|
||||
|
||||
# Subtype metadata tests
|
||||
|
||||
test "all subtypes have required metadata keys" do
|
||||
Investment::SUBTYPES.each do |key, metadata|
|
||||
assert metadata.key?(:short), "Subtype #{key} missing :short key"
|
||||
assert metadata.key?(:long), "Subtype #{key} missing :long key"
|
||||
assert metadata.key?(:tax_treatment), "Subtype #{key} missing :tax_treatment key"
|
||||
assert metadata.key?(:region), "Subtype #{key} missing :region key"
|
||||
end
|
||||
end
|
||||
|
||||
test "all subtypes have valid tax_treatment values" do
|
||||
valid_treatments = %i[taxable tax_deferred tax_exempt tax_advantaged]
|
||||
|
||||
Investment::SUBTYPES.each do |key, metadata|
|
||||
assert_includes valid_treatments, metadata[:tax_treatment],
|
||||
"Subtype #{key} has invalid tax_treatment: #{metadata[:tax_treatment]}"
|
||||
end
|
||||
end
|
||||
|
||||
test "all subtypes have valid region values" do
|
||||
valid_regions = [ "us", "uk", "ca", "au", "eu", nil ]
|
||||
|
||||
Investment::SUBTYPES.each do |key, metadata|
|
||||
assert_includes valid_regions, metadata[:region],
|
||||
"Subtype #{key} has invalid region: #{metadata[:region]}"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user