mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 12:04:08 +00:00
Add investment tracking to expenses (#381)
* Add investment tracking to expenses Add new sections to dashboard and reporting around investments. * Create investment-integration-assessment.md * Delete .claude/settings.local.json Signed-off-by: soky srm <sokysrm@gmail.com> * Category trades * Simplify * Simplification and test fixes * FIX merge * Update views * Update 20251125141213_add_category_to_trades.rb * FIX tests * FIX statements and account status * cleanup * Add default cat for csv imports * Delete docs/roadmap/investment-integration-assessment.md Signed-off-by: soky srm <sokysrm@gmail.com> * Update trend calculation Use already existing column cost basis for trend calculation - Current value: qty * price (already stored as amount) - Cost basis total: qty * cost_basis - Unrealized gain: current value - cost basis total Fixes N+1 query also --------- Signed-off-by: soky srm <sokysrm@gmail.com>
This commit is contained in:
@@ -22,9 +22,10 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||
|
||||
test "calculates totals for transactions" do
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
assert_equal Money.new(1000, @family.currency), income_statement.totals.income_money
|
||||
assert_equal Money.new(200 + 300 + 400, @family.currency), income_statement.totals.expense_money
|
||||
assert_equal 4, income_statement.totals.transactions_count
|
||||
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
||||
assert_equal Money.new(1000, @family.currency), totals.income_money
|
||||
assert_equal Money.new(200 + 300 + 400, @family.currency), totals.expense_money
|
||||
assert_equal 4, totals.transactions_count
|
||||
end
|
||||
|
||||
test "calculates expenses for a period" do
|
||||
@@ -157,7 +158,7 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||
inflow_transaction = create_transaction(account: @credit_card_account, amount: -500, kind: "funds_movement")
|
||||
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
totals = income_statement.totals
|
||||
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
||||
|
||||
# NOW WORKING: Excludes transfers correctly after refactoring
|
||||
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
||||
@@ -170,7 +171,7 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||
loan_payment = create_transaction(account: @checking_account, amount: 1000, category: nil, kind: "loan_payment")
|
||||
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
totals = income_statement.totals
|
||||
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
||||
|
||||
# CONTINUES TO WORK: Includes loan payments as expenses (loan_payment not in exclusion list)
|
||||
assert_equal 5, totals.transactions_count
|
||||
@@ -183,7 +184,7 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||
one_time_transaction = create_transaction(account: @checking_account, amount: 250, category: @groceries_category, kind: "one_time")
|
||||
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
totals = income_statement.totals
|
||||
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
||||
|
||||
# NOW WORKING: Excludes one-time transactions correctly after refactoring
|
||||
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
||||
@@ -196,7 +197,7 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||
payment_transaction = create_transaction(account: @checking_account, amount: 300, category: nil, kind: "cc_payment")
|
||||
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
totals = income_statement.totals
|
||||
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
||||
|
||||
# NOW WORKING: Excludes payment transactions correctly after refactoring
|
||||
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
||||
@@ -210,7 +211,7 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||
excluded_transaction_entry.update!(excluded: true)
|
||||
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
totals = income_statement.totals
|
||||
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
||||
|
||||
# Should exclude excluded transactions
|
||||
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
||||
@@ -278,7 +279,7 @@ class IncomeStatementTest < ActiveSupport::TestCase
|
||||
create_transaction(account: @checking_account, amount: 150, category: nil)
|
||||
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
totals = income_statement.totals
|
||||
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
||||
|
||||
# Should still include uncategorized transaction in totals
|
||||
assert_equal 5, totals.transactions_count
|
||||
|
||||
@@ -62,4 +62,54 @@ class TradeImportTest < ActiveSupport::TestCase
|
||||
|
||||
assert_equal "complete", @import.status
|
||||
end
|
||||
|
||||
test "auto-categorizes buy trades and leaves sell trades uncategorized" do
|
||||
aapl = securities(:aapl)
|
||||
aapl_resolver = mock
|
||||
aapl_resolver.stubs(:resolve).returns(aapl)
|
||||
Security::Resolver.stubs(:new).returns(aapl_resolver)
|
||||
|
||||
# Create the investment category if it doesn't exist
|
||||
account = accounts(:depository)
|
||||
family = account.family
|
||||
savings_category = family.categories.find_or_create_by!(name: "Savings & Investments") do |c|
|
||||
c.color = "#059669"
|
||||
c.classification = "expense"
|
||||
c.lucide_icon = "piggy-bank"
|
||||
end
|
||||
|
||||
import = <<~CSV
|
||||
date,ticker,qty,price,currency,name
|
||||
01/01/2024,AAPL,10,150.00,USD,Apple Buy
|
||||
01/02/2024,AAPL,-5,160.00,USD,Apple Sell
|
||||
CSV
|
||||
|
||||
@import.update!(
|
||||
account: account,
|
||||
raw_file_str: import,
|
||||
date_col_label: "date",
|
||||
ticker_col_label: "ticker",
|
||||
qty_col_label: "qty",
|
||||
price_col_label: "price",
|
||||
date_format: "%m/%d/%Y",
|
||||
signage_convention: "inflows_positive"
|
||||
)
|
||||
|
||||
@import.generate_rows_from_csv
|
||||
@import.reload
|
||||
|
||||
assert_difference -> { Trade.count } => 2 do
|
||||
@import.publish
|
||||
end
|
||||
|
||||
# Find trades created by this import
|
||||
imported_trades = Trade.joins(:entry).where(entries: { import_id: @import.id })
|
||||
buy_trade = imported_trades.find { |t| t.qty.positive? }
|
||||
sell_trade = imported_trades.find { |t| t.qty.negative? }
|
||||
|
||||
assert_not_nil buy_trade, "Buy trade should have been created"
|
||||
assert_not_nil sell_trade, "Sell trade should have been created"
|
||||
assert_equal savings_category, buy_trade.category, "Buy trade should be auto-categorized as Savings & Investments"
|
||||
assert_nil sell_trade.category, "Sell trade should not be auto-categorized"
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user