feat(server): add income

This commit is contained in:
Sergey Krylov 2025-09-16 05:57:06 +03:00
parent 59ff36cb18
commit ee6276e8ab
24 changed files with 901 additions and 0 deletions

View File

@ -26,6 +26,7 @@ model User {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
expensesLists ExpenseList[]
incomeList IncomeList[]
bankCard BankCard[]
cash Cash[]
@ -60,6 +61,34 @@ model ExpenseItem {
@@map("expenses_items")
}
model IncomeList {
id String @id @default(uuid())
items IncomeItem[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
title String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("incomes_lists")
}
model IncomeItem {
id String @id @default(uuid())
incomeList IncomeList @relation(fields: [incomeListId], references: [id], onDelete: Cascade)
incomeListId String
title String
description String?
date DateTime
amount Float
currency String @default("RUB")
count Float @default(1)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("incomes_items")
}
model BankAccount {
id String @id @default(uuid())
balance Float @default(0)

View File

@ -6,6 +6,7 @@ import { BankAccountModule } from './bank-account/bank-account.module';
import { BankCardModule } from './bank-card/bank-card.module';
import { CashModule } from './cash/cash.module';
import { ExpensesModule } from './expenses/expenses.module';
import { IncomesModule } from './incomes/incomes.module';
import { PrismaModule } from './prisma/prisma.module';
@Module({
@ -19,6 +20,7 @@ import { PrismaModule } from './prisma/prisma.module';
BankAccountModule,
BankCardModule,
CashModule,
IncomesModule,
],
})
export class AppModule {}

View File

@ -0,0 +1,30 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBearerAuth,
ApiOkResponse,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { Authorization } from '@/shared/decorators';
import { IncomeListItemDto } from '../../dto';
function Documentation() {
return applyDecorators(
ApiOperation({
summary: 'Создать элемент дохода',
description: 'Создать элемент дохода',
}),
ApiBearerAuth(),
ApiParam({ name: 'id', type: String, description: 'ID листа доходов' }),
ApiOkResponse({ description: 'Успешно создан элемент дохода', type: IncomeListItemDto }),
);
}
export function CreateIncomeListItem() {
return applyDecorators(
Authorization(),
Documentation(),
);
}

View File

@ -0,0 +1,33 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBearerAuth,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { Authorization } from '@/shared/decorators';
import { IncomeListItemDto } from '../../dto';
function Documentation() {
return applyDecorators(
ApiOperation({
summary: 'Удалить элемент дохода',
description: 'Удалить элемент дохода',
}),
ApiBearerAuth(),
ApiParam({ name: 'listId', type: String, description: 'ID листа доходов' }),
ApiParam({ name: 'id', type: String, description: 'ID элемента' }),
ApiNotFoundResponse({ description: 'Лист доходов не найден' }),
ApiNotFoundResponse({ description: 'Позиция доходов не найдена' }),
ApiOkResponse({ description: 'Успешно удален элемент дохода', type: IncomeListItemDto }),
);
}
export function DeleteIncomeListItem() {
return applyDecorators(
Authorization(),
Documentation(),
);
}

View File

@ -0,0 +1,33 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBearerAuth,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { Authorization } from '@/shared/decorators';
import { IncomeListItemDto } from '../../dto';
function Documentation() {
return applyDecorators(
ApiOperation({
summary: 'Редактировать элемент дохода',
description: 'Редактировать элемент дохода',
}),
ApiBearerAuth(),
ApiParam({ name: 'listId', type: String, description: 'ID листа доходов' }),
ApiParam({ name: 'id', type: String, description: 'ID элемента' }),
ApiOkResponse({ description: 'Успешно изменен элемент дохода', type: IncomeListItemDto }),
ApiNotFoundResponse({ description: 'Лист доходов не найден' }),
ApiNotFoundResponse({ description: 'Позиция доходов не найдена' }),
);
}
export function EditIncomeListItem() {
return applyDecorators(
Authorization(),
Documentation(),
);
}

View File

@ -0,0 +1,34 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBearerAuth,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { Authorization } from '@/shared/decorators';
import { IncomeListItemDto } from '../../dto';
function Documentation() {
return applyDecorators(
ApiOperation({
summary: 'Найти элемент дохода',
description: 'Найти элемент дохода',
}),
ApiBearerAuth(),
ApiParam({ name: 'listId', type: String, description: 'ID листа доходов' }),
ApiParam({ name: 'id', type: String, description: 'ID элемента' }),
ApiNotFoundResponse({ description: 'Лист доходов не найден' }),
ApiNotFoundResponse({ description: 'Позиция доходов не найдена' }),
ApiOkResponse({ description: 'Успешно найден элемент дохода', type: IncomeListItemDto }),
);
}
export function FindIncomeListItem() {
return applyDecorators(
Authorization(),
Documentation(),
);
}

View File

@ -0,0 +1,24 @@
import { applyDecorators } from '@nestjs/common';
import { ApiBearerAuth, ApiOkResponse, ApiOperation } from '@nestjs/swagger';
import { Authorization } from '@/shared/decorators';
import { IncomeListDto } from '../../dto';
function Documentation() {
return applyDecorators(
ApiOperation({
summary: 'Создать лист доходов',
description: 'Создать лист доходов',
}),
ApiBearerAuth(),
ApiOkResponse({ description: 'Успешно создан лист доходов', type: IncomeListDto }),
);
}
export function CreateIncomeList() {
return applyDecorators(
Authorization(),
Documentation(),
);
}

View File

@ -0,0 +1,32 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBearerAuth,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { Authorization } from '@/shared/decorators';
import { DeletedIncomeListDto } from '../../dto';
function Documentation() {
return applyDecorators(
ApiOperation({
summary: 'Удалить лист доходов',
description: 'Удалить лист доходов',
}),
ApiBearerAuth(),
ApiParam({ name: 'id', type: String, description: 'ID листа доходов' }),
ApiNotFoundResponse({ description: 'Лист доходов не найден' }),
ApiOkResponse({ description: 'Успешно удален лист доходов', type: DeletedIncomeListDto }),
);
}
export function DeleteIncomeList() {
return applyDecorators(
Authorization(),
Documentation(),
);
}

View File

@ -0,0 +1,32 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBearerAuth,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { Authorization } from '@/shared/decorators';
import { IncomeListDto } from '../../dto';
function Documentation() {
return applyDecorators(
ApiOperation({
summary: 'Редактировать лист доходов',
description: 'Редактировать лист доходов',
}),
ApiBearerAuth(),
ApiParam({ name: 'id', type: String, description: 'ID листа доходов' }),
ApiNotFoundResponse({ description: 'Лист доходов не найден' }),
ApiOkResponse({ description: 'Успешно изменен лист доходов', type: IncomeListDto }),
);
}
export function EditIncomeList() {
return applyDecorators(
Authorization(),
Documentation(),
);
}

View File

@ -0,0 +1,28 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBearerAuth,
ApiOkResponse,
ApiOperation,
} from '@nestjs/swagger';
import { Authorization } from '@/shared/decorators';
import { IncomeListDto } from '../../dto';
function Documentation() {
return applyDecorators(
ApiBearerAuth(),
ApiOperation({
summary: 'Найти все листы доходов',
description: 'Найти все листы доходов',
}),
ApiOkResponse({ description: 'Успешно создан лист доходов', type: [IncomeListDto] }),
);
}
export function FindAllIncomeLists() {
return applyDecorators(
Authorization(),
Documentation(),
);
}

View File

@ -0,0 +1,32 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBearerAuth,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { Authorization } from '@/shared/decorators';
import { IncomeListDto } from '../../dto';
function Documentation() {
return applyDecorators(
ApiBearerAuth(),
ApiOperation({
summary: 'Найти лист доходов',
description: 'Найти лист доходов',
}),
ApiParam({ name: 'id', type: String, description: 'ID листа доходов' }),
ApiNotFoundResponse({ description: 'Лист доходов не найден' }),
ApiOkResponse({ description: 'Успешно создан лист доходов', type: IncomeListDto }),
);
}
export function FindIncomeList() {
return applyDecorators(
Authorization(),
Documentation(),
);
}

View File

@ -0,0 +1,12 @@
export { CreateIncomeList } from './income-list/create-income-list.decorator';
export { CreateIncomeListItem } from './income-list-item/create-income-list-item.decorator';
export { DeleteIncomeList } from './income-list/delete-income-list.decorator';
export { DeleteIncomeListItem } from './income-list-item/delete-income-list-item.decorator';
export { EditIncomeList } from './income-list/edit-income-list.decorator';
export { EditIncomeListItem } from './income-list-item/edit-income-list-item.decorator';
export { FindIncomeList } from './income-list/find-income-list.decorator';
export { FindAllIncomeLists } from './income-list/find-all-income-list.decorator';
export { FindIncomeListItem } from './income-list-item/find-income-list-item.decorator';

View File

@ -0,0 +1,59 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
import { IsISODateTimeString } from '../../../../shared/validators';
export class CreateIncomeListItemDto {
@ApiProperty({
description: 'Заголовок дохода',
example: 'Кофе',
type: String,
})
@IsString({ message: 'Заголовок дохода должен быть строкой.' })
@IsNotEmpty({ message: 'Заголовок дохода обязателен к заполнению.' })
title: string;
@ApiProperty({
description: 'Описание',
example: 'Перед работой',
type: String,
nullable: true,
})
@IsOptional()
@IsString({ message: 'Описание дохода должно быть строкой.' })
description?: string;
@ApiProperty({
description: 'Дата',
example: '2025-08-22T04:12:03.726Z',
type: Date,
})
@IsNotEmpty({ message: 'Дата дохода обязателен к заполнению.' })
@IsISODateTimeString({ message: 'Дата должна быть строкой в формате ISO 8601' })
date: Date;
@ApiProperty({
description: 'Сумма дохода',
example: 175.50,
type: Number,
})
@IsNotEmpty({ message: 'Сумма дохода обязательна к заполнению.' })
@IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 2 }, { message: 'Сумма дохода должна быть числом.' })
amount: number;
@ApiProperty({
description: 'Валюта дохода',
example: 'RUB',
type: String,
default: 'RUB',
nullable: true,
})
@IsOptional()
@IsString({ message: 'Валюта дохода должна быть строкой.' })
currency?: string;
}

View File

@ -0,0 +1,58 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
import { IsISODateTimeString } from '../../../../shared/validators';
export class EditIncomeListItemDto {
@ApiProperty({
description: 'Заголовок дохода',
example: 'Кофе',
type: String,
})
@IsOptional()
@IsString({ message: 'Заголовок дохода должен быть строкой.' })
title?: string;
@ApiProperty({
description: 'Описание',
example: 'Перед работой',
type: String,
nullable: true,
})
@IsOptional()
@IsString({ message: 'Описание дохода должно быть строкой.' })
description?: string;
@ApiProperty({
description: 'Дата',
example: '2025-08-22T04:12:03.726Z',
type: Date,
})
@IsOptional()
@IsISODateTimeString({ message: 'Дата должна быть строкой в формате ISO 8601' })
date?: Date;
@ApiProperty({
description: 'Сумма дохода',
example: 175.50,
type: Number,
})
@IsOptional()
@IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 2 }, { message: 'Сумма дохода должна быть числом.' })
amount?: number;
@ApiProperty({
description: 'Валюта дохода',
example: 'RUB',
type: String,
default: 'RUB',
nullable: true,
})
@IsOptional()
@IsString({ message: 'Валюта дохода должна быть строкой.' })
currency?: string;
}

View File

@ -0,0 +1,66 @@
import { ApiProperty } from '@nestjs/swagger';
import { IncomeItem, IncomeList } from '@prisma/client';
export class IncomeListItemDto {
@ApiProperty({
description: 'ID',
type: String,
})
id: IncomeItem['id'];
@ApiProperty({
description: 'ID списка',
type: String,
})
incomeListId: IncomeList['id'];
@ApiProperty({
description: 'Заголовок дохода',
example: 'Кофе',
type: String,
})
title: IncomeItem['title'];
@ApiProperty({
description: 'Описание',
example: 'Перед работой',
type: String,
nullable: true,
})
description: IncomeItem['description'];
@ApiProperty({
description: 'Дата',
example: '2025-08-22T04:12:03.726Z',
type: Date,
})
date: IncomeItem['date'];
@ApiProperty({
description: 'Сумма дохода',
example: 175.50,
type: Number,
})
amount: IncomeItem['amount'];
@ApiProperty({
description: 'Валюта дохода',
example: 'RUB',
type: String,
default: 'RUB',
nullable: true,
})
currency: IncomeItem['currency'];
@ApiProperty({
description: 'Дата создания',
type: Date,
})
createdAt: Date;
@ApiProperty({
description: 'Дата обновления',
type: Date,
})
updatedAt: Date;
}

View File

@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateIncomeListDto {
@ApiProperty({
description: 'Название листа',
example: 'Ежедневные покупки',
type: String,
})
@IsString({ message: 'Название листа должен быть строкой.' })
@IsNotEmpty({ message: 'Название листа обязателно к заполнению.' })
title: string;
}

View File

@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteIncomeListDto {
@ApiProperty({
description: 'ID листа',
type: String,
})
@IsString({ message: 'ID должен быть строкой.' })
@IsNotEmpty({ message: 'ID обязателно к заполнению.' })
id: string;
}

View File

@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
export class DeletedIncomeListDto {
@ApiProperty({
description: 'ID',
type: String,
})
id: string;
}

View File

@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class EditIncomeListDto {
@ApiProperty({
description: 'Название листа',
example: 'Ежедневные покупки',
type: String,
})
@IsString({ message: 'Название листа должен быть строкой.' })
@IsNotEmpty({ message: 'Название листа обязателно к заполнению.' })
title?: string;
}

View File

@ -0,0 +1,37 @@
import { ApiProperty } from '@nestjs/swagger';
import { IncomeItem } from '@prisma/client';
import { IncomeListItemDto } from '../income-list-item/income-list-item.dto';
export class IncomeListDto {
@ApiProperty({
description: 'ID',
type: String,
})
id: string;
@ApiProperty({
description: 'Название листа',
type: String,
})
title: string;
@ApiProperty({
description: 'Список доходов',
type: [IncomeListItemDto],
nullable: true,
})
items?: IncomeItem[];
@ApiProperty({
description: 'Дата создания',
type: Date,
})
createdAt: Date;
@ApiProperty({
description: 'Дата обновления',
type: Date,
})
updatedAt: Date;
}

View File

@ -0,0 +1,11 @@
export { CreateIncomeListDto } from './income-list/create-income-list.dto';
export { CreateIncomeListItemDto } from './income-list-item/create-income-list.dto';
export { DeleteIncomeListDto } from './income-list/delete-income-list.dto';
export { DeletedIncomeListDto } from './income-list/deleted-income-list.dto';
export { EditIncomeListDto } from './income-list/edit-income-list.dto';
export { EditIncomeListItemDto } from './income-list-item/edit-income-list.dto';
export { IncomeListDto } from './income-list/income-list.dto';
export { IncomeListItemDto } from './income-list-item/income-list-item.dto';

View File

@ -0,0 +1,120 @@
import {
Controller,
Body,
Delete,
Get,
Param,
Patch,
Post,
} from '@nestjs/common';
import { IncomeItem, IncomeList, User } from '@prisma/client';
import { Authorized } from '@/shared/decorators';
import {
CreateIncomeList,
CreateIncomeListItem,
DeleteIncomeList,
DeleteIncomeListItem,
EditIncomeList,
EditIncomeListItem,
FindAllIncomeLists,
FindIncomeList,
FindIncomeListItem,
} from './decorators';
import {
CreateIncomeListDto,
CreateIncomeListItemDto,
EditIncomeListDto,
EditIncomeListItemDto,
} from './dto';
import { IncomesService } from './incomes.service';
@Controller('incomes')
export class IncomesController {
constructor(private readonly incomesService: IncomesService) {}
@Post('list')
@CreateIncomeList()
async createList(
@Body() body: CreateIncomeListDto,
@Authorized('userId') userId: User['id'],
) {
return this.incomesService.createList(body, userId);
}
@Get('list/:id')
@FindIncomeList()
async findList(
@Param('id') id: IncomeList['id'],
@Authorized('userId') userId: User['id'],
) {
return this.incomesService.getListById(id, userId);
}
@Get('list')
@FindAllIncomeLists()
async findAllLists(@Authorized('userId') userId: User['id']) {
return this.incomesService.getAllLists(userId);
}
@Patch('list/:id')
@EditIncomeList()
async editList(
@Param('id') id: IncomeList['id'],
@Body() body: EditIncomeListDto,
@Authorized('userId') userId: User['id'],
) {
return this.incomesService.editList(id, body, userId);
}
@Delete('list/:id')
@DeleteIncomeList()
async deleteList(
@Param('id') id: IncomeList['id'],
@Authorized('userId') userId: User['id'],
) {
return this.incomesService.deleteList(id, userId);
}
@Post('list/:id/items')
@CreateIncomeListItem()
async createListItem(
@Body() body: CreateIncomeListItemDto,
@Param('id') listId: IncomeList['id'],
@Authorized('userId') userId: User['id'],
) {
return this.incomesService.createListItem(body, listId, userId);
}
@Get('list/:listId/items/:id')
@FindIncomeListItem()
async findListItem(
@Param('listId') listId: IncomeList['id'],
@Param('id') id: IncomeItem['id'],
@Authorized('userId') userId: User['id'],
) {
return this.incomesService.findListItemById(id, listId, userId);
}
@Patch('list/:listId/items/:id')
@EditIncomeListItem()
async editListItem(
@Body() body: EditIncomeListItemDto,
@Param('listId') listId: IncomeList['id'],
@Param('id') id: IncomeItem['id'],
@Authorized('userId') userId: User['id'],
) {
return this.incomesService.editListItem(body, id, listId, userId);
}
@Delete('list/:listId/items/:id')
@DeleteIncomeListItem()
async deleteListItem(
@Param('listId') listId: IncomeList['id'],
@Param('id') id: IncomeItem['id'],
@Authorized('userId') userId: User['id'],
) {
return this.incomesService.deleteListItem(id, listId, userId);
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { IncomesController } from './incomes.controller';
import { IncomesService } from './incomes.service';
@Module({
controllers: [IncomesController],
providers: [IncomesService],
})
export class IncomesModule {}

View File

@ -0,0 +1,172 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { IncomeItem, IncomeList, User } from '@prisma/client';
import { PrismaService } from '@/modules/prisma/prisma.service';
import {
CreateIncomeListDto, CreateIncomeListItemDto,
DeletedIncomeListDto,
EditIncomeListDto, EditIncomeListItemDto,
IncomeListDto, IncomeListItemDto,
} from './dto';
@Injectable()
export class IncomesService {
constructor(
private readonly prismaService: PrismaService,
) {}
async createList(dto: CreateIncomeListDto, userId: User['id']): Promise<IncomeListDto> {
const { title } = dto;
const incomeList = await this.prismaService.incomeList.create({
data: { title, user: { connect: { id: userId } } },
});
return {
id: incomeList.id,
title: incomeList.title,
createdAt: incomeList.createdAt,
updatedAt: incomeList.updatedAt,
};
}
async getListById(id: IncomeList['id'], userId: User['id']): Promise<IncomeListDto> {
const list = await this.findList(id, userId);
return {
id: list.id,
title: list.title,
items: list.items,
createdAt: list.createdAt,
updatedAt: list.updatedAt,
};
}
async getAllLists(userId: User['id']): Promise<IncomeListDto[]> {
return this.prismaService.incomeList.findMany({
where: { userId },
include: { items: true },
});
}
async editList(id: IncomeList['id'], dto: EditIncomeListDto, userId: User['id']): Promise<IncomeListDto> {
const { title } = dto;
const incomeList = await this.findList(id, userId);
const updatedList = await this.prismaService.incomeList.update({
where: { id: incomeList.id },
data: { title },
});
return {
id: updatedList.id,
title: updatedList.title,
createdAt: updatedList.createdAt,
updatedAt: updatedList.updatedAt,
};
}
async deleteList(id: IncomeList['id'], userId: User['id']): Promise<DeletedIncomeListDto> {
const { id: listId } = await this.findList(id, userId);
const deletedList = await this.prismaService.incomeList.delete({ where: { id: listId } });
return {
id: deletedList.id,
};
}
async createListItem(dto: CreateIncomeListItemDto, listId: IncomeList['id'], userId: User['id']): Promise<IncomeListItemDto> {
const {
title,
description,
date,
amount,
currency,
} = dto;
const list = await this.findList(listId, userId);
return this.prismaService.incomeItem.create({
data: {
title,
description,
date,
amount,
currency,
incomeList: {
connect: { id: list.id },
},
},
});
}
async findListItemById(id: IncomeItem['id'], listId: IncomeList['id'], userId: User['id']) {
return this.findListItem(id, listId, userId);
}
// eslint-disable-next-line @typescript-eslint/max-params
async editListItem(
dto: EditIncomeListItemDto,
itemId: IncomeItem['id'],
listId: IncomeList['id'],
userId: User['id'],
): Promise<IncomeListItemDto> {
const item = await this.findListItem(itemId, listId, userId);
const {
title = item.title,
description = item.description,
date = item.date,
amount = item.amount,
currency = item.currency,
} = dto;
return this.prismaService.incomeItem.update({
where: { id: item.id },
data: {
title,
description,
date,
amount,
currency,
},
});
}
async deleteListItem(
itemId: IncomeItem['id'],
listId: IncomeList['id'],
userId: User['id'],
): Promise<IncomeListItemDto> {
const item = await this.findListItem(itemId, listId, userId);
return this.prismaService.incomeItem.delete({
where: { id: item.id },
});
}
private async findList(id: IncomeList['id'], userId: User['id']) {
const existingList = await this.prismaService.incomeList.findUnique({
where: { id, userId },
include: { items: true },
});
if (!existingList) {
throw new NotFoundException('Лист доходов не найден');
}
return existingList;
}
private async findListItem(id: IncomeItem['id'], listId: IncomeList['id'], userId: User['id']) {
const list = await this.findList(listId, userId);
const existingItem = await this.prismaService.incomeItem.findUnique({ where: { id } });
if (!existingItem || existingItem.incomeListId !== list.id) {
throw new NotFoundException('Позиция доходов не найдена');
}
return existingItem;
}
}