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; 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 } : {}), }, }); }); } }