- 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.
169 lines
4.7 KiB
TypeScript
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');
|
|
}
|
|
}
|
|
}
|