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 { 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 { 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 { return this.categoryRepository.findAllByUser(userId); } /** * Get categories with statistics */ async getWithStats(userId: string): Promise { return this.categoryRepository.getWithStats(userId); } /** * Get a single category by ID */ async getById(id: string, userId: string): Promise { 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 { 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 { 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)'); } } } }