123 lines
3.7 KiB
TypeScript
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 } : {}),
|
|
};
|
|
}
|