diff --git a/apps/client/pages/_app.tsx b/apps/client/pages/_app.tsx index d1fb65c9..b5623f02 100644 --- a/apps/client/pages/_app.tsx +++ b/apps/client/pages/_app.tsx @@ -33,16 +33,16 @@ Sentry.init({ // Providers and components only relevant to a logged-in user const WithAuth = function ({ children }: PropsWithChildren) { - const { data: session } = useSession() + const { data: session, status } = useSession() const router = useRouter() useEffect(() => { - if (!session) { + if (!session && status === 'unauthenticated') { router.push('/login') } - }, [session, router]) + }, [session, status, router]) - if (session) { + if (session && status === 'authenticated') { return ( diff --git a/apps/server/src/app/__tests__/account.integration.spec.ts b/apps/server/src/app/__tests__/account.integration.spec.ts index d8ebb18e..83ee4a04 100644 --- a/apps/server/src/app/__tests__/account.integration.spec.ts +++ b/apps/server/src/app/__tests__/account.integration.spec.ts @@ -15,17 +15,11 @@ import { startServer, stopServer } from './utils/server' import { getAxiosClient } from './utils/axios' import { resetUser } from './utils/user' import { createTestInvestmentAccount } from './utils/account' -import { default as _plaid } from '../lib/plaid' -jest.mock('../middleware/validate-plaid-jwt.ts') jest.mock('bull') -jest.mock('plaid') const prisma = new PrismaClient() -// For TypeScript support -const plaid = jest.mocked(_plaid) // eslint-disable-line - const authId = '__TEST_USER_ID__' let axios: AxiosInstance let user: User @@ -107,10 +101,6 @@ describe('/v1/accounts API', () => { mask: null, isActive: true, syncStatus: 'IDLE', - plaidType: null, - plaidSubtype: null, - plaidAccountId: null, - plaidLiability: null, currencyCode: 'USD', currentBalance: new Decimal(21_000), availableBalance: null, diff --git a/apps/server/src/app/__tests__/balance-sync.integration.spec.ts b/apps/server/src/app/__tests__/balance-sync.integration.spec.ts index 2dcbcb8a..1acbbb1c 100644 --- a/apps/server/src/app/__tests__/balance-sync.integration.spec.ts +++ b/apps/server/src/app/__tests__/balance-sync.integration.spec.ts @@ -1,4 +1,4 @@ -import type { User } from '@prisma/client' +import { InvestmentTransactionCategory, type User } from '@prisma/client' import { PrismaClient } from '@prisma/client' import { createLogger, transports } from 'winston' import { DateTime } from 'luxon' @@ -130,7 +130,7 @@ describe('balance sync strategies', () => { amount: 100, quantity: 10, price: 10, - plaidType: 'buy', + category: InvestmentTransactionCategory.buy, }, { date: DateTime.fromISO('2023-02-04').toJSDate(), @@ -139,7 +139,7 @@ describe('balance sync strategies', () => { amount: -50, quantity: 5, price: 10, - plaidType: 'sell', + category: InvestmentTransactionCategory.sell, }, { date: DateTime.fromISO('2023-02-04').toJSDate(), @@ -147,6 +147,7 @@ describe('balance sync strategies', () => { amount: 50, quantity: 50, price: 1, + category: InvestmentTransactionCategory.other, }, ], }, diff --git a/apps/server/src/app/__tests__/utils/account.ts b/apps/server/src/app/__tests__/utils/account.ts index 47e26cc3..088b2da8 100644 --- a/apps/server/src/app/__tests__/utils/account.ts +++ b/apps/server/src/app/__tests__/utils/account.ts @@ -35,7 +35,7 @@ export async function createTestInvestmentAccount( join(__dirname, `../test-data/${portfolio}/holdings.csv`) ) - const [_deleted, ...securities] = await prisma.$transaction([ + const [, ...securities] = await prisma.$transaction([ prisma.security.deleteMany({ where: { symbol: { @@ -99,7 +99,21 @@ export async function createTestInvestmentAccount( (s) => s.date === it.date && s.ticker === it.ticker )?.price - const isCashFlow = it.type === 'DEPOSIT' || it.type === 'WITHDRAW' + function getTransactionCategory(type: string) { + switch (type) { + case 'BUY': + return 'buy' + case 'SELL': + return 'sell' + case 'DIVIDEND': + return 'dividend' + case 'DEPOSIT': + case 'WITHDRAW': + return 'transfer' + default: + return undefined + } + } return { securityId: securities.find((s) => it.ticker === s.symbol)?.id, @@ -108,26 +122,7 @@ export async function createTestInvestmentAccount( amount: price ? new Prisma.Decimal(price).times(it.qty) : it.qty, quantity: price ? it.qty : 0, price: price ?? 0, - plaidType: - isCashFlow || it.type === 'DIVIDEND' - ? 'cash' - : it.type === 'BUY' - ? 'buy' - : it.type === 'SELL' - ? 'sell' - : undefined, - plaidSubtype: - it.type === 'DEPOSIT' - ? 'deposit' - : it.type === 'WITHDRAW' - ? 'withdrawal' - : it.type === 'DIVIDEND' - ? 'dividend' - : it.type === 'BUY' - ? 'buy' - : it.type === 'SELL' - ? 'sell' - : undefined, + category: getTransactionCategory(it.type), } }), }, diff --git a/libs/server/features/src/account-balance/investment-transaction-balance-sync.strategy.ts b/libs/server/features/src/account-balance/investment-transaction-balance-sync.strategy.ts index 21f0d914..83cc4954 100644 --- a/libs/server/features/src/account-balance/investment-transaction-balance-sync.strategy.ts +++ b/libs/server/features/src/account-balance/investment-transaction-balance-sync.strategy.ts @@ -70,8 +70,7 @@ export class InvestmentTransactionBalanceSyncStrategy extends BalanceSyncStrateg it.account_id = ${pAccountId} AND it.date BETWEEN ${pStart} AND now() AND ( -- filter for transactions that modify a position - it.plaid_type IN ('buy', 'sell', 'transfer') - OR it.finicity_transaction_id IS NOT NULL + it.category IN ('buy', 'sell', 'transfer') ) GROUP BY 1, 2 diff --git a/libs/server/features/src/account/account-query.service.ts b/libs/server/features/src/account/account-query.service.ts index fa206904..05e7a511 100644 --- a/libs/server/features/src/account/account-query.service.ts +++ b/libs/server/features/src/account/account-query.service.ts @@ -243,10 +243,7 @@ export class AccountQueryService implements IAccountQueryService { AND it.date BETWEEN sd.start_date AND ${pEnd} -- filter for investment_transactions that represent external flows AND ( - (it.plaid_type = 'cash' AND it.plaid_subtype IN ('contribution', 'deposit', 'withdrawal')) - OR (it.plaid_type = 'transfer' AND it.plaid_subtype IN ('transfer')) - OR (it.plaid_type = 'buy' AND it.plaid_subtype IN ('contribution')) - OR (it.finicity_transaction_id IS NOT NULL AND it.finicity_investment_transaction_type IN ('contribution', 'deposit', 'transfer')) + it.category = 'transfer' ) GROUP BY 1, 2 diff --git a/libs/server/features/src/account/insight.service.ts b/libs/server/features/src/account/insight.service.ts index 4c736ab7..f1df4568 100644 --- a/libs/server/features/src/account/insight.service.ts +++ b/libs/server/features/src/account/insight.service.ts @@ -747,10 +747,7 @@ export class InsightService implements IInsightService { WHERE it.account_id = ${accountId} AND ( - (it.plaid_type = 'cash' AND it.plaid_subtype IN ('contribution', 'deposit', 'withdrawal')) - OR (it.plaid_type = 'transfer' AND it.plaid_subtype IN ('transfer', 'send', 'request')) - OR (it.plaid_type = 'buy' AND it.plaid_subtype IN ('contribution')) - OR (it.finicity_transaction_id IS NOT NULL AND it.finicity_investment_transaction_type IN ('contribution', 'deposit', 'transfer')) + it.category = 'transfer' ) -- Exclude any contributions made prior to the start date since balances will be 0 AND (a.start_date is NULL OR it.date >= a.start_date) diff --git a/prisma/migrations/20240119180135_remove_gen_statement_investment_transaction/migration.sql b/prisma/migrations/20240119180135_remove_gen_statement_investment_transaction/migration.sql new file mode 100644 index 00000000..2add40ab --- /dev/null +++ b/prisma/migrations/20240119180135_remove_gen_statement_investment_transaction/migration.sql @@ -0,0 +1,7 @@ +ALTER TABLE + "investment_transaction" DROP COLUMN "category"; + +ALTER TABLE + "investment_transaction" +ADD + COLUMN "category" "InvestmentTransactionCategory" DEFAULT 'other' :: "InvestmentTransactionCategory" NOT NULL; diff --git a/prisma/migrations/20240119213343_replace_return_calculations_function/migration.sql b/prisma/migrations/20240119213343_replace_return_calculations_function/migration.sql new file mode 100644 index 00000000..f47d6fee --- /dev/null +++ b/prisma/migrations/20240119213343_replace_return_calculations_function/migration.sql @@ -0,0 +1,61 @@ +CREATE OR REPLACE FUNCTION calculate_return_dietz(p_account_id account.id%type, p_start date, p_end date, out percentage numeric, out amount numeric) AS $$ + DECLARE + v_start date := GREATEST(p_start, (SELECT MIN(date) FROM account_balance WHERE account_id = p_account_id)); + v_end date := p_end; + v_days int := v_end - v_start; + BEGIN + SELECT + ROUND((b1.balance - b0.balance - flows.net) / NULLIF(b0.balance + flows.weighted, 0), 4) AS "percentage", + b1.balance - b0.balance - flows.net AS "amount" + INTO + percentage, amount + FROM + account a + LEFT JOIN LATERAL ( + SELECT + COALESCE(SUM(-fw.flow), 0) AS "net", + COALESCE(SUM(-fw.flow * fw.weight), 0) AS "weighted" + FROM ( + SELECT + SUM(it.amount) AS flow, + (v_days - (it.date - v_start))::numeric / v_days AS weight + FROM + investment_transaction it + WHERE + it.account_id = a.id + AND it.date BETWEEN v_start AND v_end + -- filter for investment_transactions that represent external flows + AND ( + it.category = 'transfer' + ) + GROUP BY + it.date + ) fw + ) flows ON TRUE + LEFT JOIN LATERAL ( + SELECT + ab.balance AS "balance" + FROM + account_balance ab + WHERE + ab.account_id = a.id AND ab.date <= v_start + ORDER BY + ab.date DESC + LIMIT 1 + ) b0 ON TRUE + LEFT JOIN LATERAL ( + SELECT + COALESCE(ab.balance, a.current_balance) AS "balance" + FROM + account_balance ab + WHERE + ab.account_id = a.id AND ab.date <= v_end + ORDER BY + ab.date DESC + LIMIT 1 + ) b1 ON TRUE + WHERE + a.id = p_account_id; + END; +$$ LANGUAGE plpgsql STABLE; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d358089e..9b01c4ae 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -250,7 +250,7 @@ model InvestmentTransaction { currencyCode String @default("USD") @map("currency_code") // Derived from provider types - category InvestmentTransactionCategory @default(dbgenerated("\nCASE\n WHEN (plaid_type = 'buy'::text) THEN 'buy'::\"InvestmentTransactionCategory\"\n WHEN (plaid_type = 'sell'::text) THEN 'sell'::\"InvestmentTransactionCategory\"\n WHEN (plaid_subtype = ANY (ARRAY['dividend'::text, 'qualified dividend'::text, 'non-qualified dividend'::text])) THEN 'dividend'::\"InvestmentTransactionCategory\"\n WHEN (plaid_subtype = ANY (ARRAY['non-resident tax'::text, 'tax'::text, 'tax withheld'::text])) THEN 'tax'::\"InvestmentTransactionCategory\"\n WHEN ((plaid_type = 'fee'::text) OR (plaid_subtype = ANY (ARRAY['account fee'::text, 'legal fee'::text, 'management fee'::text, 'margin expense'::text, 'transfer fee'::text, 'trust fee'::text]))) THEN 'fee'::\"InvestmentTransactionCategory\"\n WHEN (plaid_type = 'cash'::text) THEN 'transfer'::\"InvestmentTransactionCategory\"\n WHEN (plaid_type = 'cancel'::text) THEN 'cancel'::\"InvestmentTransactionCategory\"\n WHEN (finicity_investment_transaction_type = ANY (ARRAY['purchased'::text, 'purchaseToClose'::text, 'purchaseToCover'::text, 'dividendReinvest'::text, 'reinvestOfIncome'::text])) THEN 'buy'::\"InvestmentTransactionCategory\"\n WHEN (finicity_investment_transaction_type = ANY (ARRAY['sold'::text, 'soldToClose'::text, 'soldToOpen'::text])) THEN 'sell'::\"InvestmentTransactionCategory\"\n WHEN (finicity_investment_transaction_type = 'dividend'::text) THEN 'dividend'::\"InvestmentTransactionCategory\"\n WHEN (finicity_investment_transaction_type = 'tax'::text) THEN 'tax'::\"InvestmentTransactionCategory\"\n WHEN (finicity_investment_transaction_type = 'fee'::text) THEN 'fee'::\"InvestmentTransactionCategory\"\n WHEN (finicity_investment_transaction_type = ANY (ARRAY['transfer'::text, 'contribution'::text, 'deposit'::text, 'income'::text, 'interest'::text])) THEN 'transfer'::\"InvestmentTransactionCategory\"\n WHEN (finicity_investment_transaction_type = 'cancel'::text) THEN 'cancel'::\"InvestmentTransactionCategory\"\n ELSE 'other'::\"InvestmentTransactionCategory\"\nEND")) + category InvestmentTransactionCategory @default(other) // plaid data plaidInvestmentTransactionId String? @unique @map("plaid_investment_transaction_id")