refactor: move category persistence into bank repository (WealthLedger/backend#171) #181
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user