Files
backend/src/modules/investments/tbank-connection/tbank-connection.service.ts
T
ksv741 afdb6b15fd
Backend verify / commits (pull_request) Successful in 1m28s
Backend verify / security (pull_request) Successful in 1m7s
Backend verify / verify (pull_request) Successful in 6m6s
refactor: replace deep imports with native node aliases (WealthLedger/backend#165)
2026-11-04 22:58:36 +03:00

91 lines
3.0 KiB
TypeScript

import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { HttpErrorCode } from '#src/common/error-code';
import {
supportedTbankAccounts,
TbankClient,
TbankTokenService,
} from '#src/integrations/tbank/tbank.api';
import { PrismaService } from '#src/prisma/prisma.service';
type TbankTokenValidator = Pick<TbankClient, 'validateToken'>;
@Injectable()
export class TbankConnectionService {
constructor(
private readonly prisma: PrismaService,
private readonly tokens: TbankTokenService,
@Inject(TbankClient) private readonly tbank: TbankTokenValidator,
) {}
async status(userId: string) {
const connection = await this.prisma.tbankConnection.findUnique({
where: { userId },
});
return {
connected: Boolean(connection),
connectedAt: connection?.connectedAt.toISOString() ?? null,
};
}
async put(userId: string, value: string) {
const token = value.trim();
if (!token) throw new BadRequestException({ code: HttpErrorCode.ValidationError });
const accounts = await this.tokens.translateInvalidToken(() =>
this.tbank.validateToken(token),
);
const encryptedToken = this.tokens.encrypt(token);
const confirmed = supportedTbankAccounts(accounts).map(({ id }) => id);
const connection = await this.prisma.$transaction(async (tx) => {
// Cascade old private data and attempts in the same transaction that
// publishes the validated membership and the new connection revision.
await tx.tbankConnection.deleteMany({ where: { userId } });
const now = new Date();
// Value history survives token replacement but is visible only for
// accounts confirmed by the new token.
await tx.tbankPortfolioValueHistory.updateMany({
where: { userId, accountId: { in: confirmed } },
data: { deletedAt: null },
});
await tx.tbankPortfolioValueHistory.updateMany({
where: { userId, accountId: { notIn: confirmed }, deletedAt: null },
data: { deletedAt: now },
});
return tx.tbankConnection.create({
data: {
userId,
encryptedToken,
connectedAt: now,
membershipAt: now,
membershipRevision: 1,
accounts: {
create: supportedTbankAccounts(accounts).map(
({ id, ...account }, upstreamOrder) => ({
...account,
accountId: id,
upstreamOrder,
}),
),
},
},
});
});
return {
connected: true,
connectedAt: connection.connectedAt.toISOString(),
};
}
async delete(userId: string): Promise<void> {
await this.prisma.$transaction(async (tx) => {
await tx.tbankConnection.deleteMany({ where: { userId } });
// History stays stored but hidden until a new connection reconfirms
// the same account.
await tx.tbankPortfolioValueHistory.updateMany({
where: { userId, deletedAt: null },
data: { deletedAt: new Date() },
});
});
}
}