Files
backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts
T
ksv741 d698ed9425
Backend verify / commits (pull_request) Successful in 29s
Backend verify / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 9m36s
refactor!: simplify runtime coordination #7
Keep financial ownership, atomic publication and refresh CAS.
Failed refresh leaves cookies untouched; logout still clears them.

Remove full-history candle prefetch and special metadata retries.
Use native Pino request context instead of parallel middleware.

BREAKING CHANGE: remove investment stateVersion, queued sync state,
and tbank-sync status progress/resources. Consumers must observe
resources through sync=observe with the updated frontend contract.
2026-10-03 11:38:08 +03:00

127 lines
4.2 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import type { Prisma } from '../../../../generated/prisma/client';
import type { TbankSyncAttempt } from '../../../integrations/tbank/tbank.api';
import type { InvestmentScreenDto } from './tbank-sync.dto';
import { PrismaService } from '../../../prisma/prisma.service';
export type ScreenIntent = { now: string; scope: InvestmentScreenDto };
export type ScreenProgress = {
attempts?: Record<string, string>;
children: string[];
dispatched: boolean;
};
@Injectable()
export class TbankSyncRepository {
constructor(private readonly prisma: PrismaService) {}
reserve(
userId: string,
revision: string,
scope: InvestmentScreenDto,
runId: string,
) {
return this.prisma.$transaction(async (tx) => {
const valid = await tx.tbankConnection.updateMany({
where: { userId, revision, invalidToken: false },
data: { membershipRevision: { increment: 0 } },
});
if (!valid.count) return null;
const scopeKey = `screen:${JSON.stringify(scope)}`;
const key = { userId, revision, scopeKey };
const previous = await tx.tbankSyncWork.findUnique({
where: { userId_revision_scopeKey: key },
});
if (previous?.state === 'running' && previous.processRunId === runId) {
const progress = previous.progress as ScreenProgress;
const children = progress.children ?? [];
if (
!progress.dispatched ||
(await tx.tbankSyncWork.count({
where: { userId, revision, id: { in: children }, state: 'running' },
}))
) {
return { work: previous, launch: false };
}
}
const data = {
attemptId: randomUUID(),
processRunId: runId,
state: 'running',
priority: 0,
intent: JSON.parse(
JSON.stringify({ scope, now: new Date().toISOString() }),
) as Prisma.InputJsonObject,
progress: { children: [], dispatched: false },
errorCode: null,
};
const work = await tx.tbankSyncWork.upsert({
where: { userId_revision_scopeKey: key },
create: { ...key, ...data, id: randomUUID() },
update: { ...data, id: randomUUID() },
});
return { work, launch: true };
});
}
read(userId: string, revision: string, id: string) {
return this.prisma.$transaction(async (tx) => {
const work = await tx.tbankSyncWork.findFirst({
where: { userId, revision, id },
});
if (!work) return null;
const progress = work.progress as ScreenProgress;
const children = await tx.tbankSyncWork.findMany({
where: { userId, revision, id: { in: progress.children ?? [] } },
select: { id: true, attemptId: true, state: true, errorCode: true },
});
return {
work,
children: children.map((child) =>
progress.attempts?.[child.id] &&
progress.attempts[child.id] !== child.attemptId
? { ...child, state: 'cancelled', errorCode: null }
: child,
),
progress,
};
});
}
async progress(
attempt: TbankSyncAttempt,
children: string[],
dispatched: boolean,
errorCode: null | string = null,
) {
return this.prisma.$transaction(async (tx) => {
const previous = await tx.tbankSyncWork.findFirst({ where: attempt });
if (!previous) return { count: 0 };
const old = previous.progress as ScreenProgress;
const rows = await tx.tbankSyncWork.findMany({
where: {
userId: attempt.userId,
revision: attempt.revision,
id: { in: children },
},
select: { id: true, attemptId: true },
});
const attempts = {
...Object.fromEntries(rows.map((row) => [row.id, row.attemptId])),
...old.attempts,
};
return tx.tbankSyncWork.updateMany({
where: {
...attempt,
state: 'running',
connection: { invalidToken: false },
},
data: {
progress: { children: [...new Set(children)], attempts, dispatched },
...(errorCode ? { state: 'failed', errorCode } : {}),
},
});
});
}
}