From f8d766beb06369e97f75aa4153b777e3bf266788 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 15 Oct 2025 08:01:38 +0300 Subject: [PATCH] feat(server): add income target --- server/prisma/schema.prisma | 31 ++++++---- .../modules/bank-card/bank-card.service.ts | 5 ++ server/src/modules/cash/cash.service.ts | 5 ++ .../create-income-list.dto.ts | 37 +++++++++++- .../income-list-item/income-list-item.dto.ts | 42 ++++++++++++- .../dto/income-list/income-list.dto.ts | 3 +- server/src/modules/incomes/incomes.module.ts | 7 +++ server/src/modules/incomes/incomes.service.ts | 59 +++++++++++++++++-- 8 files changed, 169 insertions(+), 20 deletions(-) diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 239d7aa..ca096cb 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -76,17 +76,19 @@ model IncomeList { } 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") + 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) + targetType IncomeItemToType? @map("target_type") + targetSourceId String? @map("target_source_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@map("incomes_items") } @@ -132,3 +134,10 @@ enum ExpenseItemFromType { @@map("expense_item_from_types") } + +enum IncomeItemToType { + BANK_CARD + CASH + + @@map("income_item_to_types") +} diff --git a/server/src/modules/bank-card/bank-card.service.ts b/server/src/modules/bank-card/bank-card.service.ts index bc2b54c..8eb64e0 100644 --- a/server/src/modules/bank-card/bank-card.service.ts +++ b/server/src/modules/bank-card/bank-card.service.ts @@ -88,6 +88,11 @@ export class BankCardService { await this.bankAccountService.changeBalance(card.bankAccount.id, card.bankAccount.balance - amount); } + public async addIncome(userId: User['id'], bankCardId: BankCard['id'], amount: number) { + const card = await this.findCardById(userId, bankCardId); + await this.bankAccountService.changeBalance(card.bankAccount.id, card.bankAccount.balance + amount); + } + private async findCardById(userId: User['id'], bankCardId: BankCard['id']) { const card = await this.prismaService.bankCard.findUnique({ where: { id: bankCardId }, diff --git a/server/src/modules/cash/cash.service.ts b/server/src/modules/cash/cash.service.ts index d4e431f..2c709e4 100644 --- a/server/src/modules/cash/cash.service.ts +++ b/server/src/modules/cash/cash.service.ts @@ -71,4 +71,9 @@ export class CashService { const cash = await this.getCashById(userId, cashId); await this.editCash(userId, cash.id, { balance: cash.balance - amount }); } + + public async addIncome(userId: User['id'], cashId: BankCard['id'], amount: number) { + const cash = await this.getCashById(userId, cashId); + await this.editCash(userId, cash.id, { balance: cash.balance + amount }); + } } 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 index 893a9c5..8f39432 100644 --- 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 @@ -1,13 +1,38 @@ +// eslint-disable-next-line max-classes-per-file import { ApiProperty } from '@nestjs/swagger'; +import { IncomeItemToType } from '@prisma/client'; +import { Type } from 'class-transformer'; import { + IsEnum, IsNotEmpty, IsNumber, IsOptional, - IsString, + IsString, ValidateNested, } from 'class-validator'; import { IsISODateTimeString } from '../../../../shared/validators'; +class IncomeSourceDto { + @ApiProperty({ + description: 'Источник дохода', + example: 'CASH', + enum: IncomeItemToType, + }) + @IsString() + @IsNotEmpty({ message: 'Источник дохода обязателен к заполнению' }) + @IsEnum(IncomeItemToType) + type: IncomeItemToType; + + @ApiProperty({ + description: 'ID карты или наличных ', + example: '123', + type: String, + }) + @IsString() + @IsNotEmpty({ message: 'ID источника дохода обязателен к заполнению' }) + id: string; +} + export class CreateIncomeListItemDto { @ApiProperty({ description: 'Заголовок дохода', @@ -56,4 +81,14 @@ export class CreateIncomeListItemDto { @IsOptional() @IsString({ message: 'Валюта дохода должна быть строкой.' }) currency?: string; + + @ApiProperty({ + description: 'Источник', + type: IncomeSourceDto, + nullable: true, + }) + @IsOptional() + @ValidateNested() + @Type(() => IncomeSourceDto) + target?: IncomeSourceDto; } 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 index 8f4b02a..15b4b52 100644 --- 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 @@ -1,5 +1,37 @@ +// eslint-disable-next-line max-classes-per-file import { ApiProperty } from '@nestjs/swagger'; -import { IncomeItem, IncomeList } from '@prisma/client'; +import { + IncomeItem, + IncomeItemToType, + IncomeList, +} from '@prisma/client'; +import { Type } from 'class-transformer'; +import { + IsEnum, + IsNotEmpty, + IsString, +} from 'class-validator'; + +class IncomeSourceDto { + @ApiProperty({ + description: 'Источник дохода', + example: 'CASH', + enum: IncomeItemToType, + }) + @IsString() + @IsNotEmpty({ message: 'Источник дохода обязателен к заполнению' }) + @IsEnum(IncomeItemToType) + type: IncomeItemToType; + + @ApiProperty({ + description: 'ID карты или наличных ', + example: '123', + type: String, + }) + @IsString() + @IsNotEmpty({ message: 'ID источника дохода обязателен к заполнению' }) + id: string; +} export class IncomeListItemDto { @ApiProperty({ @@ -52,6 +84,14 @@ export class IncomeListItemDto { }) currency: IncomeItem['currency']; + @ApiProperty({ + description: 'Источник', + type: IncomeSourceDto, + nullable: true, + }) + @Type(() => IncomeSourceDto) + target?: IncomeSourceDto | null; + @ApiProperty({ description: 'Дата создания', type: Date, 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 index 25a0f43..ee0691c 100644 --- a/server/src/modules/incomes/dto/income-list/income-list.dto.ts +++ b/server/src/modules/incomes/dto/income-list/income-list.dto.ts @@ -1,5 +1,4 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IncomeItem } from '@prisma/client'; import { IncomeListItemDto } from '../income-list-item/income-list-item.dto'; @@ -21,7 +20,7 @@ export class IncomeListDto { type: [IncomeListItemDto], nullable: true, }) - items?: IncomeItem[]; + items?: IncomeListItemDto[]; @ApiProperty({ description: 'Дата создания', diff --git a/server/src/modules/incomes/incomes.module.ts b/server/src/modules/incomes/incomes.module.ts index 598a993..7af40b9 100644 --- a/server/src/modules/incomes/incomes.module.ts +++ b/server/src/modules/incomes/incomes.module.ts @@ -1,9 +1,16 @@ import { Module } from '@nestjs/common'; +import { BankCardModule } from '@/modules/bank-card/bank-card.module'; +import { CashModule } from '@/modules/cash/cash.module'; + import { IncomesController } from './incomes.controller'; import { IncomesService } from './incomes.service'; @Module({ + imports: [ + BankCardModule, + CashModule, + ], controllers: [IncomesController], providers: [IncomesService], }) diff --git a/server/src/modules/incomes/incomes.service.ts b/server/src/modules/incomes/incomes.service.ts index 0ed74b7..f33c5e5 100644 --- a/server/src/modules/incomes/incomes.service.ts +++ b/server/src/modules/incomes/incomes.service.ts @@ -1,19 +1,28 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { IncomeItem, IncomeList, User } from '@prisma/client'; +import { + IncomeItem, IncomeItemToType, IncomeList, User, +} from '@prisma/client'; +import { BankCardService } from '@/modules/bank-card/bank-card.service'; +import { CashService } from '@/modules/cash/cash.service'; import { PrismaService } from '@/modules/prisma/prisma.service'; import { - CreateIncomeListDto, CreateIncomeListItemDto, + CreateIncomeListDto, + CreateIncomeListItemDto, DeletedIncomeListDto, - EditIncomeListDto, EditIncomeListItemDto, - IncomeListDto, IncomeListItemDto, + EditIncomeListDto, + EditIncomeListItemDto, + IncomeListDto, + IncomeListItemDto, } from './dto'; @Injectable() export class IncomesService { constructor( private readonly prismaService: PrismaService, + private readonly bankCardService: BankCardService, + private readonly cashService: CashService, ) {} async createList(dto: CreateIncomeListDto, userId: User['id']): Promise { @@ -84,9 +93,25 @@ export class IncomesService { date, amount, currency, + target, } = dto; const list = await this.findList(listId, userId); + switch (target?.type) { + case IncomeItemToType.BANK_CARD: + await this.bankCardService.addIncome(userId, target.id, amount); + break; + + case IncomeItemToType.CASH: + await this.cashService.addIncome(userId, target.id, amount); + break; + + // eslint-disable-next-line no-undefined + case undefined: + default: + break; + } + return this.prismaService.incomeItem.create({ data: { title, @@ -94,6 +119,8 @@ export class IncomesService { date, amount, currency, + targetType: target?.type, + targetSourceId: target?.id, incomeList: { connect: { id: list.id }, }, @@ -156,7 +183,29 @@ export class IncomesService { throw new NotFoundException('Лист доходов не найден'); } - return existingList; + const { items, ...rest } = existingList; + + return { + ...rest, + items: items.map((item) => { + const { targetSourceId, targetType, ...restItem } = item; + + if (targetSourceId && targetType) { + return { + ...restItem, + target: { + type: targetType, + id: targetSourceId, + }, + }; + } + + return { + ...restItem, + target: null, + }; + }), + }; } private async findListItem(id: IncomeItem['id'], listId: IncomeList['id'], userId: User['id']) {