From a0febf85f3531985bd3e1151117c649637cb9929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=97=D0=B0=D0=B8=D0=B4=20=D0=9E=D0=BC=D0=B0=D1=80=20?= =?UTF-8?q?=D0=9C=D0=B5=D0=B4=D1=85=D0=B0=D1=82?= Date: Sat, 13 Dec 2025 15:57:10 +0500 Subject: [PATCH] f --- src/modules/analytics/analytics.controller.ts | 7 +++-- src/modules/analytics/analytics.service.ts | 25 ++++++++------- .../categories/entities/category.entity.ts | 9 +++--- src/modules/goals/dto/create-goal.dto.ts | 8 ++--- src/modules/goals/dto/update-goal.dto.ts | 8 ++--- src/modules/goals/entities/goal.entity.ts | 10 ++++-- src/modules/goals/goals.service.ts | 31 +++++++++++++------ .../entities/transaction.entity.ts | 9 +++--- 8 files changed, 65 insertions(+), 42 deletions(-) diff --git a/src/modules/analytics/analytics.controller.ts b/src/modules/analytics/analytics.controller.ts index 9946d77..3e07874 100644 --- a/src/modules/analytics/analytics.controller.ts +++ b/src/modules/analytics/analytics.controller.ts @@ -14,6 +14,7 @@ import { import { AnalyticsService } from './analytics.service'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { CurrentUser, JwtPayload } from '../../common/decorators/user.decorator'; +import { TransactionType } from '../../common/constants/categories'; @ApiTags('Аналитика') @ApiBearerAuth() @@ -49,19 +50,19 @@ export class AnalyticsController { @ApiOperation({ summary: 'Разбивка по категориям' }) @ApiQuery({ name: 'startDate', required: true, example: '2024-01-01' }) @ApiQuery({ name: 'endDate', required: true, example: '2024-01-31' }) - @ApiQuery({ name: 'type', enum: ['INCOME', 'EXPENSE'], required: false }) + @ApiQuery({ name: 'type', enum: TransactionType, required: false }) @ApiResponse({ status: 200, description: 'Разбивка расходов по категориям' }) async getCategoryBreakdown( @CurrentUser() user: JwtPayload, @Query('startDate') startDate: string, @Query('endDate') endDate: string, - @Query('type') type?: 'INCOME' | 'EXPENSE', + @Query('type') type?: TransactionType, ) { return this.analyticsService.getCategoryBreakdown( user.sub, new Date(startDate), new Date(endDate), - type || 'EXPENSE', + type || TransactionType.EXPENSE, ); } diff --git a/src/modules/analytics/analytics.service.ts b/src/modules/analytics/analytics.service.ts index 0f56d34..1996ca1 100644 --- a/src/modules/analytics/analytics.service.ts +++ b/src/modules/analytics/analytics.service.ts @@ -7,6 +7,7 @@ import { Budget } from '../budgets/entities/budget.entity'; import { Goal } from '../goals/entities/goal.entity'; import { startOfMonth, endOfMonth, subMonths, format, startOfYear, endOfYear } from 'date-fns'; import { formatRubles } from '../../common/utils/currency.utils'; +import { TransactionType } from '../../common/constants/categories'; export interface MonthlyOverview { month: string; @@ -80,11 +81,11 @@ export class AnalyticsService { }); const totalIncome = transactions - .filter(t => t.type === 'INCOME') + .filter(t => t.type === TransactionType.INCOME) .reduce((sum, t) => sum + Number(t.amount), 0); const totalExpenses = transactions - .filter(t => t.type === 'EXPENSE') + .filter(t => t.type === TransactionType.EXPENSE) .reduce((sum, t) => sum + Number(t.amount), 0); const netSavings = totalIncome - totalExpenses; @@ -93,7 +94,7 @@ export class AnalyticsService { // Calculate top spending categories const categorySpending: Record = {}; transactions - .filter(t => t.type === 'EXPENSE') + .filter(t => t.type === TransactionType.EXPENSE) .forEach(t => { const catId = t.categoryId || 'other'; const catName = t.category?.nameRu || 'Другое'; @@ -138,7 +139,7 @@ export class AnalyticsService { const transactions = await this.transactionRepository.find({ where: { userId, - type: 'EXPENSE', + type: TransactionType.EXPENSE, transactionDate: Between(monthStart, monthEnd), }, }); @@ -167,7 +168,7 @@ export class AnalyticsService { userId: string, startDate: Date, endDate: Date, - type: 'INCOME' | 'EXPENSE' = 'EXPENSE', + type: TransactionType = TransactionType.EXPENSE, ): Promise { const transactions = await this.transactionRepository.find({ where: { @@ -246,11 +247,11 @@ export class AnalyticsService { }); const income = transactions - .filter(t => t.type === 'INCOME') + .filter(t => t.type === TransactionType.INCOME) .reduce((sum, t) => sum + Number(t.amount), 0); const expenses = transactions - .filter(t => t.type === 'EXPENSE') + .filter(t => t.type === TransactionType.EXPENSE) .reduce((sum, t) => sum + Number(t.amount), 0); result.push({ @@ -344,7 +345,7 @@ export class AnalyticsService { const monthTransactions = transactions.filter(t => { const tDate = new Date(t.transactionDate); - return t.type === 'EXPENSE' && tDate >= monthStart && tDate <= monthEnd; + return t.type === TransactionType.EXPENSE && tDate >= monthStart && tDate <= monthEnd; }); monthlyExpenses.push(monthTransactions.reduce((sum, t) => sum + Number(t.amount), 0)); @@ -404,11 +405,11 @@ export class AnalyticsService { }); const totalIncome = transactions - .filter(t => t.type === 'INCOME') + .filter(t => t.type === TransactionType.INCOME) .reduce((sum, t) => sum + Number(t.amount), 0); const totalExpenses = transactions - .filter(t => t.type === 'EXPENSE') + .filter(t => t.type === TransactionType.EXPENSE) .reduce((sum, t) => sum + Number(t.amount), 0); // Calculate monthly data @@ -418,7 +419,7 @@ export class AnalyticsService { if (!monthlyData[month]) { monthlyData[month] = { income: 0, expenses: 0 }; } - if (t.type === 'INCOME') { + if (t.type === TransactionType.INCOME) { monthlyData[month].income += Number(t.amount); } else { monthlyData[month].expenses += Number(t.amount); @@ -445,7 +446,7 @@ export class AnalyticsService { // Top expense categories const categoryExpenses: Record = {}; transactions - .filter(t => t.type === 'EXPENSE') + .filter(t => t.type === TransactionType.EXPENSE) .forEach(t => { const catName = t.category?.nameRu || 'Другое'; if (!categoryExpenses[catName]) { diff --git a/src/modules/categories/entities/category.entity.ts b/src/modules/categories/entities/category.entity.ts index fb2fe71..2bcd735 100644 --- a/src/modules/categories/entities/category.entity.ts +++ b/src/modules/categories/entities/category.entity.ts @@ -8,6 +8,7 @@ import { Index, } from 'typeorm'; import { User } from '../../auth/entities/user.entity'; +import { BudgetGroupType, TransactionType } from '../../../common/constants/categories'; @Entity('categories') export class Category { @@ -21,11 +22,11 @@ export class Category { nameEn: string; @Index() - @Column({ length: 10 }) - type: 'INCOME' | 'EXPENSE'; + @Column({ type: 'enum', enum: TransactionType }) + type: TransactionType; - @Column({ name: 'group_type', length: 20, nullable: true }) - groupType: 'ESSENTIAL' | 'PERSONAL' | 'SAVINGS' | null; + @Column({ name: 'group_type', type: 'enum', enum: BudgetGroupType, nullable: true }) + groupType: BudgetGroupType | null; @Column({ length: 50, nullable: true }) icon: string; diff --git a/src/modules/goals/dto/create-goal.dto.ts b/src/modules/goals/dto/create-goal.dto.ts index ad5ad79..db36972 100644 --- a/src/modules/goals/dto/create-goal.dto.ts +++ b/src/modules/goals/dto/create-goal.dto.ts @@ -11,7 +11,7 @@ import { MaxLength, Matches, } from 'class-validator'; -import { GoalPriority } from '../entities/goal.entity'; +import { GoalAutoSaveFrequency, GoalPriority } from '../entities/goal.entity'; export class CreateGoalDto { @ApiProperty({ @@ -101,10 +101,10 @@ export class CreateGoalDto { @ApiPropertyOptional({ description: 'Частота автоматического накопления', - enum: ['DAILY', 'WEEKLY', 'MONTHLY'], + enum: GoalAutoSaveFrequency, example: 'MONTHLY', }) @IsOptional() - @IsEnum(['DAILY', 'WEEKLY', 'MONTHLY']) - autoSaveFrequency?: 'DAILY' | 'WEEKLY' | 'MONTHLY'; + @IsEnum(GoalAutoSaveFrequency) + autoSaveFrequency?: GoalAutoSaveFrequency; } diff --git a/src/modules/goals/dto/update-goal.dto.ts b/src/modules/goals/dto/update-goal.dto.ts index fa1720c..2f72ad6 100644 --- a/src/modules/goals/dto/update-goal.dto.ts +++ b/src/modules/goals/dto/update-goal.dto.ts @@ -11,7 +11,7 @@ import { MaxLength, Matches, } from 'class-validator'; -import { GoalPriority, GoalStatus } from '../entities/goal.entity'; +import { GoalAutoSaveFrequency, GoalPriority, GoalStatus } from '../entities/goal.entity'; export class UpdateGoalDto { @ApiPropertyOptional({ @@ -103,12 +103,12 @@ export class UpdateGoalDto { @ApiPropertyOptional({ description: 'Частота автоматического накопления', - enum: ['DAILY', 'WEEKLY', 'MONTHLY'], + enum: GoalAutoSaveFrequency, example: 'MONTHLY', }) @IsOptional() - @IsEnum(['DAILY', 'WEEKLY', 'MONTHLY']) - autoSaveFrequency?: 'DAILY' | 'WEEKLY' | 'MONTHLY'; + @IsEnum(GoalAutoSaveFrequency) + autoSaveFrequency?: GoalAutoSaveFrequency; } export class AddFundsDto { diff --git a/src/modules/goals/entities/goal.entity.ts b/src/modules/goals/entities/goal.entity.ts index 7b7e8de..3e19c39 100644 --- a/src/modules/goals/entities/goal.entity.ts +++ b/src/modules/goals/entities/goal.entity.ts @@ -23,6 +23,12 @@ export enum GoalPriority { HIGH = 'HIGH', } +export enum GoalAutoSaveFrequency { + DAILY = 'DAILY', + WEEKLY = 'WEEKLY', + MONTHLY = 'MONTHLY', +} + @Entity('goals') export class Goal { @PrimaryGeneratedColumn('uuid') @@ -69,8 +75,8 @@ export class Goal { @Column({ name: 'auto_save_amount', type: 'decimal', precision: 15, scale: 2, nullable: true }) autoSaveAmount: number; - @Column({ name: 'auto_save_frequency', length: 20, nullable: true }) - autoSaveFrequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | null; + @Column({ name: 'auto_save_frequency', type: 'enum', enum: GoalAutoSaveFrequency, nullable: true }) + autoSaveFrequency: GoalAutoSaveFrequency | null; // Timestamps @CreateDateColumn({ name: 'created_at' }) diff --git a/src/modules/goals/goals.service.ts b/src/modules/goals/goals.service.ts index 07b734e..7b61b25 100644 --- a/src/modules/goals/goals.service.ts +++ b/src/modules/goals/goals.service.ts @@ -49,9 +49,19 @@ export class GoalsService { */ async create(userId: string, dto: CreateGoalDto): Promise { const goal = this.goalRepository.create({ - ...dto, userId, + titleRu: dto.titleRu, + descriptionRu: dto.descriptionRu, + targetAmount: dto.targetAmount, + currentAmount: dto.currentAmount ?? 0, + currency: 'RUB', targetDate: dto.targetDate ? new Date(dto.targetDate) : undefined, + priority: dto.priority, + icon: dto.icon, + color: dto.color, + autoSaveEnabled: dto.autoSaveEnabled ?? false, + autoSaveAmount: dto.autoSaveAmount, + autoSaveFrequency: dto.autoSaveFrequency ?? undefined, }); return this.goalRepository.save(goal); @@ -63,14 +73,17 @@ export class GoalsService { async update(id: string, userId: string, dto: UpdateGoalDto): Promise { const goal = await this.findOne(id, userId); - if (dto.targetDate) { - goal.targetDate = new Date(dto.targetDate); - } - - Object.assign(goal, { - ...dto, - targetDate: dto.targetDate ? new Date(dto.targetDate) : goal.targetDate, - }); + if (dto.titleRu !== undefined) goal.titleRu = dto.titleRu; + if (dto.descriptionRu !== undefined) goal.descriptionRu = dto.descriptionRu; + if (dto.targetAmount !== undefined) goal.targetAmount = dto.targetAmount; + if (dto.targetDate !== undefined) goal.targetDate = dto.targetDate ? new Date(dto.targetDate) : null as any; + if (dto.status !== undefined) goal.status = dto.status; + if (dto.priority !== undefined) goal.priority = dto.priority; + if (dto.icon !== undefined) goal.icon = dto.icon; + if (dto.color !== undefined) goal.color = dto.color; + if (dto.autoSaveEnabled !== undefined) goal.autoSaveEnabled = dto.autoSaveEnabled; + if (dto.autoSaveAmount !== undefined) goal.autoSaveAmount = dto.autoSaveAmount; + if (dto.autoSaveFrequency !== undefined) goal.autoSaveFrequency = dto.autoSaveFrequency; // Check if goal is completed if (goal.isCompleted && goal.status === GoalStatus.ACTIVE) { diff --git a/src/modules/transactions/entities/transaction.entity.ts b/src/modules/transactions/entities/transaction.entity.ts index d15150e..1a0da90 100644 --- a/src/modules/transactions/entities/transaction.entity.ts +++ b/src/modules/transactions/entities/transaction.entity.ts @@ -12,6 +12,7 @@ import { } from 'typeorm'; import { User } from '../../auth/entities/user.entity'; import { Category } from '../../categories/entities/category.entity'; +import { PaymentMethod, TransactionType } from '../../../common/constants/categories'; @Entity('transactions') @Check('"amount" > 0') @@ -30,8 +31,8 @@ export class Transaction { currency: string; @Index() - @Column({ length: 10 }) - type: 'INCOME' | 'EXPENSE'; + @Column({ type: 'enum', enum: TransactionType }) + type: TransactionType; @Index() @Column({ name: 'category_id', type: 'uuid', nullable: true }) @@ -44,8 +45,8 @@ export class Transaction { @Column({ name: 'transaction_date', type: 'date' }) transactionDate: Date; - @Column({ name: 'payment_method', length: 20, nullable: true }) - paymentMethod: 'CASH' | 'CARD' | 'BANK_TRANSFER' | null; + @Column({ name: 'payment_method', type: 'enum', enum: PaymentMethod, nullable: true }) + paymentMethod: PaymentMethod | null; // Receipt tracking @Column({ name: 'receipt_url', length: 500, nullable: true })