style: align multiline ternary operators
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 2m11s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 2m17s
Conventional commits / commits (pull_request) Successful in 34s

This commit is contained in:
2026-09-28 12:33:56 +03:00
parent 9cc9fbdcff
commit 0303e60b64
54 changed files with 986 additions and 982 deletions
+5 -1
View File
@@ -48,7 +48,11 @@ export default defineConfig(
'perfectionist/sort-classes': 'off',
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }],
'@stylistic/arrow-parens': ['error', 'always'],
'@stylistic/operator-linebreak': ['error', 'after'],
'@stylistic/operator-linebreak': [
'error',
'after',
{ overrides: { '?': 'before', ':': 'before' } },
],
'import-x/no-duplicates': ['error', { 'prefer-inline': true }],
},
},
+10 -10
View File
@@ -15,18 +15,18 @@ export class HttpExceptionFilter implements ExceptionFilter {
const context = host.switchToHttp();
const response = context.getResponse<Response>();
const status =
exception instanceof HttpException ?
exception.getStatus() :
HttpStatus.INTERNAL_SERVER_ERROR;
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const payload =
exception instanceof HttpException ? exception.getResponse() : undefined;
const isValidation = Array.isArray(
(payload as { message?: unknown })?.message,
);
const code = isValidation ?
'VALIDATION_ERROR' :
this.codeFor(status, payload);
const code = isValidation
? 'VALIDATION_ERROR'
: this.codeFor(status, payload);
response.locals ??= {};
response.locals.publicErrorCode = code;
response
@@ -35,13 +35,13 @@ export class HttpExceptionFilter implements ExceptionFilter {
.json({
status,
code,
...(isValidation ?
{
...(isValidation
? {
errors: this.validationErrors(
(payload as { message: ValidationError[] }).message,
),
} :
{}),
}
: {}),
});
}
@@ -33,26 +33,26 @@ export class RequestLoggingMiddleware implements NestMiddleware {
const routePath = (routeRequest as { route?: { path?: unknown } }).route
?.path;
const route =
typeof routePath === 'string' ?
`${request.baseUrl}${routePath}` :
'<unmatched>';
typeof routePath === 'string'
? `${request.baseUrl}${routePath}`
: '<unmatched>';
const context = {
method: request.method,
route,
status: response.statusCode,
durationMs: Math.round(performance.now() - startedAt),
...(response.locals.publicErrorCode ?
{ code: response.locals.publicErrorCode as string } :
{}),
...(response.locals.publicErrorCode
? { code: response.locals.publicErrorCode as string }
: {}),
};
const level =
response.statusCode >= httpStatus.HTTP_STATUS_INTERNAL_SERVER_ERROR ?
'error' :
response.statusCode === httpStatus.HTTP_STATUS_TOO_MANY_REQUESTS ||
response.statusCode === httpStatus.HTTP_STATUS_UNAUTHORIZED ||
response.statusCode === httpStatus.HTTP_STATUS_FORBIDDEN ?
'warn' :
'info';
response.statusCode >= httpStatus.HTTP_STATUS_INTERNAL_SERVER_ERROR
? 'error'
: response.statusCode === httpStatus.HTTP_STATUS_TOO_MANY_REQUESTS ||
response.statusCode === httpStatus.HTTP_STATUS_UNAUTHORIZED ||
response.statusCode === httpStatus.HTTP_STATUS_FORBIDDEN
? 'warn'
: 'info';
this.logger.event(level, 'http.request.completed', context);
});
+3 -3
View File
@@ -105,9 +105,9 @@ export function validateEnv(config: Record<string, unknown>): Environment {
JWT_SECRET: jwtSecret,
PORT: parsedPort,
TOKEN_ENCRYPTION_KEY: tokenEncryptionKey,
...(typeof tbankCaCertPath === 'string' && tbankCaCertPath ?
{ TBANK_CA_CERT_PATH: tbankCaCertPath } :
{}),
...(typeof tbankCaCertPath === 'string' && tbankCaCertPath
? { TBANK_CA_CERT_PATH: tbankCaCertPath }
: {}),
TBANK_GRPC_ENDPOINT: tbankEndpoint,
TBANK_RATE_LIMIT_PER_SECOND: parsedTbankRateLimit,
LOG_LEVEL: logLevel as Environment['LOG_LEVEL'],
@@ -43,12 +43,12 @@ export class InstrumentMetadataRepository {
async readMany(identities: MetadataIdentity[]) {
const identifiers = identities.flatMap((identity) => [
...(identity.instrumentUid?.trim() ?
[{ identifierType: 'uid', identifier: identity.instrumentUid.trim() }] :
[]),
...(identity.figi?.trim() ?
[{ identifierType: 'figi', identifier: identity.figi.trim() }] :
[]),
...(identity.instrumentUid?.trim()
? [{ identifierType: 'uid', identifier: identity.instrumentUid.trim() }]
: []),
...(identity.figi?.trim()
? [{ identifierType: 'figi', identifier: identity.figi.trim() }]
: []),
]);
if (!identifiers.length) {
return new Map<string, ReturnType<typeof selectMetadata>>();
@@ -233,9 +233,9 @@ export class InstrumentMetadataRepository {
update: {
...data,
dataRevision: {
increment: isDeepStrictEqual(previous?.metadata, payload) ?
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
increment: isDeepStrictEqual(previous?.metadata, payload)
? UNCHANGED_REVISION_INCREMENT
: REVISION_INCREMENT,
},
},
});
@@ -256,9 +256,9 @@ export class InstrumentMetadataRepository {
matchedUids,
fetchedAt: now,
dataRevision: {
increment: isDeepStrictEqual(previous?.matchedUids, matchedUids) ?
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
increment: isDeepStrictEqual(previous?.matchedUids, matchedUids)
? UNCHANGED_REVISION_INCREMENT
: REVISION_INCREMENT,
},
},
});
+12 -12
View File
@@ -205,10 +205,10 @@ export class TbankClient {
loadPackageDefinition(definition) as unknown as TbankNamespace
).tinkoff.public.invest.api.contract.v1;
const caPath = config.get<string | undefined>('TBANK_CA_CERT_PATH');
const credentials = caPath ?
const credentials = caPath
// eslint-disable-next-line security/detect-non-literal-fs-filename -- The CA path is trusted operator configuration, never supplied by an HTTP request.
ChannelCredentials.createSsl(readFileSync(caPath)) :
ChannelCredentials.createSsl();
? ChannelCredentials.createSsl(readFileSync(caPath))
: ChannelCredentials.createSsl();
const endpoint = config.getOrThrow<string>('TBANK_GRPC_ENDPOINT');
const create = (name: string) =>
new (
@@ -497,12 +497,12 @@ export class TbankClient {
return response;
} catch (error) {
const grpcCode = (error as ServiceError).code;
const failureContext = queueFull ?
{ code: 'TBANK_UNAVAILABLE' } :
{ grpcCode };
const retryable = queueFull ?
metadataCall :
[
const failureContext = queueFull
? { code: 'TBANK_UNAVAILABLE' }
: { grpcCode };
const retryable = queueFull
? metadataCall
: [
status.DEADLINE_EXCEEDED,
status.RESOURCE_EXHAUSTED,
status.UNAVAILABLE,
@@ -517,9 +517,9 @@ export class TbankClient {
await new Promise((resolve) =>
setTimeout(
resolve,
metadataCall ?
attempts * METADATA_RETRY_DELAY_MS :
RPC_RETRY_DELAY_MS,
metadataCall
? attempts * METADATA_RETRY_DELAY_MS
: RPC_RETRY_DELAY_MS,
),
);
continue;
@@ -103,17 +103,17 @@ export class BankCardsService {
id,
name: dto.name ?? previous.name,
bankName:
dto.bankName === undefined ?
(previous.bankName ?? undefined) :
(dto.bankName ?? undefined),
dto.bankName === undefined
? (previous.bankName ?? undefined)
: (dto.bankName ?? undefined),
lastFour:
dto.lastFour === undefined ?
(previous.lastFour ?? undefined) :
(dto.lastFour ?? undefined),
dto.lastFour === undefined
? (previous.lastFour ?? undefined)
: (dto.lastFour ?? undefined),
paymentSystem:
dto.paymentSystem === undefined ?
previous.paymentSystem :
dto.paymentSystem,
dto.paymentSystem === undefined
? previous.paymentSystem
: dto.paymentSystem,
openingBalance:
dto.openingBalance ?? rubMoney(previous.openingBalanceKopecks),
openingDate: dto.openingDate ?? previous.openingDate,
@@ -143,12 +143,12 @@ export class BankCardsService {
...input,
paymentSystem: input.paymentSystem ?? null,
version: { increment: 1 },
...(dto.archived === undefined ?
{} :
{
archivedAt: dto.archived ?
(previous.archivedAt ?? new Date()) :
null,
...(dto.archived === undefined
? {}
: {
archivedAt: dto.archived
? (previous.archivedAt ?? new Date())
: null,
}),
},
});
@@ -43,29 +43,29 @@ export class BankOperationsService {
row: BankCardOperation,
): Promise<BankOperationDto> {
const card = await this.repository.card(tx, row.userId, row.cardId);
const target = row.targetCardId ?
await this.repository.card(tx, row.userId, row.targetCardId) :
null;
const source = row.originalExpenseId ?
await this.repository.operation(tx, row.userId, row.originalExpenseId) :
null;
const target = row.targetCardId
? await this.repository.card(tx, row.userId, row.targetCardId)
: null;
const source = row.originalExpenseId
? await this.repository.operation(tx, row.userId, row.originalExpenseId)
: null;
const categoryId = source?.categoryId ?? row.categoryId;
const category = categoryId ?
await tx.expenseCategory.findFirst({
const category = categoryId
? await tx.expenseCategory.findFirst({
where: { id: categoryId, userId: row.userId },
}) :
null;
})
: null;
const refunds =
row.kind === BankOperationKind.Expense || source ?
await tx.bankCardOperation.findMany({
row.kind === BankOperationKind.Expense || source
? await tx.bankCardOperation.findMany({
where: {
userId: row.userId,
originalExpenseId: source?.id ?? row.id,
deletedAt: null,
...(source ? { id: { not: row.id } } : {}),
},
}) :
[];
})
: [];
return {
id: row.id,
kind: row.kind as BankOperationDto['kind'],
@@ -78,12 +78,12 @@ export class BankOperationsService {
originalExpenseId: row.originalExpenseId,
amount: rubMoney(row.amountKopecks),
availableRefund:
row.kind === BankOperationKind.Expense || source ?
rubMoney(
row.kind === BankOperationKind.Expense || source
? rubMoney(
(source?.amountKopecks ?? row.amountKopecks) -
refunds.reduce((sum, r) => sum + r.amountKopecks, ZERO_KOPECKS),
) :
null,
)
: null,
date: row.date,
description: row.description,
version: row.version,
@@ -336,9 +336,9 @@ export class BankOperationsService {
query: BankOperationsQueryDto | BankPageQueryDto,
expenses = false,
) {
const period = expenses ?
undefined :
periodBounds(query as BankOperationsQueryDto);
const period = expenses
? undefined
: periodBounds(query as BankOperationsQueryDto);
return this.prisma.$transaction(async (tx) => {
await this.repository.card(tx, userId, id);
const limit = query.limit ?? DEFAULT_BANK_PAGE_SIZE,
@@ -347,9 +347,9 @@ export class BankOperationsService {
userId,
deletedAt: null,
OR: [{ cardId: id }, { targetCardId: id }],
...(period ?
{ date: { gte: period.from, lte: period.to } } :
{ kind: BankOperationKind.Expense }),
...(period
? { date: { gte: period.from, lte: period.to } }
: { kind: BankOperationKind.Expense }),
};
const total = await tx.bankCardOperation.count({ where });
const rows = await tx.bankCardOperation.findMany({
@@ -81,13 +81,13 @@ export function bankSummary(
continue;
}
const expense =
operation.kind === BankOperationKind.Expense ?
operation.amountKopecks :
ZERO_KOPECKS;
operation.kind === BankOperationKind.Expense
? operation.amountKopecks
: ZERO_KOPECKS;
const refund =
operation.kind === BankOperationKind.Refund ?
operation.amountKopecks :
ZERO_KOPECKS;
operation.kind === BankOperationKind.Refund
? operation.amountKopecks
: ZERO_KOPECKS;
expenses += expense;
refunds += refund;
if (operation.categoryId !== null) {
@@ -40,9 +40,9 @@ export class BankCardsRepository {
});
} catch (error) {
const code =
error instanceof Prisma.PrismaClientKnownRequestError ?
error.code :
'';
error instanceof Prisma.PrismaClientKnownRequestError
? error.code
: '';
const busy =
code === 'P2034' ||
code === 'P2028' ||
@@ -12,15 +12,15 @@ export class BrokerageAccountsService {
async list(userId: string, observe = false) {
const saved = await this.portfolio.saved(userId, 'all', observe);
const data: BrokerageAccountDto[] | null =
saved.membership.availability === 'missing' ?
null :
saved.accounts.map((account) => ({
saved.membership.availability === 'missing'
? null
: saved.accounts.map((account) => ({
id: account.id,
name: account.name,
type: account.type as BrokerageAccountDto['type'],
openedOn: account.openedOn,
portfolioSummary: account.snapshot ?
{
portfolioSummary: account.snapshot
? {
totalsByCurrency: account.snapshot.totalsByCurrency,
stockPositionCount: account.snapshot.positions.filter(
(position) => position.type === 'STOCK',
@@ -28,8 +28,8 @@ export class BrokerageAccountsService {
bondPositionCount: account.snapshot.positions.filter(
(position) => position.type === 'BOND',
).length,
} :
null,
}
: null,
}));
return {
data,
@@ -47,18 +47,18 @@ export class BrokerageAccountsService {
)
.digest('base64url'),
availability:
data === null ?
'missing' :
saved.accounts.some((account) => !account.snapshot) ?
'partial' :
'complete',
fetchedAt: saved.membership.lastSuccessfulAt ?
new Date(saved.membership.lastSuccessfulAt) :
null,
data === null
? 'missing'
: saved.accounts.some((account) => !account.snapshot)
? 'partial'
: 'complete',
fetchedAt: saved.membership.lastSuccessfulAt
? new Date(saved.membership.lastSuccessfulAt)
: null,
freshForMs: 300_000,
work: saved.membership.sync.id ?
{ ...saved.membership.sync, id: saved.membership.sync.id } :
null,
work: saved.membership.sync.id
? { ...saved.membership.sync, id: saved.membership.sync.id }
: null,
dependencies: saved.accounts.map((account) => account.meta),
}),
};
@@ -96,9 +96,9 @@ export class IncomeAnalyticsService {
{ ...current.meta, role: 'selected' },
{ ...prior.meta, role: 'previous' },
{
...(plan.yearAgo && year ?
year.meta :
investmentResourceMeta({
...(plan.yearAgo && year
? year.meta
: investmentResourceMeta({
resourceKey: 'income/yearAgo', // gitleaks:allow public resource identifier
dataRevision: 'unavailable',
availability: 'missing',
@@ -119,9 +119,9 @@ export class IncomeAnalyticsService {
resourceKey: `income/${accountId}/${plan.selected.from}/${plan.selected.to}`,
dataRevision: current.meta.dataRevision,
availability: current.meta.availability,
fetchedAt: current.meta.lastSuccessfulAt ?
new Date(current.meta.lastSuccessfulAt) :
null,
fetchedAt: current.meta.lastSuccessfulAt
? new Date(current.meta.lastSuccessfulAt)
: null,
freshForMs: current.meta.freshness === 'fresh' ? Infinity : STALE_FRESHNESS_MS,
work: work?.id ? { ...work, id: work.id } : null,
coverage: current.meta.coverage,
@@ -144,13 +144,13 @@ export class IncomeAnalyticsService {
current.meta.lastSuccessfulAt,
),
previous:
prior.items === null ?
null :
comparison(selected, plan.previous, qualifying(prior.items)),
prior.items === null
? null
: comparison(selected, plan.previous, qualifying(prior.items)),
yearComparison:
plan.yearAgo && year?.items !== null && year ?
comparison(selected, plan.yearAgo, qualifying(year.items)) :
null,
plan.yearAgo && year?.items !== null && year
? comparison(selected, plan.yearAgo, qualifying(year.items))
: null,
bucketGranularity: plan.granularity,
buckets: buckets(plan.selected, plan.granularity, selected),
instruments: instruments(selected),
+21 -21
View File
@@ -39,24 +39,24 @@ export function planIncomePeriod(
const selected =
from && to ? { from, to } : { from: firstOfMonth(today), to: today };
assertRange(selected, today);
const previous = isFullMonth(selected) ?
previousMonth(selected.from) :
shiftDays(selected, -rangeDays(selected));
const previous = isFullMonth(selected)
? previousMonth(selected.from)
: shiftDays(selected, -rangeDays(selected));
const candidate = yearAgo(selected);
const yearAgoRange =
candidate && (!openedDate || candidate.from >= moscowDate(openedDate)) ?
candidate :
null;
candidate && (!openedDate || candidate.from >= moscowDate(openedDate))
? candidate
: null;
return {
selected,
previous,
yearAgo: yearAgoRange,
granularity:
rangeDays(selected) <= DAILY_BUCKET_MAX_DAYS ?
'day' :
rangeDays(selected) <= WEEKLY_BUCKET_MAX_DAYS ?
'week' :
'month',
rangeDays(selected) <= DAILY_BUCKET_MAX_DAYS
? 'day'
: rangeDays(selected) <= WEEKLY_BUCKET_MAX_DAYS
? 'week'
: 'month',
};
}
@@ -69,20 +69,20 @@ export function rangeBuckets(
}
const buckets: DateOnlyRange[] = [];
let cursor =
granularity === 'week' ?
addDays(
granularity === 'week'
? addDays(
range.from,
-(
(utcDate(range.from).getUTCDay() + DAYS_PER_WEEK - MONDAY_WEEKDAY_INDEX) %
DAYS_PER_WEEK
),
) :
`${range.from.slice(ISO_DATE_START, ISO_MONTH_LENGTH)}-01`;
)
: `${range.from.slice(ISO_DATE_START, ISO_MONTH_LENGTH)}-01`;
while (cursor <= range.to) {
const end =
granularity === 'week' ?
addDays(cursor, DAYS_PER_WEEK - INCLUSIVE_DAY_OFFSET) :
endOfMonth(cursor);
granularity === 'week'
? addDays(cursor, DAYS_PER_WEEK - INCLUSIVE_DAY_OFFSET)
: endOfMonth(cursor);
buckets.push({
from: cursor < range.from ? range.from : cursor,
to: end > range.to ? range.to : end,
@@ -153,9 +153,9 @@ function yearAgo(range: DateOnlyRange): DateOnlyRange | null {
const shift = (value: string) => {
const [year, month, day] = dateParts(value);
const result = `${String(year - PREVIOUS_YEAR_OFFSET).padStart(ISO_YEAR_LENGTH, '0')}-${String(month).padStart(DATE_COMPONENT_DIGITS, '0')}-${String(day).padStart(DATE_COMPONENT_DIGITS, '0')}`;
return utcDate(result).toISOString().slice(ISO_DATE_START, ISO_DATE_LENGTH) === result ?
result :
null;
return utcDate(result).toISOString().slice(ISO_DATE_START, ISO_DATE_LENGTH) === result
? result
: null;
};
const from = shift(range.from);
const to = shift(range.to);
@@ -70,9 +70,9 @@ function rangeFor(
if (!history) {
throw new ServiceUnavailableException({
code:
input.instrumentType === 'share' ?
'STOCK_HISTORY_START_UNAVAILABLE' :
'BOND_HISTORY_START_UNAVAILABLE',
input.instrumentType === 'share'
? 'STOCK_HISTORY_START_UNAVAILABLE'
: 'BOND_HISTORY_START_UNAVAILABLE',
});
}
from.setTime(utcDay(new Date(history)).valueOf());
@@ -114,9 +114,9 @@ export class CandleService {
throw new NotFoundException();
}
const now = new Date();
const range = detail.instrument ?
rangeFor(input, detail.instrument, now) :
null;
const range = detail.instrument
? rangeFor(input, detail.instrument, now)
: null;
const saved = range ? await this.repository.read(input.uid, range) : null;
const gaps = range && saved ? candleGaps(range, saved.coverage, now) : [];
if (!observe && (!saved || gaps.length)) {
@@ -139,14 +139,14 @@ export class CandleService {
});
return {
data:
!saved || !covered ?
null :
{
!saved || !covered
? null
: {
candles: saved.data,
currency:
input.instrumentType === 'share' ?
detail.instrument?.currency?.toUpperCase() || null :
null,
input.instrumentType === 'share'
? detail.instrument?.currency?.toUpperCase() || null
: null,
historyFrom: timestamp(detail?.instrument?.first_1dayCandleDate),
observedAt: fetchedAt?.toISOString() ?? now.toISOString(),
},
@@ -159,8 +159,8 @@ export class CandleService {
work,
coverage: {
requested: range ? [publicRange(range)] : [],
completeRanges: range ?
completeCoverage.flatMap((row) => {
completeRanges: range
? completeCoverage.flatMap((row) => {
const from = new Date(
Math.max(row.from.valueOf(), range.from.valueOf()),
);
@@ -168,22 +168,22 @@ export class CandleService {
Math.min(row.to.valueOf(), range.to.valueOf()),
);
return from < to ? [publicRange({ from, to })] : [];
}) :
[],
})
: [],
missingRanges: missing.map(publicRange),
asOf: fetchedAt?.toISOString() ?? null,
historyBoundary: timestamp(detail?.instrument?.first_1dayCandleDate) ?
'known' :
'unknown',
historyState: complete ?
'complete' :
work?.state === 'failed' ?
'failed' :
work?.state === 'running' ?
'loading' :
covered ?
'partial' :
'missing',
historyBoundary: timestamp(detail?.instrument?.first_1dayCandleDate)
? 'known'
: 'unknown',
historyState: complete
? 'complete'
: work?.state === 'failed'
? 'failed'
: work?.state === 'running'
? 'loading'
: covered
? 'partial'
: 'missing',
},
dependencies: [
detail.meta,
@@ -193,9 +193,9 @@ export class CandleService {
dataRevision: String(row.dataRevision),
availability: 'complete',
fetchedAt: row.fetchedAt,
freshForMs: row.isComplete ?
Number.POSITIVE_INFINITY :
tailFreshForMs,
freshForMs: row.isComplete
? Number.POSITIVE_INFINITY
: tailFreshForMs,
work: null,
}),
),
@@ -291,9 +291,9 @@ export class CandleService {
) {
const input = { uid, instrumentType, period: 'all' as const };
const range =
detail?.instrument && history ?
rangeFor(input, detail.instrument, now) :
null;
detail?.instrument && history
? rangeFor(input, detail.instrument, now)
: null;
if (
!range ||
candleGaps(
@@ -79,14 +79,14 @@ export class CatalogDiscoveryService {
async search(userId: string, input: CatalogQuery, observe = false) {
const discovery = await this.read(userId, input, observe);
const sources = discovery.data ?
await this.enrichment.readMany(
const sources = discovery.data
? await this.enrichment.readMany(
userId,
input.instrumentType,
discovery.data.map((item) => item.uid),
observe,
) :
[];
)
: [];
const dependencies = [
discovery.meta,
...sources.map((source) => source.meta),
@@ -114,22 +114,22 @@ export class CatalogDiscoveryService {
.map((dependency) => dependency.dataRevision)
.join('/'),
availability:
discovery.data === null ?
'missing' :
sources.every((source) => source.meta.availability === 'complete') ?
'complete' :
'partial',
fetchedAt: discovery.meta.lastSuccessfulAt ?
new Date(discovery.meta.lastSuccessfulAt) :
null,
discovery.data === null
? 'missing'
: sources.every((source) => source.meta.availability === 'complete')
? 'complete'
: 'partial',
fetchedAt: discovery.meta.lastSuccessfulAt
? new Date(discovery.meta.lastSuccessfulAt)
: null,
freshForMs: tbankSourceTtlMs.catalog,
work: discovery.meta.sync.id ?
{
work: discovery.meta.sync.id
? {
id: discovery.meta.sync.id,
state: discovery.meta.sync.state,
errorCode: discovery.meta.sync.errorCode,
} :
null,
}
: null,
dependencies,
}),
};
@@ -177,17 +177,17 @@ export class CatalogDiscoveryService {
.map((dependency) => dependency.dataRevision)
.join('/'),
availability: source.meta.availability,
fetchedAt: source.meta.lastSuccessfulAt ?
new Date(source.meta.lastSuccessfulAt) :
null,
fetchedAt: source.meta.lastSuccessfulAt
? new Date(source.meta.lastSuccessfulAt)
: null,
freshForMs: tbankSourceTtlMs.metadata,
work: source.meta.sync.id ?
{
work: source.meta.sync.id
? {
id: source.meta.sync.id,
state: source.meta.sync.state,
errorCode: source.meta.sync.errorCode,
} :
null,
}
: null,
dependencies,
}),
};
@@ -208,36 +208,36 @@ export class CatalogDiscoveryService {
if (lookup.data && lookup.data.length > UNAMBIGUOUS_MATCH_COUNT) {
throw new ConflictException({
code:
instrumentType === 'share' ?
'STOCK_TICKER_AMBIGUOUS' :
'BOND_TICKER_AMBIGUOUS',
instrumentType === 'share'
? 'STOCK_TICKER_AMBIGUOUS'
: 'BOND_TICKER_AMBIGUOUS',
});
}
const result = lookup.data ?
await this.detail(userId, instrumentType, lookup.data[0].uid, observe) :
null;
const result = lookup.data
? await this.detail(userId, instrumentType, lookup.data[0].uid, observe)
: null;
const dependencies = [lookup.meta, ...(result ? [result.meta] : [])];
return {
data: result?.data ?
{ ...result.data, slug: ticker.trim().toUpperCase() } :
null,
data: result?.data
? { ...result.data, slug: ticker.trim().toUpperCase() }
: null,
meta: investmentResourceMeta({
resourceKey: `${instrumentType}/ticker/${ticker.trim().toUpperCase()}`,
dataRevision: dependencies
.map((dependency) => dependency.dataRevision)
.join('/'),
availability: result?.meta.availability ?? 'missing',
fetchedAt: lookup.meta.lastSuccessfulAt ?
new Date(lookup.meta.lastSuccessfulAt) :
null,
fetchedAt: lookup.meta.lastSuccessfulAt
? new Date(lookup.meta.lastSuccessfulAt)
: null,
freshForMs: tbankSourceTtlMs.metadata,
work: lookup.meta.sync.id ?
{
work: lookup.meta.sync.id
? {
id: lookup.meta.sync.id,
state: lookup.meta.sync.state,
errorCode: lookup.meta.sync.errorCode,
} :
null,
}
: null,
dependencies,
}),
};
@@ -334,19 +334,19 @@ export class CatalogDiscoveryService {
const token = await this.access.tokenForAttempt(attempt);
if (token) {
const found =
input.instrumentType === 'share' ?
await this.tbank.findShares(token, input.query) :
await this.tbank.findBonds(token, input.query);
input.instrumentType === 'share'
? await this.tbank.findShares(token, input.query)
: await this.tbank.findBonds(token, input.query);
success = await this.repository.publish(
attempt,
input,
selectCatalogInstruments(
input.exactTicker ?
found.filter(
input.exactTicker
? found.filter(
(item) =>
item.ticker?.trim().toUpperCase() === input.query,
) :
found,
)
: found,
input.query,
input.instrumentType,
).slice(
@@ -124,9 +124,9 @@ export class CatalogEnrichmentService {
range,
);
}
const work = inputs.length ?
await this.repository.workMany(userId, revision, inputs) :
[];
const work = inputs.length
? await this.repository.workMany(userId, revision, inputs)
: [];
const byScope = new Map(work.map((row) => [row.scopeKey, row]));
return inputs.map((input) => {
const row = rows.find(
@@ -136,16 +136,16 @@ export class CatalogEnrichmentService {
const payload = row?.payload as CatalogPayload | undefined;
return {
input,
version: row ?
{
version: row
? {
instrumentUid: row.instrumentUid,
source: row.source,
kind: row.kind,
rangeKey: row.rangeKey,
dataRevision: row.dataRevision,
fetchedAt: row.fetchedAt,
} :
null,
}
: null,
payload,
instrument: payload?.instrument ?? null,
price: payload?.price ?? null,
@@ -177,9 +177,9 @@ export class CatalogEnrichmentService {
) {
const inputs = sourceInputs(instrumentType, uids, kinds, range);
const reservation = this.reservations.then(async () => {
const rows = inputs.length ?
await this.repository.readSources(inputs) :
[];
const rows = inputs.length
? await this.repository.readSources(inputs)
: [];
const ids: string[] = [];
for (const input of inputs) {
const row = rows.find(
@@ -242,18 +242,18 @@ export class CatalogEnrichmentService {
observe = false,
) {
const kinds: CatalogSource['kind'][] =
section === 'company' ?
['detail', 'description', 'fundamentals'] :
[
...(instrumentType === 'bond' ?
['bond-current' as const] :
['detail' as const]),
section === 'company'
? ['detail', 'description', 'fundamentals']
: [
...(instrumentType === 'bond'
? ['bond-current' as const]
: ['detail' as const]),
'last-price',
'trading-status',
'comparison',
...(section === 'metrics' ?
['coupons' as const, 'yield' as const, 'rating' as const] :
[]),
...(section === 'metrics'
? ['coupons' as const, 'yield' as const, 'rating' as const]
: []),
];
const sources = await this.readMany(
userId,
@@ -279,18 +279,18 @@ export class CatalogEnrichmentService {
dataRevision: dependencies
.map((dependency) => dependency.dataRevision)
.join('/'),
availability: !first.instrument ?
'missing' :
complete ?
'complete' :
'partial',
fetchedAt: first.meta.lastSuccessfulAt ?
new Date(first.meta.lastSuccessfulAt) :
null,
availability: !first.instrument
? 'missing'
: complete
? 'complete'
: 'partial',
fetchedAt: first.meta.lastSuccessfulAt
? new Date(first.meta.lastSuccessfulAt)
: null,
freshForMs:
section === 'company' ?
tbankSourceTtlMs.company :
tbankSourceTtlMs.quote,
section === 'company'
? tbankSourceTtlMs.company
: tbankSourceTtlMs.quote,
work: null,
dependencies,
});
@@ -315,9 +315,9 @@ export class CatalogEnrichmentService {
): Promise<CatalogPayload> {
if (input.kind === 'detail' || input.kind === 'bond-current') {
const instrument =
input.instrumentType === 'share' ?
await this.tbank.getShare(token, input.uid) :
await this.tbank.getBond(token, input.uid);
input.instrumentType === 'share'
? await this.tbank.getShare(token, input.uid)
: await this.tbank.getBond(token, input.uid);
if (instrument && instrument.uid !== input.uid) {
throw new Error('Catalog UID mismatch');
}
@@ -400,9 +400,9 @@ export class CatalogEnrichmentService {
}
const details = await this.repository.readSources([
{ ...input, kind: 'detail', rangeKey: undefined },
...(input.instrumentType === 'bond' ?
[{ ...input, kind: 'bond-current' as const, rangeKey: undefined }] :
[]),
...(input.instrumentType === 'bond'
? [{ ...input, kind: 'bond-current' as const, rangeKey: undefined }]
: []),
]);
const instrument = details
.map((row) => (row.payload as CatalogPayload).instrument)
@@ -415,36 +415,36 @@ export class CatalogEnrichmentService {
}
if (input.kind === 'description') {
return {
description: instrument?.assetUid ?
await this.tbank.getAssetDescription(token, instrument?.assetUid) :
null,
description: instrument?.assetUid
? await this.tbank.getAssetDescription(token, instrument?.assetUid)
: null,
};
}
if (input.kind === 'fundamentals') {
return {
fundamentals: instrument?.assetUid ?
await this.tbank.getFundamentals(token, instrument?.assetUid) :
null,
fundamentals: instrument?.assetUid
? await this.tbank.getFundamentals(token, instrument?.assetUid)
: null,
};
}
if (input.kind === 'coupons' || input.kind === 'dividends') {
const year = Number(input.rangeKey);
const [from, to] = input.rangeKey?.startsWith('[') ?
(JSON.parse(input.rangeKey) as [string, string]) :
[
const [from, to] = input.rangeKey?.startsWith('[')
? (JSON.parse(input.rangeKey) as [string, string])
: [
`${year - payoutHistoryYears}-01-01`,
`${year + payoutFutureYears}-01-01`,
];
return input.kind === 'coupons' ?
{
return input.kind === 'coupons'
? {
coupons: await this.tbank.getBondCoupons(
token,
input.uid,
from,
to,
),
} :
{
}
: {
dividends: await this.tbank.getDividends(
token,
input.uid,
@@ -461,20 +461,20 @@ export class CatalogEnrichmentService {
if (input.kind === 'yield') {
return {
effectiveYield:
instrument?.ticker && instrument.isin && instrument.classCode ?
await this.moex.bondYield(
instrument?.ticker && instrument.isin && instrument.classCode
? await this.moex.bondYield(
instrument.ticker,
instrument.isin,
instrument.classCode,
) :
null,
)
: null,
};
}
if (input.kind === 'rating') {
return {
creditRating: instrument?.isin ?
await this.ratings.rating(instrument.isin) :
null,
creditRating: instrument?.isin
? await this.ratings.rating(instrument.isin)
: null,
};
}
throw new Error('Unsupported catalog source');
@@ -533,17 +533,17 @@ export class CatalogEnrichmentService {
prices?.find((entry) => entry.instrumentUid === item.input.uid) ??
null;
const value =
item.input.kind === 'last-price' ?
{
price: price ?
{
item.input.kind === 'last-price'
? {
price: price
? {
instrumentUid: item.input.uid,
price: price.price,
time: price.time,
} :
null,
} :
payload;
}
: null,
}
: payload;
// Keep accepting observers while publication awaits SQLite.
item.observers.unshift({ attempt: item.attempt, input: item.input });
for (const observer of item.observers) {
@@ -596,17 +596,17 @@ function sourceInputs(
uid,
kind,
// eslint-disable-next-line security/detect-object-injection -- Numeric queue indexes and validated instrument UIDs select internal data only.
...(kind === 'bond-events' && (range?.tickers?.[uid] || range?.ticker) ?
...(kind === 'bond-events' && (range?.tickers?.[uid] || range?.ticker)
// eslint-disable-next-line security/detect-object-injection -- Numeric queue indexes and validated instrument UIDs select internal data only.
{ ticker: range?.tickers?.[uid] || range?.ticker } :
{}),
...(['coupons', 'dividends'].includes(kind) ?
{
rangeKey: range ?
JSON.stringify([range.from, range.to]) :
String(new Date().getUTCFullYear()),
} :
{}),
? { ticker: range?.tickers?.[uid] || range?.ticker }
: {}),
...(['coupons', 'dividends'].includes(kind)
? {
rangeKey: range
? JSON.stringify([range.from, range.to])
: String(new Date().getUTCFullYear()),
}
: {}),
})),
);
}
@@ -679,9 +679,9 @@ function sourcePolicy(kind: CatalogSource['kind']) {
}
function producerKey(input: CatalogSource) {
return input.instrumentType === 'bond' &&
(input.kind === 'detail' || input.kind === 'bond-current') ?
`bond-metadata:${input.uid}` :
catalogScope(input);
(input.kind === 'detail' || input.kind === 'bond-current')
? `bond-metadata:${input.uid}`
: catalogScope(input);
}
const quotationScale = 9;
@@ -9,9 +9,9 @@ export function selectCatalogInstruments<T extends TbankShare>(
): (T & { uid: string })[] {
const seen = new Set<string>();
const kind =
instrumentType === 'share' ?
'INSTRUMENT_TYPE_SHARE' :
'INSTRUMENT_TYPE_BOND';
instrumentType === 'share'
? 'INSTRUMENT_TYPE_SHARE'
: 'INSTRUMENT_TYPE_BOND';
const preferredClass = instrumentType === 'share' ? 'TQBR' : 'TQOB';
return found
.filter(
@@ -29,9 +29,9 @@ export function selectCatalogInstruments<T extends TbankShare>(
a.uid.localeCompare(b.uid),
)
.filter((instrument) => {
const key = instrument.isin?.trim() ?
`isin:${instrument.isin.trim().toUpperCase()}` :
`uid:${instrument.uid}`;
const key = instrument.isin?.trim()
? `isin:${instrument.isin.trim().toUpperCase()}`
: `uid:${instrument.uid}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
@@ -10,9 +10,9 @@ 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 > MIN_TIMESTAMP_SECONDS_EXCLUSIVE ?
new Date(seconds * MILLISECONDS_PER_SECOND).toISOString() :
null;
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)
@@ -43,8 +43,8 @@ export function instrumentPosition(
currentValue,
absoluteReturn,
averagePurchasePrice:
positive && totalCost ?
{
positive && totalCost
? {
currency: totalCost.currency,
value: formatDecimal(
divide(
@@ -53,15 +53,15 @@ export function instrumentPosition(
PRICE_DECIMAL_PLACES,
),
),
} :
null,
}
: null,
returnPercent: positive ? percent(absoluteReturn, totalCost) : null,
accounts: positions.map((p) => ({
...p,
returnPercent:
decimalFromString(p.quantity).value > ZERO_QUANTITY ?
percent(p.absoluteReturn, cost(p)) :
null,
decimalFromString(p.quantity).value > ZERO_QUANTITY
? percent(p.absoluteReturn, cost(p))
: null,
})),
isStale: portfolio.isStale,
observedAt: portfolio.lastSuccessfulAt,
@@ -69,8 +69,8 @@ export function instrumentPosition(
}
function cost(position: PortfolioPositionDto): MoneyDto | null {
const price = position.averagePurchasePrice;
return price ?
{
return price
? {
currency: price.currency,
value: formatDecimal(
multiply(
@@ -78,8 +78,8 @@ function cost(position: PortfolioPositionDto): MoneyDto | null {
decimalFromString(position.quantity),
),
),
} :
null;
}
: null;
}
function sumComplete(values: (MoneyDto | null)[]): MoneyDto | null {
const currency = values[0]?.currency;
@@ -37,18 +37,18 @@ export class BondCatalogService {
data:
result.data?.map((item) => {
const short = toBond(item);
const detail = item.detail ?
toBond({ ...item.detail, uid: item.uid }) :
short;
const detail = item.detail
? toBond({ ...item.detail, uid: item.uid })
: short;
const last = item.price;
return {
...detail,
classCode: detail.classCode ?? short.classCode,
isin: detail.isin ?? short.isin,
apiTradeAvailable: short.apiTradeAvailable,
pricePercent: last?.price ?
formatDecimal(decimal(last.price)) :
null,
pricePercent: last?.price
? formatDecimal(decimal(last.price))
: null,
priceAt: timestamp(last?.time),
};
}) ?? null,
@@ -63,9 +63,9 @@ export class BondCatalogService {
const result = await this.discovery.detail(userId, 'bond', uid, observe);
return {
...result,
data: result.data ?
{ ...toBond(result.data), slug: result.data.slug } :
null,
data: result.data
? { ...toBond(result.data), slug: result.data.slug }
: null,
};
}
@@ -82,9 +82,9 @@ export class BondCatalogService {
);
return {
...result,
data: result.data ?
{ ...toBond(result.data), slug: result.data.slug } :
null,
data: result.data
? { ...toBond(result.data), slug: result.data.slug }
: null,
};
}
}
@@ -62,9 +62,9 @@ export class BondIncomeService {
observe,
);
return {
data: result.values.instrument ?
bondMetrics(result.values, result.unavailable, result.observedAt) :
null,
data: result.values.instrument
? bondMetrics(result.values, result.unavailable, result.observedAt)
: null,
meta: result.meta,
};
}
@@ -99,18 +99,18 @@ export class BondIncomeService {
throw new NotFoundException();
}
const events = sources[1].payload?.coupons;
const position = portfolio.data ?
instrumentPosition(portfolio.data, uid, 'BOND') :
null;
const projected = events ?
bondCoupons(
const position = portfolio.data
? instrumentPosition(portfolio.data, uid, 'BOND')
: null;
const projected = events
? bondCoupons(
events,
position,
{ from, to, today },
sources[1].meta.lastSuccessfulAt ?? new Date().toISOString(),
!portfolio.data,
) :
undefined;
)
: undefined;
const result = await this.forecasts.result({
userId,
revision: connection.revision,
@@ -121,9 +121,9 @@ export class BondIncomeService {
candidate:
portfolio.data && sources[0].instrument ? projected : undefined,
});
return result.data || !projected ?
result :
{
return result.data || !projected
? result
: {
data: bondCoupons(
events ?? [],
null,
@@ -146,19 +146,19 @@ export function bondCoupons(
const { from, to, today } = window;
const quantity =
position?.accounts.length &&
position.accounts.every((p) => decimalFromString(p.quantity).value > ZERO_QUANTITY) ?
decimalFromString(position.quantity) :
null;
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(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) :
paymentDate >= today ?
('upcoming' as const) :
('past' as const);
const timing = !paymentDate
? ('undated' as const)
: paymentDate >= today
? ('upcoming' as const)
: ('past' as const);
return {
number: event.couponNumber ?? null,
paymentDate,
@@ -172,14 +172,14 @@ export function bondCoupons(
perBond &&
timing === 'upcoming' &&
recordDate &&
recordDate >= today ?
{
recordDate >= today
? {
currency: perBond.currency,
value: formatDecimal(
multiply(quantity, decimalFromString(perBond.value)),
),
} :
null,
}
: null,
};
})
.sort((a, b) => (a.paymentDate ?? '').localeCompare(b.paymentDate ?? ''));
@@ -225,8 +225,8 @@ export function bondMetrics(
clean &&
coupon.currency === clean.currency &&
decimalFromString(coupon.value).value >= ZERO_AMOUNT &&
decimalFromString(clean.value).value > ZERO_AMOUNT ?
formatDecimal(
decimalFromString(clean.value).value > ZERO_AMOUNT
? formatDecimal(
divide(
multiply(
decimalFromString(coupon.value),
@@ -238,14 +238,14 @@ export function bondMetrics(
),
COUPON_YIELD_DECIMAL_PLACES,
),
) :
null;
)
: null;
const couponType =
next?.couponType && next.couponType !== 'COUPON_TYPE_UNSPECIFIED' ?
next.couponType :
instrument.floatingCouponFlag === true ?
'COUPON_TYPE_FLOATING' :
null;
next?.couponType && next.couponType !== 'COUPON_TYPE_UNSPECIFIED'
? next.couponType
: instrument.floatingCouponFlag === true
? 'COUPON_TYPE_FLOATING'
: null;
return {
creditRating,
coupon,
@@ -48,9 +48,9 @@ export class BondMarketService {
observe,
);
return {
data: result.values.instrument ?
bondQuote(result.values, result.unavailable, result.observedAt) :
null,
data: result.values.instrument
? bondQuote(result.values, result.unavailable, result.observedAt)
: null,
meta: result.meta,
};
}
@@ -83,8 +83,8 @@ export function bondQuote(
const cleanPrice =
pricePercent !== null &&
nominal &&
decimalFromString(nominal.value).value > ZERO_AMOUNT ?
{
decimalFromString(nominal.value).value > ZERO_AMOUNT
? {
currency: nominal.currency,
value: formatDecimal(
divide(
@@ -96,11 +96,11 @@ export function bondQuote(
PRICE_DECIMAL_PLACES,
),
),
} :
null;
}
: null;
const dirtyPrice =
cleanPrice && aci && aci.currency === cleanPrice.currency ?
{
cleanPrice && aci && aci.currency === cleanPrice.currency
? {
currency: cleanPrice.currency,
value: formatDecimal(
add(
@@ -108,8 +108,8 @@ export function bondQuote(
decimalFromString(aci.value),
),
),
} :
null;
}
: null;
return {
pricePercent,
cleanPrice,
@@ -118,9 +118,9 @@ export function bondQuote(
priceAt,
tradingStatus,
changePercentagePoints:
last?.price && previous?.close ?
formatDecimal(subtract(decimal(last.price), decimal(previous.close))) :
null,
last?.price && previous?.close
? formatDecimal(subtract(decimal(last.price), decimal(previous.close)))
: null,
previousCloseDate: timestamp(previous?.time)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
unavailable,
observedAt,
@@ -123,9 +123,9 @@ describe('bond financial projections', () => {
{
couponDate: time('2026-10-10'),
payOneBond:
scenario === 'unknown' ?
undefined :
money('30', scenario === 'currency' ? 'usd' : 'rub'),
scenario === 'unknown'
? undefined
: money('30', scenario === 'currency' ? 'usd' : 'rub'),
couponPeriod: scenario === 'period' ? 0 : 90,
},
],
@@ -4,7 +4,7 @@ import { type TbankMoney } from '../../../../../integrations/tbank/tbank.api';
export function money(value?: TbankMoney) {
const currency = value?.currency?.toUpperCase();
return value && currency ?
{ currency, value: formatDecimal(decimal(value)) } :
null;
return value && currency
? { currency, value: formatDecimal(decimal(value)) }
: null;
}
@@ -38,9 +38,9 @@ export function candleGaps(
gaps = gaps.flatMap((gap) => {
if (covered.to <= gap.from || covered.from >= gap.to) return [gap];
return [
...(covered.from > gap.from ?
[{ from: gap.from, to: covered.from }] :
[]),
...(covered.from > gap.from
? [{ from: gap.from, to: covered.from }]
: []),
...(covered.to < gap.to ? [{ from: covered.to, to: gap.to }] : []),
];
});
@@ -100,14 +100,14 @@ export class CandleRepository {
priority: nextPriority,
intent: {
...previousIntent,
...(intent.force ?
{
...(intent.force
? {
force: true,
...(intent.now && intent.now > previousNow ?
{ now: intent.now } :
{}),
} :
{}),
...(intent.now && intent.now > previousNow
? { now: intent.now }
: {}),
}
: {}),
},
},
});
@@ -263,12 +263,12 @@ export class CandleRepository {
// Preserve coverage outside the requested reconciliation boundaries.
for (const previous of old) {
for (const part of [
...(previous.from < range.from ?
[{ from: previous.from, to: range.from }] :
[]),
...(previous.to > range.to ?
[{ from: range.to, to: previous.to }] :
[]),
...(previous.from < range.from
? [{ from: previous.from, to: range.from }]
: []),
...(previous.to > range.to
? [{ from: range.to, to: previous.to }]
: []),
]) {
await tx.tbankCandleCoverage.create({
data: { ...previous, ...part },
@@ -66,8 +66,8 @@ export class CatalogDiscoveryRepository {
read(input: CatalogQuery) {
return this.prisma.$transaction(async (tx) => {
const alias = input.exactTicker ?
await tx.tbankInstrumentLookup.findUnique({
const alias = input.exactTicker
? await tx.tbankInstrumentLookup.findUnique({
where: {
provider_identifierType_identifier: {
provider,
@@ -75,17 +75,17 @@ export class CatalogDiscoveryRepository {
identifier: input.query,
},
},
}) :
null;
const discovery = input.exactTicker ?
alias ?
{
})
: null;
const discovery = input.exactTicker
? alias
? {
instrumentUids: alias.matchedUids,
fetchedAt: alias.fetchedAt,
dataRevision: alias.dataRevision,
} :
null :
await tx.tbankCatalogDiscovery.findUnique({
}
: null
: await tx.tbankCatalogDiscovery.findUnique({
where: {
provider_instrumentType_query: {
provider,
@@ -134,9 +134,9 @@ export class CatalogDiscoveryRepository {
kind: input.kind,
source: sourceProvider(input),
rangeKey:
input.kind === 'detail' ?
input.instrumentType :
(input.rangeKey ?? ''),
input.kind === 'detail'
? input.instrumentType
: (input.rangeKey ?? ''),
})),
},
});
@@ -232,9 +232,9 @@ export class CatalogDiscoveryRepository {
increment: isDeepStrictEqual(
previousAlias?.matchedUids,
matchedUids,
) ?
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
)
? UNCHANGED_REVISION_INCREMENT
: REVISION_INCREMENT,
},
},
});
@@ -246,9 +246,9 @@ export class CatalogDiscoveryRepository {
source: sourceProvider(input),
kind: input.kind,
rangeKey:
input.kind === 'detail' ?
input.instrumentType :
(input.rangeKey ?? ''),
input.kind === 'detail'
? input.instrumentType
: (input.rangeKey ?? ''),
};
const previous = await tx.tbankInstrumentSource.findUnique({
where: { provider_instrumentUid_source_kind_rangeKey: key },
@@ -359,9 +359,9 @@ export class CatalogDiscoveryRepository {
instrumentKind: instrument.instrumentKind,
apiTradeAvailableFlag: instrument.apiTradeAvailableFlag,
currency: instrument.currency,
brand: instrument.brand ?
{ logoName: instrument.brand.logoName } :
undefined,
brand: instrument.brand
? { logoName: instrument.brand.logoName }
: undefined,
}),
) as Prisma.InputJsonObject;
await tx.tbankInstrument.upsert({
@@ -418,9 +418,9 @@ export class CatalogDiscoveryRepository {
increment: isDeepStrictEqual(
previous?.matchedUids,
instrumentUids,
) ?
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
)
? UNCHANGED_REVISION_INCREMENT
: REVISION_INCREMENT,
},
},
});
@@ -444,9 +444,9 @@ export class CatalogDiscoveryRepository {
increment: isDeepStrictEqual(
previous?.instrumentUids,
instrumentUids,
) ?
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
)
? UNCHANGED_REVISION_INCREMENT
: REVISION_INCREMENT,
},
},
});
@@ -480,9 +480,9 @@ export class CatalogDiscoveryRepository {
}
function sourceProvider(input: CatalogSource) {
return input.kind === 'yield' || input.kind === 'bond-events' ?
'moex' :
input.kind === 'rating' ?
'ncr' :
'tbank';
return input.kind === 'yield' || input.kind === 'bond-events'
? 'moex'
: input.kind === 'rating'
? 'ncr'
: 'tbank';
}
@@ -23,8 +23,8 @@ export class GetStockCompanyUseCase {
observe,
);
return {
data: result.values.instrument ?
{
data: result.values.instrument
? {
description: result.values.description ?? null,
metrics: metrics(
result.values.fundamentals ?? null,
@@ -32,8 +32,8 @@ export class GetStockCompanyUseCase {
),
unavailable: result.unavailable,
observedAt: result.observedAt,
} :
null,
}
: null,
meta: result.meta,
};
}
@@ -51,22 +51,22 @@ export function metrics(
][] = [
['marketCapitalization', 'money', null],
['peRatioTtm', 'ratio', 'TTM'],
...(bank ?
([
...(bank
? ([
['priceToBookTtm', 'ratio', 'TTM'],
['roe', 'percent', null],
] as typeof selection) :
([
] as typeof selection)
: ([
['revenueTtm', 'money', 'TTM'],
['ebitdaTtm', 'money', 'TTM'],
] as typeof selection)),
['netIncomeTtm', 'money', 'TTM'],
...(!bank ?
([
...(!bank
? ([
['totalDebtMrq', 'money', 'MRQ'],
['netDebtToEbitda', 'ratio', null],
] as typeof selection) :
[]),
] as typeof selection)
: []),
];
return selection.flatMap(([key, unit, period]) => {
// eslint-disable-next-line security/detect-object-injection -- Keys come from the fixed metric selection declared above.
@@ -74,8 +74,8 @@ export function metrics(
const currency = values.currency?.toUpperCase() || null;
return typeof value === 'number' &&
Number.isFinite(value) &&
(unit !== 'money' || currency) ?
[
(unit !== 'money' || currency)
? [
{
key,
value,
@@ -83,7 +83,7 @@ export function metrics(
period,
currency: unit === 'money' ? currency : null,
},
] :
[];
]
: [];
});
}
@@ -34,9 +34,9 @@ export class StockCatalogService {
data:
result.data?.map((item) => {
const short = stock(item);
const detail = item.detail ?
stock({ ...item.detail, uid: item.uid }) :
short;
const detail = item.detail
? stock({ ...item.detail, uid: item.uid })
: short;
const last = item.price;
return {
...detail,
@@ -44,12 +44,12 @@ export class StockCatalogService {
isin: detail.isin ?? short.isin,
apiTradeAvailable: short.apiTradeAvailable,
price:
last?.price && detail.currency ?
{
last?.price && detail.currency
? {
currency: detail.currency,
value: formatDecimal(decimal(last.price)),
} :
null,
}
: null,
priceAt: timestamp(last?.time),
};
}) ?? null,
@@ -64,9 +64,9 @@ export class StockCatalogService {
const result = await this.discovery.detail(userId, 'share', uid, observe);
return {
...result,
data: result.data ?
{ ...stock(result.data), slug: result.data.slug } :
null,
data: result.data
? { ...stock(result.data), slug: result.data.slug }
: null,
};
}
@@ -83,9 +83,9 @@ export class StockCatalogService {
);
return {
...result,
data: result.data ?
{ ...stock(result.data), slug: result.data.slug } :
null,
data: result.data
? { ...stock(result.data), slug: result.data.slug }
: null,
};
}
}
@@ -69,18 +69,18 @@ export class StockIncomeService {
throw new NotFoundException();
}
const events = sources[1].payload?.dividends;
const position = portfolio.data ?
instrumentPosition(portfolio.data, uid, 'STOCK') :
null;
const projected = events ?
stockDividends(
const position = portfolio.data
? instrumentPosition(portfolio.data, uid, 'STOCK')
: null;
const projected = events
? stockDividends(
events,
position,
{ from, to, today },
sources[1].meta.lastSuccessfulAt ?? new Date().toISOString(),
!portfolio.data,
) :
undefined;
)
: undefined;
const result = await this.forecasts.result({
userId,
revision: connection.revision,
@@ -91,9 +91,9 @@ export class StockIncomeService {
candidate:
portfolio.data && sources[0].instrument ? projected : undefined,
});
return result.data || !projected ?
result :
{
return result.data || !projected
? result
: {
data: stockDividends(
events ?? [],
null,
@@ -117,9 +117,9 @@ export function stockDividends(
const quantity =
position &&
position.accounts.length &&
position.accounts.every((p) => decimalFromString(p.quantity).value > ZERO_QUANTITY) ?
decimalFromString(position.quantity) :
null;
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) => {
@@ -128,29 +128,29 @@ export function stockDividends(
const lastBuyDate = timestamp(event.lastBuyDate)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null;
const currency = event.dividendNet?.currency?.toUpperCase();
const perShare =
currency && event.dividendNet ?
{ currency, value: formatDecimal(decimal(event.dividendNet)) } :
null;
currency && event.dividendNet
? { currency, value: formatDecimal(decimal(event.dividendNet)) }
: null;
// A future payment can have an already-fixed register: keep the event, but do not estimate entitlement from today's holding.
const eventDate = paymentDate ?? recordDate;
const timing: StockDividendDto['timing'] = eventDate ?
eventDate >= today ?
'upcoming' :
'past' :
'undated';
const timing: StockDividendDto['timing'] = eventDate
? eventDate >= today
? 'upcoming'
: 'past'
: 'undated';
const estimatedAmount =
quantity &&
perShare &&
recordDate &&
recordDate >= today &&
timing === 'upcoming' ?
{
timing === 'upcoming'
? {
currency: perShare.currency,
value: formatDecimal(
multiply(quantity, decimalFromString(perShare.value)),
),
} :
null;
}
: null;
return {
perShare,
recordDate,
@@ -44,9 +44,9 @@ export class StockMarketService {
observe,
);
return {
data: result.values.instrument ?
stockQuote(result.values, result.unavailable, result.observedAt) :
null,
data: result.values.instrument
? stockQuote(result.values, result.unavailable, result.observedAt)
: null,
meta: result.meta,
};
}
@@ -75,20 +75,20 @@ export function stockQuote(
const priceAt = timestamp(last?.time);
const currency = share?.currency?.toUpperCase();
const price =
last?.price && currency ?
{ currency, value: formatDecimal(decimal(last.price)) } :
null;
last?.price && currency
? { currency, value: formatDecimal(decimal(last.price)) }
: null;
const previousPrice =
previous?.close && currency ?
{ currency, value: formatDecimal(decimal(previous.close)) } :
null;
previous?.close && currency
? { currency, value: formatDecimal(decimal(previous.close)) }
: null;
const difference =
price && previousPrice ?
subtract(
price && previousPrice
? subtract(
decimalFromString(price.value),
decimalFromString(previousPrice.value),
) :
null;
)
: null;
return {
price,
priceAt,
@@ -96,21 +96,21 @@ export function stockQuote(
previousCloseDate: timestamp(previous?.time)?.slice(ISO_DATE_START, ISO_DATE_LENGTH) ?? null,
previousClose: previousPrice,
change:
difference && currency ?
{ currency, value: formatDecimal(difference) } :
null,
difference && currency
? { currency, value: formatDecimal(difference) }
: null,
changePercent:
difference &&
previousPrice &&
decimalFromString(previousPrice.value).value > ZERO_AMOUNT ?
formatDecimal(
decimalFromString(previousPrice.value).value > ZERO_AMOUNT
? formatDecimal(
divide(
multiply(difference, decimalFromString('100')),
decimalFromString(previousPrice.value),
CHANGE_PERCENT_DECIMAL_PLACES,
),
) :
null,
)
: null,
unavailable,
observedAt,
};
@@ -16,11 +16,11 @@ export function investmentResourceMeta(input: {
const ownSync = {
id: input.work?.id ?? null,
state:
input.work?.state === 'running' ?
('running' as const) :
input.work?.state === 'failed' ?
('failed' as const) :
('idle' as const),
input.work?.state === 'running'
? ('running' as const)
: input.work?.state === 'failed'
? ('failed' as const)
: ('idle' as const),
errorCode: input.work?.errorCode ?? null,
};
const states = [
@@ -34,11 +34,11 @@ export function investmentResourceMeta(input: {
states.find((state) => state.state === 'queued') ??
states.find((state) => state.state === 'failed') ??
ownSync;
const sourceFreshness = input.fetchedAt ?
Date.now() - input.fetchedAt.valueOf() < input.freshForMs ?
('fresh' as const) :
('stale' as const) :
('unknown' as const);
const sourceFreshness = input.fetchedAt
? Date.now() - input.fetchedAt.valueOf() < input.freshForMs
? ('fresh' as const)
: ('stale' as const)
: ('unknown' as const);
const freshness = [
sourceFreshness,
...(input.dependencies ?? [])
@@ -56,16 +56,16 @@ export function investmentResourceMeta(input: {
.sort();
const meta = {
resourceKey: input.resourceKey,
...(input.unavailableReason ?
{ unavailableReason: input.unavailableReason } :
{}),
...(input.unavailableReason
? { unavailableReason: input.unavailableReason }
: {}),
dataRevision: input.dataRevision,
availability: input.availability,
freshness: freshness.includes('unknown') ?
('unknown' as const) :
freshness.includes('stale') ?
('stale' as const) :
('fresh' as const),
freshness: freshness.includes('unknown')
? ('unknown' as const)
: freshness.includes('stale')
? ('stale' as const)
: ('fresh' as const),
lastSuccessfulAt: timestamps[0] ?? null,
sync,
...(input.coverage ? { coverage: input.coverage } : {}),
@@ -79,9 +79,9 @@ export function investmentResourceMeta(input: {
...meta,
// The requested window is identified by resourceKey. Its moving "now"
// boundary is not a change to saved coverage between status and GET.
coverage: meta.coverage ?
{ ...meta.coverage, requested: undefined } :
undefined,
coverage: meta.coverage
? { ...meta.coverage, requested: undefined }
: undefined,
}),
)
.digest('base64url'),
+43 -43
View File
@@ -84,16 +84,16 @@ export function planInvestmentScreen(
const unavailable: { reason: string; resourceKey: string }[] = [];
const operations = (from: Date, to: Date, instrumentUid?: string) => {
const targets = accounts.flatMap((account) =>
from < to ?
[
from < to
? [
{
accountId: account.accountId,
from,
to,
...(instrumentUid ? { instrumentUid } : {}),
},
] :
[],
]
: [],
);
sources.push(
...operationIntervals(targets).map((range) => ({
@@ -112,12 +112,12 @@ export function planInvestmentScreen(
})),
);
const knownOpening =
accounts.length && accounts.every((account) => account.openedOn) ?
new Date(
accounts.length && accounts.every((account) => account.openedOn)
? new Date(
// 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.
`${accounts.map((account) => account.openedOn ?? '').sort()[0]}T00:00:00+03:00`,
) :
undefined;
)
: undefined;
if (!context.membershipKnown) return { sources, unavailable, now };
if (
['account', 'accounts', 'bond', 'events', 'stock'].includes(screen.screen)
@@ -146,9 +146,9 @@ export function planInvestmentScreen(
);
for (const range of [
period.selected,
...(screen.screen === 'income' ?
[period.previous, ...(period.yearAgo ? [period.yearAgo] : [])] :
[]),
...(screen.screen === 'income'
? [period.previous, ...(period.yearAgo ? [period.yearAgo] : [])]
: []),
]) {
const bounds = dateOnlyBounds(range, now);
operations(bounds.from, bounds.to);
@@ -216,11 +216,11 @@ export function planInvestmentScreen(
}
const range =
screen.candleRange ??
(period === 'all' ?
instrument.historyFrom ?
{ from: instrument.historyFrom, to: now } :
null :
{ from, to: now });
(period === 'all'
? instrument.historyFrom
? { from: instrument.historyFrom, to: now }
: null
: { from, to: now });
if (range) {
sources.push({
...identity,
@@ -237,17 +237,17 @@ export function planInvestmentScreen(
}
if (['account', 'bond', 'events', 'stock'].includes(screen.screen)) {
const day =
screen.screen === 'stock' || screen.screen === 'bond' ?
moscowDate(now) :
now.toISOString().slice(ISO_DATE_START, ISO_DATE_LENGTH);
screen.screen === 'stock' || screen.screen === 'bond'
? moscowDate(now)
: now.toISOString().slice(ISO_DATE_START, ISO_DATE_LENGTH);
const from =
screen.screen === 'events' && screen.from ?
new Date(`${screen.from}T00:00:00Z`) :
new Date(`${day}T00:00:00Z`);
screen.screen === 'events' && screen.from
? new Date(`${screen.from}T00:00:00Z`)
: new Date(`${day}T00:00:00Z`);
const to =
screen.screen === 'events' && screen.to ?
new Date(`${screen.to}T00:00:00Z`) :
new Date(from);
screen.screen === 'events' && screen.to
? new Date(`${screen.to}T00:00:00Z`)
: new Date(from);
if (
!(screen.screen === 'events' && screen.to) &&
screen.screen !== 'stock' &&
@@ -309,9 +309,9 @@ export function accountHistory(
): OperationInterval[] {
return accounts.flatMap((account) => {
const from = new Date(
account.openedOn ?
`${account.openedOn}T00:00:00Z` :
'1970-01-01T00:00:00Z',
account.openedOn
? `${account.openedOn}T00:00:00Z`
: '1970-01-01T00:00:00Z',
);
return from < now ? [{ accountId: account.accountId, from, to: now }] : [];
});
@@ -324,22 +324,22 @@ export function ledgerRange(
accounts: Account[],
now: Date,
) {
const opening = accounts.every((account) => account.openedOn) ?
const opening = accounts.every((account) => account.openedOn)
// 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.
accounts.map((account) => account.openedOn ?? '').sort()[0] :
undefined;
const from = fromValue ?
new Date(fromValue) :
instrumentUid ?
new Date(opening ? `${opening}T00:00:00Z` : '1970-01-01T00:00:00Z') :
new Date(Date.UTC(now.getUTCFullYear(), JANUARY_INDEX, FIRST_DAY_OF_MONTH));
const end = toValue ?
new Date(
/^\d{4}-\d{2}-\d{2}$/.test(toValue) ?
`${toValue}T23:59:59.999Z` :
toValue,
) :
now;
? accounts.map((account) => account.openedOn ?? '').sort()[0]
: undefined;
const from = fromValue
? new Date(fromValue)
: instrumentUid
? new Date(opening ? `${opening}T00:00:00Z` : '1970-01-01T00:00:00Z')
: new Date(Date.UTC(now.getUTCFullYear(), JANUARY_INDEX, FIRST_DAY_OF_MONTH));
const end = toValue
? new Date(
/^\d{4}-\d{2}-\d{2}$/.test(toValue)
? `${toValue}T23:59:59.999Z`
: toValue,
)
: now;
return {
from,
to: new Date(Math.min(end.valueOf() + (toValue ? EXCLUSIVE_END_OFFSET_MS : NO_END_OFFSET_MS), now.valueOf())),
@@ -82,24 +82,24 @@ export class BrokerageOperationsSyncService {
...intervals.map((interval) => interval.to.valueOf()),
);
const targets = intervals.map((interval) =>
options.movingEnd && interval.to.valueOf() === movingTo ?
{ ...interval, to: now } :
interval,
options.movingEnd && interval.to.valueOf() === movingTo
? { ...interval, to: now }
: interval,
);
const plan = planOperationRead(targets, coverage, now, options);
const complete = plan.intervals.every(
(interval) => interval.availability === 'complete',
);
const handle =
!options.observe && plan.load.length ?
await this.request(
!options.observe && plan.load.length
? await this.request(
userId,
plan.load,
complete || Boolean(options.force),
'screen',
connection.revision,
) :
null;
)
: null;
if (!options.observe && !options.force && options.history) {
const accountIds = new Set(
intervals.map((interval) => interval.accountId),
@@ -267,9 +267,9 @@ export class BrokerageOperationsSyncService {
Math.ceil(interval.to.valueOf() / MILLISECONDS_PER_SECOND) + UPSTREAM_RANGE_MARGIN_SECONDS,
),
},
...(interval.instrumentUid ?
{ instrumentId: interval.instrumentUid } :
{}),
...(interval.instrumentUid
? { instrumentId: interval.instrumentUid }
: {}),
cursor,
limit: operationsPageSize,
state: 'OPERATION_STATE_EXECUTED',
@@ -86,13 +86,13 @@ export class BrokerageOperationsService {
const filterKey = JSON.stringify([
accountId,
from.toISOString(),
query.to ?
new Date(
/^\d{4}-\d{2}-\d{2}$/.test(query.to) ?
`${query.to}T23:59:59.999Z` :
query.to,
).toISOString() :
'now',
query.to
? new Date(
/^\d{4}-\d{2}-\d{2}$/.test(query.to)
? `${query.to}T23:59:59.999Z`
: query.to,
).toISOString()
: 'now',
query.instrumentUid ?? null,
categories,
limit,
@@ -129,9 +129,9 @@ export class BrokerageOperationsService {
source.priority === 'screen' &&
source.accountId &&
source.from &&
source.to ?
[{ accountId: source.accountId, from: source.from, to: source.to }] :
[],
source.to
? [{ accountId: source.accountId, from: source.from, to: source.to }]
: [],
);
if (query.sync !== 'observe') {
const discovery = await this.membership.request(
@@ -159,9 +159,9 @@ export class BrokerageOperationsService {
accountId === 'all' || account.accountId === accountId,
)
.flatMap((account) =>
currentRange.from < currentRange.to ?
[{ accountId: account.accountId, ...currentRange }] :
[],
currentRange.from < currentRange.to
? [{ accountId: account.accountId, ...currentRange }]
: [],
);
await this.sync.readPlan(userId, targets, {
movingEnd,
@@ -181,16 +181,16 @@ export class BrokerageOperationsService {
const saved = await this.repository.readPage(readInput);
const accounts = saved?.connection.accounts ?? [];
const targets = accounts.flatMap((account) =>
from < to ?
[
from < to
? [
{
accountId: account.accountId,
from,
to,
instrumentUid: query.instrumentUid,
},
] :
[],
]
: [],
);
const plan = planOperationRead(targets, saved?.coverage ?? [], now, {
movingEnd,
@@ -296,17 +296,17 @@ export class BrokerageOperationsService {
const meta = investmentResourceMeta({
resourceKey: `operations/${createHash('sha256').update(filterKey).digest('base64url')}`,
dataRevision: saved?.binding ?? 'missing',
availability: !available ?
'missing' :
complete ?
'complete' :
confirmed.length ?
'partial' :
'missing',
availability: !available
? 'missing'
: complete
? 'complete'
: confirmed.length
? 'partial'
: 'missing',
fetchedAt: lastSuccessfulAt,
freshForMs: plan.intervals.some((interval) => interval.stale) ?
STALE_FRESHNESS_MS :
Number.POSITIVE_INFINITY,
freshForMs: plan.intervals.some((interval) => interval.stale)
? STALE_FRESHNESS_MS
: Number.POSITIVE_INFINITY,
work: work ?? saved?.membershipWork ?? null,
dependencies: [membershipMeta, ...metadataDependencies],
coverage: {
@@ -320,8 +320,8 @@ export class BrokerageOperationsService {
(interval) =>
interval.accountId === account.accountId && interval.asOf,
),
) ?
new Date(
)
? new Date(
Math.min(
...accounts.map((account) =>
Math.max(
@@ -334,42 +334,42 @@ export class BrokerageOperationsService {
),
),
),
).toISOString() :
null,
).toISOString()
: null,
historyBoundary: knownBoundary ? 'known' : 'unknown',
historyState: saved?.work.some(
(work) => work.state === 'running' && work.priority > operationsSyncPriority.screen,
) ?
'loading' :
saved?.work.some(
)
? 'loading'
: saved?.work.some(
(work) => work.state === 'failed' && work.priority > operationsSyncPriority.screen,
) ?
'failed' :
knownBoundary &&
history.intervals.every(
(interval) => interval.availability === 'complete',
) ?
'complete' :
saved?.coverage.length ?
'partial' :
'missing',
)
? 'failed'
: knownBoundary &&
history.intervals.every(
(interval) => interval.availability === 'complete',
)
? 'complete'
: saved?.coverage.length
? 'partial'
: 'missing',
},
});
const last = rows.at(LAST_ROW_INDEX);
const hasNext = Boolean(saved && saved.rows.length > limit);
return {
data:
meta.availability === 'missing' ?
null :
{
meta.availability === 'missing'
? null
: {
accountId,
items: rows.map((row) => ({
...mapBrokerageOperation(
row.payload as TbankOperationItem,
row.occurredAt,
),
...(accountId === 'all' ?
{
...(accountId === 'all'
? {
account: {
id: row.accountId,
name:
@@ -377,8 +377,8 @@ export class BrokerageOperationsService {
(account) => account.accountId === row.accountId,
)?.name ?? row.accountId,
},
} :
{}),
}
: {}),
logoName:
instruments
.get(metadataKey(row.payload as TbankOperationItem))
@@ -386,16 +386,16 @@ export class BrokerageOperationsService {
})),
hasNext,
nextCursor:
hasNext && last && saved ?
this.encodeCursor({
hasNext && last && saved
? this.encodeCursor({
binding: saved.binding,
movingEnd,
to: to.toISOString(),
at: last.occurredAt.toISOString(),
accountId: last.accountId,
id: last.brokerOperationId,
}) :
null,
})
: null,
observedAt: now.toISOString(),
},
meta,
@@ -560,13 +560,13 @@ export class BrokerageOperationsService {
// eslint-disable-next-line security/detect-object-injection -- The numeric index enumerates the three comparison periods.
resourceKey: `income/${accountId}/${['selected', 'previous', 'yearAgo'][index]}/${period.key}`,
dataRevision: saved?.binding ?? 'missing',
availability: !known ?
'missing' :
complete ?
'complete' :
sources.length ?
'partial' :
'missing',
availability: !known
? 'missing'
: complete
? 'complete'
: sources.length
? 'partial'
: 'missing',
fetchedAt,
freshForMs: plan.intervals.some((range) => range.stale) ? STALE_FRESHNESS_MS : Infinity,
work,
@@ -579,8 +579,8 @@ export class BrokerageOperationsService {
confirmed,
).map(serialize),
asOf:
complete && targets.length ?
new Date(
complete && targets.length
? new Date(
Math.min(
...targets.map((target) =>
Math.max(
@@ -595,24 +595,24 @@ export class BrokerageOperationsService {
),
),
),
).toISOString() :
null,
).toISOString()
: null,
historyBoundary: (saved?.connection.accounts ?? []).every(
(account) => account.openedOn,
) ?
'known' :
'unknown',
historyState: complete ?
'complete' :
sources.length ?
'partial' :
'missing',
)
? 'known'
: 'unknown',
historyState: complete
? 'complete'
: sources.length
? 'partial'
: 'missing',
},
});
return {
meta,
items: complete ?
(saved?.rows ?? [])
items: complete
? (saved?.rows ?? [])
.filter((row) =>
plan.intervals.some(
(interval) =>
@@ -626,8 +626,8 @@ export class BrokerageOperationsService {
row.payload as TbankOperationItem,
row.occurredAt,
),
...(accountId === 'all' ?
{
...(accountId === 'all'
? {
account: {
id: row.accountId,
name:
@@ -635,10 +635,10 @@ export class BrokerageOperationsService {
(account) => account.accountId === row.accountId,
)?.name ?? row.accountId,
},
} :
{}),
})) :
null,
}
: {}),
}))
: null,
};
});
}
@@ -93,12 +93,12 @@ export function uncoveredOperationIntervals(
return [fragment];
}
return [
...(fragment.from < covered.from ?
[{ ...fragment, to: covered.from }] :
[]),
...(covered.to < fragment.to ?
[{ ...fragment, from: covered.to }] :
[]),
...(fragment.from < covered.from
? [{ ...fragment, to: covered.from }]
: []),
...(covered.to < fragment.to
? [{ ...fragment, from: covered.to }]
: []),
];
});
}
@@ -176,20 +176,20 @@ export function planOperationRead(
// Completeness is bounded by the observed end, never by the new wall clock.
return {
...effective,
availability: complete ?
('complete' as const) :
sources.length ?
('partial' as const) :
('missing' as const),
availability: complete
? ('complete' as const)
: sources.length
? ('partial' as const)
: ('missing' as const),
stale,
asOf: sources.length ?
new Date(
asOf: sources.length
? new Date(
Math.min(
effective.to.valueOf(),
Math.max(...sources.map((row) => row.asOf.valueOf())),
),
) :
null,
)
: null,
};
});
return { intervals, load: operationIntervals(load) };
@@ -61,14 +61,14 @@ export function mapBrokerageOperation(
logoName: null,
payment: money(operation.payment),
quantity:
(operationCategory(operation.type) === 'trade' ?
operation.quantityDone :
operation.quantity) === undefined ?
null :
String(
operationCategory(operation.type) === 'trade' ?
operation.quantityDone :
operation.quantity,
(operationCategory(operation.type) === 'trade'
? operation.quantityDone
: operation.quantity) === undefined
? null
: String(
operationCategory(operation.type) === 'trade'
? operation.quantityDone
: operation.quantity,
),
price: money(operation.price),
accruedInterest: money(operation.accruedInt),
@@ -82,9 +82,9 @@ function text(value: string | undefined): null | string {
function money(value: TbankMoney | undefined) {
const currency = value?.currency?.trim().toUpperCase();
return currency && value ?
{ currency, value: formatDecimal(decimal(value)) } :
null;
return currency && value
? { currency, value: formatDecimal(decimal(value)) }
: null;
}
export function operationTimestamp(
@@ -110,9 +110,9 @@ export class BrokerageOperationsRepository {
// 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.
const oldest = accounts.map((account) => account.openedOn).sort()[0];
const openedAt =
oldest && accounts.every((account) => account.openedOn) ?
new Date(`${oldest}T00:00:00+03:00`) :
undefined;
oldest && accounts.every((account) => account.openedOn)
? new Date(`${oldest}T00:00:00+03:00`)
: undefined;
const periods = input.periods?.(openedAt).map((period) => {
const now = new Date();
const bounds = dateOnlyBounds(period, now);
@@ -176,21 +176,21 @@ export class BrokerageOperationsRepository {
const rows = await tx.tbankOperation.findMany({
where: {
...scope,
...(periods ?
{
...(periods
? {
OR: periods.map((period) => ({
occurredAt: { gte: period.from, lt: period.to },
})),
} :
{ occurredAt: { gte: input.from, lt: input.to } }),
...(input.instrumentUid ?
{ instrumentUid: input.instrumentUid } :
{}),
...(input.categories.length ?
{ category: { in: input.categories } } :
{}),
...(after ?
{
}
: { occurredAt: { gte: input.from, lt: input.to } }),
...(input.instrumentUid
? { instrumentUid: input.instrumentUid }
: {}),
...(input.categories.length
? { category: { in: input.categories } }
: {}),
...(after
? {
OR: [
{ occurredAt: { lt: new Date(after.at) } },
{
@@ -203,8 +203,8 @@ export class BrokerageOperationsRepository {
brokerOperationId: { gt: after.id },
},
],
} :
{}),
}
: {}),
},
orderBy: [
{ occurredAt: 'desc' },
@@ -305,9 +305,9 @@ export class BrokerageOperationsRepository {
},
data: {
historyFrom: new Date(
account.openedOn ?
`${account.openedOn}T00:00:00Z` :
'1970-01-01T00:00:00Z',
account.openedOn
? `${account.openedOn}T00:00:00Z`
: '1970-01-01T00:00:00Z',
),
historyBoundary: account.openedOn ? 'known' : 'unknown',
},
@@ -383,42 +383,42 @@ export class BrokerageOperationsRepository {
) {
const pending = previousIntent?.intervals.map(decodeInterval) ?? [];
const additions =
previous.scopeKey === scopeKey ?
[] :
uncoveredOperationIntervals(intervals, pending);
previous.scopeKey === scopeKey
? []
: uncoveredOperationIntervals(intervals, pending);
const work = await tx.tbankSyncWork.update({
where: { id: previous.id },
data: {
intent: {
atomic: previousIntent?.atomic ?? publicationAtomic,
atomicIntervals: previousIntent?.atomic ?
[
atomicIntervals: previousIntent?.atomic
? [
...new Set([
...(previousIntent.atomicIntervals ??
previousIntent.intervals),
...additions.map(intervalKey),
]),
] :
[],
]
: [],
screenIntervals: [
...new Set([
...(previousIntent?.screenIntervals ??
(previous.priority === operationsSyncPriority.screen ?
(previousIntent?.intervals ?? []) :
[])),
...(priority === operationsSyncPriority.screen ?
[...pending, ...additions]
(previous.priority === operationsSyncPriority.screen
? (previousIntent?.intervals ?? [])
: [])),
...(priority === operationsSyncPriority.screen
? [...pending, ...additions]
.filter((item) =>
intervals.some((target) =>
operationIntervalsOverlap(item, target),
),
)
.map(intervalKey) :
[]),
.map(intervalKey)
: []),
]),
],
intervals: (priority < previous.priority ?
[
intervals: (priority < previous.priority
? [
...pending.filter((item) =>
intervals.some((target) =>
operationIntervalsOverlap(item, target),
@@ -431,8 +431,8 @@ export class BrokerageOperationsRepository {
operationIntervalsOverlap(item, target),
),
),
] :
[...pending, ...additions]
]
: [...pending, ...additions]
).map(intervalKey),
},
priority: Math.min(priority, previous.priority),
@@ -444,9 +444,9 @@ export class BrokerageOperationsRepository {
previous &&
previous.state !== 'complete' &&
previous.state !== 'superseded';
const retained = recovering ?
(previousIntent?.intervals.map(decodeInterval) ?? []) :
[];
const retained = recovering
? (previousIntent?.intervals.map(decodeInterval) ?? [])
: [];
// A bridge between two active groups must fence both producers before
// either can publish a subset of the newly combined result.
for (const joined of touching) {
@@ -469,23 +469,23 @@ export class BrokerageOperationsRepository {
await tx.tbankSyncStage.deleteMany({ where: { workId: previous.id } });
}
const targets =
recovering && !upgradeGroup && touching.length <= SINGLE_WORK_GROUP ?
previous?.scopeKey === scopeKey ?
retained :
[...retained, ...uncoveredOperationIntervals(intervals, retained)] :
operationIntervals([...retained, ...intervals]);
recovering && !upgradeGroup && touching.length <= SINGLE_WORK_GROUP
? previous?.scopeKey === scopeKey
? retained
: [...retained, ...uncoveredOperationIntervals(intervals, retained)]
: operationIntervals([...retained, ...intervals]);
const atomicTargets = [
...(publicationAtomic ? intervals : []),
...(recovering && previousIntent?.atomic ?
(previousIntent.atomicIntervals ?? previousIntent.intervals).map(
...(recovering && previousIntent?.atomic
? (previousIntent.atomicIntervals ?? previousIntent.intervals).map(
decodeInterval,
) :
[]),
)
: []),
...touching.flatMap((work) => {
const joined = work.intent as OperationsIntent;
return joined.atomic ?
(joined.atomicIntervals ?? joined.intervals).map(decodeInterval) :
[];
return joined.atomic
? (joined.atomicIntervals ?? joined.intervals).map(decodeInterval)
: [];
}),
];
const intent = {
@@ -506,9 +506,9 @@ export class BrokerageOperationsRepository {
)) ||
(
previousIntent?.screenIntervals ??
(previous?.priority === operationsSyncPriority.screen ?
(previousIntent?.intervals ?? []) :
[])
(previous?.priority === operationsSyncPriority.screen
? (previousIntent?.intervals ?? [])
: [])
).some((key) =>
operationIntervalsOverlap(target, decodeInterval(key)),
),
@@ -563,20 +563,20 @@ export class BrokerageOperationsRepository {
marker.payload as PageProgress,
]),
);
const group = intent.atomic ?
(intent.atomicIntervals ?? intent.intervals) :
intent.intervals;
const group = intent.atomic
? (intent.atomicIntervals ?? intent.intervals)
: intent.intervals;
const nextKey = group.find((key) => !progress.get(key)?.complete);
return {
priority: work.priority,
atomic: intent.atomic,
intervals: group.map(decodeInterval),
next: nextKey ?
{
next: nextKey
? {
interval: decodeInterval(nextKey),
cursor: progress.get(nextKey)?.cursor || undefined,
} :
null,
}
: null,
};
}
@@ -795,9 +795,9 @@ export class BrokerageOperationsRepository {
where: {
...scope,
occurredAt: { gte: interval.from, lt: interval.to },
...(interval.instrumentUid ?
{ instrumentUid: interval.instrumentUid } :
{}),
...(interval.instrumentUid
? { instrumentUid: interval.instrumentUid }
: {}),
},
});
const incoming = new Set(rows.map((row) => row.itemKey));
@@ -902,9 +902,9 @@ export class BrokerageOperationsRepository {
intent.screenIntervals ??
(work.priority === operationsSyncPriority.screen ? intent.intervals : [])
).includes(key),
) ?
operationsSyncPriority.screen :
operationsSyncPriority.history,
)
? operationsSyncPriority.screen
: operationsSyncPriority.history,
progress: { remainingIntervals: remaining.length },
},
});
@@ -36,8 +36,8 @@ export class ForecastService {
...input,
portfolioProjection: input.portfolio.dataRevision,
portfolios: (input.portfolio.dependencies ?? []).flatMap((dependency) =>
dependency.resourceKey.startsWith('portfolio/') ?
[
dependency.resourceKey.startsWith('portfolio/')
? [
{
accountId: decodeURIComponent(
dependency.resourceKey.slice('portfolio/'.length),
@@ -46,21 +46,21 @@ export class ForecastService {
(dependency as InvestmentResourceMetaDto).dataRevision,
),
},
] :
[],
]
: [],
),
sources: input.sources.flatMap((source) =>
source.version ? [source.version] : [],
),
timestamps: dependencies.flatMap((dependency) =>
dependency.lastSuccessfulAt ?
[
dependency.lastSuccessfulAt
? [
{
resourceKey: dependency.resourceKey,
at: dependency.lastSuccessfulAt,
},
] :
[],
]
: [],
),
});
const timestamps = stored?.sourceTimestamps as
@@ -78,33 +78,33 @@ export class ForecastService {
payload &&
'positionIsStale' in payload &&
'positionAt' in payload &&
typeof payload.positionAt === 'string' ?
{
typeof payload.positionAt === 'string'
? {
...payload,
positionIsStale: tbankSourceNeedsRefresh(
'portfolio',
new Date(payload.positionAt),
new Date(Date.now()),
),
} :
payload;
}
: payload;
return {
data,
meta: investmentResourceMeta({
resourceKey: input.scopeKey,
dataRevision: String(stored?.dataRevision ?? MISSING_DATA_REVISION),
availability: stored ?
'complete' :
dependencies.some(
availability: stored
? 'complete'
: dependencies.some(
(dependency) => dependency.availability !== 'missing',
) ?
'partial' :
'missing',
)
? 'partial'
: 'missing',
fetchedAt: first ? new Date(first) : null,
freshForMs:
data && 'positionIsStale' in data && data.positionIsStale === true ?
STALE_FRESHNESS_MS :
Number.POSITIVE_INFINITY,
data && 'positionIsStale' in data && data.positionIsStale === true
? STALE_FRESHNESS_MS
: Number.POSITIVE_INFINITY,
work: work?.id ? { ...work, id: work.id } : null,
dependencies,
}),
@@ -186,15 +186,15 @@ export class PortfolioService {
name: account.name,
type: account.type,
openedOn: account.openedOn,
snapshot: account.portfolio ?
{
snapshot: account.portfolio
? {
...(account.portfolio.payload as unknown as PortfolioDto),
lastSuccessfulAt: account.portfolio.fetchedAt.toISOString(),
isStale:
Date.now() - account.portfolio.fetchedAt.valueOf() >=
portfolioFreshForMs,
} :
null,
}
: null,
meta: investmentResourceMeta({
resourceKey: `portfolio/${encodeURIComponent(account.accountId)}`,
dataRevision: String(account.portfolio?.dataRevision ?? MISSING_DATA_REVISION),
@@ -220,8 +220,8 @@ export class PortfolioService {
);
const metadataDependencies = [...metadata.entries()].flatMap(
([key, source]) =>
source.complete && source.data ?
[
source.complete && source.data
? [
investmentResourceMeta({
resourceKey: `instrument/metadata/${key}`,
dataRevision: source.dataRevision,
@@ -230,20 +230,20 @@ export class PortfolioService {
freshForMs: tbankSourceTtlMs.metadata,
work: null,
}),
] :
[],
]
: [],
);
const portfolios = saved.accounts.flatMap((account) =>
account.snapshot ?
[
account.snapshot
? [
{
...account.snapshot,
positions: account.snapshot.positions.map((position) => {
const shared = metadata.get(metadataKey(position))?.data;
return {
...position,
...(shared ?
{
...(shared
? {
displayName:
shared.name?.trim() ||
shared.ticker?.trim() ||
@@ -252,29 +252,29 @@ export class PortfolioService {
isin: shared.isin?.trim() || position.isin,
logoName:
shared.brand?.logoName?.trim() || position.logoName,
} :
{}),
...(accountId === 'all' ?
{ account: { id: account.id, name: account.name } } :
{}),
}
: {}),
...(accountId === 'all'
? { account: { id: account.id, name: account.name } }
: {}),
};
}),
},
] :
[],
]
: [],
);
const lastSuccessfulAt =
// 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.
portfolios.map((portfolio) => portfolio.lastSuccessfulAt).sort()[0] ??
saved.membership.lastSuccessfulAt;
const data =
missing || !lastSuccessfulAt ?
null :
{
missing || !lastSuccessfulAt
? null
: {
lastSuccessfulAt,
isStale: portfolios.length ?
portfolios.some((portfolio) => portfolio.isStale) :
saved.membership.freshness !== 'fresh',
isStale: portfolios.length
? portfolios.some((portfolio) => portfolio.isStale)
: saved.membership.freshness !== 'fresh',
totalsByCurrency: sumMoneyByCurrency(
portfolios.flatMap((portfolio) => portfolio.totalsByCurrency),
),
@@ -306,15 +306,15 @@ export class PortfolioService {
]),
metadataDependencies.map((dependency) => dependency.dataRevision),
]),
availability: missing ?
portfolios.length ?
'partial' :
'missing' :
'complete',
availability: missing
? portfolios.length
? 'partial'
: 'missing'
: 'complete',
fetchedAt: lastSuccessfulAt ? new Date(lastSuccessfulAt) : null,
freshForMs: portfolios.length ?
portfolioFreshForMs :
membershipFreshForMs,
freshForMs: portfolios.length
? portfolioFreshForMs
: membershipFreshForMs,
work: work.id ? { ...work, id: work.id } : null,
dependencies: [
saved.membership,
@@ -478,8 +478,8 @@ export class PortfolioService {
const absoluteReturn =
currentPrice &&
averagePurchasePrice &&
currentPrice.currency === averagePurchasePrice.currency ?
{
currentPrice.currency === averagePurchasePrice.currency
? {
currency: currentPrice.currency,
value: formatDecimal(
multiply(
@@ -490,8 +490,8 @@ export class PortfolioService {
quantity,
),
),
} :
null;
}
: null;
return {
figi,
instrumentUid,
@@ -523,8 +523,8 @@ export class PortfolioService {
private money(value: TbankMoney | undefined): MoneyDto | null {
const currency = value?.currency?.trim().toUpperCase();
return currency && value ?
{ currency, value: formatDecimal(decimal(value)) } :
null;
return currency && value
? { currency, value: formatDecimal(decimal(value)) }
: null;
}
}
@@ -43,9 +43,9 @@ export function portfolioStructure(
currencies.set(money.currency, group);
};
for (const position of positions) {
const type = ['BOND', 'STOCK'].includes(position.type) ?
position.type :
'OTHER';
const type = ['BOND', 'STOCK'].includes(position.type)
? position.type
: 'OTHER';
if (position.currentValue) include(type, position.currentValue);
else unvalued.set(type, (unvalued.get(type) ?? INITIAL_UNVALUED_COUNT) + POSITION_INCREMENT);
}
@@ -65,8 +65,8 @@ export function portfolioStructure(
value: formatDecimal(value),
// Rounded to six decimal places using integer arithmetic. Only the
// resulting dimensionless percentage is passed to chart geometry.
sharePercent: canCalculateShares ?
formatDecimal({
sharePercent: canCalculateShares
? formatDecimal({
value:
(value.value *
DECIMAL_RADIX ** BigInt(total.scale - value.scale) *
@@ -74,8 +74,8 @@ export function portfolioStructure(
total.value / ROUNDING_DIVISOR) /
total.value,
scale: SHARE_PERCENT_DECIMAL_PLACES,
}) :
null,
})
: null,
}));
return {
currency,
@@ -58,9 +58,9 @@ export class ForecastRepository {
where: { userId_revision_scopeKey: key },
});
const fallback =
previous?.membershipRevision === input.membershipRevision ?
previous :
null;
previous?.membershipRevision === input.membershipRevision
? previous
: null;
if (!input.candidate) return fallback;
const portfolios = await tx.tbankPortfolio.findMany({
where: {
@@ -100,23 +100,23 @@ export function normalizeScreen(
...(accountId ? { accountId } : {}),
...(input.from ? { from: input.from } : {}),
...(input.to ? { to: input.to } : {}),
...(input.instrumentUid ?
{ instrumentUid: input.instrumentUid.toLowerCase() } :
{}),
...(input.instrumentUid
? { instrumentUid: input.instrumentUid.toLowerCase() }
: {}),
...(query ? { query } : {}),
...(input.candlePeriod ? { candlePeriod: input.candlePeriod } : {}),
...(input.candleRange ?
{
...(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.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 } : {}),
};
}
@@ -79,9 +79,9 @@ export class TbankSyncRepository {
work,
children: children.map((child) =>
progress.attempts?.[child.id] &&
progress.attempts[child.id] !== child.attemptId ?
{ ...child, state: 'cancelled', errorCode: null } :
child,
progress.attempts[child.id] !== child.attemptId
? { ...child, state: 'cancelled', errorCode: null }
: child,
),
progress,
};
@@ -120,9 +120,9 @@ export class TbankSyncService {
)
) {
for (const account of scope.screen === 'stock' ||
scope.screen === 'bond' ?
current.accounts :
selectedAccounts) {
scope.screen === 'bond'
? current.accounts
: selectedAccounts) {
const work = await this.portfolio.request(
userId,
revision,
@@ -135,9 +135,9 @@ export class TbankSyncService {
}
const saved = await this.portfolio.read(
userId,
scope.screen === 'stock' || scope.screen === 'bond' ?
'all' :
accountScope,
scope.screen === 'stock' || scope.screen === 'bond'
? 'all'
: accountScope,
true,
);
const positions = saved.data?.positions ?? [];
@@ -145,8 +145,8 @@ export class TbankSyncService {
...new Map(
positions.flatMap((position) =>
position.instrumentUid &&
(position.type === 'STOCK' || position.type === 'BOND') ?
[
(position.type === 'STOCK' || position.type === 'BOND')
? [
[
position.instrumentUid,
{
@@ -155,27 +155,27 @@ export class TbankSyncService {
historyFrom: null,
},
] as const,
] :
[],
]
: [],
),
).values(),
];
const plan = planInvestmentScreen(
{
...scope,
candleRange: scope.candleRange ?
{
candleRange: scope.candleRange
? {
from: new Date(scope.candleRange.from),
to: new Date(scope.candleRange.to),
} :
undefined,
}
: undefined,
},
{
accounts: selectedAccounts,
instruments:
scope.screen === 'stocks' || scope.screen === 'bonds' ?
[] :
instruments,
scope.screen === 'stocks' || scope.screen === 'bonds'
? []
: instruments,
membershipKnown: Boolean(current.membershipAt),
},
now,
@@ -185,18 +185,18 @@ export class TbankSyncService {
source.kind === 'operations' &&
source.accountId &&
source.from &&
source.to ?
[
source.to
? [
{
accountId: source.accountId,
from: source.from,
to: source.to,
...(source.instrumentUid ?
{ instrumentUid: source.instrumentUid } :
{}),
...(source.instrumentUid
? { instrumentUid: source.instrumentUid }
: {}),
},
] :
[],
]
: [],
);
if (intervals.length) {
await add([
@@ -278,11 +278,11 @@ export class TbankSyncService {
[uid],
true,
[
type === 'share' ?
'dividends' :
scope.screen === 'bond' ?
'coupons' :
'bond-events',
type === 'share'
? 'dividends'
: scope.screen === 'bond'
? 'coupons'
: 'bond-events',
],
{
from: source.from.toISOString().slice(ISO_DATE_START, ISO_DATE_LENGTH),
@@ -307,14 +307,14 @@ export class TbankSyncService {
uid: scope.instrumentUid,
instrumentType: scope.screen === 'bond' ? 'bond' : 'share',
period: scope.candlePeriod ?? 'month',
...(scope.candleRange ?
{
...(scope.candleRange
? {
range: {
from: new Date(scope.candleRange.from),
to: new Date(scope.candleRange.to),
},
} :
{}),
}
: {}),
},
now,
true,
@@ -346,9 +346,9 @@ export class TbankSyncService {
await this.access.local(userId, intent.scope.accountId);
const children = await Promise.all(
saved.children.map((child) =>
child.state === 'superseded' ?
this.operations.observe(userId, child.id) :
Promise.resolve(child),
child.state === 'superseded'
? this.operations.observe(userId, child.id)
: Promise.resolve(child),
),
);
const running = children.some((child) => child.state === 'running');
@@ -358,15 +358,15 @@ export class TbankSyncService {
null;
const state =
saved.work.processRunId !== this.runId &&
(!saved.progress.dispatched || running) ?
'cancelled' :
!saved.progress.dispatched || running ?
'running' :
failed ?
'failed' :
children.some((child) => child.state === 'cancelled') ?
'cancelled' :
'succeeded';
(!saved.progress.dispatched || running)
? 'cancelled'
: !saved.progress.dispatched || running
? 'running'
: failed
? 'failed'
: children.some((child) => child.state === 'cancelled')
? 'cancelled'
: 'succeeded';
const resources = await this.observe(userId, intent.scope);
if (
(await this.access.local(userId, intent.scope.accountId)).revision !==
@@ -423,9 +423,9 @@ export class TbankSyncService {
typeof input.kind === 'string'
) {
const range =
typeof input.rangeKey === 'string' && input.rangeKey.startsWith('[') ?
(JSON.parse(input.rangeKey) as string[]) :
null;
typeof input.rangeKey === 'string' && input.rangeKey.startsWith('[')
? (JSON.parse(input.rangeKey) as string[])
: null;
const sources = await this.sources.readMany(
userId,
type,
@@ -434,9 +434,9 @@ export class TbankSyncService {
[
input.kind as Parameters<
CatalogEnrichmentService['readMany']
>[4] extends (infer K)[] | undefined ?
K :
never,
>[4] extends (infer K)[] | undefined
? K
: never,
],
range ? { from: range[0], to: range[1] } : undefined,
);
@@ -492,19 +492,19 @@ export class TbankSyncService {
);
}
}
const observed = work.scopeKey.startsWith('operations:') ?
await this.operations.observe(userId, work.id) :
work;
const observed = work.scopeKey.startsWith('operations:')
? await this.operations.observe(userId, work.id)
: work;
const state =
observed.state === 'running' ?
observed.updatedAt < this.startedAt ?
'cancelled' :
'running' :
observed.state === 'failed' ?
'failed' :
observed.state === 'cancelled' ?
'cancelled' :
'succeeded';
observed.state === 'running'
? observed.updatedAt < this.startedAt
? 'cancelled'
: 'running'
: observed.state === 'failed'
? 'failed'
: observed.state === 'cancelled'
? 'cancelled'
: 'succeeded';
if ((await this.access.local(userId)).revision !== work.revision) {
throw new NotFoundException();
}
@@ -597,27 +597,27 @@ export class TbankSyncService {
uid,
instrumentType: type,
period: scope.candlePeriod ?? 'month',
...(scope.candleRange ?
{
...(scope.candleRange
? {
range: {
from: new Date(scope.candleRange.from),
to: new Date(scope.candleRange.to),
},
} :
{}),
}
: {}),
},
true,
),
);
await add(
type === 'share' ?
this.stockIncome.readPosition(userId, uid, true) :
this.bondIncome.readPosition(userId, uid, true),
type === 'share'
? this.stockIncome.readPosition(userId, uid, true)
: this.bondIncome.readPosition(userId, uid, true),
);
await add(
type === 'share' ?
this.stockIncome.dividends(userId, uid, true) :
this.bondIncome.coupons(userId, uid, true),
type === 'share'
? this.stockIncome.dividends(userId, uid, true)
: this.bondIncome.coupons(userId, uid, true),
);
}
return resources;
@@ -154,15 +154,15 @@ export class UpcomingEventsService {
};
const quantity = decimalFromString(position.quantity);
const projected =
type === 'share' ?
(calendar.payload?.dividends ?? [])
type === 'share'
? (calendar.payload?.dividends ?? [])
.filter(
(event) =>
event.dividendType?.toLowerCase() !== 'cancelled' &&
Boolean(event.recordDate),
)
.map((event) => dividendEvent(identity, quantity, event)) :
(calendar.payload?.bondEvents ?? []).map((event) =>
.map((event) => dividendEvent(identity, quantity, event))
: (calendar.payload?.bondEvents ?? []).map((event) =>
bondEvent(identity, quantity, event),
);
events.push(
@@ -172,9 +172,9 @@ export class UpcomingEventsService {
)
.map((event) => ({
...event,
...(accountId === 'all' && position.account ?
{ account: position.account } :
{}),
...(accountId === 'all' && position.account
? { account: position.account }
: {}),
})),
);
}
@@ -185,14 +185,14 @@ export class UpcomingEventsService {
a.type.localeCompare(b.type) ||
a.ticker.localeCompare(b.ticker),
);
const candidate: undefined | UpcomingEventsDto = ready ?
{
const candidate: undefined | UpcomingEventsDto = ready
? {
accountId,
calculatedAt: new Date().toISOString(),
events,
incomeTotalsByCurrency: incomeTotals(events),
} :
undefined;
}
: undefined;
return this.forecasts.result({
userId,
revision: connection.revision,
@@ -224,13 +224,13 @@ export function dividendEvent(
.toISOString()
.slice(ISO_DATE_START, ISO_DATE_LENGTH),
dateKind: 'registry',
projectedAmount: event.dividendNet ?
projectedMoney(
projectedAmount: event.dividendNet
? projectedMoney(
quantity,
formatDecimal(decimal(event.dividendNet)),
event.dividendNet.currency,
) :
null,
)
: null,
};
}
@@ -248,9 +248,9 @@ export function bondEvent(
date: event.date,
dateKind: 'event',
projectedAmount:
'value' in event ?
projectedMoney(quantity, event.value, event.currency) :
null,
'value' in event
? projectedMoney(quantity, event.value, event.currency)
: null,
};
}
+7 -7
View File
@@ -64,13 +64,13 @@ async function check(name, overrides = {}, signal) {
'user database must be untouched',
);
const expected =
signal === 'SIGINT' ?
130 :
signal === 'SIGTERM' ?
143 :
overrides.PRISMA_SCHEMA_ENGINE_BINARY ?
1 :
0;
signal === 'SIGINT'
? 130
: signal === 'SIGTERM'
? 143
: overrides.PRISMA_SCHEMA_ENGINE_BINARY
? 1
: 0;
assert.equal(code, expected, `See ${name}.log`);
console.log(`PASS ${name}: ${dirs.length} isolated databases`);
}
+5 -5
View File
@@ -9,11 +9,11 @@ const available = readdirSync(__dirname)
.sort();
const requested = process.argv.slice(2);
const suites =
requested.length === 0 ?
['app.e2e-spec.ts', 'bank-cards.e2e-spec.ts'] :
requested.length === 1 && requested[0] === '--all' ?
available :
requested;
requested.length === 0
? ['app.e2e-spec.ts', 'bank-cards.e2e-spec.ts']
: requested.length === 1 && requested[0] === '--all'
? available
: requested;
if (suites.some((suite) => !available.includes(suite))) {
throw new Error('Unknown e2e suite');
}
+3 -3
View File
@@ -240,9 +240,9 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
data: { occurredAt: new Date(observedAt.valueOf() - 1000) },
});
const query =
periodKind === 'default' ?
'' :
`&to=${observedAt.toISOString().slice(0, 10)}${periodKind === 'today timestamp' ? 'T23:59:59.999Z' : ''}`;
periodKind === 'default'
? ''
: `&to=${observedAt.toISOString().slice(0, 10)}${periodKind === 'today timestamp' ? 'T23:59:59.999Z' : ''}`;
const page = (cursor?: string, observe = false) =>
request(app.getHttpServer())
.get(
+3 -3
View File
@@ -339,9 +339,9 @@ describe('local accounts and portfolio HTTP (SQLite)', () => {
]);
await put().expect(200);
upstream.getPortfolio.mockImplementation((_token, id) =>
id === 'second' ?
Promise.reject(new Error('synthetic unavailable')) :
Promise.resolve({
id === 'second'
? Promise.reject(new Error('synthetic unavailable'))
: Promise.resolve({
positions: [
{
instrumentUid: uid,
+3 -3
View File
@@ -351,9 +351,9 @@ describe('durable shared instrument metadata (SQLite)', () => {
await service.read('owner', identity);
await settled();
const identities = Array.from({ length: 1000 }, (_, index) =>
index ?
{ instrumentUid: `uid-${index}`, figi: `FIGI-${index}` } :
identity,
index
? { instrumentUid: `uid-${index}`, figi: `FIGI-${index}` }
: identity,
);
const saved = await service.savedMany(identities);
expect(saved.size).toBe(1000);
+12 -12
View File
@@ -58,9 +58,9 @@ describe('T-Bank persisted read budget', () => {
const operations = Array.from({ length: 100_000 }, (_, index) => {
// 1,000 measured-month rows, 200 income receipts, remaining rows over 10 years.
const time =
index < 1000 ?
Date.UTC(2026, 7, 1) + index * 2_500_000 :
Date.UTC(2016, 8, 1) + (index - 1000) * 3_160_000;
index < 1000
? Date.UTC(2026, 7, 1) + index * 2_500_000
: Date.UTC(2016, 8, 1) + (index - 1000) * 3_160_000;
const instrument = instruments[index % instruments.length];
return {
id: `operation-${index}`,
@@ -298,9 +298,9 @@ describe('T-Bank persisted read budget', () => {
uid: instrument.uid,
ticker: instrument.ticker,
instrumentType:
instrument.instrumentKind === 'INSTRUMENT_TYPE_SHARE' ?
'share' :
'bond',
instrument.instrumentKind === 'INSTRUMENT_TYPE_SHARE'
? 'share'
: 'bond',
metadata: instrument,
fetchedAt: now,
},
@@ -487,13 +487,13 @@ describe('T-Bank persisted read budget', () => {
runtime: process.version,
hardware: {
cpuQuota:
process.platform === 'linux' ?
readFileSync('/sys/fs/cgroup/cpu.max', 'utf8').trim() :
null,
process.platform === 'linux'
? readFileSync('/sys/fs/cgroup/cpu.max', 'utf8').trim()
: null,
memoryLimit:
process.platform === 'linux' ?
readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim() :
null,
process.platform === 'linux'
? readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim()
: null,
cpu: cpus()[0]?.model,
cpus: cpus().length,
memoryBytes: totalmem(),
+3 -3
View File
@@ -358,9 +358,9 @@ describe('screen sync HTTP and SQLite', () => {
}),
)
.mockImplementation(() =>
outcome === 'failed' ?
Promise.reject(new Error('Synthetic outage')) :
Promise.resolve({ hasNext: false, items: [] }),
outcome === 'failed'
? Promise.reject(new Error('Synthetic outage'))
: Promise.resolve({ hasNext: false, items: [] }),
);
const accept = async (from: string, to: string) => {
const response = await post({
+3 -3
View File
@@ -296,9 +296,9 @@ describe('saved payout calendars and complete forecasts (SQLite HTTP)', () => {
.body as UpcomingEventsResourceDto;
await portfolio([position(shareUid, '7'), position(newUid, '5')]);
upstream.getDividends.mockImplementation((_token: string, uid: string) =>
uid === newUid ?
Promise.reject(new Error('new calendar unavailable')) :
Promise.resolve([]),
uid === newUid
? Promise.reject(new Error('new calendar unavailable'))
: Promise.resolve([]),
);
const pending = (await events(false).expect(200))
.body as UpcomingEventsResourceDto;