refactor: organize backend by business responsibility
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
mapBrokerageOperation,
|
||||
operationCategory,
|
||||
} from './brokerage-operation.mapper';
|
||||
|
||||
describe('mapBrokerageOperation', () => {
|
||||
it.each([
|
||||
['OPERATION_TYPE_COUPON', 'income'],
|
||||
['OPERATION_TYPE_DIVIDEND', 'income'],
|
||||
['OPERATION_TYPE_TAX', 'tax'],
|
||||
['OPERATION_TYPE_BROKER_FEE', 'fee'],
|
||||
['NEW_TYPE', 'other'],
|
||||
])('maps %s to %s', (type, category) => {
|
||||
expect(operationCategory(type)).toBe(category);
|
||||
});
|
||||
|
||||
it('keeps a trade and its commission in one exact-money entry', () => {
|
||||
expect(
|
||||
mapBrokerageOperation({
|
||||
id: 'trade',
|
||||
date: { seconds: '1' },
|
||||
type: 'OPERATION_TYPE_BUY',
|
||||
payment: { currency: 'rub', units: '-1', nano: '-500000000' },
|
||||
commission: { currency: 'RUB', nano: '-5000000' },
|
||||
}),
|
||||
).toMatchObject({
|
||||
id: 'trade',
|
||||
category: 'trade',
|
||||
payment: { currency: 'RUB', value: '-1.5' },
|
||||
commission: { currency: 'RUB', value: '-0.005' },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not invent payment for an instrument-only movement', () => {
|
||||
expect(
|
||||
mapBrokerageOperation({
|
||||
id: 'transfer',
|
||||
date: { seconds: '1' },
|
||||
type: 'OPERATION_TYPE_INPUT_SECURITIES',
|
||||
quantity: '3',
|
||||
}),
|
||||
).toMatchObject({
|
||||
category: 'transfer',
|
||||
quantity: '3',
|
||||
payment: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('executed trade quantity', () => {
|
||||
it('uses the executed part, and does not invent execution quantity when absent', () => {
|
||||
expect(
|
||||
mapBrokerageOperation({
|
||||
type: 'OPERATION_TYPE_BUY',
|
||||
quantity: '100',
|
||||
quantityDone: '7',
|
||||
}).quantity,
|
||||
).toBe('7');
|
||||
expect(
|
||||
mapBrokerageOperation({ type: 'OPERATION_TYPE_BUY', quantity: '100' })
|
||||
.quantity,
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { formatDecimal } from '../../../../common/decimal';
|
||||
import {
|
||||
TbankMoney,
|
||||
TbankOperationItem,
|
||||
} from '../../../../integrations/tbank/tbank.api';
|
||||
import { decimal } from '../../../../integrations/tbank/decimal';
|
||||
import {
|
||||
BrokerageOperationDto,
|
||||
OperationCategory,
|
||||
} from '../presentation/brokerage-operation.dto';
|
||||
|
||||
const types: Record<string, OperationCategory> = {
|
||||
OPERATION_TYPE_BUY: 'trade',
|
||||
OPERATION_TYPE_BUY_CARD: 'trade',
|
||||
OPERATION_TYPE_SELL: 'trade',
|
||||
OPERATION_TYPE_SELL_CARD: 'trade',
|
||||
OPERATION_TYPE_BUY_MARGIN: 'trade',
|
||||
OPERATION_TYPE_SELL_MARGIN: 'trade',
|
||||
OPERATION_TYPE_COUPON: 'income',
|
||||
OPERATION_TYPE_DIVIDEND: 'income',
|
||||
OPERATION_TYPE_DIVIDEND_TRANSFER: 'income',
|
||||
OPERATION_TYPE_DIV_EXT: 'income',
|
||||
OPERATION_TYPE_TRANS_IIS_BS: 'transfer',
|
||||
OPERATION_TYPE_TRANS_BS_BS: 'transfer',
|
||||
OPERATION_TYPE_BOND_TAX: 'tax',
|
||||
OPERATION_TYPE_TAX: 'tax',
|
||||
OPERATION_TYPE_DIVIDEND_TAX: 'tax',
|
||||
OPERATION_TYPE_SERVICE_FEE: 'fee',
|
||||
OPERATION_TYPE_MARGIN_FEE: 'fee',
|
||||
OPERATION_TYPE_BROKER_FEE: 'fee',
|
||||
OPERATION_TYPE_SUCCESS_FEE: 'fee',
|
||||
OPERATION_TYPE_INPUT: 'transfer',
|
||||
OPERATION_TYPE_OUTPUT: 'transfer',
|
||||
OPERATION_TYPE_INPUT_SECURITIES: 'transfer',
|
||||
OPERATION_TYPE_OUTPUT_SECURITIES: 'transfer',
|
||||
};
|
||||
|
||||
export function operationCategory(type: string | undefined): OperationCategory {
|
||||
return types[type ?? ''] ?? 'other';
|
||||
}
|
||||
|
||||
export function mapBrokerageOperation(
|
||||
operation: TbankOperationItem,
|
||||
): BrokerageOperationDto {
|
||||
return {
|
||||
id: operation.id ?? operation.cursor ?? '',
|
||||
occurredAt: timestamp(operation.date),
|
||||
type: operation.type ?? 'OPERATION_TYPE_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): string | null {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function money(value: TbankMoney | undefined) {
|
||||
const currency = value?.currency?.trim().toUpperCase();
|
||||
return currency && value
|
||||
? { currency, value: formatDecimal(decimal(value)) }
|
||||
: null;
|
||||
}
|
||||
|
||||
function timestamp(
|
||||
value: { seconds?: string | number; nanos?: number } | undefined,
|
||||
) {
|
||||
return new Date(Number(value?.seconds ?? 0) * 1000).toISOString();
|
||||
}
|
||||
Reference in New Issue
Block a user