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.
This commit is contained in:
168
backend-api/src/services/DebtAccountService.ts
Normal file
168
backend-api/src/services/DebtAccountService.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user