fix: count confirmed broker withdrawals by account reference
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [Backend 0.15.6] - 2026-08-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- Count broker withdrawals from explicit positive transfers to family accounts, linked by the broker account number in the operation description.
|
||||
|
||||
## [Backend 0.15.5] - 2026-08-28
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@family-budget/backend",
|
||||
"version": "0.15.5",
|
||||
"version": "0.15.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/app.ts",
|
||||
|
||||
@@ -36,6 +36,10 @@ type PerformancePositionRow = { account_id: number | string; quantity: string |
|
||||
|
||||
export type PortfolioCashKind = 'income' | 'contribution' | 'withdrawal' | 'other';
|
||||
|
||||
export function extractBrokerWithdrawalAccount(description: string): string | null {
|
||||
return description.toLowerCase().replace(/ё/g, 'е').match(/вывод денежных средств с брокерского счета\s+([0-9]+)/)?.[1] ?? null;
|
||||
}
|
||||
|
||||
export function classifyPortfolioCash(amount: number, description: string, matchedExternalTransfer = false): PortfolioCashKind {
|
||||
const text = description.toLowerCase().replace(/ё/g, 'е');
|
||||
if (amount > 0 && /дивиденд|купон|процент/.test(text)) return 'income';
|
||||
@@ -361,15 +365,27 @@ export async function getPortfolioPerformance(): Promise<PortfolioPerformanceRes
|
||||
]);
|
||||
const tradeMap = calculatePortfolioTradeResults(tradeResult.rows, positionResult.rows);
|
||||
const accounts = new Map<number, PortfolioPerformanceResponse['accounts'][number]>();
|
||||
const brokerRows = cashResult.rows.filter((item) => ['brokerage', 'iis'].includes(item.account_type ?? ''));
|
||||
const matchedBrokerTransfers = matchExternalCashTransfers(cashResult.rows);
|
||||
for (const row of cashResult.rows.filter((item) => ['brokerage', 'iis'].includes(item.account_type ?? ''))) {
|
||||
const accountByReference = new Map(brokerRows.map((row) => [Number(row.account_id), row.account_number.replace(/\D/g, '')]));
|
||||
for (const row of brokerRows) {
|
||||
const accountId = Number(row.account_id);
|
||||
const account = accounts.get(accountId) ?? { accountId, accountName: row.alias || `${row.bank} · ${maskAccountNumber(row.account_number)}`, contributions: 0, withdrawals: 0, income: 0, fees: 0, realizedResult: 0, unrealizedResult: null };
|
||||
const amount = Number(row.amount_signed);
|
||||
const kind = classifyPortfolioCash(amount, row.description, matchedBrokerTransfers.has(row.id));
|
||||
const kind = classifyPortfolioCash(amount, row.description, amount > 0 && matchedBrokerTransfers.has(row.id));
|
||||
if (kind === 'income') account.income += amount;
|
||||
else if (kind === 'contribution') account.contributions += amount;
|
||||
else if (kind === 'withdrawal') account.withdrawals += -amount;
|
||||
accounts.set(accountId, account);
|
||||
}
|
||||
for (const row of cashResult.rows.filter((item) => !['brokerage', 'iis'].includes(item.account_type ?? '') && Number(item.amount_signed) > 0)) {
|
||||
const reference = extractBrokerWithdrawalAccount(row.description);
|
||||
if (!reference) continue;
|
||||
const accountEntry = [...accountByReference.entries()].find(([, accountNumber]) => accountNumber.endsWith(reference) || reference.endsWith(accountNumber));
|
||||
if (!accountEntry) continue;
|
||||
const accountId = accountEntry[0];
|
||||
const source = brokerRows.find((item) => Number(item.account_id) === accountId);
|
||||
const account = accounts.get(accountId) ?? { accountId, accountName: source?.alias || `${source?.bank ?? 'Брокерский счёт'} · ${maskAccountNumber(source?.account_number ?? reference)}`, contributions: 0, withdrawals: 0, income: 0, fees: 0, realizedResult: 0, unrealizedResult: null };
|
||||
account.withdrawals += Number(row.amount_signed);
|
||||
accounts.set(accountId, account);
|
||||
}
|
||||
for (const [accountId, values] of tradeMap) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { calculatePortfolioTradeResults, classifyPortfolioCash, matchExternalCashTransfers, sumKnownPortfolioValuations, toPortfolioHistory, toPortfolioOverview } from './portfolio';
|
||||
import { calculatePortfolioTradeResults, classifyPortfolioCash, extractBrokerWithdrawalAccount, matchExternalCashTransfers, sumKnownPortfolioValuations, toPortfolioHistory, toPortfolioOverview } from './portfolio';
|
||||
|
||||
const result = toPortfolioOverview([
|
||||
{ account_id: '1', alias: 'ИИС', bank: 'ВТБ', account_number: '123456', report_period_to: '2026-08-26', total_valuation: '150.50', instrument: 'Облигация', isin: 'RU0000000001', quantity: '1', price: '100', valuation: '100' },
|
||||
@@ -34,6 +34,8 @@ assert.equal(classifyPortfolioCash(52000, 'Зачисление денежных
|
||||
assert.equal(classifyPortfolioCash(52000, 'Пополнение брокерского счёта'), 'contribution');
|
||||
assert.equal(classifyPortfolioCash(-129000, 'Вывод ДС под нерассчитанные сделки', true), 'withdrawal');
|
||||
assert.equal(classifyPortfolioCash(-129000, 'Сальдо расчетов по сделкам с ценными бумагами', true), 'withdrawal');
|
||||
assert.equal(extractBrokerWithdrawalAccount('Перевод между своими счетами. Вывод денежных средств с брокерского счета 30601 по распоряжению от 2026-05-12.'), '30601');
|
||||
assert.equal(extractBrokerWithdrawalAccount('Вывод денежных средств с брокерского счёта 30601'), '30601');
|
||||
assert.equal(sumKnownPortfolioValuations([100, null, 25]), 125);
|
||||
const matched = matchExternalCashTransfers([
|
||||
{ id: 1, account_id: 1, alias: null, bank: 'VTB', account_number: 'broker', account_type: 'brokerage', operation_at: '2026-05-12T00:00:00+03:00', amount_signed: '-5102321', description: 'Нестандартное описание операции' },
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"backend": {
|
||||
"name": "@family-budget/backend",
|
||||
"version": "0.15.5",
|
||||
"version": "0.15.6",
|
||||
"dependencies": {
|
||||
"@family-budget/shared": "*",
|
||||
"cookie-parser": "^1.4.7",
|
||||
|
||||
Reference in New Issue
Block a user