feat(server): add income target

This commit is contained in:
Sergey Krylov 2025-10-15 08:01:38 +03:00
parent eda67581ab
commit f8d766beb0
8 changed files with 169 additions and 20 deletions

View File

@ -76,17 +76,19 @@ model IncomeList {
} }
model IncomeItem { model IncomeItem {
id String @id @default(uuid()) id String @id @default(uuid())
incomeList IncomeList @relation(fields: [incomeListId], references: [id], onDelete: Cascade) incomeList IncomeList @relation(fields: [incomeListId], references: [id], onDelete: Cascade)
incomeListId String incomeListId String
title String title String
description String? description String?
date DateTime date DateTime
amount Float amount Float
currency String @default("RUB") currency String @default("RUB")
count Float @default(1) count Float @default(1)
createdAt DateTime @default(now()) @map("created_at") targetType IncomeItemToType? @map("target_type")
updatedAt DateTime @updatedAt @map("updated_at") targetSourceId String? @map("target_source_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("incomes_items") @@map("incomes_items")
} }
@ -132,3 +134,10 @@ enum ExpenseItemFromType {
@@map("expense_item_from_types") @@map("expense_item_from_types")
} }
enum IncomeItemToType {
BANK_CARD
CASH
@@map("income_item_to_types")
}

View File

@ -88,6 +88,11 @@ export class BankCardService {
await this.bankAccountService.changeBalance(card.bankAccount.id, card.bankAccount.balance - amount); 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']) { private async findCardById(userId: User['id'], bankCardId: BankCard['id']) {
const card = await this.prismaService.bankCard.findUnique({ const card = await this.prismaService.bankCard.findUnique({
where: { id: bankCardId }, where: { id: bankCardId },

View File

@ -71,4 +71,9 @@ export class CashService {
const cash = await this.getCashById(userId, cashId); const cash = await this.getCashById(userId, cashId);
await this.editCash(userId, cash.id, { balance: cash.balance - amount }); 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 });
}
} }

View File

@ -1,13 +1,38 @@
// eslint-disable-next-line max-classes-per-file
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IncomeItemToType } from '@prisma/client';
import { Type } from 'class-transformer';
import { import {
IsEnum,
IsNotEmpty, IsNotEmpty,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString, ValidateNested,
} from 'class-validator'; } from 'class-validator';
import { IsISODateTimeString } from '../../../../shared/validators'; 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 { export class CreateIncomeListItemDto {
@ApiProperty({ @ApiProperty({
description: 'Заголовок дохода', description: 'Заголовок дохода',
@ -56,4 +81,14 @@ export class CreateIncomeListItemDto {
@IsOptional() @IsOptional()
@IsString({ message: 'Валюта дохода должна быть строкой.' }) @IsString({ message: 'Валюта дохода должна быть строкой.' })
currency?: string; currency?: string;
@ApiProperty({
description: 'Источник',
type: IncomeSourceDto,
nullable: true,
})
@IsOptional()
@ValidateNested()
@Type(() => IncomeSourceDto)
target?: IncomeSourceDto;
} }

View File

@ -1,5 +1,37 @@
// eslint-disable-next-line max-classes-per-file
import { ApiProperty } from '@nestjs/swagger'; 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 { export class IncomeListItemDto {
@ApiProperty({ @ApiProperty({
@ -52,6 +84,14 @@ export class IncomeListItemDto {
}) })
currency: IncomeItem['currency']; currency: IncomeItem['currency'];
@ApiProperty({
description: 'Источник',
type: IncomeSourceDto,
nullable: true,
})
@Type(() => IncomeSourceDto)
target?: IncomeSourceDto | null;
@ApiProperty({ @ApiProperty({
description: 'Дата создания', description: 'Дата создания',
type: Date, type: Date,

View File

@ -1,5 +1,4 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IncomeItem } from '@prisma/client';
import { IncomeListItemDto } from '../income-list-item/income-list-item.dto'; import { IncomeListItemDto } from '../income-list-item/income-list-item.dto';
@ -21,7 +20,7 @@ export class IncomeListDto {
type: [IncomeListItemDto], type: [IncomeListItemDto],
nullable: true, nullable: true,
}) })
items?: IncomeItem[]; items?: IncomeListItemDto[];
@ApiProperty({ @ApiProperty({
description: 'Дата создания', description: 'Дата создания',

View File

@ -1,9 +1,16 @@
import { Module } from '@nestjs/common'; 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 { IncomesController } from './incomes.controller';
import { IncomesService } from './incomes.service'; import { IncomesService } from './incomes.service';
@Module({ @Module({
imports: [
BankCardModule,
CashModule,
],
controllers: [IncomesController], controllers: [IncomesController],
providers: [IncomesService], providers: [IncomesService],
}) })

View File

@ -1,19 +1,28 @@
import { Injectable, NotFoundException } from '@nestjs/common'; 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 { PrismaService } from '@/modules/prisma/prisma.service';
import { import {
CreateIncomeListDto, CreateIncomeListItemDto, CreateIncomeListDto,
CreateIncomeListItemDto,
DeletedIncomeListDto, DeletedIncomeListDto,
EditIncomeListDto, EditIncomeListItemDto, EditIncomeListDto,
IncomeListDto, IncomeListItemDto, EditIncomeListItemDto,
IncomeListDto,
IncomeListItemDto,
} from './dto'; } from './dto';
@Injectable() @Injectable()
export class IncomesService { export class IncomesService {
constructor( constructor(
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly bankCardService: BankCardService,
private readonly cashService: CashService,
) {} ) {}
async createList(dto: CreateIncomeListDto, userId: User['id']): Promise<IncomeListDto> { async createList(dto: CreateIncomeListDto, userId: User['id']): Promise<IncomeListDto> {
@ -84,9 +93,25 @@ export class IncomesService {
date, date,
amount, amount,
currency, currency,
target,
} = dto; } = dto;
const list = await this.findList(listId, userId); 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({ return this.prismaService.incomeItem.create({
data: { data: {
title, title,
@ -94,6 +119,8 @@ export class IncomesService {
date, date,
amount, amount,
currency, currency,
targetType: target?.type,
targetSourceId: target?.id,
incomeList: { incomeList: {
connect: { id: list.id }, connect: { id: list.id },
}, },
@ -156,7 +183,29 @@ export class IncomesService {
throw new NotFoundException('Лист доходов не найден'); 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']) { private async findListItem(id: IncomeItem['id'], listId: IncomeList['id'], userId: User['id']) {