Files
backend/src/modules/investments/tbank-sync/tbank-screen.ts
T

123 lines
3.7 KiB
TypeScript

import { BadRequestException } from '@nestjs/common';
import type { InvestmentScreenDto } from './tbank-sync.dto';
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../../common/time';
import { MIN_INSTRUMENT_QUERY_LENGTH } from '../instrument-query';
// Reject fields from other screens before reserving any source work.
export function normalizeScreen(
input: InvestmentScreenDto,
): InvestmentScreenDto {
const commonPeriod = ['from', 'to'];
const ledger = [...commonPeriod, 'categories', 'limit'];
const allowed: Record<InvestmentScreenDto['screen'], string[]> = {
accounts: [],
account: ['accountId', ...ledger],
operations: ['accountId', 'instrumentUid', ...ledger],
income: ['accountId', ...commonPeriod],
events: ['accountId', ...commonPeriod],
stocks: ['query'],
bonds: ['query'],
stock: [
'accountId',
'instrumentUid',
...ledger,
'candlePeriod',
'candleRange',
],
bond: [
'accountId',
'instrumentUid',
...ledger,
'candlePeriod',
'candleRange',
],
};
const fail = (): never => {
throw new BadRequestException({
message: [
{
property: 'scope',
constraints: { scope: 'Invalid screen descriptor' },
},
],
});
};
if (!allowed[input.screen]) fail();
for (const key of Object.keys(input)) {
if (
key !== 'screen' &&
input[key as keyof InvestmentScreenDto] !== undefined &&
!allowed[input.screen].includes(key)
) {
fail();
}
}
const accountId = input.accountId?.trim();
const query = input.query?.trim().toUpperCase();
if (
['account', 'events', 'income', 'operations'].includes(input.screen) &&
!accountId
) {
fail();
}
if (
['bonds', 'stocks'].includes(input.screen) &&
(!query || query.length < MIN_INSTRUMENT_QUERY_LENGTH)
) {
fail();
}
if (['bond', 'stock'].includes(input.screen) && !input.instrumentUid) fail();
if (input.accountId !== undefined && !accountId) fail();
if (['events', 'income'].includes(input.screen)) {
if (input.screen === 'income' && Boolean(input.from) !== Boolean(input.to)) {
fail();
}
for (const value of [input.from, input.to]) {
if (value && !/^\d{4}-\d{2}-\d{2}$/.test(value)) fail();
}
}
if (input.from && input.to && new Date(input.from) > new Date(input.to)) {
fail();
}
if (
input.screen === 'events' &&
input.to &&
(input.from ?? new Date().toISOString().slice(ISO_DATE_START, ISO_DATE_LENGTH)) > input.to
) {
fail();
}
if (
input.candleRange &&
new Date(input.candleRange.from) >= new Date(input.candleRange.to)
) {
fail();
}
// Fixed field order is part of coalescing, irrespective of JSON input order.
return {
screen: input.screen,
...(accountId ? { accountId } : {}),
...(input.from ? { from: input.from } : {}),
...(input.to ? { to: input.to } : {}),
...(input.instrumentUid
? { instrumentUid: input.instrumentUid.toLowerCase() }
: {}),
...(query ? { query } : {}),
...(input.candlePeriod ? { candlePeriod: input.candlePeriod } : {}),
...(input.candleRange
? {
candleRange: {
from: new Date(input.candleRange.from).toISOString(),
to: new Date(input.candleRange.to).toISOString(),
},
}
: {}),
...(input.categories
// eslint-disable-next-line sonarjs/no-alphabetical-sort -- Code-unit ordering is intentional for canonical keys and ISO dates; locale collation would change persisted bindings.
? { categories: [...new Set(input.categories)].sort() }
: {}),
...(input.limit !== undefined ? { limit: input.limit } : {}),
};
}