@@ -74,7 +74,6 @@
<%= f.text_area :notes,
label: t(".note_label"),
placeholder: t(".note_placeholder"),
- value: @transfer.outflow_transaction.notes,
rows: 5,
"data-auto-submit-form-target": "auto" %>
<% end %>
@@ -83,25 +82,6 @@
<%= disclosure t(".settings") do %>
- <%= styled_form_with model: @transfer,
- class: "p-3", data: { controller: "auto-submit-form" } do |f| %>
-
-
-
<%= t(".exclude_title") %>
-
<%= t(".exclude_subtitle") %>
-
-
-
- <%= f.check_box :excluded,
- checked: @transfer.inflow_transaction.excluded,
- class: "sr-only peer",
- "data-auto-submit-form-target": "auto" %>
-
-
-
- <% end %>
-
<%= t(".delete_title") %>
@@ -109,9 +89,9 @@
<%= button_to t(".delete"),
- account_transfer_path(@transfer),
+ transfer_path(@transfer),
method: :delete,
- class: "rounded-lg px-3 py-2 text-red-500 text-sm
+ class: "rounded-lg px-3 py-2 whitespace-nowrap text-red-500 text-sm
font-medium border border-alpha-black-200",
data: { turbo_confirm: true, turbo_frame: "_top" } %>
diff --git a/app/views/transfers/update.turbo_stream.erb b/app/views/transfers/update.turbo_stream.erb
new file mode 100644
index 00000000..90d5a7d5
--- /dev/null
+++ b/app/views/transfers/update.turbo_stream.erb
@@ -0,0 +1,17 @@
+<%= turbo_stream.replace @transfer %>
+
+<%= turbo_stream.replace "category_menu_account_entry_#{@transfer.inflow_transaction.entry.id}",
+ partial: "account/transactions/transaction_category",
+ locals: { entry: @transfer.inflow_transaction.entry } %>
+
+<%= turbo_stream.replace "category_menu_account_entry_#{@transfer.outflow_transaction.entry.id}",
+ partial: "account/transactions/transaction_category",
+ locals: { entry: @transfer.outflow_transaction.entry } %>
+
+<%= turbo_stream.replace "transfer_match_account_entry_#{@transfer.inflow_transaction.entry.id}",
+ partial: "account/transactions/transfer_match",
+ locals: { entry: @transfer.inflow_transaction.entry } %>
+
+<%= turbo_stream.replace "transfer_match_account_entry_#{@transfer.outflow_transaction.entry.id}",
+ partial: "account/transactions/transfer_match",
+ locals: { entry: @transfer.outflow_transaction.entry } %>
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index ce280c4d..65697755 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -103,6 +103,30 @@
],
"note": ""
},
+ {
+ "warning_type": "Dangerous Eval",
+ "warning_code": 13,
+ "fingerprint": "c193307bb82f931950d3bf2855f82f9a7f50d94c5bd950ee2803cb8a8abe5253",
+ "check_name": "Evaluation",
+ "message": "Dynamic string evaluated as code",
+ "file": "app/helpers/styled_form_builder.rb",
+ "line": 7,
+ "link": "https://brakemanscanner.org/docs/warning_types/dangerous_eval/",
+ "code": "class_eval(\" def #{selector}(method, options = {})\\n merged_options = { class: \\\"form-field__input\\\" }.merge(options)\\n label = build_label(method, options)\\n field = super(method, merged_options)\\n\\n build_styled_field(label, field, merged_options)\\n end\\n\", \"app/helpers/styled_form_builder.rb\", (7 + 1))",
+ "render_path": null,
+ "location": {
+ "type": "method",
+ "class": "StyledFormBuilder",
+ "method": null
+ },
+ "user_input": null,
+ "confidence": "Weak",
+ "cwe_id": [
+ 913,
+ 95
+ ],
+ "note": ""
+ },
{
"warning_type": "Dynamic Render Path",
"warning_code": 15,
@@ -138,6 +162,5 @@
"note": ""
}
],
- "updated": "2024-12-18 17:46:13 -0500",
- "brakeman_version": "6.2.2"
+ "brakeman_version": "7.0.0"
}
diff --git a/config/locales/models/account/transfer/en.yml b/config/locales/models/account/transfer/en.yml
deleted file mode 100644
index c0fe38d5..00000000
--- a/config/locales/models/account/transfer/en.yml
+++ /dev/null
@@ -1,17 +0,0 @@
----
-en:
- account/transfer:
- from_fallback_name: Originator
- name: Transfer from %{from_account} to %{to_account}
- to_fallback_name: Receiver
- activerecord:
- errors:
- models:
- account/transfer:
- attributes:
- entries:
- must_be_from_different_accounts: must be from different accounts
- must_be_marked_as_transfer: must be marked as transfer
- must_have_an_inflow_and_outflow_that_net_to_zero: must have an inflow
- and outflow that net to zero
- must_have_exactly_2_entries: must have exactly 2 entries
diff --git a/config/locales/models/transfer/en.yml b/config/locales/models/transfer/en.yml
new file mode 100644
index 00000000..f373cc8e
--- /dev/null
+++ b/config/locales/models/transfer/en.yml
@@ -0,0 +1,17 @@
+---
+en:
+ activerecord:
+ errors:
+ models:
+ transfer:
+ attributes:
+ base:
+ must_be_from_different_accounts: Transfer must have different accounts
+ must_be_within_date_range: Transfer transaction dates must be within
+ 4 days of each other
+ must_have_opposite_amounts: Transfer transactions must have opposite
+ amounts
+ must_have_single_currency: Transfer must have a single currency
+ transfer:
+ name: Transfer to %{to_account}
+ payment_name: Payment to %{to_account}
diff --git a/config/locales/views/account/transactions/en.yml b/config/locales/views/account/transactions/en.yml
index af05bcdf..659c5779 100644
--- a/config/locales/views/account/transactions/en.yml
+++ b/config/locales/views/account/transactions/en.yml
@@ -36,15 +36,8 @@ en:
no_transactions: No transactions for this account yet.
transaction: transaction
transactions: Transactions
- mark_transfers:
- success: Marked as transfers
new:
new_transaction: New transaction
- selection_bar:
- mark_transfers: Mark as transfers?
- mark_transfers_confirm: Mark as transfers
- mark_transfers_message: By marking transactions as transfers, they will no
- longer be included in income or spending calculations.
show:
account_label: Account
amount: Amount
@@ -55,9 +48,6 @@ en:
balances, and cannot be undone.
delete_title: Delete transaction
details: Details
- exclude_subtitle: This excludes the transaction from any in-app features or
- analytics.
- exclude_title: Exclude transaction
merchant_label: Merchant
name_label: Name
nature: Type
@@ -68,5 +58,6 @@ en:
settings: Settings
tags_label: Tags
uncategorized: "(uncategorized)"
- unmark_transfers:
- success: Transfer removed
+ transfer_matches:
+ create:
+ success: Transfer created
diff --git a/config/locales/views/account/transfers/en.yml b/config/locales/views/account/transfers/en.yml
deleted file mode 100644
index 7728d764..00000000
--- a/config/locales/views/account/transfers/en.yml
+++ /dev/null
@@ -1,38 +0,0 @@
----
-en:
- account:
- transfers:
- create:
- success: Transfer created
- destroy:
- success: Transfer removed
- form:
- amount: Amount
- date: Date
- expense: Expense
- from: From
- income: Income
- select_account: Select account
- submit: Create transfer
- to: To
- transfer: Transfer
- new:
- title: New transfer
- show:
- delete: Delete
- delete_subtitle: This permanently deletes both of the transactions related
- to the transfer. This cannot be undone.
- delete_title: Delete transfer?
- details: Details
- exclude_subtitle: This excludes the transfer from any in-app features or analytics.
- exclude_title: Exclude transfer
- note_label: Notes
- note_placeholder: Add a note to this transfer
- overview: Overview
- settings: Settings
- transfer_toggle:
- remove_transfer: Remove transfer
- remove_transfer_body: This will remove the transfer from this transaction
- remove_transfer_confirm: Confirm
- update:
- success: Transfer updated
diff --git a/config/locales/views/categories/en.yml b/config/locales/views/categories/en.yml
index 7951e328..1d6aeb6e 100644
--- a/config/locales/views/categories/en.yml
+++ b/config/locales/views/categories/en.yml
@@ -1,11 +1,8 @@
---
en:
- category:
- dropdowns:
- show:
- empty: No categories found
- bootstrap: Generate default categories
categories:
+ bootstrap:
+ success: Default categories created successfully
category:
delete: Delete category
edit: Edit category
@@ -18,15 +15,18 @@ en:
form:
placeholder: Category name
index:
+ bootstrap: Use default categories
categories: Categories
empty: No categories found
new: New category
- bootstrap: Use default categories
- bootstrap:
- success: Default categories created successfully
menu:
loading: Loading...
new:
new_category: New category
update:
success: Category updated successfully
+ category:
+ dropdowns:
+ show:
+ bootstrap: Generate default categories
+ empty: No categories found
diff --git a/config/locales/views/category/dropdowns/en.yml b/config/locales/views/category/dropdowns/en.yml
index 5b9e8248..511e86a9 100644
--- a/config/locales/views/category/dropdowns/en.yml
+++ b/config/locales/views/category/dropdowns/en.yml
@@ -6,7 +6,6 @@ en:
delete: Delete category
edit: Edit category
show:
- add_new: Add new
- clear: Clear
+ clear: Clear category
no_categories: No categories found
search_placeholder: Search
diff --git a/config/locales/views/transfers/en.yml b/config/locales/views/transfers/en.yml
new file mode 100644
index 00000000..669bae2d
--- /dev/null
+++ b/config/locales/views/transfers/en.yml
@@ -0,0 +1,31 @@
+---
+en:
+ transfers:
+ create:
+ success: Transfer created
+ destroy:
+ success: Transfer removed
+ form:
+ amount: Amount
+ date: Date
+ expense: Expense
+ from: From
+ income: Income
+ select_account: Select account
+ submit: Create transfer
+ to: To
+ transfer: Transfer
+ new:
+ title: New transfer
+ show:
+ delete: Remove transfer
+ delete_subtitle: This removes the transfer. It will not delete the underlying
+ transactions.
+ delete_title: Remove transfer?
+ details: Details
+ note_label: Notes
+ note_placeholder: Add a note to this transfer
+ overview: Overview
+ settings: Settings
+ update:
+ success: Transfer updated
diff --git a/config/routes.rb b/config/routes.rb
index 73673ced..440756ba 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -46,9 +46,7 @@ Rails.application.routes.draw do
resources :merchants, only: %i[index new create edit update destroy]
- namespace :account do
- resources :transfers, only: %i[new create destroy show update]
- end
+ resources :transfers, only: %i[new create destroy show update]
resources :imports, only: %i[index new show create destroy] do
post :publish, on: :member
@@ -81,6 +79,7 @@ Rails.application.routes.draw do
resources :entries, only: :index
resources :transactions, only: %i[show new create update destroy] do
+ resource :transfer_match, only: %i[new create]
resource :category, only: :update, controller: :transaction_categories
collection do
diff --git a/db/migrate/20241231140709_reverse_transfer_relations.rb b/db/migrate/20241231140709_reverse_transfer_relations.rb
new file mode 100644
index 00000000..5dbfe795
--- /dev/null
+++ b/db/migrate/20241231140709_reverse_transfer_relations.rb
@@ -0,0 +1,75 @@
+class ReverseTransferRelations < ActiveRecord::Migration[7.2]
+ def change
+ create_table :transfers, id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.references :inflow_transaction, null: false, foreign_key: { to_table: :account_transactions }, type: :uuid
+ t.references :outflow_transaction, null: false, foreign_key: { to_table: :account_transactions }, type: :uuid
+ t.string :status, null: false, default: "pending"
+ t.text :notes
+
+ t.index [ :inflow_transaction_id, :outflow_transaction_id ], unique: true
+ t.timestamps
+ end
+
+ reversible do |dir|
+ dir.up do
+ execute <<~SQL
+ INSERT INTO transfers (inflow_transaction_id, outflow_transaction_id, status, created_at, updated_at)
+ SELECT
+ CASE WHEN e1.amount <= 0 THEN e1.entryable_id ELSE e2.entryable_id END as inflow_transaction_id,
+ CASE WHEN e1.amount <= 0 THEN e2.entryable_id ELSE e1.entryable_id END as outflow_transaction_id,
+ 'confirmed' as status,
+ e1.created_at,
+ e1.updated_at
+ FROM account_entries e1
+ JOIN account_entries e2 ON
+ e1.transfer_id = e2.transfer_id AND
+ e1.id != e2.id AND
+ e1.id < e2.id -- Ensures we don't duplicate transfers from both sides
+ JOIN accounts a1 ON e1.account_id = a1.id
+ JOIN accounts a2 ON e2.account_id = a2.id
+ WHERE
+ e1.entryable_type = 'Account::Transaction' AND
+ e2.entryable_type = 'Account::Transaction' AND
+ e1.transfer_id IS NOT NULL AND
+ a1.family_id = a2.family_id;
+ SQL
+ end
+
+ dir.down do
+ execute <<~SQL
+ WITH new_transfers AS (
+ INSERT INTO account_transfers (created_at, updated_at)
+ SELECT created_at, updated_at
+ FROM transfers
+ RETURNING id, created_at
+ ),
+ transfer_pairs AS (
+ SELECT
+ nt.id as transfer_id,
+ ae_in.id as inflow_entry_id,
+ ae_out.id as outflow_entry_id
+ FROM transfers t
+ JOIN new_transfers nt ON nt.created_at = t.created_at
+ JOIN account_entries ae_in ON ae_in.entryable_id = t.inflow_transaction_id
+ JOIN account_entries ae_out ON ae_out.entryable_id = t.outflow_transaction_id
+ WHERE
+ ae_in.entryable_type = 'Account::Transaction' AND
+ ae_out.entryable_type = 'Account::Transaction'
+ )
+ UPDATE account_entries ae
+ SET transfer_id = tp.transfer_id
+ FROM transfer_pairs tp
+ WHERE ae.id IN (tp.inflow_entry_id, tp.outflow_entry_id);
+ SQL
+ end
+ end
+
+ remove_foreign_key :account_entries, :account_transfers, column: :transfer_id
+ remove_column :account_entries, :transfer_id, :uuid
+ remove_column :account_entries, :marked_as_transfer, :boolean
+
+ drop_table :account_transfers, id: :uuid, default: -> { "gen_random_uuid()" } do |t|
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0bdce796..7c333cc3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.2].define(version: 2024_12_27_142333) do
+ActiveRecord::Schema[7.2].define(version: 2024_12_31_140709) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -42,8 +42,6 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_27_142333) do
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.uuid "transfer_id"
- t.boolean "marked_as_transfer", default: false, null: false
t.uuid "import_id"
t.text "notes"
t.boolean "excluded", default: false
@@ -52,7 +50,6 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_27_142333) do
t.string "enriched_name"
t.index ["account_id"], name: "index_account_entries_on_account_id"
t.index ["import_id"], name: "index_account_entries_on_import_id"
- t.index ["transfer_id"], name: "index_account_entries_on_transfer_id"
end
create_table "account_holdings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
@@ -89,11 +86,6 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_27_142333) do
t.index ["merchant_id"], name: "index_account_transactions_on_merchant_id"
end
- create_table "account_transfers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- end
-
create_table "account_valuations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -606,6 +598,18 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_27_142333) do
t.index ["family_id"], name: "index_tags_on_family_id"
end
+ create_table "transfers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.uuid "inflow_transaction_id", null: false
+ t.uuid "outflow_transaction_id", null: false
+ t.string "status", default: "pending", null: false
+ t.text "notes"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["inflow_transaction_id", "outflow_transaction_id"], name: "idx_on_inflow_transaction_id_outflow_transaction_id_8cd07a28bd", unique: true
+ t.index ["inflow_transaction_id"], name: "index_transfers_on_inflow_transaction_id"
+ t.index ["outflow_transaction_id"], name: "index_transfers_on_outflow_transaction_id"
+ end
+
create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "family_id", null: false
t.string "first_name"
@@ -634,7 +638,6 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_27_142333) do
end
add_foreign_key "account_balances", "accounts", on_delete: :cascade
- add_foreign_key "account_entries", "account_transfers", column: "transfer_id"
add_foreign_key "account_entries", "accounts"
add_foreign_key "account_entries", "imports"
add_foreign_key "account_holdings", "accounts"
@@ -663,5 +666,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_27_142333) do
add_foreign_key "sessions", "users"
add_foreign_key "taggings", "tags"
add_foreign_key "tags", "families"
+ add_foreign_key "transfers", "account_transactions", column: "inflow_transaction_id"
+ add_foreign_key "transfers", "account_transactions", column: "outflow_transaction_id"
add_foreign_key "users", "families"
end
diff --git a/test/controllers/account/trades_controller_test.rb b/test/controllers/account/trades_controller_test.rb
index cdfd6add..66ab86ed 100644
--- a/test/controllers/account/trades_controller_test.rb
+++ b/test/controllers/account/trades_controller_test.rb
@@ -38,7 +38,7 @@ class Account::TradesControllerTest < ActionDispatch::IntegrationTest
assert_difference -> { Account::Entry.count } => 2,
-> { Account::Transaction.count } => 2,
- -> { Account::Transfer.count } => 1 do
+ -> { Transfer.count } => 1 do
post account_trades_url, params: {
account_entry: {
account_id: @entry.account_id,
@@ -59,7 +59,7 @@ class Account::TradesControllerTest < ActionDispatch::IntegrationTest
assert_difference -> { Account::Entry.count } => 2,
-> { Account::Transaction.count } => 2,
- -> { Account::Transfer.count } => 1 do
+ -> { Transfer.count } => 1 do
post account_trades_url, params: {
account_entry: {
account_id: @entry.account_id,
@@ -78,7 +78,7 @@ class Account::TradesControllerTest < ActionDispatch::IntegrationTest
test "deposit and withdrawal has optional transfer account" do
assert_difference -> { Account::Entry.count } => 1,
-> { Account::Transaction.count } => 1,
- -> { Account::Transfer.count } => 0 do
+ -> { Transfer.count } => 0 do
post account_trades_url, params: {
account_entry: {
account_id: @entry.account_id,
@@ -93,7 +93,6 @@ class Account::TradesControllerTest < ActionDispatch::IntegrationTest
created_entry = Account::Entry.order(created_at: :desc).first
assert created_entry.amount.positive?
- assert created_entry.marked_as_transfer
assert_redirected_to @entry.account
end
diff --git a/test/controllers/account/transactions_controller_test.rb b/test/controllers/account/transactions_controller_test.rb
index d490bfa7..1b077eb2 100644
--- a/test/controllers/account/transactions_controller_test.rb
+++ b/test/controllers/account/transactions_controller_test.rb
@@ -74,7 +74,7 @@ class Account::TransactionsControllerTest < ActionDispatch::IntegrationTest
end
test "can destroy many transactions at once" do
- transactions = @user.family.entries.account_transactions
+ transactions = @user.family.entries.account_transactions.incomes_and_expenses
delete_count = transactions.size
assert_difference([ "Account::Transaction.count", "Account::Entry.count" ], -delete_count) do
diff --git a/test/controllers/account/transfer_matches_controller_test.rb b/test/controllers/account/transfer_matches_controller_test.rb
new file mode 100644
index 00000000..0e13883e
--- /dev/null
+++ b/test/controllers/account/transfer_matches_controller_test.rb
@@ -0,0 +1,42 @@
+require "test_helper"
+
+class Account::TransferMatchesControllerTest < ActionDispatch::IntegrationTest
+ include Account::EntriesTestHelper
+
+ setup do
+ sign_in @user = users(:family_admin)
+ end
+
+ test "matches existing transaction and creates transfer" do
+ inflow_transaction = create_transaction(amount: 100, account: accounts(:depository))
+ outflow_transaction = create_transaction(amount: -100, account: accounts(:investment))
+
+ assert_difference "Transfer.count", 1 do
+ post account_transaction_transfer_match_path(inflow_transaction), params: {
+ transfer_match: {
+ method: "existing",
+ matched_entry_id: outflow_transaction.id
+ }
+ }
+ end
+
+ assert_redirected_to transactions_url
+ assert_equal "Transfer created", flash[:notice]
+ end
+
+ test "creates transfer for target account" do
+ inflow_transaction = create_transaction(amount: 100, account: accounts(:depository))
+
+ assert_difference [ "Transfer.count", "Account::Entry.count", "Account::Transaction.count" ], 1 do
+ post account_transaction_transfer_match_path(inflow_transaction), params: {
+ transfer_match: {
+ method: "new",
+ target_account_id: accounts(:investment).id
+ }
+ }
+ end
+
+ assert_redirected_to transactions_url
+ assert_equal "Transfer created", flash[:notice]
+ end
+end
diff --git a/test/controllers/account/transfers_controller_test.rb b/test/controllers/transfers_controller_test.rb
similarity index 55%
rename from test/controllers/account/transfers_controller_test.rb
rename to test/controllers/transfers_controller_test.rb
index 72e14345..391937e8 100644
--- a/test/controllers/account/transfers_controller_test.rb
+++ b/test/controllers/transfers_controller_test.rb
@@ -1,19 +1,19 @@
require "test_helper"
-class Account::TransfersControllerTest < ActionDispatch::IntegrationTest
+class TransfersControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
end
test "should get new" do
- get new_account_transfer_url
+ get new_transfer_url
assert_response :success
end
test "can create transfers" do
- assert_difference "Account::Transfer.count", 1 do
- post account_transfers_url, params: {
- account_transfer: {
+ assert_difference "Transfer.count", 1 do
+ post transfers_url, params: {
+ transfer: {
from_account_id: accounts(:depository).id,
to_account_id: accounts(:credit_card).id,
date: Date.current,
@@ -26,8 +26,8 @@ class Account::TransfersControllerTest < ActionDispatch::IntegrationTest
end
test "can destroy transfer" do
- assert_difference -> { Account::Transfer.count } => -1, -> { Account::Transaction.count } => -2 do
- delete account_transfer_url(account_transfers(:one))
+ assert_difference -> { Transfer.count } => -1, -> { Account::Transaction.count } => 0 do
+ delete transfer_url(transfers(:one))
end
end
end
diff --git a/test/fixtures/account/entries.yml b/test/fixtures/account/entries.yml
index 680710be..ccaf29bf 100644
--- a/test/fixtures/account/entries.yml
+++ b/test/fixtures/account/entries.yml
@@ -31,8 +31,6 @@ transfer_out:
amount: 100
currency: USD
account: depository
- marked_as_transfer: true
- transfer: one
entryable_type: Account::Transaction
entryable: transfer_out
@@ -42,7 +40,5 @@ transfer_in:
amount: -100
currency: USD
account: credit_card
- marked_as_transfer: true
- transfer: one
entryable_type: Account::Transaction
entryable: transfer_in
diff --git a/test/fixtures/account/transfers.yml b/test/fixtures/account/transfers.yml
deleted file mode 100644
index 6aab7788..00000000
--- a/test/fixtures/account/transfers.yml
+++ /dev/null
@@ -1 +0,0 @@
-one: { }
diff --git a/test/fixtures/transfers.yml b/test/fixtures/transfers.yml
new file mode 100644
index 00000000..90c1ea94
--- /dev/null
+++ b/test/fixtures/transfers.yml
@@ -0,0 +1,3 @@
+one:
+ inflow_transaction: transfer_in
+ outflow_transaction: transfer_out
diff --git a/test/models/account/transfer_test.rb b/test/models/account/transfer_test.rb
deleted file mode 100644
index 2c9265c6..00000000
--- a/test/models/account/transfer_test.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require "test_helper"
-
-class Account::TransferTest < ActiveSupport::TestCase
- setup do
- @outflow = account_entries(:transfer_out)
- @inflow = account_entries(:transfer_in)
- end
-
- test "transfer valid if it has inflow and outflow from different accounts for the same amount" do
- transfer = Account::Transfer.create! entries: [ @inflow, @outflow ]
-
- assert transfer.valid?
- end
-
- test "transfer must have 2 transactions" do
- invalid_transfer_1 = Account::Transfer.new entries: [ @outflow ]
- invalid_transfer_2 = Account::Transfer.new entries: [ @inflow, @outflow, account_entries(:transaction) ]
-
- assert invalid_transfer_1.invalid?
- assert invalid_transfer_2.invalid?
- end
-
- test "transfer cannot have 2 transactions from the same account" do
- account = accounts(:depository)
-
- inflow = account.entries.create! \
- date: Date.current,
- name: "Inflow",
- amount: -100,
- currency: "USD",
- marked_as_transfer: true,
- entryable: Account::Transaction.new
-
- outflow = account.entries.create! \
- date: Date.current,
- name: "Outflow",
- amount: 100,
- currency: "USD",
- marked_as_transfer: true,
- entryable: Account::Transaction.new
-
- assert_raise ActiveRecord::RecordInvalid do
- Account::Transfer.create! entries: [ inflow, outflow ]
- end
- end
-
- test "all transfer transactions must be marked as transfers" do
- @inflow.update! marked_as_transfer: false
-
- assert_raise ActiveRecord::RecordInvalid do
- Account::Transfer.create! entries: [ @inflow, @outflow ]
- end
- end
-
- test "single-currency transfer transactions must net to zero" do
- @outflow.update! amount: 105
-
- assert_raises ActiveRecord::RecordInvalid do
- Account::Transfer.create! entries: [ @inflow, @outflow ]
- end
- end
-
- test "multi-currency transfer transactions do not have to net to zero" do
- @outflow.update! amount: 105, currency: "EUR"
- transfer = Account::Transfer.create! entries: [ @inflow, @outflow ]
-
- assert transfer.valid?
- end
-end
diff --git a/test/models/family_test.rb b/test/models/family_test.rb
index 74376a7e..33088c81 100644
--- a/test/models/family_test.rb
+++ b/test/models/family_test.rb
@@ -120,11 +120,9 @@ class FamilyTest < ActiveSupport::TestCase
test "calculates rolling transaction totals" do
account = create_account(balance: 1000, accountable: Depository.new)
- liability_account = create_account(balance: 1000, accountable: Loan.new)
create_transaction(account: account, date: 2.days.ago.to_date, amount: -500)
create_transaction(account: account, date: 1.day.ago.to_date, amount: 100)
create_transaction(account: account, date: Date.current, amount: 20)
- create_transaction(account: liability_account, date: 2.days.ago.to_date, amount: -333)
snapshot = @family.snapshot_transactions
diff --git a/test/models/transfer_test.rb b/test/models/transfer_test.rb
new file mode 100644
index 00000000..a4460fd2
--- /dev/null
+++ b/test/models/transfer_test.rb
@@ -0,0 +1,100 @@
+require "test_helper"
+
+class TransferTest < ActiveSupport::TestCase
+ include Account::EntriesTestHelper
+
+ setup do
+ @outflow = account_transactions(:transfer_out)
+ @inflow = account_transactions(:transfer_in)
+ end
+
+ test "transfer has different accounts, opposing amounts, and within 4 days of each other" do
+ outflow_entry = create_transaction(date: Date.current, account: accounts(:depository), amount: 500)
+ inflow_entry = create_transaction(date: 1.day.ago.to_date, account: accounts(:credit_card), amount: -500)
+
+ assert_difference -> { Transfer.count } => 1 do
+ Transfer.create!(
+ inflow_transaction: inflow_entry.account_transaction,
+ outflow_transaction: outflow_entry.account_transaction,
+ )
+ end
+ end
+
+ test "transfer cannot have 2 transactions from the same account" do
+ outflow_entry = create_transaction(date: Date.current, account: accounts(:depository), amount: 500)
+ inflow_entry = create_transaction(date: 1.day.ago.to_date, account: accounts(:depository), amount: -500)
+
+ transfer = Transfer.new(
+ inflow_transaction: inflow_entry.account_transaction,
+ outflow_transaction: outflow_entry.account_transaction,
+ )
+
+ assert_no_difference -> { Transfer.count } do
+ transfer.save
+ end
+
+ assert_equal "Transfer must have different accounts", transfer.errors.full_messages.first
+ end
+
+ test "Transfer transactions must have opposite amounts" do
+ outflow_entry = create_transaction(date: Date.current, account: accounts(:depository), amount: 500)
+ inflow_entry = create_transaction(date: Date.current, account: accounts(:credit_card), amount: -400)
+
+ transfer = Transfer.new(
+ inflow_transaction: inflow_entry.account_transaction,
+ outflow_transaction: outflow_entry.account_transaction,
+ )
+
+ assert_no_difference -> { Transfer.count } do
+ transfer.save
+ end
+
+ assert_equal "Transfer transactions must have opposite amounts", transfer.errors.full_messages.first
+ end
+
+ test "transfer dates must be within 4 days of each other" do
+ outflow_entry = create_transaction(date: Date.current, account: accounts(:depository), amount: 500)
+ inflow_entry = create_transaction(date: 5.days.ago.to_date, account: accounts(:credit_card), amount: -500)
+
+ transfer = Transfer.new(
+ inflow_transaction: inflow_entry.account_transaction,
+ outflow_transaction: outflow_entry.account_transaction,
+ )
+
+ assert_no_difference -> { Transfer.count } do
+ transfer.save
+ end
+
+ assert_equal "Transfer transaction dates must be within 4 days of each other", transfer.errors.full_messages.first
+ end
+
+ test "from_accounts converts amounts to the to_account's currency" do
+ accounts(:depository).update!(currency: "EUR")
+
+ eur_account = accounts(:depository).reload
+ usd_account = accounts(:credit_card)
+
+ ExchangeRate.create!(
+ from_currency: "EUR",
+ to_currency: "USD",
+ rate: 1.1,
+ date: Date.current,
+ )
+
+ transfer = Transfer.from_accounts(
+ from_account: eur_account,
+ to_account: usd_account,
+ date: Date.current,
+ amount: 500,
+ )
+
+ assert_equal 500, transfer.outflow_transaction.entry.amount
+ assert_equal "EUR", transfer.outflow_transaction.entry.currency
+ assert_equal -550, transfer.inflow_transaction.entry.amount
+ assert_equal "USD", transfer.inflow_transaction.entry.currency
+
+ assert_difference -> { Transfer.count } => 1 do
+ transfer.save!
+ end
+ end
+end
diff --git a/test/system/transactions_test.rb b/test/system/transactions_test.rb
index 26db48c3..76ab1363 100644
--- a/test/system/transactions_test.rb
+++ b/test/system/transactions_test.rb
@@ -210,7 +210,7 @@ class TransactionsTest < ApplicationSystemTestCase
end
def number_of_transactions_on_page
- [ @user.family.entries.without_transfers.count, @page_size ].min
+ [ @user.family.entries.count, @page_size ].min
end
def all_transactions_checkbox
diff --git a/test/system/transfers_test.rb b/test/system/transfers_test.rb
index 601a50f8..a2481b18 100644
--- a/test/system/transfers_test.rb
+++ b/test/system/transfers_test.rb
@@ -19,71 +19,13 @@ class TransfersTest < ApplicationSystemTestCase
select checking_name, from: "From"
select savings_name, from: "To"
- fill_in "account_transfer[amount]", with: 500
+ fill_in "transfer[amount]", with: 500
fill_in "Date", with: transfer_date
click_button "Create transfer"
within "#entry-group-" + transfer_date.to_s do
- assert_text "Transfer from"
+ assert_text "Payment to"
end
end
-
- test "can match 2 transactions and create a transfer" do
- transfer_date = Date.current
- outflow = accounts(:depository).entries.create! \
- name: "Outflow from checking account",
- date: transfer_date,
- amount: 100,
- currency: "USD",
- entryable: Account::Transaction.new
-
- inflow = accounts(:credit_card).entries.create! \
- name: "Inflow to cc account",
- date: transfer_date,
- amount: -100,
- currency: "USD",
- entryable: Account::Transaction.new
-
- visit transactions_url
-
- transaction_entry_checkbox(inflow).check
- transaction_entry_checkbox(outflow).check
-
- bulk_transfer_action_button.click
-
- click_on "Mark as transfers"
-
- within "#entry-group-" + transfer_date.to_s do
- assert_text "Outflow"
- assert_text "Inflow"
- end
- end
-
- test "can mark a single transaction as a transfer" do
- txn = @user.family.entries.account_transactions.reverse_chronological.first
-
- within "#" + dom_id(txn) do
- assert_text txn.account_transaction.category.name || "Uncategorized"
- end
-
- transaction_entry_checkbox(txn).check
-
- bulk_transfer_action_button.click
- click_on "Mark as transfers"
-
- within "#" + dom_id(txn) do
- assert_no_text "Uncategorized"
- end
- end
-
- private
-
- def transaction_entry_checkbox(transaction_entry)
- find("#" + dom_id(transaction_entry, "selection"))
- end
-
- def bulk_transfer_action_button
- find("#bulk-transfer-btn")
- end
end