refactor: move category persistence into bank repository (WealthLedger/backend#171) #181

Closed
codex-bot wants to merge 1 commits from refactor/171b-bank-card-repository into develop
2 changed files with 68 additions and 62 deletions
@@ -1,81 +1,24 @@
import { Injectable } from '@nestjs/common'; 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 { MAX_CATEGORY_NAME_LENGTH } from '../domain/bank-card-limits';
import { BankCardsRepository } from '../infrastructure/bank-cards.repository'; import { BankCardsRepository } from '../infrastructure/bank-cards.repository';
import { conflict, missing, name } from './bank-card-input'; import { name } from './bank-card-input';
const EXPECTED_UPDATED_CATEGORIES = 1;
const VERSION_INCREMENT = 1;
@Injectable() @Injectable()
export class ExpenseCategoriesService { export class ExpenseCategoriesService {
constructor( constructor(private readonly repository: BankCardsRepository) {}
private readonly prisma: PrismaService,
private readonly repository: BankCardsRepository,
) {}
async categories(userId: string) { async categories(userId: string) {
return ( return this.repository.categories(userId);
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, value: string) { createCategory(userId: string, value: string) {
const label = name(value, MAX_CATEGORY_NAME_LENGTH); const label = name(value, MAX_CATEGORY_NAME_LENGTH);
return this.repository.write(async (tx) => { return this.repository.createCategory(userId, label);
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,
};
});
} }
updateCategory(userId: string, id: string, value: string, version: number) { updateCategory(userId: string, id: string, value: string, version: number) {
const label = name(value, MAX_CATEGORY_NAME_LENGTH); const label = name(value, MAX_CATEGORY_NAME_LENGTH);
return this.repository.write(async (tx) => { return this.repository.updateCategory(userId, id, label, version);
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,
};
});
} }
} }
@@ -13,6 +13,8 @@ import { BankOperationKind } from '../domain/bank-operation-kind';
const INITIAL_RETRY_COUNT = 0; const INITIAL_RETRY_COUNT = 0;
const LAST_SEED_OFFSET = 1; const LAST_SEED_OFFSET = 1;
const EXPECTED_UPDATED_CATEGORIES = 1;
const VERSION_INCREMENT = 1;
type Tx = Prisma.TransactionClient; type Tx = Prisma.TransactionClient;
const TRANSACTION_MAX_WAIT_MS = 10_000; const TRANSACTION_MAX_WAIT_MS = 10_000;
@@ -32,6 +34,67 @@ const seeds = [
@Injectable() @Injectable()
export class BankCardsRepository { export class BankCardsRepository {
constructor(private readonly prisma: PrismaService) {} 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<T>(run: (tx: Tx) => Promise<T>): Promise<T> { async write<T>(run: (tx: Tx) => Promise<T>): Promise<T> {
for (let attempt = INITIAL_RETRY_COUNT; ; attempt++) { for (let attempt = INITIAL_RETRY_COUNT; ; attempt++) {
try { try {