91 lines
3.0 KiB
TypeScript
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() },
|
|
});
|
|
});
|
|
}
|
|
}
|