Files
sure/test/models/lunchflow_account/investments/holdings_processor_test.rb
soky srm ca4fb7995c Implement holdings for lunch flow (#590)
* Implement holdings for lunch flow

* Implement holdings function call
2026-01-09 13:14:14 +01:00

266 lines
7.1 KiB
Ruby

require "test_helper"
class LunchflowAccount::Investments::HoldingsProcessorTest < ActiveSupport::TestCase
setup do
@lunchflow_account = lunchflow_accounts(:investment_account)
@account = accounts(:investment)
# Create account_provider to link lunchflow_account to account
@account_provider = AccountProvider.create!(
account: @account,
provider: @lunchflow_account
)
# Reload to ensure associations are loaded
@lunchflow_account.reload
end
test "creates holding records from Lunchflow holdings snapshot" do
# Verify setup is correct
assert_not_nil @lunchflow_account.current_account, "Account should be linked"
assert_equal "Investment", @lunchflow_account.current_account.accountable_type
# Use unique dates to avoid conflicts with existing fixture holdings
test_holdings_payload = [
{
"security" => {
"name" => "iShares Inc MSCI Brazil",
"currency" => "USD",
"tickerSymbol" => "NEWTEST1",
"figi" => nil,
"cusp" => nil,
"isin" => nil
},
"quantity" => 5,
"price" => 42.15,
"value" => 210.75,
"costBasis" => 100.0,
"currency" => "USD",
"raw" => {
"quiltt" => {
"id" => "hld_test_123"
}
}
},
{
"security" => {
"name" => "Test Security",
"currency" => "USD",
"tickerSymbol" => "NEWTEST2",
"figi" => nil,
"cusp" => nil,
"isin" => nil
},
"quantity" => 10,
"price" => 150.0,
"value" => 1500.0,
"costBasis" => 1200.0,
"currency" => "USD",
"raw" => {
"quiltt" => {
"id" => "hld_test_456"
}
}
}
]
@lunchflow_account.update!(raw_holdings_payload: test_holdings_payload)
processor = LunchflowAccount::Investments::HoldingsProcessor.new(@lunchflow_account)
assert_difference "Holding.count", 2 do
processor.process
end
holdings = Holding.where(account: @account).where.not(external_id: nil).order(:created_at)
assert_equal 2, holdings.count
assert_equal "USD", holdings.first.currency
assert_equal "lunchflow_hld_test_123", holdings.first.external_id
end
test "skips processing for non-investment accounts" do
# Create a depository account
depository_account = accounts(:depository)
depository_lunchflow_account = LunchflowAccount.create!(
lunchflow_item: lunchflow_items(:one),
account_id: "lf_depository",
name: "Depository",
currency: "USD"
)
AccountProvider.create!(
account: depository_account,
provider: depository_lunchflow_account
)
depository_lunchflow_account.reload
test_holdings_payload = [
{
"security" => { "name" => "Test", "tickerSymbol" => "TEST", "currency" => "USD" },
"quantity" => 10,
"price" => 100.0,
"value" => 1000.0,
"costBasis" => nil,
"currency" => "USD",
"raw" => { "quiltt" => { "id" => "hld_skip" } }
}
]
depository_lunchflow_account.update!(raw_holdings_payload: test_holdings_payload)
processor = LunchflowAccount::Investments::HoldingsProcessor.new(depository_lunchflow_account)
assert_no_difference "Holding.count" do
processor.process
end
end
test "creates synthetic ticker when tickerSymbol is missing" do
test_holdings_payload = [
{
"security" => {
"name" => "Custom 401k Fund",
"currency" => "USD",
"tickerSymbol" => nil,
"figi" => nil,
"cusp" => nil,
"isin" => nil
},
"quantity" => 100,
"price" => 50.0,
"value" => 5000.0,
"costBasis" => 4500.0,
"currency" => "USD",
"raw" => {
"quiltt" => {
"id" => "hld_custom_123"
}
}
}
]
@lunchflow_account.update!(raw_holdings_payload: test_holdings_payload)
processor = LunchflowAccount::Investments::HoldingsProcessor.new(@lunchflow_account)
assert_difference "Holding.count", 1 do
processor.process
end
holding = Holding.where(account: @account).where.not(external_id: nil).last
assert_equal "lunchflow_hld_custom_123", holding.external_id
assert_equal 100, holding.qty
assert_equal 5000.0, holding.amount
end
test "skips zero value holdings" do
test_holdings_payload = [
{
"security" => {
"name" => "Zero Position",
"currency" => "USD",
"tickerSymbol" => "ZERO",
"figi" => nil,
"cusp" => nil,
"isin" => nil
},
"quantity" => 0,
"price" => 0,
"value" => 0,
"costBasis" => nil,
"currency" => "USD",
"raw" => {
"quiltt" => {
"id" => "hld_zero"
}
}
}
]
@lunchflow_account.update!(raw_holdings_payload: test_holdings_payload)
Security::Resolver.any_instance.stubs(:resolve).returns(securities(:aapl))
processor = LunchflowAccount::Investments::HoldingsProcessor.new(@lunchflow_account)
assert_no_difference "Holding.count" do
processor.process
end
end
test "handles empty holdings payload gracefully" do
@lunchflow_account.update!(raw_holdings_payload: [])
processor = LunchflowAccount::Investments::HoldingsProcessor.new(@lunchflow_account)
assert_no_difference "Holding.count" do
processor.process
end
end
test "handles nil holdings payload gracefully" do
@lunchflow_account.update!(raw_holdings_payload: nil)
processor = LunchflowAccount::Investments::HoldingsProcessor.new(@lunchflow_account)
assert_no_difference "Holding.count" do
processor.process
end
end
test "continues processing other holdings when one fails" do
test_holdings_payload = [
{
"security" => {
"name" => "Good Holding",
"currency" => "USD",
"tickerSymbol" => "GOODTEST",
"figi" => nil,
"cusp" => nil,
"isin" => nil
},
"quantity" => 10,
"price" => 100.0,
"value" => 1000.0,
"costBasis" => nil,
"currency" => "USD",
"raw" => {
"quiltt" => {
"id" => "hld_good"
}
}
},
{
"security" => {
"name" => nil, # This will cause it to skip (no name, no symbol)
"currency" => "USD",
"tickerSymbol" => nil,
"figi" => nil,
"cusp" => nil,
"isin" => nil
},
"quantity" => 5,
"price" => 50.0,
"value" => 250.0,
"costBasis" => nil,
"currency" => "USD",
"raw" => {
"quiltt" => {
"id" => "hld_bad"
}
}
}
]
@lunchflow_account.update!(raw_holdings_payload: test_holdings_payload)
processor = LunchflowAccount::Investments::HoldingsProcessor.new(@lunchflow_account)
# Should create 1 holding (the good one)
assert_difference "Holding.count", 1 do
processor.process
end
end
end