diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index b71318b62..1524b25d0 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -36,7 +36,7 @@ class AccountsController < ApplicationController @chart_view = params[:chart_view] || "balance" @tab = params[:tab] @q = params.fetch(:q, {}).permit(:search, status: []) - entries = @account.entries.where(excluded: false).search(@q).reverse_chronological + entries = @account.entries.search(@q).reverse_chronological @pagy, @entries = pagy(entries, limit: params[:per_page] || "10") diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index a0fc9aaee..753b77300 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -218,7 +218,7 @@ class TransactionsController < ApplicationController def entry_params entry_params = params.require(:entry).permit( :name, :date, :amount, :currency, :excluded, :notes, :nature, :entryable_type, - entryable_attributes: [ :id, :category_id, :merchant_id, :kind, { tag_ids: [] } ] + entryable_attributes: [ :id, :category_id, :merchant_id, :kind, :investment_activity_label, { tag_ids: [] } ] ) nature = entry_params.delete(:nature) diff --git a/app/models/account/provider_import_adapter.rb b/app/models/account/provider_import_adapter.rb index e74d1f5e6..dac0da8d4 100644 --- a/app/models/account/provider_import_adapter.rb +++ b/app/models/account/provider_import_adapter.rb @@ -18,8 +18,9 @@ class Account::ProviderImportAdapter # @param notes [String, nil] Optional transaction notes/memo # @param pending_transaction_id [String, nil] Plaid's linking ID for pending→posted reconciliation # @param extra [Hash, nil] Optional provider-specific metadata to merge into transaction.extra + # @param investment_activity_label [String, nil] Optional activity type label (e.g., "Buy", "Dividend") # @return [Entry] The created or updated entry - def import_transaction(external_id:, amount:, currency:, date:, name:, source:, category_id: nil, merchant: nil, notes: nil, pending_transaction_id: nil, extra: nil) + def import_transaction(external_id:, amount:, currency:, date:, name:, source:, category_id: nil, merchant: nil, notes: nil, pending_transaction_id: nil, extra: nil, investment_activity_label: nil) raise ArgumentError, "external_id is required" if external_id.blank? raise ArgumentError, "source is required" if source.blank? @@ -114,7 +115,16 @@ class Account::ProviderImportAdapter entry.transaction.extra = existing.deep_merge(incoming) entry.transaction.save! end + + # Set investment activity label if provided and not already set + if investment_activity_label.present? && entry.entryable.is_a?(Transaction) + if entry.transaction.investment_activity_label.blank? + entry.transaction.assign_attributes(investment_activity_label: investment_activity_label) + end + end + entry.save! + entry.transaction.save! if entry.transaction.changed? # AFTER save: For NEW posted transactions, check for fuzzy matches to SUGGEST (not auto-claim) # This handles tip adjustments where auto-matching is too risky diff --git a/app/models/income_statement/category_stats.rb b/app/models/income_statement/category_stats.rb index 3bc91b839..f4eb2815f 100644 --- a/app/models/income_statement/category_stats.rb +++ b/app/models/income_statement/category_stats.rb @@ -47,7 +47,7 @@ class IncomeStatement::CategoryStats er.to_currency = :target_currency ) WHERE a.family_id = :family_id - AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment') + AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment', 'investment_contribution') AND ae.excluded = false AND (t.extra -> 'simplefin' ->> 'pending')::boolean IS DISTINCT FROM true AND (t.extra -> 'plaid' ->> 'pending')::boolean IS DISTINCT FROM true diff --git a/app/models/income_statement/family_stats.rb b/app/models/income_statement/family_stats.rb index c2a3c8f8e..48b0d9507 100644 --- a/app/models/income_statement/family_stats.rb +++ b/app/models/income_statement/family_stats.rb @@ -44,7 +44,7 @@ class IncomeStatement::FamilyStats er.to_currency = :target_currency ) WHERE a.family_id = :family_id - AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment') + AND t.kind NOT IN ('funds_movement', 'one_time', 'cc_payment', 'investment_contribution') AND ae.excluded = false AND (t.extra -> 'simplefin' ->> 'pending')::boolean IS DISTINCT FROM true AND (t.extra -> 'plaid' ->> 'pending')::boolean IS DISTINCT FROM true diff --git a/app/models/income_statement/totals.rb b/app/models/income_statement/totals.rb index 355212486..758ae6be3 100644 --- a/app/models/income_statement/totals.rb +++ b/app/models/income_statement/totals.rb @@ -69,9 +69,9 @@ class IncomeStatement::Totals er.from_currency = ae.currency AND er.to_currency = :target_currency ) - WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment') + WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment', 'investment_contribution') AND ae.excluded = false - AND a.family_id = :family_id + AND a.family_id = :family_id AND a.status IN ('draft', 'active') GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END; SQL @@ -95,9 +95,9 @@ class IncomeStatement::Totals er.from_currency = ae.currency AND er.to_currency = :target_currency ) - WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment') + WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment', 'investment_contribution') AND ae.excluded = false - AND a.family_id = :family_id + AND a.family_id = :family_id AND a.status IN ('draft', 'active') GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END SQL @@ -126,7 +126,7 @@ class IncomeStatement::Totals WHERE a.family_id = :family_id AND a.status IN ('draft', 'active') AND ae.excluded = false - AND ae.date BETWEEN :start_date AND :end_date + AND ae.date BETWEEN :start_date AND :end_date GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END, CASE WHEN t.category_id IS NULL THEN true ELSE false END SQL end diff --git a/app/models/plaid_account/investments/transactions_processor.rb b/app/models/plaid_account/investments/transactions_processor.rb index 922a3f2a6..d90657ce6 100644 --- a/app/models/plaid_account/investments/transactions_processor.rb +++ b/app/models/plaid_account/investments/transactions_processor.rb @@ -1,6 +1,24 @@ class PlaidAccount::Investments::TransactionsProcessor SecurityNotFoundError = Class.new(StandardError) + # Map Plaid investment transaction types to activity labels + # All values must be valid Transaction::ACTIVITY_LABELS + PLAID_TYPE_TO_LABEL = { + "buy" => "Buy", + "sell" => "Sell", + "cancel" => "Other", + "cash" => "Other", + "fee" => "Fee", + "transfer" => "Transfer", + "dividend" => "Dividend", + "interest" => "Interest", + "contribution" => "Contribution", + "withdrawal" => "Withdrawal", + "dividend reinvestment" => "Reinvestment", + "spin off" => "Other", + "split" => "Other" + }.freeze + def initialize(plaid_account, security_resolver:) @plaid_account = plaid_account @security_resolver = security_resolver @@ -68,10 +86,16 @@ class PlaidAccount::Investments::TransactionsProcessor currency: transaction["iso_currency_code"], date: transaction["date"], name: transaction["name"], - source: "plaid" + source: "plaid", + investment_activity_label: label_from_plaid_type(transaction) ) end + def label_from_plaid_type(transaction) + plaid_type = transaction["type"]&.downcase + PLAID_TYPE_TO_LABEL[plaid_type] || "Other" + end + def transactions plaid_account.raw_investments_payload["transactions"] || [] end diff --git a/app/models/rule/action_executor/set_investment_activity_label.rb b/app/models/rule/action_executor/set_investment_activity_label.rb new file mode 100644 index 000000000..21e421292 --- /dev/null +++ b/app/models/rule/action_executor/set_investment_activity_label.rb @@ -0,0 +1,31 @@ +class Rule::ActionExecutor::SetInvestmentActivityLabel < Rule::ActionExecutor + def label + "Set investment activity label" + end + + def type + "select" + end + + def options + Transaction::ACTIVITY_LABELS.map { |l| [ l, l ] } + end + + def execute(transaction_scope, value: nil, ignore_attribute_locks: false, rule_run: nil) + return 0 unless Transaction::ACTIVITY_LABELS.include?(value) + + scope = transaction_scope + + unless ignore_attribute_locks + scope = scope.enrichable(:investment_activity_label) + end + + count_modified_resources(scope) do |txn| + txn.enrich_attribute( + :investment_activity_label, + value, + source: "rule" + ) + end + end +end diff --git a/app/models/rule/registry/transaction_resource.rb b/app/models/rule/registry/transaction_resource.rb index d051b5837..fac1d6667 100644 --- a/app/models/rule/registry/transaction_resource.rb +++ b/app/models/rule/registry/transaction_resource.rb @@ -20,6 +20,7 @@ class Rule::Registry::TransactionResource < Rule::Registry Rule::ActionExecutor::SetTransactionTags.new(rule), Rule::ActionExecutor::SetTransactionMerchant.new(rule), Rule::ActionExecutor::SetTransactionName.new(rule), + Rule::ActionExecutor::SetInvestmentActivityLabel.new(rule), Rule::ActionExecutor::ExcludeTransaction.new(rule) ] diff --git a/app/models/transaction.rb b/app/models/transaction.rb index 70ca49da0..e1a86afb5 100644 --- a/app/models/transaction.rb +++ b/app/models/transaction.rb @@ -16,9 +16,16 @@ class Transaction < ApplicationRecord funds_movement: "funds_movement", # Movement of funds between accounts, excluded from budget analytics cc_payment: "cc_payment", # A CC payment, excluded from budget analytics (CC payments offset the sum of expense transactions) loan_payment: "loan_payment", # A payment to a Loan account, treated as an expense in budgets - one_time: "one_time" # A one-time expense/income, excluded from budget analytics + one_time: "one_time", # A one-time expense/income, excluded from budget analytics + investment_contribution: "investment_contribution" # Transfer to investment/crypto account, excluded from budget analytics } + # All valid investment activity labels (for UI dropdown) + ACTIVITY_LABELS = [ + "Buy", "Sell", "Sweep In", "Sweep Out", "Dividend", "Reinvestment", + "Interest", "Fee", "Transfer", "Contribution", "Withdrawal", "Exchange", "Other" + ].freeze + # Pending transaction scopes - filter based on provider pending flags in extra JSONB # Works with any provider that stores pending status in extra["provider_name"]["pending"] scope :pending, -> { diff --git a/app/models/transaction/search.rb b/app/models/transaction/search.rb index 6c401e0f0..e46a66472 100644 --- a/app/models/transaction/search.rb +++ b/app/models/transaction/search.rb @@ -49,8 +49,8 @@ class Transaction::Search Rails.cache.fetch("transaction_search_totals/#{cache_key_base}") do result = transactions_scope .select( - "COALESCE(SUM(CASE WHEN entries.amount >= 0 AND transactions.kind NOT IN ('funds_movement', 'cc_payment') THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as expense_total", - "COALESCE(SUM(CASE WHEN entries.amount < 0 AND transactions.kind NOT IN ('funds_movement', 'cc_payment') THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as income_total", + "COALESCE(SUM(CASE WHEN entries.amount >= 0 AND transactions.kind NOT IN ('funds_movement', 'cc_payment', 'investment_contribution') THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as expense_total", + "COALESCE(SUM(CASE WHEN entries.amount < 0 AND transactions.kind NOT IN ('funds_movement', 'cc_payment', 'investment_contribution') THEN ABS(entries.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as income_total", "COUNT(entries.id) as transactions_count" ) .joins( @@ -100,14 +100,14 @@ class Transaction::Search if parent_category_ids.empty? query = query.left_joins(:category).where( "categories.name IN (?) OR ( - categories.id IS NULL AND (transactions.kind NOT IN ('funds_movement', 'cc_payment')) + categories.id IS NULL AND (transactions.kind NOT IN ('funds_movement', 'cc_payment', 'investment_contribution')) )", categories ) else query = query.left_joins(:category).where( "categories.name IN (?) OR categories.parent_id IN (?) OR ( - categories.id IS NULL AND (transactions.kind NOT IN ('funds_movement', 'cc_payment')) + categories.id IS NULL AND (transactions.kind NOT IN ('funds_movement', 'cc_payment', 'investment_contribution')) )", categories, parent_category_ids ) diff --git a/app/models/transfer.rb b/app/models/transfer.rb index 93de0a068..d2dcbf667 100644 --- a/app/models/transfer.rb +++ b/app/models/transfer.rb @@ -16,6 +16,10 @@ class Transfer < ApplicationRecord def kind_for_account(account) if account.loan? "loan_payment" + elsif account.credit_card? + "cc_payment" + elsif account.investment? || account.crypto? + "investment_contribution" elsif account.liability? "cc_payment" else diff --git a/app/views/transactions/_transaction.html.erb b/app/views/transactions/_transaction.html.erb index ec89bb308..9c52d6acd 100644 --- a/app/views/transactions/_transaction.html.erb +++ b/app/views/transactions/_transaction.html.erb @@ -78,6 +78,13 @@ <% end %> + <%# Investment activity label badge %> + <% if transaction.investment_activity_label.present? %> + "> + <%= t("transactions.activity_labels.#{transaction.investment_activity_label.parameterize(separator: '_')}") %> + + <% end %> + <%# Pending indicator %> <% if transaction.pending? %> "> diff --git a/app/views/transactions/show.html.erb b/app/views/transactions/show.html.erb index 0b6397494..3d8910dcb 100644 --- a/app/views/transactions/show.html.erb +++ b/app/views/transactions/show.html.erb @@ -194,8 +194,8 @@ data: { controller: "auto-submit-form" } do |f| %>
-

Exclude

-

Excluded transactions will be removed from budgeting calculations and reports.

+

<%= t(".exclude") %>

+

<%= t(".exclude_description") %>

<%= f.toggle :excluded, { data: { auto_submit_form_target: "auto" } } %> @@ -203,6 +203,33 @@ <% end %>
+ <% if @entry.account.investment? || @entry.account.crypto? %> +
+ <%= styled_form_with model: @entry, + url: transaction_path(@entry), + class: "p-3", + data: { controller: "auto-submit-form" } do |f| %> + <%= f.fields_for :entryable do |ef| %> +
+
+

<%= t(".activity_type") %>

+

<%= t(".activity_type_description") %>

+
+ + <%= ef.select :investment_activity_label, + options_for_select( + [["—", nil]] + Transaction::ACTIVITY_LABELS.map { |l| [t("transactions.activity_labels.#{l.parameterize(separator: '_')}"), l] }, + @entry.entryable.investment_activity_label + ), + { label: false }, + { class: "form-field__input border border-secondary rounded-lg px-3 py-1.5 max-w-40 text-sm", + data: { auto_submit_form_target: "auto" } } %> +
+ <% end %> + <% end %> +
+ <% end %> +
<%= styled_form_with model: @entry, url: transaction_path(@entry), @@ -211,8 +238,8 @@ <%= f.fields_for :entryable do |ef| %>
-

One-time <%= @entry.amount.negative? ? "Income" : "Expense" %>

-

One-time transactions will be excluded from certain budgeting calculations and reports to help you see what's really important.

+

<%= t(".one_time_title", type: @entry.amount.negative? ? t("transactions.form.income") : t("transactions.form.expense")) %>

+

<%= t(".one_time_description") %>

<%= ef.toggle :kind, { diff --git a/config/locales/views/transactions/en.yml b/config/locales/views/transactions/en.yml index 717d146b1..ae7bb1cb3 100644 --- a/config/locales/views/transactions/en.yml +++ b/config/locales/views/transactions/en.yml @@ -31,26 +31,47 @@ en: balances, and cannot be undone. delete_title: Delete transaction details: Details - mark_recurring: Mark as Recurring - mark_recurring_subtitle: Track this as a recurring transaction. Amount variance is automatically calculated from past 6 months of similar transactions. - mark_recurring_title: Recurring Transaction - merchant_label: Merchant - name_label: Name - nature: Type - none: "(none)" - note_label: Notes - note_placeholder: Enter a note - overview: Overview - settings: Settings - tags_label: Tags - uncategorized: "(uncategorized)" - potential_duplicate_title: Possible duplicate detected - potential_duplicate_description: This pending transaction may be the same as the posted transaction below. If so, merge them to avoid double-counting. - merge_duplicate: Yes, merge them - keep_both: No, keep both + exclude: Exclude + exclude_description: Excluded transactions will be removed from budgeting calculations and reports. + activity_type: Activity Type + activity_type_description: Type of investment activity (Buy, Sell, Dividend, etc.). Auto-detected or set manually. + one_time_title: One-time %{type} + one_time_description: One-time transactions will be excluded from certain budgeting calculations and reports to help you see what's really important. + activity_labels: + buy: Buy + sell: Sell + sweep_in: Sweep In + sweep_out: Sweep Out + dividend: Dividend + reinvestment: Reinvestment + interest: Interest + fee: Fee + transfer: Transfer + contribution: Contribution + withdrawal: Withdrawal + exchange: Exchange + other: Other + mark_recurring: Mark as Recurring + mark_recurring_subtitle: Track this as a recurring transaction. Amount variance is automatically calculated from past 6 months of similar transactions. + mark_recurring_title: Recurring Transaction + merchant_label: Merchant + name_label: Name + nature: Type + none: "(none)" + note_label: Notes + note_placeholder: Enter a note + overview: Overview + settings: Settings + tags_label: Tags + uncategorized: "(uncategorized)" + potential_duplicate_title: Possible duplicate detected + potential_duplicate_description: This pending transaction may be the same as the posted transaction below. If so, merge them to avoid double-counting. + merge_duplicate: Yes, merge them + keep_both: No, keep both transaction: pending: Pending pending_tooltip: Pending transaction — may change when posted + activity_type_tooltip: Investment activity type possible_duplicate: Duplicate? potential_duplicate_tooltip: This may be a duplicate of another transaction review_recommended: Review diff --git a/db/migrate/20260110120000_add_investment_cashflow_support.rb b/db/migrate/20260110120000_add_investment_cashflow_support.rb new file mode 100644 index 000000000..39765f4f1 --- /dev/null +++ b/db/migrate/20260110120000_add_investment_cashflow_support.rb @@ -0,0 +1,5 @@ +class AddInvestmentCashflowSupport < ActiveRecord::Migration[7.2] + # No-op: exclude_from_cashflow was consolidated into the existing 'excluded' toggle + def change + end +end diff --git a/db/migrate/20260110180000_add_investment_activity_label_to_transactions.rb b/db/migrate/20260110180000_add_investment_activity_label_to_transactions.rb new file mode 100644 index 000000000..658900bc2 --- /dev/null +++ b/db/migrate/20260110180000_add_investment_activity_label_to_transactions.rb @@ -0,0 +1,8 @@ +class AddInvestmentActivityLabelToTransactions < ActiveRecord::Migration[7.2] + def change + # Label for investment activity type (Buy, Sell, Sweep In, Dividend, etc.) + # Provides human-readable context for why a transaction is excluded from cashflow + add_column :transactions, :investment_activity_label, :string + add_index :transactions, :investment_activity_label + end +end diff --git a/db/schema.rb b/db/schema.rb index a4b314661..ad548839c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -342,14 +342,12 @@ ActiveRecord::Schema[7.2].define(version: 2026_01_12_065106) do t.jsonb "locked_attributes", default: {} t.string "external_id" t.string "source" - t.boolean "exclude_from_cashflow", default: false, null: false t.index "lower((name)::text)", name: "index_entries_on_lower_name" t.index ["account_id", "date"], name: "index_entries_on_account_id_and_date" t.index ["account_id", "source", "external_id"], name: "index_entries_on_account_source_and_external_id", unique: true, where: "((external_id IS NOT NULL) AND (source IS NOT NULL))" t.index ["account_id"], name: "index_entries_on_account_id" t.index ["date"], name: "index_entries_on_date" t.index ["entryable_type"], name: "index_entries_on_entryable_type" - t.index ["exclude_from_cashflow"], name: "index_entries_on_exclude_from_cashflow" t.index ["import_id"], name: "index_entries_on_import_id" end diff --git a/test/models/income_statement_test.rb b/test/models/income_statement_test.rb index b152917c6..e4af61d66 100644 --- a/test/models/income_statement_test.rb +++ b/test/models/income_statement_test.rb @@ -285,4 +285,22 @@ class IncomeStatementTest < ActiveSupport::TestCase assert_equal 5, totals.transactions_count assert_equal Money.new(1050, @family.currency), totals.expense_money # 900 + 150 end + + test "excludes investment_contribution transactions from income statement" do + # Create a transfer to investment account (marked as investment_contribution) + investment_contribution = create_transaction( + account: @checking_account, + amount: 1000, + category: nil, + kind: "investment_contribution" + ) + + income_statement = IncomeStatement.new(@family) + totals = income_statement.totals(date_range: Period.last_30_days.date_range) + + # investment_contribution should be excluded (it's in the exclusion list) + assert_equal 4, totals.transactions_count # Only original 4 transactions + assert_equal Money.new(1000, @family.currency), totals.income_money + assert_equal Money.new(900, @family.currency), totals.expense_money + end end diff --git a/test/models/rule/action_test.rb b/test/models/rule/action_test.rb index db3933ea0..bcb8846af 100644 --- a/test/models/rule/action_test.rb +++ b/test/models/rule/action_test.rb @@ -100,4 +100,36 @@ class Rule::ActionTest < ActiveSupport::TestCase assert_equal new_name, transaction.reload.entry.name end end + + test "set_investment_activity_label" do + # Does not modify transactions that are locked (user edited them) + @txn1.lock_attr!(:investment_activity_label) + + action = Rule::Action.new( + rule: @transaction_rule, + action_type: "set_investment_activity_label", + value: "Dividend" + ) + + action.apply(@rule_scope) + + assert_nil @txn1.reload.investment_activity_label + + [ @txn2, @txn3 ].each do |transaction| + assert_equal "Dividend", transaction.reload.investment_activity_label + end + end + + test "set_investment_activity_label ignores invalid values" do + action = Rule::Action.new( + rule: @transaction_rule, + action_type: "set_investment_activity_label", + value: "InvalidLabel" + ) + + result = action.apply(@rule_scope) + + assert_equal 0, result + assert_nil @txn1.reload.investment_activity_label + end end diff --git a/test/models/transaction_test.rb b/test/models/transaction_test.rb index 7b5f2faeb..9064f6b97 100644 --- a/test/models/transaction_test.rb +++ b/test/models/transaction_test.rb @@ -18,4 +18,36 @@ class TransactionTest < ActiveSupport::TestCase assert_not transaction.pending? end + + test "investment_contribution is a valid kind" do + transaction = Transaction.new(kind: "investment_contribution") + + assert_equal "investment_contribution", transaction.kind + assert transaction.investment_contribution? + end + + test "all transaction kinds are valid" do + valid_kinds = %w[standard funds_movement cc_payment loan_payment one_time investment_contribution] + + valid_kinds.each do |kind| + transaction = Transaction.new(kind: kind) + assert_equal kind, transaction.kind, "#{kind} should be a valid transaction kind" + end + end + + test "ACTIVITY_LABELS contains all valid labels" do + assert_includes Transaction::ACTIVITY_LABELS, "Buy" + assert_includes Transaction::ACTIVITY_LABELS, "Sell" + assert_includes Transaction::ACTIVITY_LABELS, "Sweep In" + assert_includes Transaction::ACTIVITY_LABELS, "Sweep Out" + assert_includes Transaction::ACTIVITY_LABELS, "Dividend" + assert_includes Transaction::ACTIVITY_LABELS, "Reinvestment" + assert_includes Transaction::ACTIVITY_LABELS, "Interest" + assert_includes Transaction::ACTIVITY_LABELS, "Fee" + assert_includes Transaction::ACTIVITY_LABELS, "Transfer" + assert_includes Transaction::ACTIVITY_LABELS, "Contribution" + assert_includes Transaction::ACTIVITY_LABELS, "Withdrawal" + assert_includes Transaction::ACTIVITY_LABELS, "Exchange" + assert_includes Transaction::ACTIVITY_LABELS, "Other" + end end diff --git a/test/models/transfer_test.rb b/test/models/transfer_test.rb index 331638165..e47d200d2 100644 --- a/test/models/transfer_test.rb +++ b/test/models/transfer_test.rb @@ -104,4 +104,24 @@ class TransferTest < ActiveSupport::TestCase Transfer.create!(inflow_transaction: inflow_entry2.transaction, outflow_transaction: outflow_entry.transaction) end end + + test "kind_for_account returns investment_contribution for investment accounts" do + assert_equal "investment_contribution", Transfer.kind_for_account(accounts(:investment)) + end + + test "kind_for_account returns investment_contribution for crypto accounts" do + assert_equal "investment_contribution", Transfer.kind_for_account(accounts(:crypto)) + end + + test "kind_for_account returns loan_payment for loan accounts" do + assert_equal "loan_payment", Transfer.kind_for_account(accounts(:loan)) + end + + test "kind_for_account returns cc_payment for credit card accounts" do + assert_equal "cc_payment", Transfer.kind_for_account(accounts(:credit_card)) + end + + test "kind_for_account returns funds_movement for depository accounts" do + assert_equal "funds_movement", Transfer.kind_for_account(accounts(:depository)) + end end