feat: add portfolio overview
This commit is contained in:
@@ -1,10 +1,25 @@
|
||||
import crypto from 'crypto';
|
||||
import type { PoolClient } from 'pg';
|
||||
import { pool } from '../db/pool';
|
||||
import type { ImportPortfolioResponse, PortfolioFile, PortfolioTrade } from '@family-budget/shared';
|
||||
import { maskAccountNumber } from '../utils';
|
||||
import type { ImportPortfolioResponse, PortfolioFile, PortfolioOverviewResponse, PortfolioTrade } from '@family-budget/shared';
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
type PortfolioOverviewRow = {
|
||||
account_id: number | string;
|
||||
alias: string | null;
|
||||
bank: string;
|
||||
account_number: string;
|
||||
report_period_to: string;
|
||||
total_valuation: string | number | null;
|
||||
instrument: string | null;
|
||||
isin: string | null;
|
||||
quantity: string | number | null;
|
||||
price: string | number | null;
|
||||
valuation: string | number | null;
|
||||
};
|
||||
|
||||
function decimalValue(value: unknown, field: string, required = false): string | null {
|
||||
if (value == null || value === '') {
|
||||
if (required) throw new Error(`${field} is required`);
|
||||
@@ -103,3 +118,53 @@ export async function importPortfolio(body: unknown, db: Pick<typeof pool, 'conn
|
||||
if (ownsTransaction) client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export function toPortfolioOverview(rows: PortfolioOverviewRow[]): PortfolioOverviewResponse {
|
||||
const accounts = new Map<number, PortfolioOverviewResponse['accounts'][number]>();
|
||||
for (const row of rows) {
|
||||
const accountId = Number(row.account_id);
|
||||
let account = accounts.get(accountId);
|
||||
if (!account) {
|
||||
account = {
|
||||
accountId,
|
||||
accountName: row.alias || `${row.bank} · ${maskAccountNumber(row.account_number)}`,
|
||||
reportPeriodTo: row.report_period_to,
|
||||
totalValuation: row.total_valuation === null ? null : String(row.total_valuation),
|
||||
positions: [],
|
||||
};
|
||||
accounts.set(accountId, account);
|
||||
}
|
||||
if (row.instrument !== null) {
|
||||
const valuation = row.valuation === null ? null : String(row.valuation);
|
||||
account.positions.push({
|
||||
instrument: row.instrument,
|
||||
isin: row.isin,
|
||||
quantity: String(row.quantity),
|
||||
price: row.price === null ? null : String(row.price),
|
||||
valuation,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { accounts: [...accounts.values()] };
|
||||
}
|
||||
|
||||
export async function getPortfolioOverview(): Promise<PortfolioOverviewResponse> {
|
||||
const { rows } = await pool.query<PortfolioOverviewRow>(
|
||||
`SELECT a.id AS account_id, a.alias, a.bank, a.account_number,
|
||||
r.report_period_to,
|
||||
SUM(p.valuation) OVER (PARTITION BY a.id) AS total_valuation,
|
||||
p.instrument, p.isin, p.quantity, p.price, p.valuation
|
||||
FROM accounts a
|
||||
JOIN LATERAL (
|
||||
SELECT id, report_period_to, reported_at, imported_at
|
||||
FROM portfolio_reports
|
||||
WHERE account_id = a.id
|
||||
ORDER BY report_period_to DESC, imported_at DESC, id DESC
|
||||
LIMIT 1
|
||||
) r ON TRUE
|
||||
LEFT JOIN portfolio_positions p ON p.report_id = r.id
|
||||
WHERE a.account_type IN ('brokerage', 'iis')
|
||||
ORDER BY a.id, p.valuation DESC NULLS LAST, p.id`,
|
||||
);
|
||||
return toPortfolioOverview(rows);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user