1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

Flatten Holding model

This commit is contained in:
Zach Gollwitzer 2025-04-13 08:37:39 -04:00
parent 48c8499b70
commit 21ed7d27d6
34 changed files with 149 additions and 142 deletions

View file

@ -87,19 +87,19 @@ Below are examples of necessary vs. unnecessary tests:
# GOOD!!
# Necessary test - in this case, we're testing critical domain business logic
test "syncs balances" do
Account::Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
@account.expects(:start_date).returns(2.days.ago.to_date)
Account::Balance::ForwardCalculator.any_instance.expects(:calculate).returns(
Balance::ForwardCalculator.any_instance.expects(:calculate).returns(
[
Account::Balance.new(date: 1.day.ago.to_date, balance: 1000, cash_balance: 1000, currency: "USD"),
Account::Balance.new(date: Date.current, balance: 1000, cash_balance: 1000, currency: "USD")
Balance.new(date: 1.day.ago.to_date, balance: 1000, cash_balance: 1000, currency: "USD"),
Balance.new(date: Date.current, balance: 1000, cash_balance: 1000, currency: "USD")
]
)
assert_difference "@account.balances.count", 2 do
Account::Balance::Syncer.new(@account, strategy: :forward).sync_balances
Balance::Syncer.new(@account, strategy: :forward).sync_balances
end
end

View file

@ -55,13 +55,13 @@ All balances are calculated daily by [balance_calculator.rb](mdc:app/models/acco
### Account Holdings
An account [holding.rb](mdc:app/models/account/holding.rb) applies to [investment.rb](mdc:app/models/investment.rb) type accounts and represents a `qty` of a certain [security.rb](mdc:app/models/security.rb) at a specific `price` on a specific `date`.
An account [holding.rb](mdc:app/models/holding.rb) applies to [investment.rb](mdc:app/models/investment.rb) type accounts and represents a `qty` of a certain [security.rb](mdc:app/models/security.rb) at a specific `price` on a specific `date`.
For investment accounts with holdings, [holding_calculator.rb](mdc:app/models/account/holding_calculator.rb) is used to calculate the daily historical holding quantities and prices, which are then rolled up into a final "Balance" for the account in [balance_calculator.rb](mdc:app/models/account/balance_calculator.rb).
For investment accounts with holdings, [base_calculator.rb](mdc:app/models/holding/base_calculator.rb) is used to calculate the daily historical holding quantities and prices, which are then rolled up into a final "Balance" for the account in [base_calculator.rb](mdc:app/models/account/balance/base_calculator.rb).
### Account Entries
An account [entry.rb](mdc:app/models/account/entry.rb) is also a Rails "delegated type". `Account::Entry` represents any record that _modifies_ an `Account` [balance.rb](mdc:app/models/account/balance.rb) and/or [holding.rb](mdc:app/models/account/holding.rb). Therefore, every entry must have a `date`, `amount`, and `currency`.
An account [entry.rb](mdc:app/models/account/entry.rb) is also a Rails "delegated type". `Account::Entry` represents any record that _modifies_ an `Account` [balance.rb](mdc:app/models/account/balance.rb) and/or [holding.rb](mdc:app/models/holding.rb). Therefore, every entry must have a `date`, `amount`, and `currency`.
The `amount` of an [entry.rb](mdc:app/models/account/entry.rb) is a signed value. A _negative_ amount is an "inflow" of money to that account. A _positive_ value is an "outflow" of money from that account. For example:
@ -115,7 +115,7 @@ The most important type of sync is the account sync. It is orchestrated by the
- Auto-matches transfer records for the account
- Calculates daily [balance.rb](mdc:app/models/account/balance.rb) records for the account from `account.start_date` to `Date.current` using [base_calculator.rb](mdc:app/models/account/balance/base_calculator.rb)
- Balances are dependent on the calculation of [holding.rb](mdc:app/models/account/holding.rb), which uses [base_calculator.rb](mdc:app/models/account/holding/base_calculator.rb)
- Balances are dependent on the calculation of [holding.rb](mdc:app/models/holding.rb), which uses [base_calculator.rb](mdc:app/models/account/holding/base_calculator.rb)
- Enriches transaction data if enabled by user
An account sync happens every time an [entry.rb](mdc:app/models/account/entry.rb) is updated.

View file

@ -1,4 +1,4 @@
class Account::HoldingsController < ApplicationController
class HoldingsController < ApplicationController
before_action :set_holding, only: %i[show destroy]
def index

View file

@ -11,7 +11,7 @@ class Account < ApplicationRecord
has_many :transactions, through: :entries, source: :entryable, source_type: "Account::Transaction"
has_many :valuations, through: :entries, source: :entryable, source_type: "Account::Valuation"
has_many :trades, through: :entries, source: :entryable, source_type: "Account::Trade"
has_many :holdings, dependent: :destroy, class_name: "Account::Holding"
has_many :holdings, dependent: :destroy
has_many :balances, dependent: :destroy
monetize :balance, :cash_balance

View file

@ -26,7 +26,7 @@ class Account::Balance::Syncer
private
def sync_holdings
@holdings = Account::Holding::Syncer.new(account, strategy: strategy).sync_holdings
@holdings = Holding::Syncer.new(account, strategy: strategy).sync_holdings
end
def update_account_info

View file

@ -1,4 +1,6 @@
class Account::Holding < ApplicationRecord
class Holding < ApplicationRecord
self.table_name = "account_holdings"
include Monetizable, Gapfillable
monetize :amount

View file

@ -1,4 +1,4 @@
class Account::Holding::BaseCalculator
class Holding::BaseCalculator
attr_reader :account
def initialize(account)
@ -8,13 +8,13 @@ class Account::Holding::BaseCalculator
def calculate
Rails.logger.tagged(self.class.name) do
holdings = calculate_holdings
Account::Holding.gapfill(holdings)
Holding.gapfill(holdings)
end
end
private
def portfolio_cache
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account)
@portfolio_cache ||= Holding::PortfolioCache.new(account)
end
def empty_portfolio
@ -49,7 +49,7 @@ class Account::Holding::BaseCalculator
next
end
Account::Holding.new(
Holding.new(
account_id: account.id,
security_id: security_id,
date: date,

View file

@ -1,7 +1,7 @@
class Account::Holding::ForwardCalculator < Account::Holding::BaseCalculator
class Holding::ForwardCalculator < Holding::BaseCalculator
private
def portfolio_cache
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account)
@portfolio_cache ||= Holding::PortfolioCache.new(account)
end
def calculate_holdings

View file

@ -1,4 +1,4 @@
module Account::Holding::Gapfillable
module Holding::Gapfillable
extend ActiveSupport::Concern
class_methods do
@ -19,7 +19,7 @@ module Account::Holding::Gapfillable
previous_holding = holding
else
# Create a new holding based on the previous day's data
filled_holdings << Account::Holding.new(
filled_holdings << Holding.new(
account: previous_holding.account,
security: previous_holding.security,
date: date,

View file

@ -1,4 +1,4 @@
class Account::Holding::PortfolioCache
class Holding::PortfolioCache
attr_reader :account, :use_holdings
class SecurityNotFound < StandardError

View file

@ -1,10 +1,10 @@
class Account::Holding::ReverseCalculator < Account::Holding::BaseCalculator
class Holding::ReverseCalculator < Holding::BaseCalculator
private
# Reverse calculators will use the existing holdings as a source of security ids and prices
# since it is common for a provider to supply "current day" holdings but not all the historical
# trades that make up those holdings.
def portfolio_cache
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account, use_holdings: true)
@portfolio_cache ||= Holding::PortfolioCache.new(account, use_holdings: true)
end
def calculate_holdings

View file

@ -1,4 +1,4 @@
class Account::Holding::Syncer
class Holding::Syncer
def initialize(account, strategy:)
@account = account
@strategy = strategy
@ -50,9 +50,9 @@ class Account::Holding::Syncer
def calculator
if strategy == :reverse
Account::Holding::ReverseCalculator.new(account)
Holding::ReverseCalculator.new(account)
else
Account::Holding::ForwardCalculator.new(account)
Holding::ForwardCalculator.new(account)
end
end
end

View file

@ -6,7 +6,7 @@
<%= image_tag "https://logo.synthfinance.com/ticker/#{holding.ticker}", class: "w-9 h-9 rounded-full", loading: "lazy" %>
<div class="space-y-0.5">
<%= link_to holding.name, account_holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
<%= link_to holding.name, holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
<% if holding.amount %>
<%= tag.p holding.ticker, class: "text-secondary text-xs uppercase" %>

View file

@ -21,12 +21,12 @@
</div>
<div class="rounded-lg bg-container shadow-border-xs">
<%= render "account/holdings/cash", account: @account %>
<%= render "holdings/cash", account: @account %>
<%= render "account/holdings/ruler" %>
<%= render "holdings/ruler" %>
<% if @account.current_holdings.any? %>
<%= render partial: "account/holdings/holding", collection: @account.current_holdings, spacer_template: "ruler" %>
<%= render partial: "holdings/holding", collection: @account.current_holdings, spacer_template: "ruler" %>
<% end %>
</div>
</div>

View file

@ -102,7 +102,7 @@
</div>
<%= button_to t(".delete"),
account_holding_path(@holding),
holding_path(@holding),
method: :delete,
class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-secondary",
data: { turbo_confirm: true } %>

View file

@ -1,5 +1,5 @@
<%# locals: (account:) %>
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account_id: account.id) do %>
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account) do %>
<%= render "account/entries/loading" %>
<% end %>

View file

@ -1,38 +0,0 @@
---
en:
account:
holdings:
cash:
brokerage_cash: Brokerage cash
destroy:
success: Holding deleted
holding:
per_share: per share
shares: "%{qty} shares"
index:
cost: cost
holdings: Holdings
name: name
new_holding: New transaction
no_holdings: No holdings to show.
return: total return
weight: weight
missing_price_tooltip:
description: This investment has missing values and we could not calculate
its returns or value.
missing_data: Missing data
show:
avg_cost_label: Average Cost
current_market_price_label: Current Market Price
delete: Delete
delete_subtitle: This will delete the holding and all your associated trades
on this account. This action cannot be undone.
delete_title: Delete holding
history: History
overview: Overview
portfolio_weight_label: Portfolio Weight
settings: Settings
ticker_label: Ticker
trade_history_entry: "%{qty} shares of %{security} at %{price}"
trend_label: Trend
unknown: Unknown

View file

@ -0,0 +1,37 @@
---
en:
holdings:
cash:
brokerage_cash: Brokerage cash
destroy:
success: Holding deleted
holding:
per_share: per share
shares: "%{qty} shares"
index:
cost: cost
holdings: Holdings
name: name
new_holding: New transaction
no_holdings: No holdings to show.
return: total return
weight: weight
missing_price_tooltip:
description: This investment has missing values and we could not calculate
its returns or value.
missing_data: Missing data
show:
avg_cost_label: Average Cost
current_market_price_label: Current Market Price
delete: Delete
delete_subtitle: This will delete the holding and all your associated trades
on this account. This action cannot be undone.
delete_title: Delete holding
history: History
overview: Overview
portfolio_weight_label: Portfolio Weight
settings: Settings
ticker_label: Ticker
trade_history_entry: "%{qty} shares of %{security} at %{price}"
trend_label: Trend
unknown: Unknown

View file

@ -105,13 +105,13 @@ Rails.application.routes.draw do
get :chart
get :sparkline
end
resources :holdings, only: %i[index new show destroy], shallow: true
end
resources :accountable_sparklines, only: :show, param: :accountable_type
namespace :account do
resources :holdings, only: %i[index new show destroy]
resources :transactions, only: %i[show new create update destroy] do
resource :transfer_match, only: %i[new create]
resource :category, only: :update, controller: :transaction_categories

View file

@ -81,7 +81,7 @@ namespace :securities do
puts " Duplicate without MIC: #{security.id}"
# Count affected records
holdings_count = Account::Holding.where(security_id: security.id).count
holdings_count = Holding.where(security_id: security.id).count
trades_count = Account::Trade.where(security_id: security.id).count
prices_count = Security::Price.where(security_id: security.id).count
@ -94,7 +94,7 @@ namespace :securities do
begin
ActiveRecord::Base.transaction do
# Update all references to point to the canonical security
Account::Holding.where(security_id: security.id).update_all(security_id: canonical.id)
Holding.where(security_id: security.id).update_all(security_id: canonical.id)
Account::Trade.where(security_id: security.id).update_all(security_id: canonical.id)
Security::Price.where(security_id: security.id).update_all(security_id: canonical.id)
@ -134,7 +134,7 @@ namespace :securities do
puts " Duplicates: #{duplicates.map(&:id).join(', ')}"
# Count affected records
holdings_count = Account::Holding.where(security_id: duplicates).count
holdings_count = Holding.where(security_id: duplicates).count
trades_count = Account::Trade.where(security_id: duplicates).count
prices_count = Security::Price.where(security_id: duplicates).count
@ -151,7 +151,7 @@ namespace :securities do
begin
ActiveRecord::Base.transaction do
# Update all references to point to the canonical security
Account::Holding.where(security_id: duplicates).update_all(security_id: canonical.id)
Holding.where(security_id: duplicates).update_all(security_id: canonical.id)
Account::Trade.where(security_id: duplicates).update_all(security_id: canonical.id)
Security::Price.where(security_id: duplicates).update_all(security_id: canonical.id)

View file

@ -1,27 +1,33 @@
require "test_helper"
class Account::HoldingsControllerTest < ActionDispatch::IntegrationTest
class HoldingsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
@account = accounts(:investment)
@holding = @account.holdings.first
end
test "gets index" do
get account_holdings_url(@account)
assert_response :success
end
test "gets holdings" do
get account_holdings_url(account_id: @account.id)
assert_response :success
end
test "gets holding" do
get account_holding_path(@holding)
get holding_path(@holding)
assert_response :success
end
test "destroys holding and associated entries" do
assert_difference -> { Account::Holding.count } => -1,
assert_difference -> { Holding.count } => -1,
-> { Account::Entry.count } => -1 do
delete account_holding_path(@holding)
delete holding_path(@holding)
end
assert_redirected_to account_path(@holding.account)

View file

@ -64,7 +64,7 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
assert_not ExchangeRate.exists?(exchange_rate.id)
assert_not Security::Price.exists?(security_price.id)
assert_not Account::Holding.exists?(holding.id)
assert_not Holding.exists?(holding.id)
assert_not Account::Balance.exists?(account_balance.id)
end

View file

@ -14,7 +14,7 @@ class Account::Balance::SyncerTest < ActiveSupport::TestCase
end
test "syncs balances" do
Account::Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
@account.expects(:start_date).returns(2.days.ago.to_date)

View file

@ -1,6 +1,6 @@
require "test_helper"
class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
class Holding::ForwardCalculatorTest < ActiveSupport::TestCase
include Account::EntriesTestHelper
setup do
@ -14,7 +14,7 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
end
test "no holdings" do
calculated = Account::Holding::ForwardCalculator.new(@account).calculate
calculated = Holding::ForwardCalculator.new(@account).calculate
assert_equal [], calculated
end
@ -35,32 +35,32 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
expected = [
# 4 days ago
Account::Holding.new(security: @voo, date: 4.days.ago.to_date, qty: 0, price: 460, amount: 0),
Account::Holding.new(security: @wmt, date: 4.days.ago.to_date, qty: 0, price: 100, amount: 0),
Account::Holding.new(security: @amzn, date: 4.days.ago.to_date, qty: 0, price: 200, amount: 0),
Holding.new(security: @voo, date: 4.days.ago.to_date, qty: 0, price: 460, amount: 0),
Holding.new(security: @wmt, date: 4.days.ago.to_date, qty: 0, price: 100, amount: 0),
Holding.new(security: @amzn, date: 4.days.ago.to_date, qty: 0, price: 200, amount: 0),
# 3 days ago
Account::Holding.new(security: @voo, date: 3.days.ago.to_date, qty: 20, price: 470, amount: 9400),
Account::Holding.new(security: @wmt, date: 3.days.ago.to_date, qty: 0, price: 100, amount: 0),
Account::Holding.new(security: @amzn, date: 3.days.ago.to_date, qty: 0, price: 200, amount: 0),
Holding.new(security: @voo, date: 3.days.ago.to_date, qty: 20, price: 470, amount: 9400),
Holding.new(security: @wmt, date: 3.days.ago.to_date, qty: 0, price: 100, amount: 0),
Holding.new(security: @amzn, date: 3.days.ago.to_date, qty: 0, price: 200, amount: 0),
# 2 days ago
Account::Holding.new(security: @voo, date: 2.days.ago.to_date, qty: 5, price: 480, amount: 2400),
Account::Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
Account::Holding.new(security: @amzn, date: 2.days.ago.to_date, qty: 1, price: 200, amount: 200),
Holding.new(security: @voo, date: 2.days.ago.to_date, qty: 5, price: 480, amount: 2400),
Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
Holding.new(security: @amzn, date: 2.days.ago.to_date, qty: 1, price: 200, amount: 200),
# 1 day ago
Account::Holding.new(security: @voo, date: 1.day.ago.to_date, qty: 10, price: 490, amount: 4900),
Account::Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
Account::Holding.new(security: @amzn, date: 1.day.ago.to_date, qty: 0, price: 200, amount: 0),
Holding.new(security: @voo, date: 1.day.ago.to_date, qty: 10, price: 490, amount: 4900),
Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
Holding.new(security: @amzn, date: 1.day.ago.to_date, qty: 0, price: 200, amount: 0),
# Today
Account::Holding.new(security: @voo, date: Date.current, qty: 10, price: 500, amount: 5000),
Account::Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000),
Account::Holding.new(security: @amzn, date: Date.current, qty: 0, price: 200, amount: 0)
Holding.new(security: @voo, date: Date.current, qty: 10, price: 500, amount: 5000),
Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000),
Holding.new(security: @amzn, date: Date.current, qty: 0, price: 200, amount: 0)
]
calculated = Account::Holding::ForwardCalculator.new(@account).calculate
calculated = Holding::ForwardCalculator.new(@account).calculate
assert_equal expected.length, calculated.length
assert_holdings(expected, calculated)
@ -74,9 +74,9 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
create_trade(@wmt, qty: 100, date: 1.day.ago.to_date, price: 100, account: @account)
expected = [
Account::Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
Account::Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
Account::Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000)
Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000)
]
# Price missing today, so we should carry forward the holding from 1 day ago
@ -85,7 +85,7 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
Security::Price.stubs(:find_price).with(security: @wmt, date: 1.day.ago.to_date).returns(Security::Price.new(price: 100))
Security::Price.stubs(:find_price).with(security: @wmt, date: Date.current).returns(nil)
calculated = Account::Holding::ForwardCalculator.new(@account).calculate
calculated = Holding::ForwardCalculator.new(@account).calculate
assert_equal expected.length, calculated.length
assert_holdings(expected, calculated)
@ -98,13 +98,13 @@ class Account::Holding::ForwardCalculatorTest < ActiveSupport::TestCase
create_trade(offline_security, qty: 1, date: 1.day.ago.to_date, price: 100, account: @account)
expected = [
Account::Holding.new(security: offline_security, date: 3.days.ago.to_date, qty: 1, price: 90, amount: 90),
Account::Holding.new(security: offline_security, date: 2.days.ago.to_date, qty: 1, price: 90, amount: 90),
Account::Holding.new(security: offline_security, date: 1.day.ago.to_date, qty: 2, price: 100, amount: 200),
Account::Holding.new(security: offline_security, date: Date.current, qty: 2, price: 100, amount: 200)
Holding.new(security: offline_security, date: 3.days.ago.to_date, qty: 1, price: 90, amount: 90),
Holding.new(security: offline_security, date: 2.days.ago.to_date, qty: 1, price: 90, amount: 90),
Holding.new(security: offline_security, date: 1.day.ago.to_date, qty: 2, price: 100, amount: 200),
Holding.new(security: offline_security, date: Date.current, qty: 2, price: 100, amount: 200)
]
calculated = Account::Holding::ForwardCalculator.new(@account).calculate
calculated = Holding::ForwardCalculator.new(@account).calculate
assert_equal expected.length, calculated.length
assert_holdings(expected, calculated)

View file

@ -1,6 +1,6 @@
require "test_helper"
class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
class Holding::PortfolioCacheTest < ActiveSupport::TestCase
include Account::EntriesTestHelper, ProviderTestHelper
setup do
@ -30,7 +30,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
expect_provider_prices([], start_date: @account.start_date)
cache = Account::Holding::PortfolioCache.new(@account)
cache = Holding::PortfolioCache.new(@account)
assert_equal db_price, cache.get_price(@security.id, Date.current).price
end
@ -46,7 +46,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
expect_provider_prices([ provider_price ], start_date: @account.start_date)
cache = Account::Holding::PortfolioCache.new(@account)
cache = Holding::PortfolioCache.new(@account)
assert_equal provider_price.price, cache.get_price(@security.id, Date.current).price
end
@ -54,7 +54,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
Security::Price.destroy_all
expect_provider_prices([], start_date: @account.start_date)
cache = Account::Holding::PortfolioCache.new(@account)
cache = Holding::PortfolioCache.new(@account)
assert_equal @trade.price, cache.get_price(@security.id, @trade.entry.date).price
end
@ -62,7 +62,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
Security::Price.delete_all
Account::Entry.delete_all
holding = Account::Holding.create!(
holding = Holding.create!(
security: @security,
account: @account,
date: Date.current,
@ -74,7 +74,7 @@ class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
expect_provider_prices([], start_date: @account.start_date)
cache = Account::Holding::PortfolioCache.new(@account, use_holdings: true)
cache = Holding::PortfolioCache.new(@account, use_holdings: true)
assert_equal holding.price, cache.get_price(@security.id, holding.date).price
end

View file

@ -1,6 +1,6 @@
require "test_helper"
class Account::Holding::ReverseCalculatorTest < ActiveSupport::TestCase
class Holding::ReverseCalculatorTest < ActiveSupport::TestCase
include Account::EntriesTestHelper
setup do
@ -14,7 +14,7 @@ class Account::Holding::ReverseCalculatorTest < ActiveSupport::TestCase
end
test "no holdings" do
calculated = Account::Holding::ReverseCalculator.new(@account).calculate
calculated = Holding::ReverseCalculator.new(@account).calculate
assert_equal [], calculated
end
@ -26,7 +26,7 @@ class Account::Holding::ReverseCalculatorTest < ActiveSupport::TestCase
create_trade(voo, qty: -10, date: Date.current, price: 470, account: @account)
calculated = Account::Holding::ReverseCalculator.new(@account).calculate
calculated = Holding::ReverseCalculator.new(@account).calculate
assert_equal 2, calculated.length
end
@ -47,32 +47,32 @@ class Account::Holding::ReverseCalculatorTest < ActiveSupport::TestCase
expected = [
# 4 days ago
Account::Holding.new(security: @voo, date: 4.days.ago.to_date, qty: 0, price: 460, amount: 0),
Account::Holding.new(security: @wmt, date: 4.days.ago.to_date, qty: 0, price: 100, amount: 0),
Account::Holding.new(security: @amzn, date: 4.days.ago.to_date, qty: 0, price: 200, amount: 0),
Holding.new(security: @voo, date: 4.days.ago.to_date, qty: 0, price: 460, amount: 0),
Holding.new(security: @wmt, date: 4.days.ago.to_date, qty: 0, price: 100, amount: 0),
Holding.new(security: @amzn, date: 4.days.ago.to_date, qty: 0, price: 200, amount: 0),
# 3 days ago
Account::Holding.new(security: @voo, date: 3.days.ago.to_date, qty: 20, price: 470, amount: 9400),
Account::Holding.new(security: @wmt, date: 3.days.ago.to_date, qty: 0, price: 100, amount: 0),
Account::Holding.new(security: @amzn, date: 3.days.ago.to_date, qty: 0, price: 200, amount: 0),
Holding.new(security: @voo, date: 3.days.ago.to_date, qty: 20, price: 470, amount: 9400),
Holding.new(security: @wmt, date: 3.days.ago.to_date, qty: 0, price: 100, amount: 0),
Holding.new(security: @amzn, date: 3.days.ago.to_date, qty: 0, price: 200, amount: 0),
# 2 days ago
Account::Holding.new(security: @voo, date: 2.days.ago.to_date, qty: 5, price: 480, amount: 2400),
Account::Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
Account::Holding.new(security: @amzn, date: 2.days.ago.to_date, qty: 1, price: 200, amount: 200),
Holding.new(security: @voo, date: 2.days.ago.to_date, qty: 5, price: 480, amount: 2400),
Holding.new(security: @wmt, date: 2.days.ago.to_date, qty: 0, price: 100, amount: 0),
Holding.new(security: @amzn, date: 2.days.ago.to_date, qty: 1, price: 200, amount: 200),
# 1 day ago
Account::Holding.new(security: @voo, date: 1.day.ago.to_date, qty: 10, price: 490, amount: 4900),
Account::Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
Account::Holding.new(security: @amzn, date: 1.day.ago.to_date, qty: 0, price: 200, amount: 0),
Holding.new(security: @voo, date: 1.day.ago.to_date, qty: 10, price: 490, amount: 4900),
Holding.new(security: @wmt, date: 1.day.ago.to_date, qty: 100, price: 100, amount: 10000),
Holding.new(security: @amzn, date: 1.day.ago.to_date, qty: 0, price: 200, amount: 0),
# Today
Account::Holding.new(security: @voo, date: Date.current, qty: 10, price: 500, amount: 5000),
Account::Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000),
Account::Holding.new(security: @amzn, date: Date.current, qty: 0, price: 200, amount: 0)
Holding.new(security: @voo, date: Date.current, qty: 10, price: 500, amount: 5000),
Holding.new(security: @wmt, date: Date.current, qty: 100, price: 100, amount: 10000),
Holding.new(security: @amzn, date: Date.current, qty: 0, price: 200, amount: 0)
]
calculated = Account::Holding::ReverseCalculator.new(@account).calculate
calculated = Holding::ReverseCalculator.new(@account).calculate
assert_equal expected.length, calculated.length

View file

@ -1,6 +1,6 @@
require "test_helper"
class Account::Holding::SyncerTest < ActiveSupport::TestCase
class Holding::SyncerTest < ActiveSupport::TestCase
include Account::EntriesTestHelper
setup do
@ -14,16 +14,16 @@ class Account::Holding::SyncerTest < ActiveSupport::TestCase
# Should have yesterday's and today's holdings
assert_difference "@account.holdings.count", 2 do
Account::Holding::Syncer.new(@account, strategy: :forward).sync_holdings
Holding::Syncer.new(@account, strategy: :forward).sync_holdings
end
end
test "purges stale holdings for unlinked accounts" do
# Since the account has no entries, there should be no holdings
Account::Holding.create!(account: @account, security: @aapl, qty: 1, price: 100, amount: 100, currency: "USD", date: Date.current)
Holding.create!(account: @account, security: @aapl, qty: 1, price: 100, amount: 100, currency: "USD", date: Date.current)
assert_difference "Account::Holding.count", -1 do
Account::Holding::Syncer.new(@account, strategy: :forward).sync_holdings
assert_difference "Holding.count", -1 do
Holding::Syncer.new(@account, strategy: :forward).sync_holdings
end
end
end

View file

@ -1,7 +1,7 @@
require "test_helper"
require "ostruct"
class Account::HoldingTest < ActiveSupport::TestCase
class HoldingTest < ActiveSupport::TestCase
include Account::EntriesTestHelper, SecuritiesTestHelper
setup do

View file

@ -45,7 +45,7 @@ class PlaidInvestmentSyncTest < ActiveSupport::TestCase
# Cash holding should be ignored, resulting in 1, NOT 2 total holdings after sync
assert_difference -> { Account::Trade.count } => 1,
-> { Account::Transaction.count } => 0,
-> { Account::Holding.count } => 1,
-> { Holding.count } => 1,
-> { Security.count } => 0 do
PlaidInvestmentSync.new(@plaid_account).sync!(
transactions: transactions,