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.
127 lines
4.2 KiB
TypeScript
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 } : {}),
|
|
},
|
|
});
|
|
});
|
|
}
|
|
}
|