refactor: move bank card queries into repository (WealthLedger/backend#171)

This commit is contained in:
2026-11-04 22:28:32 +03:00
parent b3ca11d141
commit b3d4801cfe
2 changed files with 51 additions and 24 deletions
@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { BankCard, Prisma } from '../../../../../generated/prisma/client';
import { PrismaService } from '../../../../prisma/prisma.service';
import { bankBalance } from '../domain/bank-card.calculations';
import { rubMoney } from '../domain/rub-money';
import { BankCardsRepository } from '../infrastructure/bank-cards.repository';
@@ -20,15 +19,10 @@ import {
missing,
} from './bank-card-input';
const EXPECTED_UPDATED_CARDS = 1;
type Tx = Prisma.TransactionClient;
@Injectable()
export class BankCardsService {
constructor(
private readonly prisma: PrismaService,
private readonly repository: BankCardsRepository,
) {}
constructor(private readonly repository: BankCardsRepository) {}
private async cardView(tx: Tx, card: BankCard): Promise<BankCardDto> {
const rows = await this.repository.cardOperations(tx, card);
@@ -64,7 +58,7 @@ export class BankCardsService {
const input = cardInput(dto),
hash = fingerprint(input);
return this.repository.write(async (tx) => {
const existing = await tx.bankCard.findUnique({ where: { id: dto.id } });
const existing = await this.repository.cardById(tx, dto.id);
if (existing) {
if (existing.userId !== userId) missing();
// eslint-disable-next-line security/detect-possible-timing-attacks -- This compares an idempotency fingerprint after ownership validation, not a secret.
@@ -77,8 +71,11 @@ export class BankCardsService {
await this.repository.seed(tx, userId);
return this.cardView(
tx,
await tx.bankCard.create({
data: { ...input, id: dto.id, userId, createFingerprint: hash },
await this.repository.createCard(tx, {
...input,
id: dto.id,
userId,
createFingerprint: hash,
}),
);
});
@@ -111,20 +108,16 @@ export class BankCardsService {
}
currentDate(input.openingDate);
if (
await tx.bankCardOperation.findFirst({
where: {
userId,
deletedAt: null,
date: { lt: input.openingDate },
OR: [{ cardId: id }, { targetCardId: id }],
},
})
await this.repository.hasOperationsBefore(tx, userId, id, input.openingDate)
) {
fail(BankErrorCode.OpeningDateConflict);
}
const changed = await tx.bankCard.updateMany({
where: { id, userId, version: dto.version },
data: {
const updated = await this.repository.updateCard(
tx,
userId,
id,
dto.version,
{
...input,
paymentSystem: input.paymentSystem ?? null,
version: { increment: 1 },
@@ -136,9 +129,8 @@ export class BankCardsService {
: null,
}),
},
});
if (changed.count !== EXPECTED_UPDATED_CARDS) conflict();
return this.cardView(tx, await this.repository.card(tx, userId, id));
);
return this.cardView(tx, updated);
});
}
}
@@ -13,6 +13,7 @@ import { BankOperationKind } from '../domain/bank-operation-kind';
const INITIAL_RETRY_COUNT = 0;
const LAST_SEED_OFFSET = 1;
const EXPECTED_UPDATED_CARDS = 1;
const EXPECTED_UPDATED_CATEGORIES = 1;
const VERSION_INCREMENT = 1;
@@ -56,6 +57,40 @@ export class BankCardsRepository {
});
}
cardById(tx: Tx, id: string) {
return tx.bankCard.findUnique({ where: { id } });
}
createCard(tx: Tx, data: Prisma.BankCardUncheckedCreateInput) {
return tx.bankCard.create({ data });
}
async updateCard(
tx: Tx,
userId: string,
id: string,
version: number,
data: Prisma.BankCardUpdateManyMutationInput,
) {
const changed = await tx.bankCard.updateMany({
where: { id, userId, version },
data,
});
if (changed.count !== EXPECTED_UPDATED_CARDS) conflict();
return this.card(tx, userId, id);
}
hasOperationsBefore(tx: Tx, userId: string, id: string, openingDate: string) {
return tx.bankCardOperation.findFirst({
where: {
userId,
deletedAt: null,
date: { lt: openingDate },
OR: [{ cardId: id }, { targetCardId: id }],
},
});
}
async categories(userId: string) {
return (
await this.prisma.expenseCategory.findMany({