Add cost basis source tracking with manual override and lock protection (#623)

* 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>
This commit is contained in:
LPW
2026-01-12 08:05:46 -05:00
committed by Josh Waldrep
parent 96022cbe9a
commit e5fbdfb593

11
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2026_01_10_180000) do
ActiveRecord::Schema[7.2].define(version: 2026_01_12_065106) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -489,6 +489,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_01_10_180000) do
t.string "external_id"
t.decimal "cost_basis", precision: 19, scale: 4
t.uuid "account_provider_id"
t.string "cost_basis_source"
t.boolean "cost_basis_locked", default: false, null: false
t.index ["account_id", "external_id"], name: "idx_holdings_on_account_id_external_id_unique", unique: true, where: "(external_id IS NOT NULL)"
t.index ["account_id", "security_id", "date", "currency"], name: "idx_on_account_id_security_id_date_currency_5323e39f8b", unique: true
t.index ["account_id"], name: "index_holdings_on_account_id"
@@ -1129,7 +1131,14 @@ ActiveRecord::Schema[7.2].define(version: 2026_01_10_180000) do
t.string "currency"
t.jsonb "locked_attributes", default: {}
t.uuid "category_id"
t.decimal "realized_gain", precision: 19, scale: 4
t.decimal "cost_basis_amount", precision: 19, scale: 4
t.string "cost_basis_currency"
t.integer "holding_period_days"
t.string "realized_gain_confidence"
t.string "realized_gain_currency"
t.index ["category_id"], name: "index_trades_on_category_id"
t.index ["realized_gain"], name: "index_trades_on_realized_gain_not_null", where: "(realized_gain IS NOT NULL)"
t.index ["security_id"], name: "index_trades_on_security_id"
end