refactor: centralize provider projections

This commit is contained in:
2026-10-01 20:53:00 +03:00
parent 190eb90ffe
commit 7fe88a12c7
7 changed files with 179 additions and 154 deletions
@@ -1,16 +1,110 @@
import type { Money } from '../../../../common/money.types';
import type { PortfolioDto, PortfolioPositionDto } from '../presentation/portfolio.dto';
import {
add,
decimalFromString,
formatDecimal,
isZero,
multiply,
subtract,
} from '../../../../common/decimal';
import { decimal } from '../../../../integrations/tbank/decimal';
import {
metadataKey,
type TbankInstrument,
type TbankMoney,
TbankPortfolio,
TbankPositions,
} from '../../../../integrations/tbank/tbank.api';
import { portfolioStructure } from '../domain/portfolio-structure';
const types: Record<string, string> = {
share: 'STOCK',
bond: 'BOND',
etf: 'FUND',
currency: 'CURRENCY',
futures: 'FUTURE',
option: 'OPTION',
sp: 'STRUCTURED_NOTE',
dfa: 'DIGITAL_FINANCIAL_ASSET',
};
function mapPortfolio(
upstream: NonNullable<TbankPortfolio['positions']>,
cash: TbankMoney[],
metadata: ReadonlyMap<string, { data?: null | TbankInstrument }>,
lastSuccessfulAt: string,
): PortfolioDto {
const positions = upstream
.map((position) => mapPosition(position, metadata.get(metadataKey(position))?.data ?? null))
.filter((position): position is PortfolioPositionDto => position !== null);
const balances = new Map<string, ReturnType<typeof decimal>>();
for (const value of cash) {
const currency = value.currency?.trim().toUpperCase();
const amount = decimal(value);
if (!currency || isZero(amount)) continue;
balances.set(currency, add(balances.get(currency) ?? decimal({}), amount));
}
const cashBalances = [...balances].map(([currency, value]) => ({
currency,
value: formatDecimal(value),
}));
return {
lastSuccessfulAt,
isStale: false,
totalsByCurrency: totalsByCurrency(upstream, cash),
positions,
cashBalances,
structure: portfolioStructure(positions, cashBalances),
};
}
function mapPosition(
position: NonNullable<TbankPortfolio['positions']>[number],
metadata: null | TbankInstrument,
): null | PortfolioPositionDto {
const quantity = decimal(position.quantity ?? {});
if (isZero(quantity)) return null;
const figi = position.figi?.trim() || null;
const instrumentUid = position.instrumentUid?.trim() || null;
const currentPrice = money(position.currentPrice);
const averagePurchasePrice = money(position.averagePositionPrice);
const currentValue = currentPrice && {
currency: currentPrice.currency,
value: formatDecimal(multiply(quantity, decimalFromString(currentPrice.value))),
};
const absoluteReturn =
currentPrice && averagePurchasePrice && currentPrice.currency === averagePurchasePrice.currency
? {
currency: currentPrice.currency,
value: formatDecimal(multiply(
subtract(decimalFromString(currentPrice.value), decimalFromString(averagePurchasePrice.value)),
quantity,
)),
}
: null;
return {
figi,
instrumentUid,
displayName: metadata?.name?.trim() || metadata?.ticker?.trim() || figi || 'Unknown instrument',
type: types[position.instrumentType?.toLowerCase() ?? ''] ?? 'OTHER',
quantity: formatDecimal(quantity),
currentValue,
averagePurchasePrice,
absoluteReturn,
isin: metadata?.isin?.trim() || null,
ticker: metadata?.ticker?.trim() || null,
logoName: metadata?.brand?.logoName?.trim() || null,
};
}
function money(value: TbankMoney | undefined): Money | null {
const currency = value?.currency?.trim().toUpperCase();
return currency && value
? { currency, value: formatDecimal(decimal(value)) }
: null;
}
function totalsByCurrency(
positions: NonNullable<TbankPortfolio['positions']>,
@@ -44,4 +138,4 @@ function totalsByCurrency(
.map(([currency, value]) => ({ currency, value: formatDecimal(value) }));
}
export { totalsByCurrency };
export { mapPortfolio, totalsByCurrency };