Files
backend/src/modules/investments/tbank-sync/tbank-sync.service.ts
T
codex-bot a80bbbf1e2
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 59s
Backend verify / verify (push) Failing after 6m3s
refactor: check persisted json payload shape instead of double casts (#152) (#154)
2026-11-02 07:31:24 +03:00

103 lines
3.2 KiB
TypeScript

import {
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { TbankAccessService } from '../../../integrations/tbank/tbank.api';
import { BrokerageOperationsSyncService } from '../operations/operations.api';
import { TbankSyncStatusDto } from './tbank-sync.dto';
import { screenIntent, TbankSyncRepository } from './tbank-sync.repository';
@Injectable()
export class TbankSyncService {
private readonly runId = randomUUID();
private readonly startedAt = new Date();
constructor(
private readonly repository: TbankSyncRepository,
private readonly access: TbankAccessService,
private readonly operations: BrokerageOperationsSyncService,
) {}
async status(userId: string, id: string): Promise<TbankSyncStatusDto> {
const connection = await this.access
.local(userId)
.catch((error: unknown) => {
if (error instanceof ConflictException) throw new NotFoundException();
throw error;
});
const saved = await this.repository.read(userId, connection.revision, id);
if (!saved) throw new NotFoundException();
if (!saved.work.scopeKey.startsWith('screen:')) {
return this.sourceStatus(userId, saved.work);
}
const intent = screenIntent(saved.work.intent);
await this.access.local(userId, intent.scope.accountId);
const children = await Promise.all(
saved.children.map((child) =>
child.state === 'superseded'
? this.operations.observe(userId, child.id)
: Promise.resolve(child),
),
);
const running = children.some((child) => child.state === 'running');
const failed =
saved.work.errorCode ??
children.find((child) => child.errorCode)?.errorCode ??
null;
const state =
saved.work.processRunId !== this.runId &&
(!saved.progress.dispatched || running)
? 'cancelled'
: !saved.progress.dispatched || running
? 'running'
: failed
? 'failed'
: children.some((child) => child.state === 'cancelled')
? 'cancelled'
: 'succeeded';
if (
(await this.access.local(userId, intent.scope.accountId)).revision !==
connection.revision
) {
throw new NotFoundException();
}
return {
id,
...intent,
state,
errorCode: failed,
};
}
private async sourceStatus(
userId: string,
work: NonNullable<Awaited<ReturnType<TbankSyncRepository['read']>>>['work'],
): Promise<TbankSyncStatusDto> {
const observed = work.scopeKey.startsWith('operations:')
? await this.operations.observe(userId, work.id)
: work;
const state =
observed.state === 'running'
? observed.updatedAt < this.startedAt
? 'cancelled'
: 'running'
: observed.state === 'failed'
? 'failed'
: observed.state === 'cancelled'
? 'cancelled'
: 'succeeded';
if ((await this.access.local(userId)).revision !== work.revision) {
throw new NotFoundException();
}
return {
id: work.id,
scope: null,
now: work.createdAt.toISOString(),
state,
errorCode: observed.errorCode,
};
}
}