From b3d4801cfe67ecf301c58c894676b13b8833c4a6 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 4 Nov 2026 16:34:40 +0300 Subject: [PATCH] refactor: move bank card queries into repository (WealthLedger/backend#171) --- .../application/bank-cards.service.ts | 40 ++++++++----------- .../infrastructure/bank-cards.repository.ts | 35 ++++++++++++++++ 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/modules/banking/bank-cards/application/bank-cards.service.ts b/src/modules/banking/bank-cards/application/bank-cards.service.ts index a8c4bd6..96685b0 100644 --- a/src/modules/banking/bank-cards/application/bank-cards.service.ts +++ b/src/modules/banking/bank-cards/application/bank-cards.service.ts @@ -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 { 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); }); } } diff --git a/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts b/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts index 8856aaf..3943cf4 100644 --- a/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts +++ b/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts @@ -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({