chore: strengthen backend eslint profile

This commit is contained in:
2026-09-28 11:04:18 +03:00
parent ca5dbcb382
commit 9b3558f96c
179 changed files with 4485 additions and 2716 deletions
@@ -1,41 +1,42 @@
import {
ledgerRange,
accountHistory,
planInvestmentScreen,
} from '../../investment-sync-plan';
import { ConfigService } from '@nestjs/config';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import { BrokerageOperationsRepository } from '../infrastructure/brokerage-operations.repository';
import { BrokerageOperationsSyncService } from './brokerage-operations-sync.service';
import {
planOperationRead,
uncoveredOperationIntervals,
operationIntervalsOverlap,
} from '../domain/operations-sync-plan';
import { BadRequestException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
import {
InstrumentMetadataService,
metadataKey,
TbankAccessService,
TbankMembershipService,
tbankSourceTtlMs,
type TbankOperationItem,
tbankSourceTtlMs,
} from '../../../../integrations/tbank/tbank.api';
import { DateOnlyRange, dateOnlyBounds } from '../../investment-calendar';
import { dateOnlyBounds, DateOnlyRange } from '../../investment-calendar';
import { investmentResourceMeta } from '../../investment-resource.mapper';
import {
DEFAULT_OPERATIONS_PAGE_SIZE,
BrokerageOperationsQueryDto,
} from '../presentation/brokerage-operation.dto';
accountHistory,
ledgerRange,
planInvestmentScreen,
} from '../../investment-sync-plan';
import {
operationIntervalsOverlap,
planOperationRead,
uncoveredOperationIntervals,
} from '../domain/operations-sync-plan';
import { mapBrokerageOperation } from '../infrastructure/brokerage-operation.mapper';
import { BrokerageOperationsRepository } from '../infrastructure/brokerage-operations.repository';
import {
BrokerageOperationsQueryDto,
DEFAULT_OPERATIONS_PAGE_SIZE,
} from '../presentation/brokerage-operation.dto';
import { BrokerageOperationsSyncService } from './brokerage-operations-sync.service';
type LocalOperationsCursor = {
movingEnd?: boolean;
binding: string;
to: string;
at: string;
accountId: string;
at: string;
binding: string;
id: string;
movingEnd?: boolean;
to: string;
};
@Injectable()
@@ -70,18 +71,19 @@ export class BrokerageOperationsService {
const from = range.from;
const to = cursor ? new Date(cursor.to) : range.to;
const movingEnd = cursor?.movingEnd ?? range.to.valueOf() === now.valueOf();
// 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 categories = [...new Set(query.categories ?? [])].sort();
const limit = query.limit ?? DEFAULT_OPERATIONS_PAGE_SIZE;
const filterKey = JSON.stringify([
accountId,
from.toISOString(),
query.to
? new Date(
/^\d{4}-\d{2}-\d{2}$/.test(query.to)
? `${query.to}T23:59:59.999Z`
: query.to,
).toISOString()
: 'now',
query.to ?
new Date(
/^\d{4}-\d{2}-\d{2}$/.test(query.to) ?
`${query.to}T23:59:59.999Z` :
query.to,
).toISOString() :
'now',
query.instrumentUid ?? null,
categories,
limit,
@@ -118,9 +120,9 @@ export class BrokerageOperationsService {
source.priority === 'screen' &&
source.accountId &&
source.from &&
source.to
? [{ accountId: source.accountId, from: source.from, to: source.to }]
: [],
source.to ?
[{ accountId: source.accountId, from: source.from, to: source.to }] :
[],
);
if (query.sync !== 'observe') {
const discovery = await this.membership.request(
@@ -148,9 +150,9 @@ export class BrokerageOperationsService {
accountId === 'all' || account.accountId === accountId,
)
.flatMap((account) =>
currentRange.from < currentRange.to
? [{ accountId: account.accountId, ...currentRange }]
: [],
currentRange.from < currentRange.to ?
[{ accountId: account.accountId, ...currentRange }] :
[],
);
await this.sync.readPlan(userId, targets, {
movingEnd,
@@ -170,16 +172,16 @@ export class BrokerageOperationsService {
const saved = await this.repository.readPage(readInput);
const accounts = saved?.connection.accounts ?? [];
const targets = accounts.flatMap((account) =>
from < to
? [
from < to ?
[
{
accountId: account.accountId,
from,
to,
instrumentUid: query.instrumentUid,
},
]
: [],
] :
[],
);
const plan = planOperationRead(targets, saved?.coverage ?? [], now, {
movingEnd,
@@ -191,8 +193,8 @@ export class BrokerageOperationsService {
row.accountId === target.accountId &&
(!row.instrumentUid ||
row.instrumentUid === target.instrumentUid) &&
row.from < target.to &&
target.from < row.to,
row.from < target.to &&
target.from < row.to,
)
.map((row) => ({
accountId: target.accountId,
@@ -230,13 +232,8 @@ export class BrokerageOperationsService {
sources
.map((row) => row.fetchedAt)
.sort((a, b) => a.valueOf() - b.valueOf())[0] ??
(complete ? saved?.connection.membershipAt : null) ??
null;
const serialize = (range: { accountId: string; from: Date; to: Date }) => ({
accountId: range.accountId,
from: range.from.toISOString(),
to: range.to.toISOString(),
});
(complete ? saved?.connection.membershipAt : null) ??
null;
const historyTargets = accountHistory(accounts, now);
const history = planOperationRead(
historyTargets,
@@ -273,9 +270,11 @@ export class BrokerageOperationsService {
] as const;
}),
);
for (const identity of identities.values())
if (!instruments.get(metadataKey(identity))?.fresh)
for (const identity of identities.values()) {
if (!instruments.get(metadataKey(identity))?.fresh) {
await this.metadata.request(userId, connection.revision, identity);
}
}
}
const membershipMeta = investmentResourceMeta({
resourceKey: 'accounts/membership',
@@ -288,17 +287,17 @@ export class BrokerageOperationsService {
const meta = investmentResourceMeta({
resourceKey: `operations/${createHash('sha256').update(filterKey).digest('base64url')}`,
dataRevision: saved?.binding ?? 'missing',
availability: !available
? 'missing'
: complete
? 'complete'
: confirmed.length
? 'partial'
: 'missing',
availability: !available ?
'missing' :
complete ?
'complete' :
confirmed.length ?
'partial' :
'missing',
fetchedAt: lastSuccessfulAt,
freshForMs: plan.intervals.some((interval) => interval.stale)
? 0
: Number.POSITIVE_INFINITY,
freshForMs: plan.intervals.some((interval) => interval.stale) ?
0 :
Number.POSITIVE_INFINITY,
work: work ?? saved?.membershipWork ?? null,
dependencies: [membershipMeta, ...metadataDependencies],
coverage: {
@@ -312,8 +311,8 @@ export class BrokerageOperationsService {
(interval) =>
interval.accountId === account.accountId && interval.asOf,
),
)
? new Date(
) ?
new Date(
Math.min(
...accounts.map((account) =>
Math.max(
@@ -326,42 +325,42 @@ export class BrokerageOperationsService {
),
),
),
).toISOString()
: null,
).toISOString() :
null,
historyBoundary: knownBoundary ? 'known' : 'unknown',
historyState: saved?.work.some(
(work) => work.state === 'running' && work.priority > 1,
)
? 'loading'
: saved?.work.some(
(work) => work.state === 'failed' && work.priority > 1,
)
? 'failed'
: knownBoundary &&
history.intervals.every(
(interval) => interval.availability === 'complete',
)
? 'complete'
: saved?.coverage.length
? 'partial'
: 'missing',
) ?
'loading' :
saved?.work.some(
(work) => work.state === 'failed' && work.priority > 1,
) ?
'failed' :
knownBoundary &&
history.intervals.every(
(interval) => interval.availability === 'complete',
) ?
'complete' :
saved?.coverage.length ?
'partial' :
'missing',
},
});
const last = rows.at(-1);
const hasNext = Boolean(saved && saved.rows.length > limit);
return {
data:
meta.availability === 'missing'
? null
: {
meta.availability === 'missing' ?
null :
{
accountId,
items: rows.map((row) => ({
...mapBrokerageOperation(
row.payload as TbankOperationItem,
row.occurredAt,
),
...(accountId === 'all'
? {
...(accountId === 'all' ?
{
account: {
id: row.accountId,
name:
@@ -369,8 +368,8 @@ export class BrokerageOperationsService {
(account) => account.accountId === row.accountId,
)?.name ?? row.accountId,
},
}
: {}),
} :
{}),
logoName:
instruments
.get(metadataKey(row.payload as TbankOperationItem))
@@ -378,16 +377,16 @@ export class BrokerageOperationsService {
})),
hasNext,
nextCursor:
hasNext && last && saved
? this.encodeCursor({
hasNext && last && saved ?
this.encodeCursor({
binding: saved.binding,
movingEnd,
to: to.toISOString(),
at: last.occurredAt.toISOString(),
accountId: last.accountId,
id: last.brokerOperationId,
})
: null,
}) :
null,
observedAt: now.toISOString(),
},
meta,
@@ -413,23 +412,25 @@ export class BrokerageOperationsService {
extra ||
actual.length !== expected.length ||
!timingSafeEqual(actual, expected)
)
) {
throw new Error();
}
const cursor = JSON.parse(
Buffer.from(body, 'base64url').toString(),
) as Partial<LocalOperationsCursor>;
if (
(cursor.movingEnd !== undefined &&
typeof cursor.movingEnd !== 'boolean') ||
!cursor.binding ||
!cursor.accountId ||
!cursor.id ||
!cursor.at ||
!cursor.to ||
!Number.isFinite(Date.parse(cursor.at)) ||
!Number.isFinite(Date.parse(cursor.to))
)
!cursor.binding ||
!cursor.accountId ||
!cursor.id ||
!cursor.at ||
!cursor.to ||
!Number.isFinite(Date.parse(cursor.at)) ||
!Number.isFinite(Date.parse(cursor.to))
) {
throw new Error();
}
return cursor as LocalOperationsCursor;
} catch {
throw new BadRequestException({ code: 'INVALID_OPERATIONS_CURSOR' });
@@ -465,12 +466,13 @@ export class BrokerageOperationsService {
...period,
})),
);
if (targets.length)
if (targets.length) {
await this.sync.readPlan(userId, targets, {
movingEnd: snapshot.periods?.some((period) => period.movingEnd),
history: true,
revision: connection.revision,
});
}
};
if (!observe) {
const discovery = await this.membership.request(
@@ -530,17 +532,8 @@ export class BrokerageOperationsService {
sources
.map((row) => row.fetchedAt)
.sort((a, b) => a.valueOf() - b.valueOf())[0] ??
(complete ? saved?.connection.membershipAt : null) ??
null;
const serialize = (range: {
accountId: string;
from: Date;
to: Date;
}) => ({
accountId: range.accountId,
from: range.from.toISOString(),
to: range.to.toISOString(),
});
(complete ? saved?.connection.membershipAt : null) ??
null;
const confirmed = plan.intervals.flatMap((target) =>
sources
.filter(
@@ -555,15 +548,16 @@ export class BrokerageOperationsService {
})),
);
const meta = investmentResourceMeta({
// eslint-disable-next-line security/detect-object-injection -- The numeric index enumerates the three comparison periods.
resourceKey: `income/${accountId}/${['selected', 'previous', 'yearAgo'][index]}/${period.key}`,
dataRevision: saved?.binding ?? 'missing',
availability: !known
? 'missing'
: complete
? 'complete'
: sources.length
? 'partial'
: 'missing',
availability: !known ?
'missing' :
complete ?
'complete' :
sources.length ?
'partial' :
'missing',
fetchedAt,
freshForMs: plan.intervals.some((range) => range.stale) ? 0 : Infinity,
work,
@@ -576,8 +570,8 @@ export class BrokerageOperationsService {
confirmed,
).map(serialize),
asOf:
complete && targets.length
? new Date(
complete && targets.length ?
new Date(
Math.min(
...targets.map((target) =>
Math.max(
@@ -592,24 +586,24 @@ export class BrokerageOperationsService {
),
),
),
).toISOString()
: null,
).toISOString() :
null,
historyBoundary: (saved?.connection.accounts ?? []).every(
(account) => account.openedOn,
)
? 'known'
: 'unknown',
historyState: complete
? 'complete'
: sources.length
? 'partial'
: 'missing',
) ?
'known' :
'unknown',
historyState: complete ?
'complete' :
sources.length ?
'partial' :
'missing',
},
});
return {
meta,
items: complete
? (saved?.rows ?? [])
items: complete ?
(saved?.rows ?? [])
.filter((row) =>
plan.intervals.some(
(interval) =>
@@ -623,8 +617,8 @@ export class BrokerageOperationsService {
row.payload as TbankOperationItem,
row.occurredAt,
),
...(accountId === 'all'
? {
...(accountId === 'all' ?
{
account: {
id: row.accountId,
name:
@@ -632,11 +626,15 @@ export class BrokerageOperationsService {
(account) => account.accountId === row.accountId,
)?.name ?? row.accountId,
},
}
: {}),
}))
: null,
} :
{}),
})) :
null,
};
});
}
}
function serialize(range: { accountId: string; from: Date; to: Date }) {
return { accountId: range.accountId, from: range.from.toISOString(), to: range.to.toISOString() };
}