Files
backend/src/modules/investments/operations/infrastructure/brokerage-operation.mapper.ts
T
ksv741 2d14081d27
Backend security / security (push) Successful in 8s
Backend security / security (pull_request) Canceled after 0s
Backend verify / verify (push) Canceled after 9s
Backend verify / verify (pull_request) Canceled after 4s
Conventional commits / commits (pull_request) Successful in 31s
refactor: enforce enum machine codes
Use owner enums for error and T-Bank protocol values with ESLint regression checks.

Preserve open wire fallbacks and keep protocol imports independent of DI and Prisma.

Verified npm ci, verify, 226 unit tests, 262 isolated e2e tests and unchanged OpenAPI.
2026-09-28 13:18:06 +03:00

96 lines
3.4 KiB
TypeScript

import { formatDecimal } from '../../../../common/decimal';
import { MILLISECONDS_PER_SECOND } from '../../../../common/time';
import { decimal } from '../../../../integrations/tbank/decimal';
import { TbankOperationType } from '../../../../integrations/tbank/tbank-protocol';
import { TbankMoney, TbankOperationItem } from '../../../../integrations/tbank/tbank.api';
import {
BrokerageOperationDto,
OperationCategory,
} from '../presentation/brokerage-operation.dto';
const MISSING_TIMESTAMP_COMPONENT = 0;
const NANOSECONDS_PER_MILLISECOND = 1_000_000;
const types: Record<string, OperationCategory> = {
[TbankOperationType.Buy]: 'trade',
[TbankOperationType.BuyCard]: 'trade',
[TbankOperationType.Sell]: 'trade',
[TbankOperationType.SellCard]: 'trade',
[TbankOperationType.BuyMargin]: 'trade',
[TbankOperationType.SellMargin]: 'trade',
[TbankOperationType.Coupon]: 'income',
[TbankOperationType.Dividend]: 'income',
[TbankOperationType.DividendTransfer]: 'income',
[TbankOperationType.DivExt]: 'income',
[TbankOperationType.TransIisBs]: 'transfer',
[TbankOperationType.TransBsBs]: 'transfer',
[TbankOperationType.BondTax]: 'tax',
[TbankOperationType.Tax]: 'tax',
[TbankOperationType.DividendTax]: 'tax',
[TbankOperationType.ServiceFee]: 'fee',
[TbankOperationType.MarginFee]: 'fee',
[TbankOperationType.BrokerFee]: 'fee',
[TbankOperationType.SuccessFee]: 'fee',
[TbankOperationType.Input]: 'transfer',
[TbankOperationType.Output]: 'transfer',
[TbankOperationType.InputSecurities]: 'transfer',
[TbankOperationType.OutputSecurities]: 'transfer',
};
export function operationCategory(type: string | undefined): OperationCategory {
return types[type ?? ''] ?? 'other';
}
export function mapBrokerageOperation(
operation: TbankOperationItem,
occurredAt = operationTimestamp(operation.date),
): BrokerageOperationDto {
return {
id: operation.id ?? operation.cursor ?? '',
occurredAt: occurredAt.toISOString(),
type: operation.type ?? TbankOperationType.Unspecified,
category: operationCategory(operation.type),
description: operation.description?.trim() || operation.name?.trim() || '',
figi: text(operation.figi),
instrumentUid: text(operation.instrumentUid),
ticker: text(operation.ticker),
instrumentType: text(operation.instrumentType),
logoName: null,
payment: money(operation.payment),
quantity:
(operationCategory(operation.type) === 'trade'
? operation.quantityDone
: operation.quantity) === undefined
? null
: String(
operationCategory(operation.type) === 'trade'
? operation.quantityDone
: operation.quantity,
),
price: money(operation.price),
accruedInterest: money(operation.accruedInt),
commission: money(operation.commission),
};
}
function text(value: string | undefined): null | string {
return value?.trim() || null;
}
function money(value: TbankMoney | undefined) {
const currency = value?.currency?.trim().toUpperCase();
return currency && value
? { currency, value: formatDecimal(decimal(value)) }
: null;
}
export function operationTimestamp(
value: undefined | { nanos?: number; seconds?: number | string },
) {
return new Date(
Number(value?.seconds ?? MISSING_TIMESTAMP_COMPONENT) * MILLISECONDS_PER_SECOND +
Math.floor((value?.nanos ?? MISSING_TIMESTAMP_COMPONENT) / NANOSECONDS_PER_MILLISECOND),
);
}