1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-25 08:09:38 +02:00

Allow user to add buy and sell trade transactions for investment accounts (#1066)

* Consolidate modal form structure into partial + helper

* Scaffold out trade transaction form

* Normalize translations

* Add buy and sell trade form with tests

* Move entryable lists to dedicated controllers

* Delegate entry group contents rendering

* More cleanup

* Extract transaction and valuation update logic from entries controller

* Delegate edit and show actions to entryables

* Trade builder

* Update paths for transaction updates
This commit is contained in:
Zach Gollwitzer 2024-08-09 11:22:57 -04:00 committed by GitHub
parent 6bca35fa22
commit e05f03b314
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 801 additions and 624 deletions

View file

@ -3,6 +3,9 @@ require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
setup do
Capybara.default_max_wait_time = 5
# Prevent "auto sync" from running when tests execute enqueued jobs
families(:dylan_family).update! last_synced_at: Time.now
end
driven_by :selenium, using: ENV["CI"].present? ? :headless_chrome : :chrome, screen_size: [ 1400, 1400 ]

View file

@ -5,114 +5,15 @@ class Account::EntriesControllerTest < ActionDispatch::IntegrationTest
sign_in @user = users(:family_admin)
@transaction = account_entries :transaction
@valuation = account_entries :valuation
@trade = account_entries :trade
end
test "should edit valuation entry" do
get edit_account_entry_url(@valuation.account, @valuation)
assert_response :success
end
# =================
# Shared
# =================
test "should show transaction entry" do
get account_entry_url(@transaction.account, @transaction)
assert_response :success
end
test "should show valuation entry" do
get account_entry_url(@valuation.account, @valuation)
assert_response :success
end
test "should get list of transaction entries" do
get transaction_account_entries_url(@transaction.account)
assert_response :success
end
test "should get list of valuation entries" do
get valuation_account_entries_url(@valuation.account)
assert_response :success
end
test "gets new entry by type" do
get new_account_entry_url(@valuation.account, entryable_type: "Account::Valuation")
assert_response :success
end
test "should create valuation" do
assert_difference [ "Account::Entry.count", "Account::Valuation.count" ], 1 do
post account_entries_url(@valuation.account), params: {
account_entry: {
name: "Manual valuation",
amount: 19800,
date: Date.current,
currency: @valuation.account.currency,
entryable_type: "Account::Valuation",
entryable_attributes: {}
}
}
end
assert_equal "Valuation created", flash[:notice]
assert_enqueued_with job: AccountSyncJob
assert_redirected_to account_path(@valuation.account)
end
test "error when valuation already exists for date" do
assert_no_difference_in_entries do
post account_entries_url(@valuation.account), params: {
account_entry: {
amount: 19800,
date: @valuation.date,
currency: @valuation.currency,
entryable_type: "Account::Valuation",
entryable_attributes: {}
}
}
end
assert_equal "Date has already been taken", flash[:alert]
assert_redirected_to account_path(@valuation.account)
end
test "can update entry without entryable attributes" do
assert_no_difference_in_entries do
patch account_entry_url(@valuation.account, @valuation), params: {
account_entry: {
name: "Updated name"
}
}
end
assert_redirected_to account_entry_url(@valuation.account, @valuation)
assert_enqueued_with(job: AccountSyncJob)
end
test "should update transaction entry with entryable attributes" do
assert_no_difference_in_entries do
patch account_entry_url(@transaction.account, @transaction), params: {
account_entry: {
name: "Updated name",
date: Date.current,
currency: "USD",
amount: 20,
entryable_type: @transaction.entryable_type,
entryable_attributes: {
id: @transaction.entryable_id,
tag_ids: [ Tag.first.id, Tag.second.id ],
category_id: Category.first.id,
merchant_id: Merchant.first.id,
notes: "test notes",
excluded: false
}
}
}
end
assert_redirected_to account_entry_url(@transaction.account, @transaction)
assert_enqueued_with(job: AccountSyncJob)
end
test "should destroy transaction entry" do
[ @transaction, @valuation ].each do |entry|
test "should destroy entry" do
[ @transaction, @valuation, @trade ].each do |entry|
assert_difference -> { Account::Entry.count } => -1, -> { entry.entryable_class.count } => -1 do
delete account_entry_url(entry.account, entry)
end
@ -122,6 +23,38 @@ class Account::EntriesControllerTest < ActionDispatch::IntegrationTest
end
end
test "gets show" do
[ @transaction, @valuation, @trade ].each do |entry|
get account_entry_url(entry.account, entry)
assert_response :success
end
end
test "gets edit" do
[ @valuation ].each do |entry|
get edit_account_entry_url(entry.account, entry)
assert_response :success
end
end
test "can update generic entry" do
[ @transaction, @valuation, @trade ].each do |entry|
assert_no_difference_in_entries do
patch account_entry_url(entry.account, entry), params: {
account_entry: {
name: "Name",
date: Date.current,
currency: "USD",
amount: 100
}
}
end
assert_redirected_to account_entry_url(entry.account, entry)
assert_enqueued_with(job: AccountSyncJob)
end
end
private
# Simple guard to verify that nested attributes are passed the record ID to avoid new creation of record

View file

@ -0,0 +1,63 @@
require "test_helper"
class Account::TradesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
@entry = account_entries :trade
end
test "should get index" do
get account_trades_url(@entry.account)
assert_response :success
end
test "should get new" do
get new_account_trade_url(@entry.account)
assert_response :success
end
test "creates trade buy entry" do
assert_difference [ "Account::Entry.count", "Account::Trade.count", "Security.count" ], 1 do
post account_trades_url(@entry.account), params: {
account_entry: {
type: "buy",
date: Date.current,
ticker: "NVDA",
qty: 10,
price: 10
}
}
end
created_entry = Account::Entry.order(created_at: :desc).first
assert created_entry.amount.positive?
assert created_entry.account_trade.qty.positive?
assert_equal "Transaction created successfully.", flash[:notice]
assert_enqueued_with job: AccountSyncJob
assert_redirected_to account_path(@entry.account)
end
test "creates trade sell entry" do
assert_difference [ "Account::Entry.count", "Account::Trade.count" ], 1 do
post account_trades_url(@entry.account), params: {
account_entry: {
type: "sell",
ticker: "AAPL",
date: Date.current,
currency: "USD",
qty: 10,
price: 10
}
}
end
created_entry = Account::Entry.order(created_at: :desc).first
assert created_entry.amount.negative?
assert created_entry.account_trade.qty.negative?
assert_equal "Transaction created successfully.", flash[:notice]
assert_enqueued_with job: AccountSyncJob
assert_redirected_to account_path(@entry.account)
end
end

View file

@ -0,0 +1,40 @@
require "test_helper"
class Account::TransactionsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
@entry = account_entries :transaction
end
test "should get index" do
get account_transactions_url(@entry.account)
assert_response :success
end
test "update" do
assert_no_difference [ "Account::Entry.count", "Account::Transaction.count" ] do
patch account_transaction_url(@entry.account, @entry), params: {
account_entry: {
name: "Name",
date: Date.current,
currency: "USD",
amount: 100,
nature: "income",
entryable_type: @entry.entryable_type,
entryable_attributes: {
id: @entry.entryable_id,
tag_ids: [ Tag.first.id, Tag.second.id ],
category_id: Category.first.id,
merchant_id: Merchant.first.id,
notes: "test notes",
excluded: false
}
}
}
end
assert_equal "Transaction updated successfully.", flash[:notice]
assert_redirected_to account_entry_url(@entry.account, @entry)
assert_enqueued_with(job: AccountSyncJob)
end
end

View file

@ -0,0 +1,50 @@
require "test_helper"
class Account::ValuationsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
@entry = account_entries :valuation
end
test "should get index" do
get account_valuations_url(@entry.account)
assert_response :success
end
test "should get new" do
get new_account_valuation_url(@entry.account)
assert_response :success
end
test "create" do
assert_difference [ "Account::Entry.count", "Account::Valuation.count" ], 1 do
post account_valuations_url(@entry.account), params: {
account_entry: {
name: "Manual valuation",
amount: 19800,
date: Date.current,
currency: "USD"
}
}
end
assert_equal "Valuation created successfully.", flash[:notice]
assert_enqueued_with job: AccountSyncJob
assert_redirected_to account_valuations_path(@entry.account)
end
test "error when valuation already exists for date" do
assert_no_difference [ "Account::Entry.count", "Account::Valuation.count" ] do
post account_valuations_url(@entry.account), params: {
account_entry: {
amount: 19800,
date: @entry.date,
currency: "USD"
}
}
end
assert_equal "Date has already been taken", flash[:alert]
assert_redirected_to account_path(@entry.account)
end
end

View file

@ -2,3 +2,4 @@ one:
security: aapl
qty: 10
price: 214
currency: USD

View file

@ -34,7 +34,8 @@ module Account::EntriesTestHelper
trade = Account::Trade.new \
qty: qty,
security: security,
price: trade_price
price: trade_price,
currency: "USD"
account.entries.create! \
name: "Trade",

View file

@ -0,0 +1,67 @@
require "application_system_test_case"
class TradesTest < ApplicationSystemTestCase
include ActiveJob::TestHelper
setup do
sign_in @user = users(:family_admin)
@account = accounts(:investment)
visit_account_trades
end
test "can create buy transaction" do
shares_qty = 25.0
open_new_trade_modal
fill_in "Ticker symbol", with: "NVDA"
fill_in "Date", with: Date.current
fill_in "Quantity", with: shares_qty
fill_in "account_entry[price]", with: 214.23
click_button "Add transaction"
visit_account_trades
within_trades do
assert_text "Purchase 10 shares of AAPL"
assert_text "Buy #{shares_qty} shares of NVDA"
end
end
test "can create sell transaction" do
aapl = @account.holdings.current.find { |h| h.security.ticker == "AAPL" }
open_new_trade_modal
select "Sell", from: "Type"
fill_in "Ticker symbol", with: aapl.ticker
fill_in "Date", with: Date.current
fill_in "Quantity", with: aapl.qty
fill_in "account_entry[price]", with: 215.33
click_button "Add transaction"
visit_account_trades
within_trades do
assert_text "Sell #{aapl.qty} shares of AAPL"
end
end
private
def open_new_trade_modal
click_link "new_trade_account_#{@account.id}"
end
def within_trades(&block)
within "#" + dom_id(@account, "trades"), &block
end
def visit_account_trades
visit account_url(@account, tab: "trades")
end
end