feat(server): add payment type

This commit is contained in:
Sergey Krylov 2025-09-25 04:58:58 +03:00
parent 457f4bbed2
commit 3fba931281
10 changed files with 188 additions and 37 deletions

View File

@ -71,6 +71,7 @@ export default [
}, },
}, },
], ],
'@typescript-eslint/prefer-nullish-coalescing': 'off',
}, },
}, },
]; ];

View File

@ -46,17 +46,19 @@ model ExpenseList {
} }
model ExpenseItem { model ExpenseItem {
id String @id @default(uuid()) id String @id @default(uuid())
expenseList ExpenseList @relation(fields: [expenseListId], references: [id], onDelete: Cascade) expenseList ExpenseList @relation(fields: [expenseListId], references: [id], onDelete: Cascade)
expenseListId String expenseListId 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") paymentType ExpenseItemFromType? @map("payment_type")
updatedAt DateTime @updatedAt @map("updated_at") paymentSourceId String? @map("payment_source_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("expenses_items") @@map("expenses_items")
} }
@ -123,3 +125,10 @@ model Cash {
@@map("cash") @@map("cash")
} }
enum ExpenseItemFromType {
BANK_CARD
CASH
@@map("expense_item_from_types")
}

View File

@ -9,5 +9,6 @@ import { BankCardService } from './bank-card.service';
controllers: [BankCardController], controllers: [BankCardController],
providers: [BankCardService], providers: [BankCardService],
imports: [BankAccountModule], imports: [BankAccountModule],
exports: [BankCardService],
}) })
export class BankCardModule {} export class BankCardModule {}

View File

@ -13,7 +13,7 @@ export class BankCardService {
private readonly bankAccountService: BankAccountService, private readonly bankAccountService: BankAccountService,
) {} ) {}
async createBankCard(userId: User['id'], dto: CreateBankCardDto): Promise<BankCardDto> { public async createBankCard(userId: User['id'], dto: CreateBankCardDto): Promise<BankCardDto> {
const { name, balance } = dto; const { name, balance } = dto;
const bankAccount = await this.bankAccountService.createBankAccount({ const bankAccount = await this.bankAccountService.createBankAccount({
@ -36,20 +36,13 @@ export class BankCardService {
return this.toDto(card); return this.toDto(card);
} }
async getBankCardById(userId: User['id'], bankCardId: BankCard['id']): Promise<BankCardDto> { public async getBankCardById(userId: User['id'], bankCardId: BankCard['id']): Promise<BankCardDto> {
const card = await this.prismaService.bankCard.findUnique({ const card = await this.findCardById(userId, bankCardId);
where: { id: bankCardId },
include: { bankAccount: true },
});
if (card?.userId !== userId) {
throw new NotFoundException('Карта не найдена');
}
return this.toDto(card); return this.toDto(card);
} }
async getBankCardsByUserId(userId: User['id']): Promise<BankCardDto[]> { public async getBankCardsByUserId(userId: User['id']): Promise<BankCardDto[]> {
const cards = await this.prismaService.bankCard.findMany({ const cards = await this.prismaService.bankCard.findMany({
where: { userId }, where: { userId },
include: { bankAccount: true }, include: { bankAccount: true },
@ -59,7 +52,7 @@ export class BankCardService {
return cards.map((card) => this.toDto(card)); return cards.map((card) => this.toDto(card));
} }
async editBankCard(userId: User['id'], bankCardId: BankCard['id'], dto: EditBankCardDto): Promise<BankCardDto> { public async editBankCard(userId: User['id'], bankCardId: BankCard['id'], dto: EditBankCardDto): Promise<BankCardDto> {
const { name, balance } = dto; const { name, balance } = dto;
const card = await this.getBankCardById(userId, bankCardId); const card = await this.getBankCardById(userId, bankCardId);
@ -79,7 +72,7 @@ export class BankCardService {
return this.toDto(updatedCard); return this.toDto(updatedCard);
} }
async deleteBankCard(userId: User['id'], bankCardId: BankCard['id']): Promise<BankCardDto> { public async deleteBankCard(userId: User['id'], bankCardId: BankCard['id']): Promise<BankCardDto> {
const card = await this.getBankCardById(userId, bankCardId); const card = await this.getBankCardById(userId, bankCardId);
const deletedCard = await this.prismaService.bankCard.delete({ const deletedCard = await this.prismaService.bankCard.delete({
@ -90,7 +83,25 @@ export class BankCardService {
return this.toDto(deletedCard); 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 { return {
id: card.id, id: card.id,
name: card.name, name: card.name,

View File

@ -6,5 +6,6 @@ import { CashService } from './cash.service';
@Module({ @Module({
controllers: [CashController], controllers: [CashController],
providers: [CashService], providers: [CashService],
exports: [CashService],
}) })
export class CashModule {} export class CashModule {}

View File

@ -1,5 +1,5 @@
import { Injectable, NotFoundException } from '@nestjs/common'; 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'; import { PrismaService } from '@/modules/prisma/prisma.service';
@ -9,7 +9,7 @@ import { CashDto, CreateCashDto, EditCashDto } from './dto';
export class CashService { export class CashService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
async createCash(userId: User['id'], dto: CreateCashDto): Promise<CashDto> { public async createCash(userId: User['id'], dto: CreateCashDto): Promise<CashDto> {
const { balance, name } = dto; const { balance, name } = dto;
return this.prismaService.cash.create({ return this.prismaService.cash.create({
@ -23,7 +23,7 @@ export class CashService {
}); });
} }
async getCashById(userId: User['id'], cashId: Cash['id']): Promise<CashDto> { public async getCashById(userId: User['id'], cashId: Cash['id']): Promise<CashDto> {
const cash = await this.prismaService.cash.findUnique({ const cash = await this.prismaService.cash.findUnique({
where: { where: {
id: cashId, id: cashId,
@ -38,14 +38,14 @@ export class CashService {
return cash; return cash;
} }
async getAllCashByUserId(userId: User['id']): Promise<CashDto[]> { public async getAllCashByUserId(userId: User['id']): Promise<CashDto[]> {
return this.prismaService.cash.findMany({ return this.prismaService.cash.findMany({
where: { userId }, where: { userId },
orderBy: { createdAt: 'asc' }, orderBy: { createdAt: 'asc' },
}); });
} }
async editCash(userId: User['id'], cashId: Cash['id'], dto: EditCashDto): Promise<CashDto> { public async editCash(userId: User['id'], cashId: Cash['id'], dto: EditCashDto): Promise<CashDto> {
const { balance, name } = dto; const { balance, name } = dto;
const cash = await this.getCashById(userId, cashId); const cash = await this.getCashById(userId, cashId);
@ -59,11 +59,16 @@ export class CashService {
}); });
} }
async deleteCash(userId: User['id'], cashId: Cash['id']): Promise<CashDto> { public async deleteCash(userId: User['id'], cashId: Cash['id']): Promise<CashDto> {
const cash = await this.getCashById(userId, cashId); const cash = await this.getCashById(userId, cashId);
return this.prismaService.cash.delete({ return this.prismaService.cash.delete({
where: { id: cash.id }, 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 });
}
} }

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 { ExpenseItemFromType } 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 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 { export class CreateExpenseListItemDto {
@ApiProperty({ @ApiProperty({
description: 'Заголовок расхода', description: 'Заголовок расхода',
@ -67,4 +92,14 @@ export class CreateExpenseListItemDto {
@IsOptional() @IsOptional()
@IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 2 }, { message: 'Количество должно быть числом.' }) @IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 2 }, { message: 'Количество должно быть числом.' })
count?: number; count?: number;
@ApiProperty({
description: 'Оплата',
type: ExpensePaymentDto,
nullable: true,
})
@IsOptional()
@ValidateNested()
@Type(() => ExpensePaymentDto)
payment?: ExpensePaymentDto;
} }

View File

@ -1,5 +1,25 @@
// eslint-disable-next-line max-classes-per-file
import { ApiProperty } from '@nestjs/swagger'; 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 { export class ExpenseListItemDto {
@ApiProperty({ @ApiProperty({
@ -61,6 +81,14 @@ export class ExpenseListItemDto {
}) })
count: ExpenseItem['count']; count: ExpenseItem['count'];
@ApiProperty({
description: 'Оплата',
type: ExpensePaymentDto,
nullable: true,
})
@Type(() => ExpensePaymentDto)
payment?: ExpensePaymentDto;
@ApiProperty({ @ApiProperty({
description: 'Дата создания', description: 'Дата создания',
type: Date, type: Date,

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 { ExpensesController } from './expenses.controller'; import { ExpensesController } from './expenses.controller';
import { ExpensesService } from './expenses.service'; import { ExpensesService } from './expenses.service';
@Module({ @Module({
imports: [
BankCardModule,
CashModule,
],
controllers: [ExpensesController], controllers: [ExpensesController],
providers: [ExpensesService], providers: [ExpensesService],
}) })

View File

@ -1,5 +1,10 @@
import { Injectable, NotFoundException } from '@nestjs/common'; 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'; import { PrismaService } from '../prisma/prisma.service';
@ -17,6 +22,8 @@ import {
export class ExpensesService { export class ExpensesService {
constructor( constructor(
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly bankCardService: BankCardService,
private readonly cashService: CashService,
) {} ) {}
async createList(dto: CreateExpenseListDto, userId: User['id']): Promise<ExpenseListDto> { async createList(dto: CreateExpenseListDto, userId: User['id']): Promise<ExpenseListDto> {
@ -88,10 +95,26 @@ export class ExpensesService {
amount, amount,
currency, currency,
count, count,
payment,
} = dto; } = dto;
const list = await this.findList(listId, userId); 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: { data: {
title, title,
description, description,
@ -99,11 +122,15 @@ export class ExpensesService {
amount, amount,
currency, currency,
count, count,
paymentType: payment?.type,
paymentSourceId: payment?.id,
expenseList: { expenseList: {
connect: { id: list.id }, connect: { id: list.id },
}, },
}, },
}); });
return this.toDto(item);
} }
async findListItemById(id: ExpenseItem['id'], listId: ExpenseList['id'], userId: User['id']) { async findListItemById(id: ExpenseItem['id'], listId: ExpenseList['id'], userId: User['id']) {
@ -128,7 +155,7 @@ export class ExpensesService {
count = item.count, count = item.count,
} = dto; } = dto;
return this.prismaService.expenseItem.update({ const updatedItem = await this.prismaService.expenseItem.update({
where: { id: item.id }, where: { id: item.id },
data: { data: {
title, title,
@ -139,6 +166,8 @@ export class ExpensesService {
count, count,
}, },
}); });
return this.toDto(updatedItem);
} }
async deleteListItem( async deleteListItem(
@ -148,9 +177,11 @@ export class ExpensesService {
): Promise<ExpenseListItemDto> { ): Promise<ExpenseListItemDto> {
const item = await this.findListItem(itemId, listId, userId); const item = await this.findListItem(itemId, listId, userId);
return this.prismaService.expenseItem.delete({ const deletedItem = await this.prismaService.expenseItem.delete({
where: { id: item.id }, where: { id: item.id },
}); });
return this.toDto(deletedItem);
} }
private async findList(id: ExpenseList['id'], userId: User['id']) { private async findList(id: ExpenseList['id'], userId: User['id']) {
@ -163,6 +194,9 @@ export class ExpensesService {
throw new NotFoundException('Лист расходов не найден'); throw new NotFoundException('Лист расходов не найден');
} }
// todo нужно преобразовать paymentSourceId и paymentType в объект payment
// for (let i = 0; i<= existingList.items.length)
return existingList; return existingList;
} }
@ -176,4 +210,23 @@ export class ExpensesService {
return existingItem; 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,
},
};
}
} }