This commit is contained in:
Заид Омар Медхат 2025-12-13 15:57:10 +05:00
parent 33602d0fe9
commit a0febf85f3
8 changed files with 65 additions and 42 deletions

View File

@ -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,
);
}

View File

@ -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<string, { name: string; amount: number }> = {};
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<CategoryBreakdown[]> {
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<string, { name: string; amount: number }> = {};
transactions
.filter(t => t.type === 'EXPENSE')
.filter(t => t.type === TransactionType.EXPENSE)
.forEach(t => {
const catName = t.category?.nameRu || 'Другое';
if (!categoryExpenses[catName]) {

View File

@ -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;

View File

@ -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;
}

View File

@ -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 {

View File

@ -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' })

View File

@ -49,9 +49,19 @@ export class GoalsService {
*/
async create(userId: string, dto: CreateGoalDto): Promise<Goal> {
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<Goal> {
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) {

View File

@ -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 })