mirror of
https://github.com/we-promise/sure.git
synced 2026-04-14 09:34:05 +00:00
* Add tax treatment metrics to reports, forms, and models - Implement `build_gains_by_tax_treatment` for grouping gains by tax treatment - Update investment performance view with tax treatment breakdown - Add tax treatment field to crypto and investments forms - Introduce `realized_gain_loss` calculation in the Trade model - Group investment subtypes by region for improved dropdown organization * Optimize investment performance report by reducing N+1 queries - Eager-load associations in `build_gains_by_tax_treatment` to minimize database queries - Preload holdings for realized gain/loss calculations in trades - Refactor views to standardize "no data" placeholder using translations - Adjust styling in tax treatment breakdown for improved layout * Enhance investment performance translations and optimize holdings lookup logic - Update `holdings_count` and `sells_count` translations to handle pluralization - Refactor views to use pluralized translation keys with count interpolation - Optimize preloaded holdings lookup in `Trade` to ensure deterministic selection using `select` and `max_by` * Refine preloaded holdings logic in `Trade` model - Treat empty preloaded holdings as authoritative to prevent unnecessary DB queries - Add explicit fallback behavior for database query when holdings are not preloaded --------- Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
85 lines
2.3 KiB
Ruby
85 lines
2.3 KiB
Ruby
class Trade < ApplicationRecord
|
|
include Entryable, Monetizable
|
|
|
|
monetize :price
|
|
|
|
belongs_to :security
|
|
|
|
# Use the same activity labels as Transaction
|
|
ACTIVITY_LABELS = Transaction::ACTIVITY_LABELS.dup.freeze
|
|
|
|
validates :qty, presence: true
|
|
validates :price, :currency, presence: true
|
|
validates :investment_activity_label, inclusion: { in: ACTIVITY_LABELS }, allow_nil: true
|
|
|
|
# Trade types for categorization
|
|
def buy?
|
|
qty.positive?
|
|
end
|
|
|
|
def sell?
|
|
qty.negative?
|
|
end
|
|
|
|
class << self
|
|
def build_name(type, qty, ticker)
|
|
prefix = type == "buy" ? "Buy" : "Sell"
|
|
"#{prefix} #{qty.to_d.abs} shares of #{ticker}"
|
|
end
|
|
end
|
|
|
|
def unrealized_gain_loss
|
|
return nil if qty.negative?
|
|
current_price = security.current_price
|
|
return nil if current_price.nil?
|
|
|
|
current_value = current_price * qty.abs
|
|
cost_basis = price_money * qty.abs
|
|
|
|
Trend.new(current: current_value, previous: cost_basis)
|
|
end
|
|
|
|
# Calculates realized gain/loss for sell trades based on avg_cost at time of sale
|
|
# Returns nil for buy trades or when cost basis cannot be determined
|
|
def realized_gain_loss
|
|
return @realized_gain_loss if defined?(@realized_gain_loss)
|
|
|
|
@realized_gain_loss = calculate_realized_gain_loss
|
|
end
|
|
|
|
# Trades are always excluded from expense budgets
|
|
# They represent portfolio management, not living expenses
|
|
def excluded_from_budget?
|
|
true
|
|
end
|
|
|
|
private
|
|
|
|
def calculate_realized_gain_loss
|
|
return nil unless sell?
|
|
|
|
# Use preloaded holdings if available (set by reports controller to avoid N+1)
|
|
# Treat defined-but-empty preload as authoritative to prevent DB fallback
|
|
holding = if defined?(@preloaded_holdings)
|
|
# Use select + max_by for deterministic selection regardless of array order
|
|
(@preloaded_holdings || [])
|
|
.select { |h| h.security_id == security_id && h.date <= entry.date }
|
|
.max_by(&:date)
|
|
else
|
|
# Fall back to database query only when not preloaded
|
|
entry.account.holdings
|
|
.where(security_id: security_id)
|
|
.where("date <= ?", entry.date)
|
|
.order(date: :desc)
|
|
.first
|
|
end
|
|
|
|
return nil unless holding&.avg_cost
|
|
|
|
cost_basis = holding.avg_cost * qty.abs
|
|
sale_proceeds = price_money * qty.abs
|
|
|
|
Trend.new(current: sale_proceeds, previous: cost_basis)
|
|
end
|
|
end
|