diff --git a/app/models/account.rb b/app/models/account.rb
index 305cc1fd0..d4cdf0684 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -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
diff --git a/app/models/concerns/accountable.rb b/app/models/concerns/accountable.rb
index d7f2a3315..3b764c245 100644
--- a/app/models/concerns/accountable.rb
+++ b/app/models/concerns/accountable.rb
@@ -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
diff --git a/app/models/concerns/tax_treatable.rb b/app/models/concerns/tax_treatable.rb
new file mode 100644
index 000000000..1aa85638d
--- /dev/null
+++ b/app/models/concerns/tax_treatable.rb
@@ -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
diff --git a/app/models/crypto.rb b/app/models/crypto.rb
index 7724a9535..16e894265 100644
--- a/app/models/crypto.rb
+++ b/app/models/crypto.rb
@@ -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"
diff --git a/app/models/investment.rb b/app/models/investment.rb
index f8b44e8b9..e701b2f80 100644
--- a/app/models/investment.rb
+++ b/app/models/investment.rb
@@ -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
diff --git a/app/views/accounts/_tax_treatment_badge.html.erb b/app/views/accounts/_tax_treatment_badge.html.erb
new file mode 100644
index 000000000..070e870ba
--- /dev/null
+++ b/app/views/accounts/_tax_treatment_badge.html.erb
@@ -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
+%>
+">
+ <%= account.tax_treatment_label %>
+
diff --git a/app/views/accounts/show/_header.html.erb b/app/views/accounts/show/_header.html.erb
index 6d151059a..41d731236 100644
--- a/app/views/accounts/show/_header.html.erb
+++ b/app/views/accounts/show/_header.html.erb
@@ -9,6 +9,9 @@
"><%= title %>
+ <% 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",
diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml
index c321cfba3..939d175af 100644
--- a/config/locales/views/accounts/en.yml
+++ b/config/locales/views/accounts/en.yml
@@ -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 %{account_name} from %{provider_name}. This will convert it to a manual account."
diff --git a/config/locales/views/investments/en.yml b/config/locales/views/investments/en.yml
index 451609e3b..7a4875ba1 100644
--- a/config/locales/views/investments/en.yml
+++ b/config/locales/views/investments/en.yml
@@ -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
diff --git a/db/migrate/20260117200000_add_tax_treatment_to_cryptos.rb b/db/migrate/20260117200000_add_tax_treatment_to_cryptos.rb
new file mode 100644
index 000000000..e25fc1838
--- /dev/null
+++ b/db/migrate/20260117200000_add_tax_treatment_to_cryptos.rb
@@ -0,0 +1,5 @@
+class AddTaxTreatmentToCryptos < ActiveRecord::Migration[7.2]
+ def change
+ add_column :cryptos, :tax_treatment, :string, default: "taxable", null: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d7a150a26..976bb9da4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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|
diff --git a/test/models/account_test.rb b/test/models/account_test.rb
index 911b0bfa4..8a52192af 100644
--- a/test/models/account_test.rb
+++ b/test/models/account_test.rb
@@ -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
diff --git a/test/models/crypto_test.rb b/test/models/crypto_test.rb
new file mode 100644
index 000000000..d32ba009b
--- /dev/null
+++ b/test/models/crypto_test.rb
@@ -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
diff --git a/test/models/investment_test.rb b/test/models/investment_test.rb
new file mode 100644
index 000000000..dbae27153
--- /dev/null
+++ b/test/models/investment_test.rb
@@ -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