require "test_helper" class SimplefinItemTest < ActiveSupport::TestCase setup do @family = families(:dylan_family) @simplefin_item = SimplefinItem.create!( family: @family, name: "Test SimpleFin Connection", access_url: "https://example.com/access_token" ) end test "belongs to family" do assert_equal @family, @simplefin_item.family end test "has many simplefin_accounts" do account = @simplefin_item.simplefin_accounts.create!( name: "Test Account", account_id: "test_123", currency: "USD", account_type: "checking", current_balance: 1000.00 ) assert_includes @simplefin_item.simplefin_accounts, account end test "has good status by default" do assert_equal "good", @simplefin_item.status end test "setup token update is required when requires_update has no successful account sync" do @simplefin_item.update!(status: :requires_update) Sync.create!( syncable: @simplefin_item, status: "failed", failed_at: Time.current, error: "SimpleFIN access forbidden", sync_stats: { "total_accounts" => 0 } ) assert @simplefin_item.setup_token_update_required? assert_equal "requires_update", @simplefin_item.effective_status assert_includes @simplefin_item.attention_summary, "Connection needs update" end test "setup token update is not required when latest sync returned accounts" do @simplefin_item.update!(status: :requires_update, pending_account_setup: true) # Must be a terminal sync: a pending/in-progress sync short-circuits before # the account-count check, so this would pass without exercising the branch. Sync.create!( syncable: @simplefin_item, status: "completed", completed_at: Time.current, sync_stats: { "total_accounts" => 18, "error_buckets" => { "auth" => 1 }, "errors" => [ "Connection to Cash App may need attention. Auth required" ] } ) refute @simplefin_item.setup_token_update_required? assert_equal "good", @simplefin_item.effective_status refute_includes @simplefin_item.attention_summary, "Connection needs update" assert_includes @simplefin_item.attention_summary, "Accounts need setup" end test "setup token update is not required when latest sync stats use symbol keys" do @simplefin_item.update!(status: :requires_update) latest_sync = Sync.new( syncable: @simplefin_item, status: "completed", completed_at: Time.current, sync_stats: { total_accounts: 18, error_buckets: { auth: 1 } } ) refute @simplefin_item.setup_token_update_required?(latest_sync:) assert_equal "good", @simplefin_item.effective_status(latest_sync:) end test "setup token update is not required while latest sync is unresolved" do @simplefin_item.update!(status: :requires_update) Sync.create!( syncable: @simplefin_item, status: "pending", sync_stats: { "import_started" => true } ) refute @simplefin_item.setup_token_update_required? assert_equal "good", @simplefin_item.effective_status end test "setup token update is required when latest sync is stale without accounts" do @simplefin_item.update!(status: :requires_update) Sync.create!( syncable: @simplefin_item, status: "stale", sync_stats: { "import_started" => true } ) assert @simplefin_item.setup_token_update_required? assert_equal "requires_update", @simplefin_item.effective_status end test "can be marked for deletion" do refute @simplefin_item.scheduled_for_deletion? @simplefin_item.destroy_later assert @simplefin_item.scheduled_for_deletion? end test "is syncable" do assert_respond_to @simplefin_item, :sync_later assert_respond_to @simplefin_item, :syncing? end test "memoizes accounts across repeated calls" do queries = capture_sql_queries do @simplefin_item.accounts @simplefin_item.accounts end assert_equal 1, queries.grep(/FROM "simplefin_accounts"/).size end test "scopes work correctly" do # Create one for deletion item_for_deletion = SimplefinItem.create!( family: @family, name: "Delete Me", access_url: "https://example.com/delete_token", scheduled_for_deletion: true ) active_items = SimplefinItem.active ordered_items = SimplefinItem.ordered assert_includes active_items, @simplefin_item refute_includes active_items, item_for_deletion assert_equal [ @simplefin_item, item_for_deletion ].sort_by(&:created_at).reverse, ordered_items.to_a end test "upserts institution data correctly" do org_data = { id: "bank123", name: "Test Bank", domain: "testbank.com", url: "https://testbank.com", "sfin-url": "https://sfin.testbank.com" } @simplefin_item.upsert_institution_data!(org_data) assert_equal "bank123", @simplefin_item.institution_id assert_equal "Test Bank", @simplefin_item.institution_name assert_equal "testbank.com", @simplefin_item.institution_domain assert_equal "https://testbank.com", @simplefin_item.institution_url assert_equal org_data.stringify_keys, @simplefin_item.raw_institution_payload end test "institution display name fallback works" do # No institution data assert_equal @simplefin_item.name, @simplefin_item.institution_display_name # With institution name @simplefin_item.update!(institution_name: "Chase Bank") assert_equal "Chase Bank", @simplefin_item.institution_display_name # With domain fallback @simplefin_item.update!(institution_name: nil, institution_domain: "chase.com") assert_equal "chase.com", @simplefin_item.institution_display_name end test "connected institutions returns unique institutions" do # Create accounts with different institutions account1 = @simplefin_item.simplefin_accounts.create!( name: "Checking", account_id: "acc1", currency: "USD", account_type: "checking", current_balance: 1000, org_data: { "name" => "Chase Bank", "domain" => "chase.com" } ) account2 = @simplefin_item.simplefin_accounts.create!( name: "Savings", account_id: "acc2", currency: "USD", account_type: "savings", current_balance: 2000, org_data: { "name" => "Wells Fargo", "domain" => "wellsfargo.com" } ) institutions = @simplefin_item.connected_institutions assert_equal 2, institutions.count assert_includes institutions.map { |i| i["name"] }, "Chase Bank" assert_includes institutions.map { |i| i["name"] }, "Wells Fargo" end test "institution summary with multiple institutions" do # No institutions assert_equal "No institutions connected", @simplefin_item.institution_summary # One institution @simplefin_item.simplefin_accounts.create!( name: "Checking", account_id: "acc1", currency: "USD", account_type: "checking", current_balance: 1000, org_data: { "name" => "Chase Bank" } ) assert_equal "Chase Bank", @simplefin_item.institution_summary # Multiple institutions @simplefin_item.simplefin_accounts.create!( name: "Savings", account_id: "acc2", currency: "USD", account_type: "savings", current_balance: 2000, org_data: { "name" => "Wells Fargo" } ) assert_equal "2 institutions", @simplefin_item.institution_summary end private def capture_sql_queries queries = [] callback = lambda do |_name, _start, _finish, _message_id, payload| sql = payload[:sql].to_s name = payload[:name].to_s next if name == "SCHEMA" next if sql.match?(/\A(?:BEGIN|COMMIT|ROLLBACK)\b/i) queries << sql end ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do yield end queries end end