UI Suggestions for Account Types in Setup Modal + Stats-Based Inactive Handling (#368)

* - Add tests for `Simplefin::AccountTypeMapper` and `AccountSimplefinCreation`
- Implement `Simplefin::AccountTypeMapper` for account type inference with fallback-only logic
- Enhance inactive state handling for `SimplefinItem::Importer`
- Improve subtype selection handling in views with confidence-based inference

* Remove unnecessary `.presence` check for `openai_uri_base` in hostings settings

* Refine zero balance detection logic in `SimplefinItem::Importer` and add regression test for missing balances scenario

* Enhance account type and subtype inference logic with explicit investment subtype mapping, improved regex handling, and institution-based credit card detection

* Refine retirement subtype mapping in `AccountTypeMapper` tests with explicit case-based assertions

* Expand `AccountTypeMapper` investment subtype mapping to include `403b` and `tsp` with updated regex definitions

* Remove unused `retirement_hint?` method in `AccountTypeMapper` to simplify codebase

---------

Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
This commit is contained in:
LPW
2025-11-24 08:07:14 -05:00
committed by GitHub
parent eb4b978a97
commit cf5e7de65f
10 changed files with 336 additions and 4 deletions

View File

@@ -0,0 +1,38 @@
require "test_helper"
class AccountSimplefinCreationTest < ActiveSupport::TestCase
setup do
@family = families(:dylan_family)
@item = SimplefinItem.create!(family: @family, name: "SF Conn", access_url: "https://example.com/access")
end
test "requires explicit account_type at creation" do
sfa = SimplefinAccount.create!(
simplefin_item: @item,
name: "Brokerage",
account_id: "acct_1",
currency: "USD",
account_type: "investment",
current_balance: 1000
)
assert_raises(ArgumentError) do
Account.create_from_simplefin_account(sfa, nil)
end
end
test "uses provided account_type without inference" do
sfa = SimplefinAccount.create!(
simplefin_item: @item,
name: "My Loan",
account_id: "acct_2",
currency: "USD",
account_type: "loan",
current_balance: -5000
)
account = Account.create_from_simplefin_account(sfa, "Loan")
assert_equal "Loan", account.accountable_type
end
end

View File

@@ -0,0 +1,40 @@
require "test_helper"
class Simplefin::AccountTypeMapperTest < ActiveSupport::TestCase
test "holdings present implies Investment" do
inf = Simplefin::AccountTypeMapper.infer(name: "Vanguard Brokerage", holdings: [ { symbol: "VTI" } ])
assert_equal "Investment", inf.accountable_type
assert_nil inf.subtype
end
test "explicit retirement tokens map to exact subtypes" do
cases = {
"My Roth IRA" => "roth_ira",
"401k Fidelity" => "401k"
}
cases.each do |name, expected_subtype|
inf = Simplefin::AccountTypeMapper.infer(name: name, holdings: [ { symbol: "VTI" } ])
assert_equal "Investment", inf.accountable_type
assert_equal expected_subtype, inf.subtype
end
end
test "credit card names map to CreditCard" do
[ "Chase Credit Card", "VISA Card", "CREDIT" ] .each do |name|
inf = Simplefin::AccountTypeMapper.infer(name: name)
assert_equal "CreditCard", inf.accountable_type
end
end
test "loan-like names map to Loan" do
[ "Mortgage", "Student Loan", "HELOC", "Line of Credit" ].each do |name|
inf = Simplefin::AccountTypeMapper.infer(name: name)
assert_equal "Loan", inf.accountable_type
end
end
test "default is Depository" do
inf = Simplefin::AccountTypeMapper.infer(name: "Everyday Checking")
assert_equal "Depository", inf.accountable_type
end
end

View File

@@ -0,0 +1,62 @@
require "test_helper"
class SimplefinItem::ImporterInactiveTest < ActiveSupport::TestCase
setup do
@family = families(:dylan_family)
@item = SimplefinItem.create!(family: @family, name: "SF Conn", access_url: "https://example.com/access")
@sync = Sync.create!(syncable: @item)
end
def importer
@importer ||= SimplefinItem::Importer.new(@item, simplefin_provider: mock(), sync: @sync)
end
test "marks inactive when payload indicates closed or hidden" do
account_data = { id: "a1", name: "Old Checking", balance: 0, currency: "USD", closed: true }
importer.send(:import_account, account_data)
stats = @sync.reload.sync_stats
assert stats.dig("inactive", "a1"), "should be inactive when closed flag present"
end
test "marks inactive after three consecutive zero runs with no holdings" do
account_data = { id: "a2", name: "Dormant", balance: 0, "available-balance": 0, currency: "USD" }
2.times { importer.send(:import_account, account_data) }
stats = @sync.reload.sync_stats
assert_equal 2, stats.dig("zero_runs", "a2"), "should count zero runs"
assert_equal false, stats.dig("inactive", "a2"), "should not be inactive before threshold"
importer.send(:import_account, account_data)
stats = @sync.reload.sync_stats
assert_equal true, stats.dig("inactive", "a2"), "should be inactive at threshold"
end
test "resets zero_runs_count and inactive when activity returns" do
account_data = { id: "a3", name: "Dormant", balance: 0, "available-balance": 0, currency: "USD" }
3.times { importer.send(:import_account, account_data) }
stats = @sync.reload.sync_stats
assert_equal true, stats.dig("inactive", "a3")
# Activity returns: non-zero balance or holdings
active_data = { id: "a3", name: "Dormant", balance: 10, currency: "USD" }
importer.send(:import_account, active_data)
stats = @sync.reload.sync_stats
assert_equal 0, stats.dig("zero_runs", "a3")
assert_equal false, stats.dig("inactive", "a3")
end
end
# Additional regression: no balances present should not increment zero_runs or mark inactive
class SimplefinItem::ImporterInactiveTest < ActiveSupport::TestCase
test "does not count zero run when both balances are missing and no holdings" do
account_data = { id: "a4", name: "Unknown", currency: "USD" } # no balance keys, no holdings
importer.send(:import_account, account_data)
stats = @sync.reload.sync_stats
assert_equal 0, stats.dig("zero_runs", "a4").to_i
assert_equal false, stats.dig("inactive", "a4")
end
end