Rename raw_investments_payload to raw_holdings_payload for Plaid accounts (#760)

* refactor: rename `raw_investments_payload` to `raw_holdings_payload`

- Update references and models to use consistent naming.
- Adjust migrations, tests, and encryption setup accordingly.

* fix: improve safety when accessing raw_holdings_payload keys

- Use `dig` with safe navigation to prevent potential nil errors.
- Add support for decryption from the old column name `raw_investments_payload`.
- Adjust related methods and calculations for consistency.

---------

Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
This commit is contained in:
LPW
2026-01-24 05:16:26 -05:00
committed by GitHub
parent 8c9764f1ad
commit d98711d4ea
15 changed files with 35 additions and 29 deletions

View File

@@ -5,7 +5,8 @@ class PlaidAccount < ApplicationRecord
if encryption_ready?
encrypts :raw_payload
encrypts :raw_transactions_payload
encrypts :raw_investments_payload
# Support reading data encrypted under the old column name after rename
encrypts :raw_holdings_payload, previous: { attribute: :raw_investments_payload }
encrypts :raw_liabilities_payload
end
@@ -48,9 +49,9 @@ class PlaidAccount < ApplicationRecord
save!
end
def upsert_plaid_investments_snapshot!(investments_snapshot)
def upsert_plaid_holdings_snapshot!(holdings_snapshot)
assign_attributes(
raw_investments_payload: investments_snapshot
raw_holdings_payload: holdings_snapshot
)
save!

View File

@@ -23,7 +23,7 @@ class PlaidAccount::Importer
end
def import_investments
plaid_account.upsert_plaid_investments_snapshot!(account_snapshot.investments_data)
plaid_account.upsert_plaid_holdings_snapshot!(account_snapshot.investments_data)
end
def import_liabilities

View File

@@ -44,7 +44,7 @@ class PlaidAccount::Investments::BalanceCalculator
attr_reader :plaid_account, :security_resolver
def holdings
plaid_account.raw_investments_payload["holdings"] || []
plaid_account.raw_holdings_payload&.dig("holdings") || []
end
def calculate_investment_brokerage_cash

View File

@@ -51,7 +51,7 @@ class PlaidAccount::Investments::HoldingsProcessor
end
def holdings
plaid_account.raw_investments_payload&.[]("holdings") || []
plaid_account.raw_holdings_payload&.[]("holdings") || []
end
def parse_decimal(value)

View File

@@ -43,7 +43,7 @@ class PlaidAccount::Investments::SecurityResolver
Response = Struct.new(:security, :cash_equivalent?, :brokerage_cash?, keyword_init: true)
def securities
plaid_account.raw_investments_payload["securities"] || []
plaid_account.raw_holdings_payload&.dig("securities") || []
end
# Tries to find security, or returns the "proxy security" (common with options contracts that have underlying securities)

View File

@@ -98,7 +98,7 @@ class PlaidAccount::Investments::TransactionsProcessor
end
def transactions
plaid_account.raw_investments_payload["transactions"] || []
plaid_account.raw_holdings_payload&.dig("transactions") || []
end
# Plaid unfortunately returns incorrect signage on some `quantity` values. They claim all "sell" transactions

View File

@@ -61,7 +61,7 @@ class PlaidItem::Syncer
def count_holdings(plaid_accounts)
plaid_accounts.sum do |pa|
Array(pa.raw_investments_payload).size
pa.raw_holdings_payload&.dig("holdings")&.size || 0
end
end
end

View File

@@ -0,0 +1,5 @@
class RenameRawInvestmentsPayloadToRawHoldingsPayload < ActiveRecord::Migration[7.2]
def change
rename_column :plaid_accounts, :raw_investments_payload, :raw_holdings_payload
end
end

4
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_23_000000) do
ActiveRecord::Schema[7.2].define(version: 2026_01_23_214127) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -943,7 +943,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_01_23_000000) do
t.datetime "updated_at", null: false
t.jsonb "raw_payload", default: {}
t.jsonb "raw_transactions_payload", default: {}
t.jsonb "raw_investments_payload", default: {}
t.jsonb "raw_holdings_payload", default: {}
t.jsonb "raw_liabilities_payload", default: {}
t.index ["plaid_id"], name: "index_plaid_accounts_on_plaid_id", unique: true
t.index ["plaid_item_id"], name: "index_plaid_accounts_on_plaid_item_id"

View File

@@ -52,7 +52,7 @@ namespace :security do
results[:enable_banking_items] = backfill_model(EnableBankingItem, %i[client_certificate session_id raw_payload raw_institution_payload], batch_size, dry_run)
# Provider accounts
results[:plaid_accounts] = backfill_model(PlaidAccount, %i[raw_payload raw_transactions_payload raw_investments_payload raw_liabilities_payload], batch_size, dry_run)
results[:plaid_accounts] = backfill_model(PlaidAccount, %i[raw_payload raw_transactions_payload raw_holdings_payload raw_liabilities_payload], batch_size, dry_run)
results[:simplefin_accounts] = backfill_model(SimplefinAccount, %i[raw_payload raw_transactions_payload raw_holdings_payload], batch_size, dry_run)
results[:lunchflow_accounts] = backfill_model(LunchflowAccount, %i[raw_payload raw_transactions_payload], batch_size, dry_run)
results[:enable_banking_accounts] = backfill_model(EnableBankingAccount, %i[raw_payload raw_transactions_payload], batch_size, dry_run)

View File

@@ -38,7 +38,7 @@ class PlaidAccount::ImporterTest < ActiveSupport::TestCase
@plaid_account.expects(:upsert_plaid_snapshot!).with(account_data)
@plaid_account.expects(:upsert_plaid_transactions_snapshot!).with(transactions_data)
@plaid_account.expects(:upsert_plaid_investments_snapshot!).with(investments_data)
@plaid_account.expects(:upsert_plaid_holdings_snapshot!).with(investments_data)
@plaid_account.expects(:upsert_plaid_liabilities_snapshot!).with(liabilities_data)
PlaidAccount::Importer.new(@plaid_account, account_snapshot: @mock_account_snapshot).import

View File

@@ -67,7 +67,7 @@ class PlaidAccount::Investments::BalanceCalculatorTest < ActiveSupport::TestCase
]
}
@plaid_account.update!(raw_investments_payload: test_investments)
@plaid_account.update!(raw_holdings_payload: test_investments)
security_resolver = PlaidAccount::Investments::SecurityResolver.new(@plaid_account)
balance_calculator = PlaidAccount::Investments::BalanceCalculator.new(@plaid_account, security_resolver: security_resolver)

View File

@@ -27,7 +27,7 @@ class PlaidAccount::Investments::HoldingsProcessorTest < ActiveSupport::TestCase
transactions: [] # not relevant for test
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve)
.with(plaid_security_id: "123")
@@ -125,7 +125,7 @@ class PlaidAccount::Investments::HoldingsProcessorTest < ActiveSupport::TestCase
transactions: []
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
# Mock security resolver for all three securities
@security_resolver.expects(:resolve)
@@ -175,7 +175,7 @@ class PlaidAccount::Investments::HoldingsProcessorTest < ActiveSupport::TestCase
transactions: []
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
# First security fails to resolve
@security_resolver.expects(:resolve)
@@ -213,7 +213,7 @@ class PlaidAccount::Investments::HoldingsProcessorTest < ActiveSupport::TestCase
transactions: []
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve)
.with(plaid_security_id: "string_values")
@@ -264,7 +264,7 @@ class PlaidAccount::Investments::HoldingsProcessorTest < ActiveSupport::TestCase
transactions: []
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve)
.with(plaid_security_id: "missing_quantity")
@@ -310,7 +310,7 @@ class PlaidAccount::Investments::HoldingsProcessorTest < ActiveSupport::TestCase
transactions: []
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve)
.with(plaid_security_id: "no_currency")

View File

@@ -11,7 +11,7 @@ class PlaidAccount::Investments::SecurityResolverTest < ActiveSupport::TestCase
missing_id = "missing_security_id"
# Ensure there are *no* securities that reference the missing ID
@plaid_account.update!(raw_investments_payload: {
@plaid_account.update!(raw_holdings_payload: {
securities: [
{
"security_id" => "some_other_id",
@@ -35,7 +35,7 @@ class PlaidAccount::Investments::SecurityResolverTest < ActiveSupport::TestCase
test "identifies brokerage cash plaid securities" do
brokerage_cash_id = "brokerage_cash_security_id"
@plaid_account.update!(raw_investments_payload: {
@plaid_account.update!(raw_holdings_payload: {
securities: [
{
"security_id" => brokerage_cash_id,
@@ -58,7 +58,7 @@ class PlaidAccount::Investments::SecurityResolverTest < ActiveSupport::TestCase
test "identifies cash equivalent plaid securities" do
mmf_security_id = "money_market_security_id"
@plaid_account.update!(raw_investments_payload: {
@plaid_account.update!(raw_holdings_payload: {
securities: [
{
"security_id" => mmf_security_id,
@@ -87,7 +87,7 @@ class PlaidAccount::Investments::SecurityResolverTest < ActiveSupport::TestCase
test "resolves normal plaid securities" do
security_id = "regular_security_id"
@plaid_account.update!(raw_investments_payload: {
@plaid_account.update!(raw_holdings_payload: {
securities: [
{
"security_id" => security_id,

View File

@@ -23,7 +23,7 @@ class PlaidAccount::Investments::TransactionsProcessorTest < ActiveSupport::Test
]
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.stubs(:resolve).returns(OpenStruct.new(
security: securities(:aapl)
@@ -58,7 +58,7 @@ class PlaidAccount::Investments::TransactionsProcessorTest < ActiveSupport::Test
]
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve).never # Cash transactions don't have a security
@@ -91,7 +91,7 @@ class PlaidAccount::Investments::TransactionsProcessorTest < ActiveSupport::Test
]
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve).never # Cash transactions don't have a security
@@ -127,7 +127,7 @@ class PlaidAccount::Investments::TransactionsProcessorTest < ActiveSupport::Test
]
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve).returns(OpenStruct.new(
security: securities(:aapl)
@@ -163,7 +163,7 @@ class PlaidAccount::Investments::TransactionsProcessorTest < ActiveSupport::Test
]
}
@plaid_account.update!(raw_investments_payload: test_investments_payload)
@plaid_account.update!(raw_holdings_payload: test_investments_payload)
@security_resolver.expects(:resolve).never