require "test_helper" class Family::DataImporterTest < ActiveSupport::TestCase setup do @family = families(:empty) end test "imports accounts with accountable data" do ndjson = build_ndjson([ { type: "Account", data: { id: "old-account-1", name: "Test Checking", balance: "1500.00", currency: "USD", accountable_type: "Depository", accountable: { subtype: "checking" } } } ]) importer = Family::DataImporter.new(@family, ndjson) result = importer.import! assert_equal 1, result[:accounts].count account = result[:accounts].first assert_equal "Test Checking", account.name assert_equal 1500.0, account.balance.to_f assert_equal "USD", account.currency assert_equal "Depository", account.accountable_type end test "imports categories with parent relationships" do ndjson = build_ndjson([ { type: "Category", data: { id: "cat-parent", name: "Shopping", color: "#FF5733", classification: "expense" } }, { type: "Category", data: { id: "cat-child", name: "Groceries", color: "#33FF57", classification: "expense", parent_id: "cat-parent" } } ]) importer = Family::DataImporter.new(@family, ndjson) importer.import! parent = @family.categories.find_by(name: "Shopping") child = @family.categories.find_by(name: "Groceries") assert_not_nil parent assert_not_nil child assert_equal parent.id, child.parent_id end test "imports tags" do ndjson = build_ndjson([ { type: "Tag", data: { id: "tag-1", name: "Important", color: "#FF0000" } } ]) importer = Family::DataImporter.new(@family, ndjson) importer.import! tag = @family.tags.find_by(name: "Important") assert_not_nil tag assert_equal "#FF0000", tag.color end test "imports merchants" do ndjson = build_ndjson([ { type: "Merchant", data: { id: "merchant-1", name: "Amazon" } } ]) importer = Family::DataImporter.new(@family, ndjson) importer.import! merchant = @family.merchants.find_by(name: "Amazon") assert_not_nil merchant end test "imports transactions with references" do ndjson = build_ndjson([ { type: "Account", data: { id: "acct-1", name: "Main Account", balance: "5000", currency: "USD", accountable_type: "Depository" } }, { type: "Category", data: { id: "cat-1", name: "Food", color: "#FF0000", classification: "expense" } }, { type: "Tag", data: { id: "tag-1", name: "Essential" } }, { type: "Transaction", data: { id: "txn-1", account_id: "acct-1", date: "2024-01-15", amount: "-50.00", name: "Grocery Store", currency: "USD", category_id: "cat-1", tag_ids: [ "tag-1" ], notes: "Weekly groceries" } } ]) importer = Family::DataImporter.new(@family, ndjson) result = importer.import! assert_equal 1, result[:entries].count transaction = @family.transactions.first assert_not_nil transaction assert_equal "Grocery Store", transaction.entry.name assert_equal -50.0, transaction.entry.amount.to_f assert_equal "Food", transaction.category.name assert_equal 1, transaction.tags.count assert_equal "Essential", transaction.tags.first.name assert_equal "Weekly groceries", transaction.entry.notes end test "imports trades with securities" do ndjson = build_ndjson([ { type: "Account", data: { id: "inv-acct-1", name: "Investment Account", balance: "10000", currency: "USD", accountable_type: "Investment" } }, { type: "Trade", data: { id: "trade-1", account_id: "inv-acct-1", date: "2024-01-15", ticker: "AAPL", qty: "10", price: "150.00", amount: "-1500.00", currency: "USD" } } ]) importer = Family::DataImporter.new(@family, ndjson) result = importer.import! # Account + Opening balance + Trade entry assert_equal 1, result[:entries].count trade = @family.trades.first assert_not_nil trade assert_equal "AAPL", trade.security.ticker assert_equal 10.0, trade.qty.to_f assert_equal 150.0, trade.price.to_f end test "imports valuations" do ndjson = build_ndjson([ { type: "Account", data: { id: "prop-acct-1", name: "Property", balance: "500000", currency: "USD", accountable_type: "Property" } }, { type: "Valuation", data: { id: "val-1", account_id: "prop-acct-1", date: "2024-06-15", amount: "520000", name: "Updated valuation", currency: "USD" } } ]) importer = Family::DataImporter.new(@family, ndjson) result = importer.import! assert_equal 1, result[:entries].count account = @family.accounts.find_by(name: "Property") valuation = account.valuations.joins(:entry).find_by(entries: { name: "Updated valuation" }) assert_not_nil valuation assert_equal 520000.0, valuation.entry.amount.to_f end test "imports budgets" do ndjson = build_ndjson([ { type: "Budget", data: { id: "budget-1", start_date: "2024-01-01", end_date: "2024-01-31", budgeted_spending: "3000.00", expected_income: "5000.00", currency: "USD" } } ]) importer = Family::DataImporter.new(@family, ndjson) importer.import! budget = @family.budgets.first assert_not_nil budget assert_equal Date.parse("2024-01-01"), budget.start_date assert_equal Date.parse("2024-01-31"), budget.end_date assert_equal 3000.0, budget.budgeted_spending.to_f assert_equal 5000.0, budget.expected_income.to_f end test "imports budget_categories" do ndjson = build_ndjson([ { type: "Category", data: { id: "cat-groceries", name: "Groceries", color: "#00FF00", classification: "expense" } }, { type: "Budget", data: { id: "budget-1", start_date: "2024-01-01", end_date: "2024-01-31", budgeted_spending: "3000.00", expected_income: "5000.00", currency: "USD" } }, { type: "BudgetCategory", data: { id: "bc-1", budget_id: "budget-1", category_id: "cat-groceries", budgeted_spending: "500.00", currency: "USD" } } ]) importer = Family::DataImporter.new(@family, ndjson) importer.import! budget = @family.budgets.first budget_category = budget.budget_categories.first assert_not_nil budget_category assert_equal "Groceries", budget_category.category.name assert_equal 500.0, budget_category.budgeted_spending.to_f end test "imports rules with conditions and actions" do ndjson = build_ndjson([ { type: "Rule", version: 1, data: { name: "Categorize Coffee", resource_type: "transaction", active: true, conditions: [ { condition_type: "transaction_name", operator: "like", value: "starbucks" } ], actions: [ { action_type: "set_transaction_category", value: "Coffee" } ] } } ]) importer = Family::DataImporter.new(@family, ndjson) importer.import! rule = @family.rules.find_by(name: "Categorize Coffee") assert_not_nil rule assert rule.active assert_equal "transaction", rule.resource_type assert_equal 1, rule.conditions.count condition = rule.conditions.first assert_equal "transaction_name", condition.condition_type assert_equal "like", condition.operator assert_equal "starbucks", condition.value assert_equal 1, rule.actions.count action = rule.actions.first assert_equal "set_transaction_category", action.action_type # Category should be created category = @family.categories.find_by(name: "Coffee") assert_not_nil category assert_equal category.id, action.value end test "imports rules with compound conditions" do ndjson = build_ndjson([ { type: "Rule", version: 1, data: { name: "Compound Rule", resource_type: "transaction", active: true, conditions: [ { condition_type: "compound", operator: "or", sub_conditions: [ { condition_type: "transaction_name", operator: "like", value: "walmart" }, { condition_type: "transaction_name", operator: "like", value: "target" } ] } ], actions: [ { action_type: "auto_categorize" } ] } } ]) importer = Family::DataImporter.new(@family, ndjson) importer.import! rule = @family.rules.find_by(name: "Compound Rule") assert_not_nil rule parent_condition = rule.conditions.first assert_equal "compound", parent_condition.condition_type assert_equal "or", parent_condition.operator assert_equal 2, parent_condition.sub_conditions.count end test "skips invalid records gracefully" do ndjson = "not valid json\n" + build_ndjson([ { type: "Account", data: { id: "valid-acct", name: "Valid Account", balance: "1000", currency: "USD", accountable_type: "Depository" } } ]) importer = Family::DataImporter.new(@family, ndjson) result = importer.import! assert_equal 1, result[:accounts].count assert_equal "Valid Account", result[:accounts].first.name end test "skips unsupported record types" do ndjson = build_ndjson([ { type: "UnsupportedType", data: { id: "unknown" } }, { type: "Account", data: { id: "valid-acct", name: "Known Account", balance: "1000", currency: "USD", accountable_type: "Depository" } } ]) importer = Family::DataImporter.new(@family, ndjson) result = importer.import! assert_equal 1, result[:accounts].count end test "full import scenario with all entity types" do ndjson = build_ndjson([ # Account { type: "Account", data: { id: "acct-main", name: "Main Checking", balance: "5000", currency: "USD", accountable_type: "Depository" } }, # Category { type: "Category", data: { id: "cat-food", name: "Food", color: "#FF5733", classification: "expense" } }, # Tag { type: "Tag", data: { id: "tag-weekly", name: "Weekly" } }, # Merchant { type: "Merchant", data: { id: "merchant-1", name: "Local Grocery" } }, # Transaction { type: "Transaction", data: { id: "txn-1", account_id: "acct-main", date: "2024-01-15", amount: "-75.50", name: "Weekly groceries", currency: "USD", category_id: "cat-food", merchant_id: "merchant-1", tag_ids: [ "tag-weekly" ] } }, # Budget { type: "Budget", data: { id: "budget-jan", start_date: "2024-01-01", end_date: "2024-01-31", budgeted_spending: "2000", expected_income: "4000", currency: "USD" } }, # BudgetCategory { type: "BudgetCategory", data: { id: "bc-food", budget_id: "budget-jan", category_id: "cat-food", budgeted_spending: "500", currency: "USD" } }, # Rule { type: "Rule", version: 1, data: { name: "Auto-tag groceries", resource_type: "transaction", active: true, conditions: [ { condition_type: "transaction_name", operator: "like", value: "grocery" } ], actions: [ { action_type: "set_transaction_tags", value: "Weekly" } ] } } ]) importer = Family::DataImporter.new(@family, ndjson) result = importer.import! # Verify all entities were created assert_equal 1, result[:accounts].count assert_equal 1, @family.categories.count assert_equal 1, @family.tags.count assert_equal 1, @family.merchants.count assert_equal 1, @family.transactions.count assert_equal 1, @family.budgets.count assert_equal 1, @family.budget_categories.count assert_equal 1, @family.rules.count # Verify relationships transaction = @family.transactions.first assert_equal "Food", transaction.category.name assert_equal "Local Grocery", transaction.merchant.name assert_equal "Weekly", transaction.tags.first.name end private def build_ndjson(records) records.map(&:to_json).join("\n") end end