mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
* Add cost basis tracking and management to holdings - Added migration to introduce `cost_basis_source` and `cost_basis_locked` fields to `holdings`. - Implemented backfill for existing holdings to set `cost_basis_source` based on heuristics. - Introduced `Holding::CostBasisReconciler` to manage cost basis resolution logic. - Added user interface components for editing and locking cost basis in holdings. - Updated `materializer` to integrate reconciliation logic and respect locked holdings. - Extended tests for cost basis-related workflows to ensure accuracy and reliability. * Fix cost basis calculation in holdings controller - Ensure `cost_basis` is converted to decimal for accurate arithmetic. - Fix conditional check to properly validate positive `cost_basis`. * Improve cost basis validation and error handling in holdings controller - Allow zero as a valid cost basis for gifted/inherited shares. - Add error handling with user feedback for invalid cost basis values. --------- Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
59 lines
2.1 KiB
Ruby
59 lines
2.1 KiB
Ruby
# Determines the appropriate cost_basis value and source when updating a holding.
|
|
#
|
|
# Used by both Materializer (for trade-derived calculations) and
|
|
# ProviderImportAdapter (for provider-supplied values) to ensure consistent
|
|
# reconciliation logic across all data sources.
|
|
#
|
|
# Priority hierarchy: manual > calculated > provider > unknown
|
|
#
|
|
class Holding::CostBasisReconciler
|
|
# Determines the appropriate cost_basis value and source for a holding update
|
|
#
|
|
# @param existing_holding [Holding, nil] The existing holding record (nil for new)
|
|
# @param incoming_cost_basis [BigDecimal, nil] The incoming cost_basis value
|
|
# @param incoming_source [String] The source of incoming data ('calculated', 'provider')
|
|
# @return [Hash] { cost_basis: value, cost_basis_source: source, should_update: boolean }
|
|
def self.reconcile(existing_holding:, incoming_cost_basis:, incoming_source:)
|
|
# Treat zero cost_basis from provider as unknown
|
|
if incoming_source == "provider" && (incoming_cost_basis.nil? || incoming_cost_basis.zero?)
|
|
incoming_cost_basis = nil
|
|
end
|
|
|
|
# New holding - use whatever we have
|
|
if existing_holding.nil?
|
|
return {
|
|
cost_basis: incoming_cost_basis,
|
|
cost_basis_source: incoming_cost_basis.present? ? incoming_source : nil,
|
|
should_update: true
|
|
}
|
|
end
|
|
|
|
# Locked - never overwrite
|
|
if existing_holding.cost_basis_locked?
|
|
return {
|
|
cost_basis: existing_holding.cost_basis,
|
|
cost_basis_source: existing_holding.cost_basis_source,
|
|
should_update: false
|
|
}
|
|
end
|
|
|
|
# Check priority - can the incoming source replace the existing?
|
|
if existing_holding.cost_basis_replaceable_by?(incoming_source)
|
|
if incoming_cost_basis.present?
|
|
return {
|
|
cost_basis: incoming_cost_basis,
|
|
cost_basis_source: incoming_source,
|
|
should_update: true
|
|
}
|
|
end
|
|
end
|
|
|
|
# Keep existing (equal or lower priority, or incoming is nil)
|
|
{
|
|
cost_basis: existing_holding.cost_basis,
|
|
cost_basis_source: existing_holding.cost_basis_source,
|
|
should_update: false
|
|
}
|
|
end
|
|
end
|