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:
parent
6bca35fa22
commit
e05f03b314
75 changed files with 801 additions and 624 deletions
|
@ -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 ]
|
||||
|
|
|
@ -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
|
||||
|
|
63
test/controllers/account/trades_controller_test.rb
Normal file
63
test/controllers/account/trades_controller_test.rb
Normal 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
|
40
test/controllers/account/transactions_controller_test.rb
Normal file
40
test/controllers/account/transactions_controller_test.rb
Normal 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
|
50
test/controllers/account/valuations_controller_test.rb
Normal file
50
test/controllers/account/valuations_controller_test.rb
Normal 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
|
1
test/fixtures/account/trades.yml
vendored
1
test/fixtures/account/trades.yml
vendored
|
@ -2,3 +2,4 @@ one:
|
|||
security: aapl
|
||||
qty: 10
|
||||
price: 214
|
||||
currency: USD
|
||||
|
|
|
@ -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",
|
||||
|
|
67
test/system/trades_test.rb
Normal file
67
test/system/trades_test.rb
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue