refactor: centralize domain values and enforce numeric lint

This commit is contained in:
2026-09-13 14:37:39 +03:00
parent 8ba005346d
commit 8637ad2f34
44 changed files with 543 additions and 174 deletions
@@ -15,6 +15,8 @@ import { decimal } from '../../../../../integrations/tbank/decimal';
import { optional, timestamp } from '../../application/instrument-market';
import { StockDto, StockSearchResultDto } from '../presentation/stock.dto';
const SEARCH_RESULTS_LIMIT = 30;
type IdentifiedShare = TbankShare & { uid: string };
@Injectable()
@@ -30,7 +32,7 @@ export class StockCatalogService {
search(userId: string, query: string): Promise<StockSearchResultDto[]> {
return this.cached(userId, `search:${query}`, async (token) => {
const found = await this.tbank.findShares(token, query);
const shares = selectShares(found, query).slice(0, 30);
const shares = selectShares(found, query).slice(0, SEARCH_RESULTS_LIMIT);
if (!shares.length) return [];
// One market-data request for the list; failed enrichment must not hide a catalog result.
const [details, prices] = await Promise.all([
@@ -20,6 +20,9 @@ import {
} from '../presentation/stock.dto';
import { StockCatalogService } from './stock-catalog.service';
const ISO_YEAR_LENGTH = 4;
const INCOME_HISTORY_YEARS = 5;
@Injectable()
export class StockIncomeService {
constructor(
@@ -36,8 +39,8 @@ export class StockIncomeService {
return this.catalog.cached(userId, `dividends:${uid}`, async (token) => {
await this.catalog.share(token, uid);
const today = moscowDay(new Date().toISOString());
const from = `${Number(today.slice(0, 4)) - 5}${today.slice(4)}`;
const to = `${Number(today.slice(0, 4)) + 1}${today.slice(4)}`;
const from = `${Number(today.slice(0, ISO_YEAR_LENGTH)) - INCOME_HISTORY_YEARS}${today.slice(ISO_YEAR_LENGTH)}`;
const to = `${Number(today.slice(0, ISO_YEAR_LENGTH)) + 1}${today.slice(ISO_YEAR_LENGTH)}`;
const unavailable: string[] = [];
const [events, position] = await Promise.all([
this.tbank.getDividends(token, uid, from, to),
@@ -22,6 +22,12 @@ import {
const dayMs = 86_400_000;
import { StockCatalogService } from './stock-catalog.service';
const PREVIOUS_CLOSE_LOOKBACK_DAYS = 20;
const HALF_YEAR_MONTHS = 6;
const YEAR_MONTHS = 12;
const CANDLE_WINDOW_DAYS = 365;
const CHANGE_PERCENT_DECIMAL_PLACES = 6;
@Injectable()
export class StockMarketService {
constructor(
@@ -58,7 +64,9 @@ export class StockMarketService {
await this.tbank.getCandles(
token,
uid,
new Date(end.valueOf() - 20 * dayMs),
new Date(
end.valueOf() - PREVIOUS_CLOSE_LOOKBACK_DAYS * dayMs,
),
end,
),
priceAt,
@@ -97,7 +105,7 @@ export class StockMarketService {
divide(
multiply(difference, decimalFromString('100')),
decimalFromString(previousPrice.value),
6,
CHANGE_PERCENT_DECIMAL_PLACES,
),
)
: null,
@@ -121,7 +129,11 @@ export class StockMarketService {
const from = new Date(to);
from.setUTCMonth(
from.getUTCMonth() -
(period === 'month' ? 1 : period === 'halfYear' ? 6 : 12),
(period === 'month'
? 1
: period === 'halfYear'
? HALF_YEAR_MONTHS
: YEAR_MONTHS),
);
if (period === 'all') {
if (!historyFrom)
@@ -134,7 +146,10 @@ export class StockMarketService {
const collected = new Map<string, StockCandlesDto['candles'][number]>();
// Conservative one-year windows also work with older deployed RPC limits.
for (let start = from.valueOf(); start < to.valueOf();) {
const end = Math.min(start + 365 * dayMs, to.valueOf());
const end = Math.min(
start + CANDLE_WINDOW_DAYS * dayMs,
to.valueOf(),
);
for (const candle of await this.tbank.getCandles(
token,
uid,
@@ -11,25 +11,27 @@ import {
import { MoneyDto } from '../../../../../common/money.dto';
import { PortfolioPositionDto } from '../../../portfolio/portfolio.api';
const MAX_INSTRUMENT_QUERY_LENGTH = 80;
export class StockSearchQueryDto {
@ApiProperty({ minLength: 2, maxLength: 80 })
@ApiProperty({ minLength: 2, maxLength: MAX_INSTRUMENT_QUERY_LENGTH })
@Transform(({ value }: { value: unknown }) =>
typeof value === 'string' ? value.trim() : value,
)
@IsString()
@MinLength(2)
@MaxLength(80)
@MaxLength(MAX_INSTRUMENT_QUERY_LENGTH)
declare query: string;
}
export class StockTickerParamsDto {
@ApiProperty({
minLength: 1,
maxLength: 80,
maxLength: MAX_INSTRUMENT_QUERY_LENGTH,
pattern: '^[A-Za-z0-9][A-Za-z0-9._@+-]*$',
})
@IsString()
@MinLength(1)
@MaxLength(80)
@MaxLength(MAX_INSTRUMENT_QUERY_LENGTH)
@Matches(/^[A-Za-z0-9][A-Za-z0-9._@+-]*$/)
declare ticker: string;
}