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 = { 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 } : {}), }; }