From 0f65ba7f133a932bb228705c09ef22f495c7576c Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 4 Nov 2026 14:52:38 +0300 Subject: [PATCH] refactor: move category persistence into bank repository (WealthLedger/backend#171) --- .../application/expense-categories.service.ts | 67 ++----------------- .../infrastructure/bank-cards.repository.ts | 63 +++++++++++++++++ 2 files changed, 68 insertions(+), 62 deletions(-) diff --git a/src/modules/banking/bank-cards/application/expense-categories.service.ts b/src/modules/banking/bank-cards/application/expense-categories.service.ts index 4dbad85..65dadef 100644 --- a/src/modules/banking/bank-cards/application/expense-categories.service.ts +++ b/src/modules/banking/bank-cards/application/expense-categories.service.ts @@ -1,81 +1,24 @@ import { Injectable } from '@nestjs/common'; -import { randomUUID } from 'node:crypto'; -import { PrismaService } from '../../../../prisma/prisma.service'; import { MAX_CATEGORY_NAME_LENGTH } from '../domain/bank-card-limits'; import { BankCardsRepository } from '../infrastructure/bank-cards.repository'; -import { conflict, missing, name } from './bank-card-input'; - -const EXPECTED_UPDATED_CATEGORIES = 1; -const VERSION_INCREMENT = 1; +import { name } from './bank-card-input'; @Injectable() export class ExpenseCategoriesService { - constructor( - private readonly prisma: PrismaService, - private readonly repository: BankCardsRepository, - ) {} + constructor(private readonly repository: BankCardsRepository) {} async categories(userId: string) { - return ( - await this.prisma.expenseCategory.findMany({ - where: { userId }, - orderBy: [{ name: 'asc' }, { id: 'asc' }], - }) - ).map(({ id, name, seedKey, version }) => ({ - id, - name, - isDefault: seedKey === 'other', - version, - })); + return this.repository.categories(userId); } createCategory(userId: string, value: string) { const label = name(value, MAX_CATEGORY_NAME_LENGTH); - return this.repository.write(async (tx) => { - await this.repository.seed(tx, userId); - const row = await tx.expenseCategory.create({ - data: { - id: randomUUID(), - userId, - name: label, - nameKey: label.toLocaleLowerCase('ru-RU'), - }, - }); - return { - id: row.id, - name: row.name, - isDefault: false, - version: row.version, - }; - }); + return this.repository.createCategory(userId, label); } updateCategory(userId: string, id: string, value: string, version: number) { const label = name(value, MAX_CATEGORY_NAME_LENGTH); - return this.repository.write(async (tx) => { - const row = await tx.expenseCategory.findFirst({ where: { id, userId } }); - if (!row) missing(); - if ( - ( - await tx.expenseCategory.updateMany({ - where: { id, userId, version }, - data: { - name: label, - nameKey: label.toLocaleLowerCase('ru-RU'), - version: { increment: 1 }, - }, - }) - ).count !== EXPECTED_UPDATED_CATEGORIES - ) { - conflict(); - } - return { - id, - name: label, - isDefault: row.seedKey === 'other', - version: version + VERSION_INCREMENT, - }; - }); + return this.repository.updateCategory(userId, id, label, version); } } 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 4f33ba9..488dd68 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,8 @@ import { BankOperationKind } from '../domain/bank-operation-kind'; const INITIAL_RETRY_COUNT = 0; const LAST_SEED_OFFSET = 1; +const EXPECTED_UPDATED_CATEGORIES = 1; +const VERSION_INCREMENT = 1; type Tx = Prisma.TransactionClient; const TRANSACTION_MAX_WAIT_MS = 10_000; @@ -32,6 +34,67 @@ const seeds = [ @Injectable() export class BankCardsRepository { constructor(private readonly prisma: PrismaService) {} + async categories(userId: string) { + return ( + await this.prisma.expenseCategory.findMany({ + where: { userId }, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }) + ).map(({ id, name, seedKey, version }) => ({ + id, + name, + isDefault: seedKey === 'other', + version, + })); + } + + createCategory(userId: string, label: string) { + return this.write(async (tx) => { + await this.seed(tx, userId); + const row = await tx.expenseCategory.create({ + data: { + id: randomUUID(), + userId, + name: label, + nameKey: label.toLocaleLowerCase('ru-RU'), + }, + }); + return { + id: row.id, + name: row.name, + isDefault: false, + version: row.version, + }; + }); + } + + updateCategory(userId: string, id: string, label: string, version: number) { + return this.write(async (tx) => { + const row = await tx.expenseCategory.findFirst({ where: { id, userId } }); + if (!row) missing(); + if ( + ( + await tx.expenseCategory.updateMany({ + where: { id, userId, version }, + data: { + name: label, + nameKey: label.toLocaleLowerCase('ru-RU'), + version: { increment: 1 }, + }, + }) + ).count !== EXPECTED_UPDATED_CATEGORIES + ) { + conflict(); + } + return { + id, + name: label, + isDefault: row.seedKey === 'other', + version: version + VERSION_INCREMENT, + }; + }); + } + async write(run: (tx: Tx) => Promise): Promise { for (let attempt = INITIAL_RETRY_COUNT; ; attempt++) { try {