diff --git a/server/eslint.config.ts b/server/eslint.config.ts index a1d23ee..5e5c436 100644 --- a/server/eslint.config.ts +++ b/server/eslint.config.ts @@ -71,6 +71,7 @@ export default [ }, }, ], + '@typescript-eslint/prefer-nullish-coalescing': 'off', }, }, ]; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 246352c..239d7aa 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -46,17 +46,19 @@ model ExpenseList { } model ExpenseItem { - id String @id @default(uuid()) - expenseList ExpenseList @relation(fields: [expenseListId], references: [id], onDelete: Cascade) - expenseListId 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()) + expenseList ExpenseList @relation(fields: [expenseListId], references: [id], onDelete: Cascade) + expenseListId String + title String + description String? + date DateTime + amount Float + currency String @default("RUB") + count Float @default(1) + paymentType ExpenseItemFromType? @map("payment_type") + paymentSourceId String? @map("payment_source_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@map("expenses_items") } @@ -123,3 +125,10 @@ model Cash { @@map("cash") } + +enum ExpenseItemFromType { + BANK_CARD + CASH + + @@map("expense_item_from_types") +} diff --git a/server/src/modules/bank-card/bank-card.module.ts b/server/src/modules/bank-card/bank-card.module.ts index 81a78b4..03216c9 100644 --- a/server/src/modules/bank-card/bank-card.module.ts +++ b/server/src/modules/bank-card/bank-card.module.ts @@ -9,5 +9,6 @@ import { BankCardService } from './bank-card.service'; controllers: [BankCardController], providers: [BankCardService], imports: [BankAccountModule], + exports: [BankCardService], }) export class BankCardModule {} diff --git a/server/src/modules/bank-card/bank-card.service.ts b/server/src/modules/bank-card/bank-card.service.ts index 1d951d6..bc2b54c 100644 --- a/server/src/modules/bank-card/bank-card.service.ts +++ b/server/src/modules/bank-card/bank-card.service.ts @@ -13,7 +13,7 @@ export class BankCardService { private readonly bankAccountService: BankAccountService, ) {} - async createBankCard(userId: User['id'], dto: CreateBankCardDto): Promise { + public async createBankCard(userId: User['id'], dto: CreateBankCardDto): Promise { const { name, balance } = dto; const bankAccount = await this.bankAccountService.createBankAccount({ @@ -36,20 +36,13 @@ export class BankCardService { return this.toDto(card); } - async getBankCardById(userId: User['id'], bankCardId: BankCard['id']): Promise { - const card = await this.prismaService.bankCard.findUnique({ - where: { id: bankCardId }, - include: { bankAccount: true }, - }); - - if (card?.userId !== userId) { - throw new NotFoundException('Карта не найдена'); - } + public async getBankCardById(userId: User['id'], bankCardId: BankCard['id']): Promise { + const card = await this.findCardById(userId, bankCardId); return this.toDto(card); } - async getBankCardsByUserId(userId: User['id']): Promise { + public async getBankCardsByUserId(userId: User['id']): Promise { const cards = await this.prismaService.bankCard.findMany({ where: { userId }, include: { bankAccount: true }, @@ -59,7 +52,7 @@ export class BankCardService { return cards.map((card) => this.toDto(card)); } - async editBankCard(userId: User['id'], bankCardId: BankCard['id'], dto: EditBankCardDto): Promise { + public async editBankCard(userId: User['id'], bankCardId: BankCard['id'], dto: EditBankCardDto): Promise { const { name, balance } = dto; const card = await this.getBankCardById(userId, bankCardId); @@ -79,7 +72,7 @@ export class BankCardService { return this.toDto(updatedCard); } - async deleteBankCard(userId: User['id'], bankCardId: BankCard['id']): Promise { + public async deleteBankCard(userId: User['id'], bankCardId: BankCard['id']): Promise { const card = await this.getBankCardById(userId, bankCardId); const deletedCard = await this.prismaService.bankCard.delete({ @@ -90,7 +83,25 @@ export class BankCardService { return this.toDto(deletedCard); } - toDto(card: BankCard & { bankAccount: BankAccount }): BankCardDto { + public async addExpense(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 }, + include: { bankAccount: true }, + }); + + if (card?.userId !== userId) { + throw new NotFoundException('Карта не найдена'); + } + + return card; + } + + private toDto(card: BankCard & { bankAccount: BankAccount }): BankCardDto { return { id: card.id, name: card.name, diff --git a/server/src/modules/cash/cash.module.ts b/server/src/modules/cash/cash.module.ts index 5b0ef0e..b070971 100644 --- a/server/src/modules/cash/cash.module.ts +++ b/server/src/modules/cash/cash.module.ts @@ -6,5 +6,6 @@ import { CashService } from './cash.service'; @Module({ controllers: [CashController], providers: [CashService], + exports: [CashService], }) export class CashModule {} diff --git a/server/src/modules/cash/cash.service.ts b/server/src/modules/cash/cash.service.ts index c7c3578..d4e431f 100644 --- a/server/src/modules/cash/cash.service.ts +++ b/server/src/modules/cash/cash.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { Cash, User } from '@prisma/client'; +import { BankCard, Cash, User } from '@prisma/client'; import { PrismaService } from '@/modules/prisma/prisma.service'; @@ -9,7 +9,7 @@ import { CashDto, CreateCashDto, EditCashDto } from './dto'; export class CashService { constructor(private readonly prismaService: PrismaService) {} - async createCash(userId: User['id'], dto: CreateCashDto): Promise { + public async createCash(userId: User['id'], dto: CreateCashDto): Promise { const { balance, name } = dto; return this.prismaService.cash.create({ @@ -23,7 +23,7 @@ export class CashService { }); } - async getCashById(userId: User['id'], cashId: Cash['id']): Promise { + public async getCashById(userId: User['id'], cashId: Cash['id']): Promise { const cash = await this.prismaService.cash.findUnique({ where: { id: cashId, @@ -38,14 +38,14 @@ export class CashService { return cash; } - async getAllCashByUserId(userId: User['id']): Promise { + public async getAllCashByUserId(userId: User['id']): Promise { return this.prismaService.cash.findMany({ where: { userId }, orderBy: { createdAt: 'asc' }, }); } - async editCash(userId: User['id'], cashId: Cash['id'], dto: EditCashDto): Promise { + public async editCash(userId: User['id'], cashId: Cash['id'], dto: EditCashDto): Promise { const { balance, name } = dto; const cash = await this.getCashById(userId, cashId); @@ -59,11 +59,16 @@ export class CashService { }); } - async deleteCash(userId: User['id'], cashId: Cash['id']): Promise { + public async deleteCash(userId: User['id'], cashId: Cash['id']): Promise { const cash = await this.getCashById(userId, cashId); return this.prismaService.cash.delete({ where: { id: cash.id }, }); } + + public async addExpense(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/expenses/dto/expense-list-item/create-expense-list.dto.ts b/server/src/modules/expenses/dto/expense-list-item/create-expense-list.dto.ts index 34fe39c..881529a 100644 --- a/server/src/modules/expenses/dto/expense-list-item/create-expense-list.dto.ts +++ b/server/src/modules/expenses/dto/expense-list-item/create-expense-list.dto.ts @@ -1,13 +1,38 @@ +// eslint-disable-next-line max-classes-per-file import { ApiProperty } from '@nestjs/swagger'; +import { ExpenseItemFromType } 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 ExpensePaymentDto { + @ApiProperty({ + description: 'Способ оплаты', + example: 'CASH', + enum: ExpenseItemFromType, + }) + @IsString() + @IsNotEmpty({ message: 'Способ оплаты обязателен к заполнению' }) + @IsEnum(ExpenseItemFromType) + type: ExpenseItemFromType; + + @ApiProperty({ + description: 'ID карты или наличных ', + example: '123', + type: String, + }) + @IsString() + @IsNotEmpty({ message: 'ID источника оплаты обязателен к заполнению' }) + id: string; +} + export class CreateExpenseListItemDto { @ApiProperty({ description: 'Заголовок расхода', @@ -67,4 +92,14 @@ export class CreateExpenseListItemDto { @IsOptional() @IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 2 }, { message: 'Количество должно быть числом.' }) count?: number; + + @ApiProperty({ + description: 'Оплата', + type: ExpensePaymentDto, + nullable: true, + }) + @IsOptional() + @ValidateNested() + @Type(() => ExpensePaymentDto) + payment?: ExpensePaymentDto; } diff --git a/server/src/modules/expenses/dto/expense-list-item/expense-list-item.dto.ts b/server/src/modules/expenses/dto/expense-list-item/expense-list-item.dto.ts index d589780..e89b4d8 100644 --- a/server/src/modules/expenses/dto/expense-list-item/expense-list-item.dto.ts +++ b/server/src/modules/expenses/dto/expense-list-item/expense-list-item.dto.ts @@ -1,5 +1,25 @@ +// eslint-disable-next-line max-classes-per-file import { ApiProperty } from '@nestjs/swagger'; -import { ExpenseItem, ExpenseList } from '@prisma/client'; +import { ExpenseItem, ExpenseItemFromType, ExpenseList } from '@prisma/client'; +import { Type } from 'class-transformer'; + +class ExpensePaymentDto { + @ApiProperty({ + description: 'Способ оплаты', + example: 'CASH', + enum: ExpenseItemFromType, + nullable: true, + }) + type: ExpenseItemFromType | null; + + @ApiProperty({ + description: 'ID карты или наличных ', + example: '123', + type: String, + nullable: true, + }) + id: string | null; +} export class ExpenseListItemDto { @ApiProperty({ @@ -61,6 +81,14 @@ export class ExpenseListItemDto { }) count: ExpenseItem['count']; + @ApiProperty({ + description: 'Оплата', + type: ExpensePaymentDto, + nullable: true, + }) + @Type(() => ExpensePaymentDto) + payment?: ExpensePaymentDto; + @ApiProperty({ description: 'Дата создания', type: Date, diff --git a/server/src/modules/expenses/expenses.module.ts b/server/src/modules/expenses/expenses.module.ts index b3d71fa..e999e9f 100644 --- a/server/src/modules/expenses/expenses.module.ts +++ b/server/src/modules/expenses/expenses.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 { ExpensesController } from './expenses.controller'; import { ExpensesService } from './expenses.service'; @Module({ + imports: [ + BankCardModule, + CashModule, + ], controllers: [ExpensesController], providers: [ExpensesService], }) diff --git a/server/src/modules/expenses/expenses.service.ts b/server/src/modules/expenses/expenses.service.ts index 5178ce2..1341d4e 100644 --- a/server/src/modules/expenses/expenses.service.ts +++ b/server/src/modules/expenses/expenses.service.ts @@ -1,5 +1,10 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { ExpenseItem, ExpenseList, User } from '@prisma/client'; +import { + ExpenseItem, ExpenseItemFromType, ExpenseList, User, +} from '@prisma/client'; + +import { BankCardService } from '@/modules/bank-card/bank-card.service'; +import { CashService } from '@/modules/cash/cash.service'; import { PrismaService } from '../prisma/prisma.service'; @@ -17,6 +22,8 @@ import { export class ExpensesService { constructor( private readonly prismaService: PrismaService, + private readonly bankCardService: BankCardService, + private readonly cashService: CashService, ) {} async createList(dto: CreateExpenseListDto, userId: User['id']): Promise { @@ -88,10 +95,26 @@ export class ExpensesService { amount, currency, count, + payment, } = dto; const list = await this.findList(listId, userId); - return this.prismaService.expenseItem.create({ + switch (payment?.type) { + case ExpenseItemFromType.BANK_CARD: + await this.bankCardService.addExpense(userId, payment.id, amount * (count || 1)); + break; + + case ExpenseItemFromType.CASH: + await this.cashService.addExpense(userId, payment.id, amount * (count || 1)); + break; + + // eslint-disable-next-line no-undefined + case undefined: + default: + break; + } + + const item = await this.prismaService.expenseItem.create({ data: { title, description, @@ -99,11 +122,15 @@ export class ExpensesService { amount, currency, count, + paymentType: payment?.type, + paymentSourceId: payment?.id, expenseList: { connect: { id: list.id }, }, }, }); + + return this.toDto(item); } async findListItemById(id: ExpenseItem['id'], listId: ExpenseList['id'], userId: User['id']) { @@ -128,7 +155,7 @@ export class ExpensesService { count = item.count, } = dto; - return this.prismaService.expenseItem.update({ + const updatedItem = await this.prismaService.expenseItem.update({ where: { id: item.id }, data: { title, @@ -139,6 +166,8 @@ export class ExpensesService { count, }, }); + + return this.toDto(updatedItem); } async deleteListItem( @@ -148,9 +177,11 @@ export class ExpensesService { ): Promise { const item = await this.findListItem(itemId, listId, userId); - return this.prismaService.expenseItem.delete({ + const deletedItem = await this.prismaService.expenseItem.delete({ where: { id: item.id }, }); + + return this.toDto(deletedItem); } private async findList(id: ExpenseList['id'], userId: User['id']) { @@ -163,6 +194,9 @@ export class ExpensesService { throw new NotFoundException('Лист расходов не найден'); } + // todo нужно преобразовать paymentSourceId и paymentType в объект payment + // for (let i = 0; i<= existingList.items.length) + return existingList; } @@ -176,4 +210,23 @@ export class ExpensesService { return existingItem; } + + private toDto(item: ExpenseItem): ExpenseListItemDto { + return { + id: item.id, + title: item.title, + description: item.description, + date: item.date, + amount: item.amount, + currency: item.currency, + count: item.count, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + expenseListId: item.expenseListId, + payment: { + type: item.paymentType, + id: item.paymentSourceId, + }, + }; + } }