refactor: replace ignored numeric literals with constants
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 2m17s
Conventional commits / commits (pull_request) Successful in 31s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 1m56s

This commit is contained in:
2026-09-28 11:24:59 +03:00
parent 9b3558f96c
commit 9cc9fbdcff
60 changed files with 510 additions and 283 deletions
@@ -27,6 +27,11 @@ import { CandleRepository } from '../infrastructure/candle.repository';
import { CatalogEnrichmentService } from './catalog-enrichment.service';
import { timestamp } from './instrument-market';
const FIRST_BATCH_OFFSET = 0;
const BACKGROUND_SYNC_PRIORITY = 2;
const REPLACED_WINDOW_COUNT = 1;
const NEXT_WINDOW_OFFSET = 1;
type CandleInput = {
instrumentType: 'bond' | 'share';
period: 'all' | 'halfYear' | 'month' | 'year';
@@ -246,7 +251,7 @@ export class CandleService {
)
.map(([uid]) => uid);
for (
let offset = 0;
let offset = FIRST_BATCH_OFFSET;
offset < uids.length;
offset += prefetchReadBatchSize
) {
@@ -297,7 +302,7 @@ export class CandleService {
now,
).length
) {
await this.request(userId, revision, input, now, false, 2);
await this.request(userId, revision, input, now, false, BACKGROUND_SYNC_PRIORITY);
}
}
}
@@ -393,7 +398,7 @@ export class CandleService {
const gaps = candleGaps(window, latest.coverage, now)
.flatMap(candleWindows)
.reverse();
ranges.splice(index, 1, ...gaps);
ranges.splice(index, REPLACED_WINDOW_COUNT, ...gaps);
// eslint-disable-next-line security/detect-object-injection -- The index is the internal numeric window cursor.
window = ranges[index];
}
@@ -430,7 +435,7 @@ export class CandleService {
) {
return false;
}
pass.nextWindow = index + 1;
pass.nextWindow = index + NEXT_WINDOW_OFFSET;
if (pass.nextWindow < ranges.length) return null;
}
return true;
@@ -512,7 +517,7 @@ export class CandleService {
{ ...producer.input, period: 'all' },
producer.now,
false,
2,
BACKGROUND_SYNC_PRIORITY,
);
}
}
@@ -26,6 +26,10 @@ import {
selectCatalogInstruments,
} from './catalog-selection';
const UNAMBIGUOUS_MATCH_COUNT = 1;
const NO_MATCHES = 0;
const FIRST_RESULT_OFFSET = 0;
const tickerAmbiguityLimit = 2;
type Producer = {
@@ -160,7 +164,7 @@ export class CatalogDiscoveryService {
{ instrumentType, query: instrument.ticker, exactTicker: true },
observe,
);
if (alias.data?.length === 1 && alias.data[0].uid === uid) {
if (alias.data?.length === UNAMBIGUOUS_MATCH_COUNT && alias.data[0].uid === uid) {
slug = instrument.ticker.toUpperCase();
}
dependencies.push(alias.meta);
@@ -200,8 +204,8 @@ export class CatalogDiscoveryService {
{ instrumentType, query: ticker, exactTicker: true },
observe,
);
if (lookup.data?.length === 0) throw new NotFoundException();
if (lookup.data && lookup.data.length > 1) {
if (lookup.data?.length === NO_MATCHES) throw new NotFoundException();
if (lookup.data && lookup.data.length > UNAMBIGUOUS_MATCH_COUNT) {
throw new ConflictException({
code:
instrumentType === 'share' ?
@@ -346,7 +350,7 @@ export class CatalogDiscoveryService {
input.query,
input.instrumentType,
).slice(
0,
FIRST_RESULT_OFFSET,
input.exactTicker ? tickerAmbiguityLimit : catalogSearchLimit,
),
new Date(),
@@ -364,7 +368,7 @@ export class CatalogDiscoveryService {
if (success) {
const saved = await this.repository.read(input);
for (const observer of [attempt, ...producer.observers]) {
if (!input.exactTicker || saved.data?.length === 1) {
if (!input.exactTicker || saved.data?.length === UNAMBIGUOUS_MATCH_COUNT) {
await this.enrichment.requestMany(
observer.userId,
observer.revision,
@@ -39,6 +39,10 @@ import {
import { catalogSearchLimit } from './catalog-selection';
import { previousClose, timestamp } from './instrument-market';
const MISSING_DATA_REVISION = 0;
const FIRST_QUEUE_INDEX = 0;
const DEQUEUED_ITEM_COUNT = 1;
type Producer = {
attempt: TbankSyncAttempt;
input: CatalogSource;
@@ -147,7 +151,7 @@ export class CatalogEnrichmentService {
price: payload?.price ?? null,
meta: investmentResourceMeta({
resourceKey: catalogScope(input),
dataRevision: String(row?.dataRevision ?? 0),
dataRevision: String(row?.dataRevision ?? MISSING_DATA_REVISION),
availability: row ? 'complete' : 'missing',
fetchedAt: row?.fetchedAt ?? null,
freshForMs: tbankSourceTtlMs[sourcePolicy(input.kind)],
@@ -485,7 +489,7 @@ export class CatalogEnrichmentService {
// One batch RPC for queued prices sharing the same authorized producer.
if (producer.input.kind === 'last-price') {
for (
let i = 0;
let i = FIRST_QUEUE_INDEX;
i < this.queue.length && batch.length < catalogSearchLimit;
) {
// eslint-disable-next-line security/detect-object-injection -- Numeric queue indexes and validated instrument UIDs select internal data only.
@@ -496,7 +500,7 @@ export class CatalogEnrichmentService {
next.attempt.revision === producer.attempt.revision
) {
batch.push(next);
this.queue.splice(i, 1);
this.queue.splice(i, DEQUEUED_ITEM_COUNT);
} else i++;
}
}
@@ -3,19 +3,21 @@ import type {
TbankTimestamp,
} from '../../../../integrations/tbank/tbank.api';
import { MILLISECONDS_PER_SECOND } from '../../../../common/time';
import { ISO_DATE_LENGTH, ISO_DATE_START, MILLISECONDS_PER_SECOND } from '../../../../common/time';
const MIN_TIMESTAMP_SECONDS_EXCLUSIVE = 0;
const MOSCOW_OFFSET_MS = 10_800_000;
export function timestamp(value?: TbankTimestamp): null | string {
const seconds = Number(value?.seconds);
return Number.isFinite(seconds) && seconds > 0 ?
return Number.isFinite(seconds) && seconds > MIN_TIMESTAMP_SECONDS_EXCLUSIVE ?
new Date(seconds * MILLISECONDS_PER_SECOND).toISOString() :
null;
}
export function moscowDay(iso: string): string {
return new Date(Date.parse(iso) + MOSCOW_OFFSET_MS)
.toISOString()
.slice(0, 10);
.slice(ISO_DATE_START, ISO_DATE_LENGTH);
}
export function previousClose(
candles: TbankCandle[],
@@ -29,7 +31,7 @@ export function previousClose(
candle.isComplete &&
candle.close &&
time &&
time.slice(0, 10) < moscowDay(priceAt)
time.slice(ISO_DATE_START, ISO_DATE_LENGTH) < moscowDay(priceAt)
);
})
.sort((a, b) => Number(b.time?.seconds) - Number(a.time?.seconds))[0] ??
@@ -12,6 +12,10 @@ import {
} from '../../portfolio/portfolio.api';
import { StockPositionDto } from '../stocks/presentation/stock.dto';
const EMPTY_POSITIONS_COUNT = 0;
const ZERO_QUANTITY = 0n;
const ZERO_AMOUNT = 0n;
const PRICE_DECIMAL_PLACES = 9;
const RETURN_PERCENT_DECIMAL_PLACES = 6;
@@ -32,8 +36,8 @@ export function instrumentPosition(
const currentValue = sumComplete(positions.map((p) => p.currentValue));
const absoluteReturn = sumComplete(positions.map((p) => p.absoluteReturn));
const positive =
positions.length > 0 &&
positions.every((p) => decimalFromString(p.quantity).value > 0n);
positions.length > EMPTY_POSITIONS_COUNT &&
positions.every((p) => decimalFromString(p.quantity).value > ZERO_QUANTITY);
return {
quantity: formatDecimal(quantity),
currentValue,
@@ -55,7 +59,7 @@ export function instrumentPosition(
accounts: positions.map((p) => ({
...p,
returnPercent:
decimalFromString(p.quantity).value > 0n ?
decimalFromString(p.quantity).value > ZERO_QUANTITY ?
percent(p.absoluteReturn, cost(p)) :
null,
})),
@@ -103,7 +107,7 @@ function percent(
!base ||
!result ||
base.currency !== result.currency ||
decimalFromString(base.value).value <= 0n
decimalFromString(base.value).value <= ZERO_AMOUNT
) {
return null;
}
@@ -14,8 +14,11 @@ import {
type IdentifiedBond = TbankBond & { uid: string };
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../../../../common/time';
import { money } from '../infrastructure/bond-money.mapper';
const MIN_LOT_EXCLUSIVE = 0;
@Injectable()
export class BondCatalogService {
constructor(private readonly discovery: CatalogDiscoveryService) {}
@@ -100,13 +103,13 @@ function toBond(bond: IdentifiedBond): BondDto {
currency: bond.currency?.toUpperCase() || null,
bondType: bond.bondType || null,
sector: bond.sector || null,
lot: bond.lot && bond.lot > 0 ? bond.lot : null,
lot: bond.lot && bond.lot > MIN_LOT_EXCLUSIVE ? bond.lot : null,
historyFrom: timestamp(bond.first_1dayCandleDate),
nominal: money(bond.nominal),
initialNominal: money(bond.initialNominal),
aci: money(bond.aciValue),
maturityDate: timestamp(bond.maturityDate)?.slice(0, 10) ?? null,
callDate: timestamp(bond.callDate)?.slice(0, 10) ?? null,
maturityDate: timestamp(bond.maturityDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
callDate: timestamp(bond.callDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
couponQuantityPerYear: bond.couponQuantityPerYear ?? null,
floatingCoupon: bond.floatingCouponFlag ?? null,
perpetual: bond.perpetualFlag ?? null,
@@ -6,6 +6,7 @@ import {
formatDecimal,
multiply,
} from '../../../../../common/decimal';
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../../../../common/time';
import {
TbankAccessService,
type TbankBond,
@@ -30,6 +31,12 @@ import {
BondPositionDto,
} from '../presentation/bond.dto';
import { bondQuote } from './bond-market.service';
const INCOME_FORECAST_YEARS = 1;
const ZERO_QUANTITY = 0n;
const MIN_COUPON_PERIOD_EXCLUSIVE = 0;
const ZERO_AMOUNT = 0n;
const ISO_YEAR_LENGTH = 4;
const INCOME_HISTORY_YEARS = 5;
const COUPON_YIELD_DECIMAL_PLACES = 4;
@@ -77,8 +84,8 @@ export class BondIncomeService {
): Promise<BondCouponsResourceDto> {
const connection = await this.access.local(userId);
const today = moscowDay(new Date().toISOString());
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 from = `${Number(today.slice(ISO_DATE_START, ISO_YEAR_LENGTH)) - INCOME_HISTORY_YEARS}${today.slice(ISO_YEAR_LENGTH)}`;
const to = `${Number(today.slice(ISO_DATE_START, ISO_YEAR_LENGTH)) + INCOME_FORECAST_YEARS}${today.slice(ISO_YEAR_LENGTH)}`;
const portfolio = await this.portfolios.read(userId, 'all', observe);
const sources = await this.sources.readMany(
userId,
@@ -139,13 +146,13 @@ export function bondCoupons(
const { from, to, today } = window;
const quantity =
position?.accounts.length &&
position.accounts.every((p) => decimalFromString(p.quantity).value > 0n) ?
position.accounts.every((p) => decimalFromString(p.quantity).value > ZERO_QUANTITY) ?
decimalFromString(position.quantity) :
null;
const coupons = events
.map((event) => {
const paymentDate = timestamp(event.couponDate)?.slice(0, 10) ?? null;
const recordDate = timestamp(event.fixDate)?.slice(0, 10) ?? null;
const paymentDate = timestamp(event.couponDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null;
const recordDate = timestamp(event.fixDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null;
const perBond = money(event.payOneBond);
const timing = !paymentDate ?
('undated' as const) :
@@ -198,10 +205,10 @@ export function bondMetrics(
const effectiveYield = values.effectiveYield ?? null;
const creditRating = values.creditRating ?? null;
const today = moscowDay(new Date().toISOString());
const to = `${Number(today.slice(0, ISO_YEAR_LENGTH)) + 1}${today.slice(ISO_YEAR_LENGTH)}`;
const to = `${Number(today.slice(ISO_DATE_START, ISO_YEAR_LENGTH)) + INCOME_FORECAST_YEARS}${today.slice(ISO_YEAR_LENGTH)}`;
const next = events
?.filter((event) => {
const date = timestamp(event.couponDate)?.slice(0, 10);
const date = timestamp(event.couponDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH);
return date && date >= today && date <= to;
})
.sort(
@@ -210,15 +217,15 @@ export function bondMetrics(
const coupon = money(next?.payOneBond);
const period = next?.couponPeriod;
const couponPeriodDays =
period && Number.isInteger(period) && period > 0 ? period : null;
period && Number.isInteger(period) && period > MIN_COUPON_PERIOD_EXCLUSIVE ? period : null;
const clean = quote?.cleanPrice;
const couponYieldPercent =
coupon &&
couponPeriodDays &&
clean &&
coupon.currency === clean.currency &&
decimalFromString(coupon.value).value >= 0n &&
decimalFromString(clean.value).value > 0n ?
decimalFromString(coupon.value).value >= ZERO_AMOUNT &&
decimalFromString(clean.value).value > ZERO_AMOUNT ?
formatDecimal(
divide(
multiply(
@@ -242,7 +249,7 @@ export function bondMetrics(
return {
creditRating,
coupon,
couponDate: timestamp(next?.couponDate)?.slice(0, 10) ?? null,
couponDate: timestamp(next?.couponDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
couponType,
couponPeriodDays,
couponYieldPercent,
@@ -10,6 +10,7 @@ import {
multiply,
subtract,
} from '../../../../../common/decimal';
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../../../../common/time';
import { decimal } from '../../../../../integrations/tbank/decimal';
import { CandleService } from '../../application/candle.service';
import {
@@ -24,6 +25,8 @@ import {
BondQuoteResourceDto,
} from '../presentation/bond.dto';
const ZERO_AMOUNT = 0n;
const PRICE_DECIMAL_PLACES = 9;
@Injectable()
export class BondMarketService {
@@ -80,7 +83,7 @@ export function bondQuote(
const cleanPrice =
pricePercent !== null &&
nominal &&
decimalFromString(nominal.value).value > 0n ?
decimalFromString(nominal.value).value > ZERO_AMOUNT ?
{
currency: nominal.currency,
value: formatDecimal(
@@ -118,7 +121,7 @@ export function bondQuote(
last?.price && previous?.close ?
formatDecimal(subtract(decimal(last.price), decimal(previous.close))) :
null,
previousCloseDate: timestamp(previous?.time)?.slice(0, 10) ?? null,
previousCloseDate: timestamp(previous?.time)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
unavailable,
observedAt,
};
@@ -1,5 +1,7 @@
import { MILLISECONDS_PER_DAY } from '../../../../common/time';
const FIRST_RANGE_END_INDEX = 1;
export type CandleRange = { from: Date; to: Date };
export type CandleCoverage = CandleRange & {
fetchedAt: Date;
@@ -80,7 +82,7 @@ export function confirmedCandleCoverage(
}
}
const sorted = [...boundaries].sort((a, b) => a - b);
return sorted.slice(1).map((to, index) => {
return sorted.slice(FIRST_RANGE_END_INDEX).map((to, index) => {
// eslint-disable-next-line security/detect-object-injection -- The index comes from the sorted numeric boundary array.
const from = sorted[index];
return {
@@ -11,6 +11,8 @@ import {
type SavedCandle,
} from '../domain/candle-coverage';
const NO_UPDATED_ROWS = 0;
const provider = 'tbank';
const interval = 'day';
type CandleIntent = {
@@ -323,7 +325,7 @@ export class CandleRepository {
},
data: { state: 'complete', errorCode: null },
});
return result.count > 0;
return result.count > NO_UPDATED_ROWS;
});
}
@@ -11,6 +11,9 @@ import type {
import { PrismaService } from '../../../../prisma/prisma.service';
const UNCHANGED_REVISION_INCREMENT = 0;
const REVISION_INCREMENT = 1;
export type CatalogQuery = {
exactTicker?: boolean;
instrumentType: 'bond' | 'share';
@@ -205,7 +208,7 @@ export class CatalogDiscoveryRepository {
ticker: instrument.ticker ?? null,
fetchedAt: now,
dataRevision: {
increment: isDeepStrictEqual(previous.metadata, metadata) ? 0 : 1,
increment: isDeepStrictEqual(previous.metadata, metadata) ? UNCHANGED_REVISION_INCREMENT : REVISION_INCREMENT,
},
},
});
@@ -230,8 +233,8 @@ export class CatalogDiscoveryRepository {
previousAlias?.matchedUids,
matchedUids,
) ?
0 :
1,
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
},
},
});
@@ -257,7 +260,7 @@ export class CatalogDiscoveryRepository {
payload,
fetchedAt: now,
dataRevision: {
increment: isDeepStrictEqual(previous?.payload, payload) ? 0 : 1,
increment: isDeepStrictEqual(previous?.payload, payload) ? UNCHANGED_REVISION_INCREMENT : REVISION_INCREMENT,
},
},
});
@@ -390,7 +393,7 @@ export class CatalogDiscoveryRepository {
payload,
fetchedAt: now,
dataRevision: {
increment: isDeepStrictEqual(previous?.payload, payload) ? 0 : 1,
increment: isDeepStrictEqual(previous?.payload, payload) ? UNCHANGED_REVISION_INCREMENT : REVISION_INCREMENT,
},
},
});
@@ -416,8 +419,8 @@ export class CatalogDiscoveryRepository {
previous?.matchedUids,
instrumentUids,
) ?
0 :
1,
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
},
},
});
@@ -442,8 +445,8 @@ export class CatalogDiscoveryRepository {
previous?.instrumentUids,
instrumentUids,
) ?
0 :
1,
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
},
},
});
@@ -12,6 +12,8 @@ import {
StockSearchResourceDto,
} from '../presentation/stock.dto';
const MIN_LOT_EXCLUSIVE = 0;
type IdentifiedShare = TbankShare & { uid: string };
@Injectable()
@@ -102,7 +104,7 @@ function stock(share: IdentifiedShare): StockDto {
currency: share.currency?.toUpperCase() || null,
shareType: share.shareType || null,
sector: share.sector || null,
lot: share.lot && share.lot > 0 ? share.lot : null,
lot: share.lot && share.lot > MIN_LOT_EXCLUSIVE ? share.lot : null,
historyFrom: timestamp(share.first_1dayCandleDate),
};
}
@@ -5,6 +5,7 @@ import {
formatDecimal,
multiply,
} from '../../../../../common/decimal';
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../../../../common/time';
import { decimal } from '../../../../../integrations/tbank/decimal';
import {
TbankAccessService,
@@ -23,6 +24,10 @@ import {
StockDividendsResourceDto,
StockPositionDto,
} from '../presentation/stock.dto';
const INCOME_FORECAST_YEARS = 1;
const ZERO_QUANTITY = 0n;
const ISO_YEAR_LENGTH = 4;
const INCOME_HISTORY_YEARS = 5;
@Injectable()
@@ -49,8 +54,8 @@ export class StockIncomeService {
): Promise<StockDividendsResourceDto> {
const connection = await this.access.local(userId);
const today = moscowDay(new Date().toISOString());
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 from = `${Number(today.slice(ISO_DATE_START, ISO_YEAR_LENGTH)) - INCOME_HISTORY_YEARS}${today.slice(ISO_YEAR_LENGTH)}`;
const to = `${Number(today.slice(ISO_DATE_START, ISO_YEAR_LENGTH)) + INCOME_FORECAST_YEARS}${today.slice(ISO_YEAR_LENGTH)}`;
const portfolio = await this.portfolios.read(userId, 'all', observe);
const sources = await this.sources.readMany(
userId,
@@ -112,15 +117,15 @@ export function stockDividends(
const quantity =
position &&
position.accounts.length &&
position.accounts.every((p) => decimalFromString(p.quantity).value > 0n) ?
position.accounts.every((p) => decimalFromString(p.quantity).value > ZERO_QUANTITY) ?
decimalFromString(position.quantity) :
null;
const dividends: StockDividendDto[] = events
.filter((event) => event.dividendType?.toLowerCase() !== 'cancelled')
.map((event) => {
const recordDate = timestamp(event.recordDate)?.slice(0, 10) ?? null;
const paymentDate = timestamp(event.paymentDate)?.slice(0, 10) ?? null;
const lastBuyDate = timestamp(event.lastBuyDate)?.slice(0, 10) ?? null;
const recordDate = timestamp(event.recordDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null;
const paymentDate = timestamp(event.paymentDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null;
const lastBuyDate = timestamp(event.lastBuyDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null;
const currency = event.dividendNet?.currency?.toUpperCase();
const perShare =
currency && event.dividendNet ?
@@ -7,6 +7,7 @@ import {
multiply,
subtract,
} from '../../../../../common/decimal';
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../../../../common/time';
import { decimal } from '../../../../../integrations/tbank/decimal';
import { CandleService } from '../../application/candle.service';
import {
@@ -20,6 +21,8 @@ import {
StockQuoteResourceDto,
} from '../presentation/stock.dto';
const ZERO_AMOUNT = 0n;
const CHANGE_PERCENT_DECIMAL_PLACES = 6;
@Injectable()
export class StockMarketService {
@@ -90,7 +93,7 @@ export function stockQuote(
price,
priceAt,
tradingStatus,
previousCloseDate: timestamp(previous?.time)?.slice(0, 10) ?? null,
previousCloseDate: timestamp(previous?.time)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
previousClose: previousPrice,
change:
difference && currency ?
@@ -99,7 +102,7 @@ export function stockQuote(
changePercent:
difference &&
previousPrice &&
decimalFromString(previousPrice.value).value > 0n ?
decimalFromString(previousPrice.value).value > ZERO_AMOUNT ?
formatDecimal(
divide(
multiply(difference, decimalFromString('100')),
@@ -10,32 +10,36 @@ import {
} from 'class-validator';
import { MoneyDto } from '../../../../../common/money.dto';
import {
MAX_INSTRUMENT_QUERY_LENGTH,
MIN_INSTRUMENT_QUERY_LENGTH,
} from '../../../instrument-query';
import {
InvestmentReadQueryDto,
InvestmentResourceMetaDto,
} from '../../../investment-resource.dto';
import { PortfolioPositionDto } from '../../../portfolio/portfolio.api';
const MAX_INSTRUMENT_QUERY_LENGTH = 80;
const MIN_TICKER_LENGTH = 1;
export class StockSearchQueryDto extends InvestmentReadQueryDto {
@ApiProperty({ minLength: 2, maxLength: MAX_INSTRUMENT_QUERY_LENGTH })
@ApiProperty({ minLength: MIN_INSTRUMENT_QUERY_LENGTH, maxLength: MAX_INSTRUMENT_QUERY_LENGTH })
@Transform(({ value }: { value: unknown }) =>
typeof value === 'string' ? value.trim() : value,
)
@IsString()
@MinLength(2)
@MinLength(MIN_INSTRUMENT_QUERY_LENGTH)
@MaxLength(MAX_INSTRUMENT_QUERY_LENGTH)
declare query: string;
}
export class StockTickerParamsDto {
@ApiProperty({
minLength: 1,
minLength: MIN_TICKER_LENGTH,
maxLength: MAX_INSTRUMENT_QUERY_LENGTH,
pattern: '^[A-Za-z0-9][A-Za-z0-9._@+-]*$',
})
@IsString()
@MinLength(1)
@MinLength(MIN_TICKER_LENGTH)
@MaxLength(MAX_INSTRUMENT_QUERY_LENGTH)
@Matches(/^[A-Z0-9][\w.@+-]*$/i)
declare ticker: string;