- Added '@typescript-eslint/no-unused-vars' rule to ESLint configuration for better variable management in TypeScript files. - Updated database.ts to ensure consistent logging format. - Refactored AuthController and CashflowController to improve variable naming and maintainability. - Added spacing for better readability in multiple controller methods. - Adjusted error handling in middleware and repository files for improved clarity. - Enhanced various service and repository methods to ensure consistent return types and error handling. - Made minor formatting adjustments across frontend components for improved user experience.
157 lines
4.6 KiB
TypeScript
157 lines
4.6 KiB
TypeScript
import {DebtCategory} from '@prisma/client';
|
|
import {DebtCategoryRepository, DebtCategoryWithStats} from '../repositories/DebtCategoryRepository';
|
|
import {NotFoundError, ValidationError, ForbiddenError, ConflictError} from '../utils/errors';
|
|
|
|
export interface CreateDebtCategoryDTO {
|
|
name: string;
|
|
description?: string;
|
|
color?: string;
|
|
}
|
|
|
|
export interface UpdateDebtCategoryDTO {
|
|
name?: string;
|
|
description?: string;
|
|
color?: string;
|
|
}
|
|
|
|
/**
|
|
* Service for DebtCategory business logic
|
|
* Implements Single Responsibility Principle - handles only business logic
|
|
* Implements Dependency Inversion - depends on repository abstraction
|
|
*/
|
|
export class DebtCategoryService {
|
|
constructor(private categoryRepository: DebtCategoryRepository) {}
|
|
|
|
/**
|
|
* Create default debt categories for a new user
|
|
*/
|
|
async createDefaultCategories(userId: string): Promise<DebtCategory[]> {
|
|
const defaultCategories = [
|
|
{name: 'Credit Cards', description: 'Credit card debts', color: '#ef4444'},
|
|
{name: 'Student Loans', description: 'Student loan debts', color: '#3b82f6'},
|
|
{name: 'Auto Loans', description: 'Car and vehicle loans', color: '#10b981'},
|
|
{name: 'Mortgages', description: 'Home mortgages', color: '#f59e0b'},
|
|
{name: 'Personal Loans', description: 'Personal loan debts', color: '#8b5cf6'},
|
|
{name: 'Other', description: 'Other debt types', color: '#6b7280'}
|
|
];
|
|
|
|
const categories: DebtCategory[] = [];
|
|
|
|
for (const category of defaultCategories) {
|
|
const created = await this.categoryRepository.create({
|
|
name: category.name,
|
|
description: category.description,
|
|
color: category.color,
|
|
user: {
|
|
connect: {id: userId}
|
|
}
|
|
});
|
|
categories.push(created);
|
|
}
|
|
|
|
return categories;
|
|
}
|
|
|
|
/**
|
|
* Create a new debt category
|
|
*/
|
|
async create(userId: string, data: CreateDebtCategoryDTO): Promise<DebtCategory> {
|
|
this.validateCategoryData(data);
|
|
|
|
// Check for duplicate name
|
|
const existing = await this.categoryRepository.findByName(userId, data.name);
|
|
if (existing) {
|
|
throw new ConflictError('A category with this name already exists');
|
|
}
|
|
|
|
return this.categoryRepository.create({
|
|
name: data.name,
|
|
description: data.description,
|
|
color: data.color,
|
|
user: {
|
|
connect: {id: userId}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all categories for a user
|
|
*/
|
|
async getAllByUser(userId: string): Promise<DebtCategory[]> {
|
|
return this.categoryRepository.findAllByUser(userId);
|
|
}
|
|
|
|
/**
|
|
* Get categories with statistics
|
|
*/
|
|
async getWithStats(userId: string): Promise<DebtCategoryWithStats[]> {
|
|
return this.categoryRepository.getWithStats(userId);
|
|
}
|
|
|
|
/**
|
|
* Get a single category by ID
|
|
*/
|
|
async getById(id: string, userId: string): Promise<DebtCategory> {
|
|
const category = await this.categoryRepository.findById(id);
|
|
|
|
if (!category) {
|
|
throw new NotFoundError('Debt category not found');
|
|
}
|
|
|
|
if (category.userId !== userId) {
|
|
throw new ForbiddenError('Access denied');
|
|
}
|
|
|
|
return category;
|
|
}
|
|
|
|
/**
|
|
* Update a category
|
|
*/
|
|
async update(id: string, userId: string, data: UpdateDebtCategoryDTO): Promise<DebtCategory> {
|
|
await this.getById(id, userId);
|
|
|
|
if (data.name) {
|
|
this.validateCategoryData(data as CreateDebtCategoryDTO);
|
|
|
|
// Check for duplicate name (excluding current category)
|
|
const existing = await this.categoryRepository.findByName(userId, data.name);
|
|
if (existing && existing.id !== id) {
|
|
throw new ConflictError('A category with this name already exists');
|
|
}
|
|
}
|
|
|
|
return this.categoryRepository.update(id, data);
|
|
}
|
|
|
|
/**
|
|
* Delete a category
|
|
*/
|
|
async delete(id: string, userId: string): Promise<void> {
|
|
await this.getById(id, userId);
|
|
|
|
// Check if category has accounts
|
|
// Note: Cascade delete will remove all accounts and payments
|
|
// You might want to prevent deletion if there are accounts
|
|
// Uncomment below to prevent deletion:
|
|
// if (category.accounts && category.accounts.length > 0) {
|
|
// throw new ValidationError('Cannot delete category with existing accounts');
|
|
// }
|
|
|
|
await this.categoryRepository.delete(id);
|
|
}
|
|
|
|
/**
|
|
* Validate category data
|
|
*/
|
|
private validateCategoryData(data: CreateDebtCategoryDTO | UpdateDebtCategoryDTO): void {
|
|
if (data.color) {
|
|
// Validate hex color format
|
|
const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
|
if (!hexColorRegex.test(data.color)) {
|
|
throw new ValidationError('Color must be a valid hex color (e.g., #FF5733)');
|
|
}
|
|
}
|
|
}
|
|
}
|