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