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