chore: strengthen backend eslint profile
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { MILLISECONDS_PER_SECOND } from '../../../../common/time';
|
||||
import { formatDecimal } from '../../../../common/decimal';
|
||||
import { MILLISECONDS_PER_SECOND } from '../../../../common/time';
|
||||
import { decimal } from '../../../../integrations/tbank/decimal';
|
||||
import {
|
||||
TbankMoney,
|
||||
TbankOperationItem,
|
||||
} from '../../../../integrations/tbank/tbank.api';
|
||||
import { decimal } from '../../../../integrations/tbank/decimal';
|
||||
import {
|
||||
BrokerageOperationDto,
|
||||
OperationCategory,
|
||||
@@ -59,14 +59,14 @@ export function mapBrokerageOperation(
|
||||
logoName: null,
|
||||
payment: money(operation.payment),
|
||||
quantity:
|
||||
(operationCategory(operation.type) === 'trade'
|
||||
? operation.quantityDone
|
||||
: operation.quantity) === undefined
|
||||
? null
|
||||
: String(
|
||||
operationCategory(operation.type) === 'trade'
|
||||
? operation.quantityDone
|
||||
: operation.quantity,
|
||||
(operationCategory(operation.type) === 'trade' ?
|
||||
operation.quantityDone :
|
||||
operation.quantity) === undefined ?
|
||||
null :
|
||||
String(
|
||||
operationCategory(operation.type) === 'trade' ?
|
||||
operation.quantityDone :
|
||||
operation.quantity,
|
||||
),
|
||||
price: money(operation.price),
|
||||
accruedInterest: money(operation.accruedInt),
|
||||
@@ -74,22 +74,22 @@ export function mapBrokerageOperation(
|
||||
};
|
||||
}
|
||||
|
||||
function text(value: string | undefined): string | null {
|
||||
function text(value: string | undefined): null | string {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function money(value: TbankMoney | undefined) {
|
||||
const currency = value?.currency?.trim().toUpperCase();
|
||||
return currency && value
|
||||
? { currency, value: formatDecimal(decimal(value)) }
|
||||
: null;
|
||||
return currency && value ?
|
||||
{ currency, value: formatDecimal(decimal(value)) } :
|
||||
null;
|
||||
}
|
||||
|
||||
export function operationTimestamp(
|
||||
value: { seconds?: string | number; nanos?: number } | undefined,
|
||||
value: undefined | { nanos?: number; seconds?: number | string },
|
||||
) {
|
||||
return new Date(
|
||||
Number(value?.seconds ?? 0) * MILLISECONDS_PER_SECOND +
|
||||
Math.floor((value?.nanos ?? 0) / NANOSECONDS_PER_MILLISECOND),
|
||||
Math.floor((value?.nanos ?? 0) / NANOSECONDS_PER_MILLISECOND),
|
||||
);
|
||||
}
|
||||
|
||||
+142
-114
@@ -5,46 +5,48 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
||||
|
||||
import type { Prisma } from '../../../../../generated/prisma/client';
|
||||
import type {
|
||||
TbankOperationItem,
|
||||
TbankOperationsByCursorResponse,
|
||||
} from '../../../../integrations/tbank/tbank.api';
|
||||
|
||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
||||
import {
|
||||
DateOnlyRange,
|
||||
dateOnlyBounds,
|
||||
DateOnlyRange,
|
||||
moscowDate,
|
||||
} from '../../investment-calendar';
|
||||
import {
|
||||
operationCategory,
|
||||
operationTimestamp,
|
||||
} from './brokerage-operation.mapper';
|
||||
import {
|
||||
type OperationInterval,
|
||||
operationIntervals,
|
||||
operationIntervalsOverlap,
|
||||
uncoveredOperationIntervals,
|
||||
} from '../domain/operations-sync-plan';
|
||||
import {
|
||||
operationCategory,
|
||||
operationTimestamp,
|
||||
} from './brokerage-operation.mapper';
|
||||
export type { OperationInterval } from '../domain/operations-sync-plan';
|
||||
|
||||
export type OperationAttempt = {
|
||||
id: string;
|
||||
attemptId: string;
|
||||
userId: string;
|
||||
id: string;
|
||||
revision: string;
|
||||
userId: string;
|
||||
};
|
||||
type OperationsIntent = {
|
||||
intervals: string[];
|
||||
atomic: boolean;
|
||||
atomicIntervals?: string[];
|
||||
intervals: string[];
|
||||
screenIntervals?: string[];
|
||||
};
|
||||
type PageProgress = {
|
||||
complete: boolean;
|
||||
cursor: string;
|
||||
seen: string[];
|
||||
pages: number;
|
||||
seen: string[];
|
||||
};
|
||||
const progressItem = '';
|
||||
|
||||
@@ -53,17 +55,17 @@ export class BrokerageOperationsRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async readPage(input: {
|
||||
userId: string;
|
||||
revision: string;
|
||||
accountId: string;
|
||||
from: Date;
|
||||
to: Date;
|
||||
instrumentUid?: string;
|
||||
categories: string[];
|
||||
cursor?: { accountId: string; at: string; binding: string; id: string };
|
||||
filterKey: string;
|
||||
from: Date;
|
||||
instrumentUid?: string;
|
||||
limit?: number;
|
||||
periods?: (openedAt: Date | undefined) => DateOnlyRange[];
|
||||
filterKey: string;
|
||||
cursor?: { binding: string; at: string; accountId: string; id: string };
|
||||
revision: string;
|
||||
to: Date;
|
||||
userId: string;
|
||||
}) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const connection = await tx.tbankConnection.findUnique({
|
||||
@@ -92,14 +94,16 @@ export class BrokerageOperationsRepository {
|
||||
connection.membershipAt &&
|
||||
input.accountId !== 'all' &&
|
||||
!connection.accounts.length
|
||||
)
|
||||
) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
const accounts = connection.accounts;
|
||||
// eslint-disable-next-line sonarjs/no-alphabetical-sort -- Code-unit ordering is intentional for canonical keys and ISO dates; locale collation would change persisted bindings.
|
||||
const oldest = accounts.map((account) => account.openedOn).sort()[0];
|
||||
const openedAt =
|
||||
oldest && accounts.every((account) => account.openedOn)
|
||||
? new Date(`${oldest}T00:00:00+03:00`)
|
||||
: undefined;
|
||||
oldest && accounts.every((account) => account.openedOn) ?
|
||||
new Date(`${oldest}T00:00:00+03:00`) :
|
||||
undefined;
|
||||
const periods = input.periods?.(openedAt).map((period) => {
|
||||
const now = new Date();
|
||||
const bounds = dateOnlyBounds(period, now);
|
||||
@@ -123,8 +127,9 @@ export class BrokerageOperationsRepository {
|
||||
]),
|
||||
)
|
||||
.digest('base64url');
|
||||
if (input.cursor && input.cursor.binding !== binding)
|
||||
if (input.cursor && input.cursor.binding !== binding) {
|
||||
throw new BadRequestException({ code: 'INVALID_OPERATIONS_CURSOR' });
|
||||
}
|
||||
const scope = {
|
||||
userId: input.userId,
|
||||
revision: input.revision,
|
||||
@@ -162,21 +167,21 @@ export class BrokerageOperationsRepository {
|
||||
const rows = await tx.tbankOperation.findMany({
|
||||
where: {
|
||||
...scope,
|
||||
...(periods
|
||||
? {
|
||||
...(periods ?
|
||||
{
|
||||
OR: periods.map((period) => ({
|
||||
occurredAt: { gte: period.from, lt: period.to },
|
||||
})),
|
||||
}
|
||||
: { occurredAt: { gte: input.from, lt: input.to } }),
|
||||
...(input.instrumentUid
|
||||
? { instrumentUid: input.instrumentUid }
|
||||
: {}),
|
||||
...(input.categories.length
|
||||
? { category: { in: input.categories } }
|
||||
: {}),
|
||||
...(after
|
||||
? {
|
||||
} :
|
||||
{ occurredAt: { gte: input.from, lt: input.to } }),
|
||||
...(input.instrumentUid ?
|
||||
{ instrumentUid: input.instrumentUid } :
|
||||
{}),
|
||||
...(input.categories.length ?
|
||||
{ category: { in: input.categories } } :
|
||||
{}),
|
||||
...(after ?
|
||||
{
|
||||
OR: [
|
||||
{ occurredAt: { lt: new Date(after.at) } },
|
||||
{
|
||||
@@ -189,8 +194,8 @@ export class BrokerageOperationsRepository {
|
||||
brokerOperationId: { gt: after.id },
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
} :
|
||||
{}),
|
||||
},
|
||||
orderBy: [
|
||||
{ occurredAt: 'desc' },
|
||||
@@ -247,6 +252,7 @@ export class BrokerageOperationsRepository {
|
||||
const scopeKey =
|
||||
'operations:' +
|
||||
createHash('sha256')
|
||||
// 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.
|
||||
.update(JSON.stringify([...new Set(intervals.map(intervalKey))].sort()))
|
||||
.digest('hex');
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
@@ -271,14 +277,15 @@ export class BrokerageOperationsRepository {
|
||||
(account) => account.accountId === interval.accountId,
|
||||
),
|
||||
)
|
||||
)
|
||||
) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (priority > 1) {
|
||||
for (const account of accounts.filter((account) =>
|
||||
intervals.some(
|
||||
(interval) => interval.accountId === account.accountId,
|
||||
),
|
||||
))
|
||||
)) {
|
||||
await tx.tbankAccount.update({
|
||||
where: {
|
||||
userId_revision_accountId: {
|
||||
@@ -289,13 +296,14 @@ export class BrokerageOperationsRepository {
|
||||
},
|
||||
data: {
|
||||
historyFrom: new Date(
|
||||
account.openedOn
|
||||
? `${account.openedOn}T00:00:00Z`
|
||||
: '1970-01-01T00:00:00Z',
|
||||
account.openedOn ?
|
||||
`${account.openedOn}T00:00:00Z` :
|
||||
'1970-01-01T00:00:00Z',
|
||||
),
|
||||
historyBoundary: account.openedOn ? 'known' : 'unknown',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
const coverage = await tx.tbankOperationCoverage.findMany({
|
||||
where: {
|
||||
@@ -366,42 +374,42 @@ export class BrokerageOperationsRepository {
|
||||
) {
|
||||
const pending = previousIntent?.intervals.map(decodeInterval) ?? [];
|
||||
const additions =
|
||||
previous.scopeKey === scopeKey
|
||||
? []
|
||||
: uncoveredOperationIntervals(intervals, pending);
|
||||
previous.scopeKey === scopeKey ?
|
||||
[] :
|
||||
uncoveredOperationIntervals(intervals, pending);
|
||||
const work = await tx.tbankSyncWork.update({
|
||||
where: { id: previous.id },
|
||||
data: {
|
||||
intent: {
|
||||
atomic: previousIntent?.atomic ?? publicationAtomic,
|
||||
atomicIntervals: previousIntent?.atomic
|
||||
? [
|
||||
atomicIntervals: previousIntent?.atomic ?
|
||||
[
|
||||
...new Set([
|
||||
...(previousIntent.atomicIntervals ??
|
||||
previousIntent.intervals),
|
||||
...additions.map(intervalKey),
|
||||
]),
|
||||
]
|
||||
: [],
|
||||
] :
|
||||
[],
|
||||
screenIntervals: [
|
||||
...new Set([
|
||||
...(previousIntent?.screenIntervals ??
|
||||
(previous.priority === 1
|
||||
? (previousIntent?.intervals ?? [])
|
||||
: [])),
|
||||
...(priority === 1
|
||||
? [...pending, ...additions]
|
||||
(previous.priority === 1 ?
|
||||
(previousIntent?.intervals ?? []) :
|
||||
[])),
|
||||
...(priority === 1 ?
|
||||
[...pending, ...additions]
|
||||
.filter((item) =>
|
||||
intervals.some((target) =>
|
||||
operationIntervalsOverlap(item, target),
|
||||
),
|
||||
)
|
||||
.map(intervalKey)
|
||||
: []),
|
||||
.map(intervalKey) :
|
||||
[]),
|
||||
]),
|
||||
],
|
||||
intervals: (priority < previous.priority
|
||||
? [
|
||||
intervals: (priority < previous.priority ?
|
||||
[
|
||||
...pending.filter((item) =>
|
||||
intervals.some((target) =>
|
||||
operationIntervalsOverlap(item, target),
|
||||
@@ -414,8 +422,8 @@ export class BrokerageOperationsRepository {
|
||||
operationIntervalsOverlap(item, target),
|
||||
),
|
||||
),
|
||||
]
|
||||
: [...pending, ...additions]
|
||||
] :
|
||||
[...pending, ...additions]
|
||||
).map(intervalKey),
|
||||
},
|
||||
priority: Math.min(priority, previous.priority),
|
||||
@@ -427,9 +435,9 @@ export class BrokerageOperationsRepository {
|
||||
previous &&
|
||||
previous.state !== 'complete' &&
|
||||
previous.state !== 'superseded';
|
||||
const retained = recovering
|
||||
? (previousIntent?.intervals.map(decodeInterval) ?? [])
|
||||
: [];
|
||||
const retained = recovering ?
|
||||
(previousIntent?.intervals.map(decodeInterval) ?? []) :
|
||||
[];
|
||||
// A bridge between two active groups must fence both producers before
|
||||
// either can publish a subset of the newly combined result.
|
||||
for (const joined of touching) {
|
||||
@@ -448,26 +456,27 @@ export class BrokerageOperationsRepository {
|
||||
});
|
||||
await tx.tbankSyncStage.deleteMany({ where: { workId: joined.id } });
|
||||
}
|
||||
if (previous)
|
||||
if (previous) {
|
||||
await tx.tbankSyncStage.deleteMany({ where: { workId: previous.id } });
|
||||
}
|
||||
const targets =
|
||||
recovering && !upgradeGroup && touching.length <= 1
|
||||
? previous?.scopeKey === scopeKey
|
||||
? retained
|
||||
: [...retained, ...uncoveredOperationIntervals(intervals, retained)]
|
||||
: operationIntervals([...retained, ...intervals]);
|
||||
recovering && !upgradeGroup && touching.length <= 1 ?
|
||||
previous?.scopeKey === scopeKey ?
|
||||
retained :
|
||||
[...retained, ...uncoveredOperationIntervals(intervals, retained)] :
|
||||
operationIntervals([...retained, ...intervals]);
|
||||
const atomicTargets = [
|
||||
...(publicationAtomic ? intervals : []),
|
||||
...(recovering && previousIntent?.atomic
|
||||
? (previousIntent.atomicIntervals ?? previousIntent.intervals).map(
|
||||
...(recovering && previousIntent?.atomic ?
|
||||
(previousIntent.atomicIntervals ?? previousIntent.intervals).map(
|
||||
decodeInterval,
|
||||
)
|
||||
: []),
|
||||
) :
|
||||
[]),
|
||||
...touching.flatMap((work) => {
|
||||
const joined = work.intent as OperationsIntent;
|
||||
return joined.atomic
|
||||
? (joined.atomicIntervals ?? joined.intervals).map(decodeInterval)
|
||||
: [];
|
||||
return joined.atomic ?
|
||||
(joined.atomicIntervals ?? joined.intervals).map(decodeInterval) :
|
||||
[];
|
||||
}),
|
||||
];
|
||||
const intent = {
|
||||
@@ -486,14 +495,14 @@ export class BrokerageOperationsRepository {
|
||||
intervals.some((incoming) =>
|
||||
operationIntervalsOverlap(target, incoming),
|
||||
)) ||
|
||||
(
|
||||
previousIntent?.screenIntervals ??
|
||||
(previous?.priority === 1
|
||||
? (previousIntent?.intervals ?? [])
|
||||
: [])
|
||||
).some((key) =>
|
||||
operationIntervalsOverlap(target, decodeInterval(key)),
|
||||
),
|
||||
(
|
||||
previousIntent?.screenIntervals ??
|
||||
(previous?.priority === 1 ?
|
||||
(previousIntent?.intervals ?? []) :
|
||||
[])
|
||||
).some((key) =>
|
||||
operationIntervalsOverlap(target, decodeInterval(key)),
|
||||
),
|
||||
)
|
||||
.map(intervalKey),
|
||||
atomic: atomicTargets.length > 0,
|
||||
@@ -545,20 +554,20 @@ export class BrokerageOperationsRepository {
|
||||
marker.payload as PageProgress,
|
||||
]),
|
||||
);
|
||||
const group = intent.atomic
|
||||
? (intent.atomicIntervals ?? intent.intervals)
|
||||
: intent.intervals;
|
||||
const group = intent.atomic ?
|
||||
(intent.atomicIntervals ?? intent.intervals) :
|
||||
intent.intervals;
|
||||
const nextKey = group.find((key) => !progress.get(key)?.complete);
|
||||
return {
|
||||
priority: work.priority,
|
||||
atomic: intent.atomic,
|
||||
intervals: group.map(decodeInterval),
|
||||
next: nextKey
|
||||
? {
|
||||
next: nextKey ?
|
||||
{
|
||||
interval: decodeInterval(nextKey),
|
||||
cursor: progress.get(nextKey)?.cursor || undefined,
|
||||
}
|
||||
: null,
|
||||
} :
|
||||
null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -582,8 +591,9 @@ export class BrokerageOperationsRepository {
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
if (!work || work.state !== 'superseded')
|
||||
if (!work || work.state !== 'superseded') {
|
||||
return work ? { ...work, id } : null;
|
||||
}
|
||||
target = (work.progress as { successorId: string }).successorId;
|
||||
}
|
||||
throw new Error('Cyclic operations work successor');
|
||||
@@ -599,7 +609,7 @@ export class BrokerageOperationsRepository {
|
||||
errorCode: invalidToken ? 'INVALID_TBANK_TOKEN' : 'TBANK_UNAVAILABLE',
|
||||
},
|
||||
});
|
||||
if (invalidToken)
|
||||
if (invalidToken) {
|
||||
await tx.tbankConnection.update({
|
||||
where: {
|
||||
userId_revision: {
|
||||
@@ -609,6 +619,7 @@ export class BrokerageOperationsRepository {
|
||||
},
|
||||
data: { invalidToken: true },
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -636,34 +647,40 @@ export class BrokerageOperationsRepository {
|
||||
},
|
||||
});
|
||||
const progress = marker?.payload as PageProgress | undefined;
|
||||
if (progress?.complete || (progress?.cursor ?? '') !== (cursor ?? ''))
|
||||
if (progress?.complete || (progress?.cursor ?? '') !== (cursor ?? '')) {
|
||||
throw new Error('Unexpected operations page');
|
||||
if (response.hasNext === undefined)
|
||||
}
|
||||
if (response.hasNext === undefined) {
|
||||
throw new Error('Missing operations pagination state');
|
||||
}
|
||||
const nextCursor = response.nextCursor ?? '';
|
||||
if (
|
||||
response.hasNext &&
|
||||
(!nextCursor ||
|
||||
nextCursor === cursor ||
|
||||
progress?.seen.includes(nextCursor))
|
||||
)
|
||||
) {
|
||||
throw new Error('Invalid operations continuation');
|
||||
}
|
||||
for (const operation of response.items ?? []) {
|
||||
if (!operation.id?.trim())
|
||||
if (!operation.id?.trim()) {
|
||||
throw new Error('Missing broker operation identity');
|
||||
}
|
||||
const occurredAt = operationTimestamp(operation.date);
|
||||
if (
|
||||
operation.date?.seconds === undefined ||
|
||||
!Number.isFinite(occurredAt.valueOf())
|
||||
)
|
||||
) {
|
||||
throw new Error('Invalid operation timestamp');
|
||||
}
|
||||
if (
|
||||
occurredAt < interval.from ||
|
||||
occurredAt >= interval.to ||
|
||||
(interval.instrumentUid &&
|
||||
operation.instrumentUid !== interval.instrumentUid)
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const payload = JSON.parse(
|
||||
JSON.stringify({ ...operation, cursor: undefined }),
|
||||
) as Prisma.InputJsonObject;
|
||||
@@ -672,10 +689,12 @@ export class BrokerageOperationsRepository {
|
||||
const previous = await tx.tbankSyncStage.findUnique({
|
||||
where: { workId_attemptId_sourceKey_itemKey: identity },
|
||||
});
|
||||
if (previous && !isDeepStrictEqual(previous.payload, payload))
|
||||
if (previous && !isDeepStrictEqual(previous.payload, payload)) {
|
||||
throw new Error('Conflicting broker operation identity');
|
||||
if (!previous)
|
||||
}
|
||||
if (!previous) {
|
||||
await tx.tbankSyncStage.create({ data: { ...identity, payload } });
|
||||
}
|
||||
}
|
||||
const payload: PageProgress = {
|
||||
complete: !response.hasNext,
|
||||
@@ -712,8 +731,9 @@ export class BrokerageOperationsRepository {
|
||||
fetchedAt: Date,
|
||||
advanceWork = false,
|
||||
): Promise<boolean> {
|
||||
if (!intervals.length)
|
||||
if (!intervals.length) {
|
||||
throw new Error('Empty operations publication group');
|
||||
}
|
||||
return this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
if (!(await claim(tx, attempt))) return false;
|
||||
@@ -734,8 +754,9 @@ export class BrokerageOperationsRepository {
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!(marker?.payload as PageProgress | undefined)?.complete)
|
||||
if (!(marker?.payload as PageProgress | undefined)?.complete) {
|
||||
throw new Error('Incomplete operations publication group');
|
||||
}
|
||||
const staged = await tx.tbankSyncStage.findMany({
|
||||
where: { ...stage, itemKey: { not: progressItem } },
|
||||
});
|
||||
@@ -745,10 +766,11 @@ export class BrokerageOperationsRepository {
|
||||
if (
|
||||
previous !== undefined &&
|
||||
!isDeepStrictEqual(previous, row.payload)
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Conflicting broker operation identity across intervals',
|
||||
);
|
||||
}
|
||||
groupRows.set(identity, row.payload);
|
||||
}
|
||||
const rows = staged.filter((row) => {
|
||||
@@ -764,16 +786,16 @@ export class BrokerageOperationsRepository {
|
||||
where: {
|
||||
...scope,
|
||||
occurredAt: { gte: interval.from, lt: interval.to },
|
||||
...(interval.instrumentUid
|
||||
? { instrumentUid: interval.instrumentUid }
|
||||
: {}),
|
||||
...(interval.instrumentUid ?
|
||||
{ instrumentUid: interval.instrumentUid } :
|
||||
{}),
|
||||
},
|
||||
});
|
||||
const incoming = new Set(rows.map((row) => row.itemKey));
|
||||
const removed = existing.filter(
|
||||
(row) => !incoming.has(row.brokerOperationId),
|
||||
);
|
||||
for (const row of removed)
|
||||
for (const row of removed) {
|
||||
await tx.tbankOperation.delete({
|
||||
where: {
|
||||
userId_revision_accountId_brokerOperationId: {
|
||||
@@ -782,6 +804,7 @@ export class BrokerageOperationsRepository {
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (removed.length) changedAccounts.add(interval.accountId);
|
||||
for (const row of rows) {
|
||||
const operation = row.payload as TbankOperationItem;
|
||||
@@ -789,8 +812,9 @@ export class BrokerageOperationsRepository {
|
||||
const previous = await tx.tbankOperation.findUnique({
|
||||
where: { userId_revision_accountId_brokerOperationId: identity },
|
||||
});
|
||||
if (previous && isDeepStrictEqual(previous.payload, operation))
|
||||
if (previous && isDeepStrictEqual(previous.payload, operation)) {
|
||||
continue;
|
||||
}
|
||||
const data = {
|
||||
occurredAt: operationTimestamp(operation.date),
|
||||
instrumentUid: operation.instrumentUid ?? null,
|
||||
@@ -818,7 +842,7 @@ export class BrokerageOperationsRepository {
|
||||
update: { fetchedAt, asOf: interval.to },
|
||||
});
|
||||
}
|
||||
for (const accountId of changedAccounts)
|
||||
for (const accountId of changedAccounts) {
|
||||
await tx.tbankAccount.update({
|
||||
where: {
|
||||
userId_revision_accountId: {
|
||||
@@ -829,7 +853,8 @@ export class BrokerageOperationsRepository {
|
||||
},
|
||||
data: { operationsRevision: { increment: 1 } },
|
||||
});
|
||||
for (const interval of intervals)
|
||||
}
|
||||
for (const interval of intervals) {
|
||||
await tx.tbankSyncStage.deleteMany({
|
||||
where: {
|
||||
workId: attempt.id,
|
||||
@@ -837,6 +862,7 @@ export class BrokerageOperationsRepository {
|
||||
sourceKey: intervalKey(interval),
|
||||
},
|
||||
});
|
||||
}
|
||||
if (advanceWork) {
|
||||
const work = await tx.tbankSyncWork.findUniqueOrThrow({
|
||||
where: { id: attempt.id },
|
||||
@@ -867,9 +893,9 @@ export class BrokerageOperationsRepository {
|
||||
intent.screenIntervals ??
|
||||
(work.priority === 1 ? intent.intervals : [])
|
||||
).includes(key),
|
||||
)
|
||||
? 1
|
||||
: 2,
|
||||
) ?
|
||||
1 :
|
||||
2,
|
||||
progress: { remainingIntervals: remaining.length },
|
||||
},
|
||||
});
|
||||
@@ -882,8 +908,10 @@ export class BrokerageOperationsRepository {
|
||||
}
|
||||
|
||||
function intervalKey(interval: OperationInterval): string {
|
||||
if (!(interval.from < interval.to))
|
||||
// eslint-disable-next-line sonarjs/no-inverted-boolean-check -- Negation also rejects Invalid Date; >= would silently accept NaN.
|
||||
if (!(interval.from < interval.to)) {
|
||||
throw new Error('Invalid operations interval');
|
||||
}
|
||||
return JSON.stringify([
|
||||
interval.accountId,
|
||||
interval.from.toISOString(),
|
||||
|
||||
Reference in New Issue
Block a user