mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
Improve handling of cost_basis during holding materialization and display (#619)
- Refactored `persist_holdings` to separate and conditionally upsert holdings with and without cost_basis. - Updated `avg_cost` logic to treat 0 cost_basis as unknown and return nil when cost_basis cannot be determined. - Modified trend and investment calculation to exclude holdings with unknown cost_basis. - Adjusted `average_cost` formatting to handle nil values in API responses and views. - Added comprehensive tests to ensure cost_basis preservation and fallback behavior. - Localized `unknown` label for display when cost_basis is unavailable. Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
This commit is contained in:
@@ -26,4 +26,53 @@ class Holding::MaterializerTest < ActiveSupport::TestCase
|
||||
Holding::Materializer.new(@account, strategy: :forward).materialize_holdings
|
||||
end
|
||||
end
|
||||
|
||||
test "preserves provider cost_basis when trade-derived cost_basis is nil" do
|
||||
# Simulate a provider-imported holding with cost_basis (e.g., from SimpleFIN)
|
||||
# This is the realistic scenario: linked account with provider holdings but no trades
|
||||
provider_cost_basis = BigDecimal("150.00")
|
||||
holding = Holding.create!(
|
||||
account: @account,
|
||||
security: @aapl,
|
||||
qty: 10,
|
||||
price: 200,
|
||||
amount: 2000,
|
||||
currency: "USD",
|
||||
date: Date.current,
|
||||
cost_basis: provider_cost_basis
|
||||
)
|
||||
|
||||
# Use :reverse strategy (what linked accounts use) - doesn't purge holdings
|
||||
# The AAPL holding has no trades, so computed cost_basis is nil
|
||||
# The materializer should preserve the provider cost_basis, not overwrite with nil
|
||||
Holding::Materializer.new(@account, strategy: :reverse).materialize_holdings
|
||||
|
||||
holding.reload
|
||||
assert_equal provider_cost_basis, holding.cost_basis,
|
||||
"Provider cost_basis should be preserved when no trades exist for this security"
|
||||
end
|
||||
|
||||
test "updates cost_basis when trade-derived cost_basis is available" do
|
||||
# Create a holding with provider cost_basis
|
||||
Holding.create!(
|
||||
account: @account,
|
||||
security: @aapl,
|
||||
qty: 10,
|
||||
price: 200,
|
||||
amount: 2000,
|
||||
currency: "USD",
|
||||
date: Date.current,
|
||||
cost_basis: BigDecimal("150.00") # Provider says $150
|
||||
)
|
||||
|
||||
# Create a trade that gives us a different cost basis
|
||||
create_trade(@aapl, account: @account, qty: 10, price: 180, date: Date.current)
|
||||
|
||||
# Use :reverse strategy - with trades, it should compute cost_basis from them
|
||||
Holding::Materializer.new(@account, strategy: :reverse).materialize_holdings
|
||||
|
||||
holding = @account.holdings.find_by(security: @aapl, date: Date.current)
|
||||
assert_equal BigDecimal("180.00"), holding.cost_basis,
|
||||
"Trade-derived cost_basis should override provider cost_basis when available"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,6 +75,43 @@ class HoldingTest < ActiveSupport::TestCase
|
||||
assert_in_delta -1.6, @nvda.trend.percent, 0.001
|
||||
end
|
||||
|
||||
test "avg_cost returns nil when no trades exist and no stored cost_basis" do
|
||||
# Holdings created without trades should return nil for avg_cost
|
||||
# This prevents displaying fake $0 gain/loss based on current market price
|
||||
assert_nil @amzn.avg_cost
|
||||
assert_nil @nvda.avg_cost
|
||||
end
|
||||
|
||||
test "avg_cost uses stored cost_basis when available" do
|
||||
# Simulate provider-supplied cost_basis (e.g., from SimpleFIN)
|
||||
@amzn.update!(cost_basis: 200.00)
|
||||
|
||||
assert_equal Money.new(200.00, "USD"), @amzn.avg_cost
|
||||
end
|
||||
|
||||
test "avg_cost treats zero cost_basis as unknown" do
|
||||
# Some providers return 0 when they don't have cost basis data
|
||||
# This should be treated as "unknown" (return nil), not as $0 cost
|
||||
@amzn.update!(cost_basis: 0)
|
||||
|
||||
assert_nil @amzn.avg_cost
|
||||
end
|
||||
|
||||
test "trend returns nil when cost basis is unknown" do
|
||||
# Without cost basis, we can't calculate unrealized gain/loss
|
||||
assert_nil @amzn.trend
|
||||
assert_nil @nvda.trend
|
||||
end
|
||||
|
||||
test "trend works when avg_cost is available" do
|
||||
@amzn.update!(cost_basis: 214.00)
|
||||
|
||||
# Current price is 216, cost basis is 214
|
||||
# Qty is 15, so gain = 15 * (216 - 214) = $30
|
||||
assert_not_nil @amzn.trend
|
||||
assert_equal Money.new(30), @amzn.trend.value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_holdings
|
||||
|
||||
Reference in New Issue
Block a user