diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 32a4c2b..246352c 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -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) diff --git a/server/src/modules/app.module.ts b/server/src/modules/app.module.ts index c68997a..e5554df 100644 --- a/server/src/modules/app.module.ts +++ b/server/src/modules/app.module.ts @@ -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 {} diff --git a/server/src/modules/incomes/decorators/income-list-item/create-income-list-item.decorator.ts b/server/src/modules/incomes/decorators/income-list-item/create-income-list-item.decorator.ts new file mode 100644 index 0000000..f3054a5 --- /dev/null +++ b/server/src/modules/incomes/decorators/income-list-item/create-income-list-item.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/incomes/decorators/income-list-item/delete-income-list-item.decorator.ts b/server/src/modules/incomes/decorators/income-list-item/delete-income-list-item.decorator.ts new file mode 100644 index 0000000..2c15ef2 --- /dev/null +++ b/server/src/modules/incomes/decorators/income-list-item/delete-income-list-item.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/incomes/decorators/income-list-item/edit-income-list-item.decorator.ts b/server/src/modules/incomes/decorators/income-list-item/edit-income-list-item.decorator.ts new file mode 100644 index 0000000..db6540b --- /dev/null +++ b/server/src/modules/incomes/decorators/income-list-item/edit-income-list-item.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/incomes/decorators/income-list-item/find-income-list-item.decorator.ts b/server/src/modules/incomes/decorators/income-list-item/find-income-list-item.decorator.ts new file mode 100644 index 0000000..1178e57 --- /dev/null +++ b/server/src/modules/incomes/decorators/income-list-item/find-income-list-item.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/incomes/decorators/income-list/create-income-list.decorator.ts b/server/src/modules/incomes/decorators/income-list/create-income-list.decorator.ts new file mode 100644 index 0000000..b1530d7 --- /dev/null +++ b/server/src/modules/incomes/decorators/income-list/create-income-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/incomes/decorators/income-list/delete-income-list.decorator.ts b/server/src/modules/incomes/decorators/income-list/delete-income-list.decorator.ts new file mode 100644 index 0000000..e38d922 --- /dev/null +++ b/server/src/modules/incomes/decorators/income-list/delete-income-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/incomes/decorators/income-list/edit-income-list.decorator.ts b/server/src/modules/incomes/decorators/income-list/edit-income-list.decorator.ts new file mode 100644 index 0000000..bf47802 --- /dev/null +++ b/server/src/modules/incomes/decorators/income-list/edit-income-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/incomes/decorators/income-list/find-all-income-list.decorator.ts b/server/src/modules/incomes/decorators/income-list/find-all-income-list.decorator.ts new file mode 100644 index 0000000..67d7600 --- /dev/null +++ b/server/src/modules/incomes/decorators/income-list/find-all-income-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/incomes/decorators/income-list/find-income-list.decorator.ts b/server/src/modules/incomes/decorators/income-list/find-income-list.decorator.ts new file mode 100644 index 0000000..b4a2670 --- /dev/null +++ b/server/src/modules/incomes/decorators/income-list/find-income-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/incomes/decorators/index.ts b/server/src/modules/incomes/decorators/index.ts new file mode 100644 index 0000000..a5a5f04 --- /dev/null +++ b/server/src/modules/incomes/decorators/index.ts @@ -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'; diff --git a/server/src/modules/incomes/dto/income-list-item/create-income-list.dto.ts b/server/src/modules/incomes/dto/income-list-item/create-income-list.dto.ts new file mode 100644 index 0000000..893a9c5 --- /dev/null +++ b/server/src/modules/incomes/dto/income-list-item/create-income-list.dto.ts @@ -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; +} diff --git a/server/src/modules/incomes/dto/income-list-item/edit-income-list.dto.ts b/server/src/modules/incomes/dto/income-list-item/edit-income-list.dto.ts new file mode 100644 index 0000000..f0b43a6 --- /dev/null +++ b/server/src/modules/incomes/dto/income-list-item/edit-income-list.dto.ts @@ -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; +} diff --git a/server/src/modules/incomes/dto/income-list-item/income-list-item.dto.ts b/server/src/modules/incomes/dto/income-list-item/income-list-item.dto.ts new file mode 100644 index 0000000..8f4b02a --- /dev/null +++ b/server/src/modules/incomes/dto/income-list-item/income-list-item.dto.ts @@ -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; +} diff --git a/server/src/modules/incomes/dto/income-list/create-income-list.dto.ts b/server/src/modules/incomes/dto/income-list/create-income-list.dto.ts new file mode 100644 index 0000000..76c5f20 --- /dev/null +++ b/server/src/modules/incomes/dto/income-list/create-income-list.dto.ts @@ -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; +} diff --git a/server/src/modules/incomes/dto/income-list/delete-income-list.dto.ts b/server/src/modules/incomes/dto/income-list/delete-income-list.dto.ts new file mode 100644 index 0000000..21c3df8 --- /dev/null +++ b/server/src/modules/incomes/dto/income-list/delete-income-list.dto.ts @@ -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; +} diff --git a/server/src/modules/incomes/dto/income-list/deleted-income-list.dto.ts b/server/src/modules/incomes/dto/income-list/deleted-income-list.dto.ts new file mode 100644 index 0000000..ed7c998 --- /dev/null +++ b/server/src/modules/incomes/dto/income-list/deleted-income-list.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DeletedIncomeListDto { + @ApiProperty({ + description: 'ID', + type: String, + }) + id: string; +} diff --git a/server/src/modules/incomes/dto/income-list/edit-income-list.dto.ts b/server/src/modules/incomes/dto/income-list/edit-income-list.dto.ts new file mode 100644 index 0000000..73bb7bb --- /dev/null +++ b/server/src/modules/incomes/dto/income-list/edit-income-list.dto.ts @@ -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; +} diff --git a/server/src/modules/incomes/dto/income-list/income-list.dto.ts b/server/src/modules/incomes/dto/income-list/income-list.dto.ts new file mode 100644 index 0000000..25a0f43 --- /dev/null +++ b/server/src/modules/incomes/dto/income-list/income-list.dto.ts @@ -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; +} diff --git a/server/src/modules/incomes/dto/index.ts b/server/src/modules/incomes/dto/index.ts new file mode 100644 index 0000000..a103b52 --- /dev/null +++ b/server/src/modules/incomes/dto/index.ts @@ -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'; diff --git a/server/src/modules/incomes/incomes.controller.ts b/server/src/modules/incomes/incomes.controller.ts new file mode 100644 index 0000000..d9f159f --- /dev/null +++ b/server/src/modules/incomes/incomes.controller.ts @@ -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); + } +} diff --git a/server/src/modules/incomes/incomes.module.ts b/server/src/modules/incomes/incomes.module.ts new file mode 100644 index 0000000..598a993 --- /dev/null +++ b/server/src/modules/incomes/incomes.module.ts @@ -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 {} diff --git a/server/src/modules/incomes/incomes.service.ts b/server/src/modules/incomes/incomes.service.ts new file mode 100644 index 0000000..0ed74b7 --- /dev/null +++ b/server/src/modules/incomes/incomes.service.ts @@ -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 { + 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 { + 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 { + return this.prismaService.incomeList.findMany({ + where: { userId }, + include: { items: true }, + }); + } + + async editList(id: IncomeList['id'], dto: EditIncomeListDto, userId: User['id']): Promise { + 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 { + 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 { + 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 { + 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 { + 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; + } +}