Files
personal-finance/backend-api/src/services/DebtAccountService.ts
Alexander Zinn cd93dcbfd2 Add backend API for personal finance management application
- Introduced a comprehensive backend API using TypeScript, Fastify, and PostgreSQL.
- Added essential files including architecture documentation, environment configuration, and Docker setup.
- Implemented RESTful routes for managing assets, liabilities, clients, invoices, and cashflow.
- Established a robust database schema with Prisma for data management.
- Integrated middleware for authentication and error handling.
- Created service and repository layers to adhere to SOLID principles and clean architecture.
- Included example environment variables for development, staging, and production setups.
2025-12-07 12:59:09 -05:00

169 lines
4.7 KiB
TypeScript

import {DebtAccount} from '@prisma/client';
import {DebtAccountRepository} from '../repositories/DebtAccountRepository';
import {DebtCategoryRepository} from '../repositories/DebtCategoryRepository';
import {NotFoundError, ValidationError, ForbiddenError} from '../utils/errors';
export interface CreateDebtAccountDTO {
categoryId: string;
name: string;
creditor: string;
accountNumber?: string;
originalBalance: number;
currentBalance: number;
interestRate?: number;
minimumPayment?: number;
dueDate?: Date;
notes?: string;
}
export interface UpdateDebtAccountDTO {
name?: string;
creditor?: string;
accountNumber?: string;
currentBalance?: number;
interestRate?: number;
minimumPayment?: number;
dueDate?: Date;
notes?: string;
}
/**
* Service for DebtAccount business logic
* Implements Single Responsibility Principle - handles only business logic
* Implements Dependency Inversion - depends on repository abstractions
*/
export class DebtAccountService {
constructor(
private accountRepository: DebtAccountRepository,
private categoryRepository: DebtCategoryRepository
) {}
/**
* Create a new debt account
*/
async create(userId: string, data: CreateDebtAccountDTO): Promise<DebtAccount> {
this.validateAccountData(data);
// Verify category ownership
const category = await this.categoryRepository.findById(data.categoryId);
if (!category) {
throw new NotFoundError('Debt category not found');
}
if (category.userId !== userId) {
throw new ForbiddenError('Access denied');
}
return this.accountRepository.create({
name: data.name,
creditor: data.creditor,
accountNumber: data.accountNumber,
originalBalance: data.originalBalance,
currentBalance: data.currentBalance,
interestRate: data.interestRate,
minimumPayment: data.minimumPayment,
dueDate: data.dueDate,
notes: data.notes,
category: {
connect: {id: data.categoryId},
},
});
}
/**
* Get all debt accounts for a user
*/
async getAllByUser(userId: string): Promise<DebtAccount[]> {
return this.accountRepository.findAllByUser(userId);
}
/**
* Get debt accounts with statistics
*/
async getWithStats(userId: string): Promise<any[]> {
return this.accountRepository.getWithStats(userId);
}
/**
* Get debt accounts by category
*/
async getByCategory(categoryId: string, userId: string): Promise<DebtAccount[]> {
// Verify category ownership
const category = await this.categoryRepository.findById(categoryId);
if (!category) {
throw new NotFoundError('Debt category not found');
}
if (category.userId !== userId) {
throw new ForbiddenError('Access denied');
}
return this.accountRepository.findByCategory(categoryId);
}
/**
* Get a single debt account by ID
*/
async getById(id: string, userId: string): Promise<DebtAccount> {
const account = await this.accountRepository.findById(id);
if (!account) {
throw new NotFoundError('Debt account not found');
}
// Verify ownership through category
if (account.category.userId !== userId) {
throw new ForbiddenError('Access denied');
}
return account;
}
/**
* Update a debt account
*/
async update(id: string, userId: string, data: UpdateDebtAccountDTO): Promise<DebtAccount> {
await this.getById(id, userId);
if (data.currentBalance !== undefined || data.interestRate !== undefined || data.minimumPayment !== undefined) {
this.validateAccountData(data as CreateDebtAccountDTO);
}
return this.accountRepository.update(id, data);
}
/**
* Delete a debt account
*/
async delete(id: string, userId: string): Promise<void> {
await this.getById(id, userId);
await this.accountRepository.delete(id);
}
/**
* Get total debt for a user
*/
async getTotalDebt(userId: string): Promise<number> {
return this.accountRepository.getTotalDebt(userId);
}
/**
* Validate account data
*/
private validateAccountData(data: CreateDebtAccountDTO | UpdateDebtAccountDTO): void {
if ('originalBalance' in data && data.originalBalance < 0) {
throw new ValidationError('Original balance cannot be negative');
}
if (data.currentBalance !== undefined && data.currentBalance < 0) {
throw new ValidationError('Current balance cannot be negative');
}
if (data.interestRate !== undefined && (data.interestRate < 0 || data.interestRate > 100)) {
throw new ValidationError('Interest rate must be between 0 and 100');
}
if (data.minimumPayment !== undefined && data.minimumPayment < 0) {
throw new ValidationError('Minimum payment cannot be negative');
}
}
}