feat(server): add income
This commit is contained in:
parent
59ff36cb18
commit
ee6276e8ab
@ -26,6 +26,7 @@ model User {
|
|||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
expensesLists ExpenseList[]
|
expensesLists ExpenseList[]
|
||||||
|
incomeList IncomeList[]
|
||||||
bankCard BankCard[]
|
bankCard BankCard[]
|
||||||
cash Cash[]
|
cash Cash[]
|
||||||
|
|
||||||
@ -60,6 +61,34 @@ model ExpenseItem {
|
|||||||
@@map("expenses_items")
|
@@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 {
|
model BankAccount {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
balance Float @default(0)
|
balance Float @default(0)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { BankAccountModule } from './bank-account/bank-account.module';
|
|||||||
import { BankCardModule } from './bank-card/bank-card.module';
|
import { BankCardModule } from './bank-card/bank-card.module';
|
||||||
import { CashModule } from './cash/cash.module';
|
import { CashModule } from './cash/cash.module';
|
||||||
import { ExpensesModule } from './expenses/expenses.module';
|
import { ExpensesModule } from './expenses/expenses.module';
|
||||||
|
import { IncomesModule } from './incomes/incomes.module';
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -19,6 +20,7 @@ import { PrismaModule } from './prisma/prisma.module';
|
|||||||
BankAccountModule,
|
BankAccountModule,
|
||||||
BankCardModule,
|
BankCardModule,
|
||||||
CashModule,
|
CashModule,
|
||||||
|
IncomesModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
12
server/src/modules/incomes/decorators/index.ts
Normal file
12
server/src/modules/incomes/decorators/index.ts
Normal 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';
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class DeletedIncomeListDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
11
server/src/modules/incomes/dto/index.ts
Normal file
11
server/src/modules/incomes/dto/index.ts
Normal 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';
|
||||||
120
server/src/modules/incomes/incomes.controller.ts
Normal file
120
server/src/modules/incomes/incomes.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
server/src/modules/incomes/incomes.module.ts
Normal file
10
server/src/modules/incomes/incomes.module.ts
Normal 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 {}
|
||||||
172
server/src/modules/incomes/incomes.service.ts
Normal file
172
server/src/modules/incomes/incomes.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user