Enhance ESLint configuration and improve code consistency

- 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.
This commit is contained in:
2025-12-11 02:19:05 -05:00
parent 40210c454e
commit df2cf418ea
48 changed files with 247 additions and 61 deletions

View File

@@ -12,9 +12,10 @@ class DatabaseConnection {
public static getInstance(): PrismaClient {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
}
return DatabaseConnection.instance;
}

View File

@@ -55,7 +55,7 @@ export class AuthController {
email: user.email
});
const {password: _, ...userWithoutPassword} = user;
const {password: _password, ...userWithoutPassword} = user;
return reply.send({
user: userWithoutPassword,

View File

@@ -46,12 +46,14 @@ export class CashflowController {
const userId = getUserId(request);
const data = createIncomeSchema.parse(request.body);
const income = await this.cashflowService.createIncome(userId, data);
return reply.status(201).send({income});
}
async getAllIncome(request: FastifyRequest, reply: FastifyReply) {
const userId = getUserId(request);
const income = await this.cashflowService.getAllIncome(userId);
return reply.send({income});
}
@@ -59,6 +61,7 @@ export class CashflowController {
const userId = getUserId(request);
const {id} = request.params as {id: string};
const income = await this.cashflowService.getIncomeById(id, userId);
return reply.send({income});
}
@@ -67,6 +70,7 @@ export class CashflowController {
const {id} = request.params as {id: string};
const data = updateIncomeSchema.parse(request.body);
const income = await this.cashflowService.updateIncome(id, userId, data);
return reply.send({income});
}
@@ -74,12 +78,14 @@ export class CashflowController {
const userId = getUserId(request);
const {id} = request.params as {id: string};
await this.cashflowService.deleteIncome(id, userId);
return reply.status(204).send();
}
async getTotalMonthlyIncome(request: FastifyRequest, reply: FastifyReply) {
const userId = getUserId(request);
const total = await this.cashflowService.getTotalMonthlyIncome(userId);
return reply.send({total});
}
@@ -88,6 +94,7 @@ export class CashflowController {
const userId = getUserId(request);
const data = createExpenseSchema.parse(request.body);
const expense = await this.cashflowService.createExpense(userId, data);
return reply.status(201).send({expense});
}
@@ -97,10 +104,12 @@ export class CashflowController {
if (byCategory === 'true') {
const expenses = await this.cashflowService.getExpensesByCategory(userId);
return reply.send({expenses});
}
const expenses = await this.cashflowService.getAllExpenses(userId);
return reply.send({expenses});
}
@@ -108,6 +117,7 @@ export class CashflowController {
const userId = getUserId(request);
const {id} = request.params as {id: string};
const expense = await this.cashflowService.getExpenseById(id, userId);
return reply.send({expense});
}
@@ -116,6 +126,7 @@ export class CashflowController {
const {id} = request.params as {id: string};
const data = updateExpenseSchema.parse(request.body);
const expense = await this.cashflowService.updateExpense(id, userId, data);
return reply.send({expense});
}
@@ -123,12 +134,14 @@ export class CashflowController {
const userId = getUserId(request);
const {id} = request.params as {id: string};
await this.cashflowService.deleteExpense(id, userId);
return reply.status(204).send();
}
async getTotalMonthlyExpenses(request: FastifyRequest, reply: FastifyReply) {
const userId = getUserId(request);
const total = await this.cashflowService.getTotalMonthlyExpenses(userId);
return reply.send({total});
}
@@ -137,6 +150,7 @@ export class CashflowController {
const userId = getUserId(request);
const data = createTransactionSchema.parse(request.body);
const transaction = await this.cashflowService.createTransaction(userId, data);
return reply.status(201).send({transaction});
}
@@ -150,15 +164,18 @@ export class CashflowController {
if (type) {
const transactions = await this.cashflowService.getTransactionsByType(userId, type);
return reply.send({transactions});
}
if (startDate && endDate) {
const transactions = await this.cashflowService.getTransactionsByDateRange(userId, new Date(startDate), new Date(endDate));
return reply.send({transactions});
}
const transactions = await this.cashflowService.getAllTransactions(userId);
return reply.send({transactions});
}
@@ -166,6 +183,7 @@ export class CashflowController {
const userId = getUserId(request);
const {id} = request.params as {id: string};
const transaction = await this.cashflowService.getTransactionById(id, userId);
return reply.send({transaction});
}
@@ -173,6 +191,7 @@ export class CashflowController {
const userId = getUserId(request);
const {id} = request.params as {id: string};
await this.cashflowService.deleteTransaction(id, userId);
return reply.status(204).send();
}

View File

@@ -47,10 +47,12 @@ export class ClientController {
if (withStats === 'true') {
const clients = await this.clientService.getWithStats(userId);
return reply.send({clients});
}
const clients = await this.clientService.getAllByUser(userId);
return reply.send({clients});
}

View File

@@ -61,15 +61,18 @@ export class DebtAccountController {
if (categoryId) {
const accounts = await this.accountService.getByCategory(categoryId, userId);
return reply.send({accounts});
}
if (withStats === 'true') {
const accounts = await this.accountService.getWithStats(userId);
return reply.send({accounts});
}
const accounts = await this.accountService.getAllByUser(userId);
return reply.send({accounts});
}

View File

@@ -49,10 +49,12 @@ export class DebtCategoryController {
if (withStats === 'true') {
const categories = await this.categoryService.getWithStats(userId);
return reply.send({categories});
}
const categories = await this.categoryService.getAllByUser(userId);
return reply.send({categories});
}

View File

@@ -42,15 +42,18 @@ export class DebtPaymentController {
if (accountId) {
const payments = await this.paymentService.getByAccount(accountId, userId);
return reply.send({payments});
}
if (startDate && endDate) {
const payments = await this.paymentService.getByDateRange(userId, new Date(startDate), new Date(endDate));
return reply.send({payments});
}
const payments = await this.paymentService.getAllByUser(userId);
return reply.send({payments});
}

View File

@@ -5,10 +5,10 @@ import {UnauthorizedError} from '../utils/errors';
* Authentication Middleware
* Verifies JWT token and attaches user to request
*/
export async function authenticate(request: FastifyRequest, reply: FastifyReply) {
export async function authenticate(request: FastifyRequest, _reply: FastifyReply) {
try {
await request.jwtVerify();
} catch (err) {
} catch (_err) {
throw new UnauthorizedError('Invalid or expired token');
}
}
@@ -20,5 +20,6 @@ export function getUserId(request: FastifyRequest): string {
if (!request.user || !request.user.id) {
throw new UnauthorizedError('User not authenticated');
}
return request.user.id;
}

View File

@@ -38,7 +38,7 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
// Handle Prisma errors
if (error.name === 'PrismaClientKnownRequestError') {
const prismaError = error as any;
const prismaError = error as FastifyError & {code?: string};
if (prismaError.code === 'P2002') {
return reply.status(409).send({
error: 'ConflictError',

View File

@@ -16,7 +16,7 @@ export class AssetRepository {
});
}
async findAllByUser(userId: string, filters?: Record<string, any>): Promise<Asset[]> {
async findAllByUser(userId: string, filters?: Record<string, unknown>): Promise<Asset[]> {
return prisma.asset.findMany({
where: {userId, ...filters},
orderBy: {createdAt: 'desc'}
@@ -43,6 +43,7 @@ export class AssetRepository {
where: {userId},
_sum: {value: true}
});
return result._sum.value || 0;
}
}

View File

@@ -39,6 +39,7 @@ export class IncomeSourceRepository {
where: {userId},
_sum: {amount: true}
});
return result._sum.amount || 0;
}
}
@@ -79,15 +80,18 @@ export class ExpenseRepository {
where: {userId},
_sum: {amount: true}
});
return result._sum.amount || 0;
}
async getByCategory(userId: string): Promise<Record<string, Expense[]>> {
const expenses = await this.findAllByUser(userId);
return expenses.reduce(
(acc, expense) => {
if (!acc[expense.category]) acc[expense.category] = [];
acc[expense.category].push(expense);
return acc;
},
{} as Record<string, Expense[]>

View File

@@ -3,6 +3,17 @@ import {DatabaseConnection} from '../config/database';
const prisma = DatabaseConnection.getInstance();
export interface ClientStats {
totalInvoices: number;
paidInvoices: number;
totalRevenue: number;
outstandingAmount: number;
}
export interface ClientWithStats extends Client {
stats: ClientStats;
}
/**
* Repository for Client data access
* Implements Single Responsibility Principle - handles only database operations
@@ -97,7 +108,7 @@ export class ClientRepository {
/**
* Get clients with their invoice statistics
*/
async getWithStats(userId: string): Promise<any[]> {
async getWithStats(userId: string): Promise<ClientWithStats[]> {
const clients = await prisma.client.findMany({
where: {userId},
include: {

View File

@@ -3,6 +3,17 @@ import {DatabaseConnection} from '../config/database';
const prisma = DatabaseConnection.getInstance();
export interface DebtAccountStats {
totalPaid: number;
numberOfPayments: number;
lastPaymentDate: Date | null;
lastPaymentAmount: number | null;
}
export interface DebtAccountWithStats extends DebtAccount {
stats: DebtAccountStats;
}
/**
* Repository for DebtAccount data access
* Implements Single Responsibility Principle - handles only database operations
@@ -101,7 +112,7 @@ export class DebtAccountRepository {
/**
* Get accounts with payment statistics
*/
async getWithStats(userId: string): Promise<any[]> {
async getWithStats(userId: string): Promise<DebtAccountWithStats[]> {
const accounts = await prisma.debtAccount.findMany({
where: {userId},
include: {

View File

@@ -4,6 +4,16 @@ import {IUserScopedRepository} from './interfaces/IRepository';
const prisma = DatabaseConnection.getInstance();
export interface DebtCategoryStats {
totalAccounts: number;
totalDebt: number;
totalPayments: number;
}
export interface DebtCategoryWithStats extends DebtCategory {
stats: DebtCategoryStats;
}
/**
* Repository for DebtCategory data access
* Implements Single Responsibility Principle - handles only database operations
@@ -91,7 +101,7 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
/**
* Get categories with debt statistics
*/
async getWithStats(userId: string): Promise<any[]> {
async getWithStats(userId: string): Promise<DebtCategoryWithStats[]> {
const categories = await this.findAllByUser(userId);
return Promise.all(

View File

@@ -61,6 +61,7 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
...(excludeId && {id: {not: excludeId}})
}
});
return count > 0;
}
@@ -72,6 +73,7 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
invoiceNumber: {startsWith: `INV-${year}-`}
}
});
return `INV-${year}-${String(count + 1).padStart(3, '0')}`;
}
}

View File

@@ -73,6 +73,7 @@ export class LiabilityRepository {
acc[type] = [];
}
acc[type].push(liability);
return acc;
},
{} as Record<string, Liability[]>

View File

@@ -4,6 +4,13 @@ import {IUserScopedRepository} from './interfaces/IRepository';
const prisma = DatabaseConnection.getInstance();
export interface GrowthStats {
date: Date;
netWorth: number;
growthAmount: number;
growthPercent: number;
}
/**
* Repository for NetWorthSnapshot data access
* Implements Single Responsibility Principle - handles only database operations
@@ -84,7 +91,7 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
/**
* Get growth over time (percentage change between snapshots)
*/
async getGrowthStats(userId: string, limit: number = 12): Promise<any[]> {
async getGrowthStats(userId: string, limit: number = 12): Promise<GrowthStats[]> {
const snapshots = await prisma.netWorthSnapshot.findMany({
where: {userId},
orderBy: {date: 'desc'},

View File

@@ -46,6 +46,7 @@ export class UserRepository implements IRepository<User> {
async emailExists(email: string): Promise<boolean> {
const count = await prisma.user.count({where: {email}});
return count > 0;
}
}

View File

@@ -34,6 +34,7 @@ export class AssetService {
if (!asset) {
throw new NotFoundError('Asset not found');
}
return asset;
}
@@ -80,6 +81,7 @@ export class AssetService {
async getByType(userId: string): Promise<Record<string, Asset[]>> {
const assets = await this.assetRepository.findAllByUser(userId);
return assets.reduce(
(acc, asset) => {
const type = asset.type;
@@ -87,6 +89,7 @@ export class AssetService {
acc[type] = [];
}
acc[type].push(asset);
return acc;
},
{} as Record<string, Asset[]>

View File

@@ -40,7 +40,8 @@ export class AuthService {
await this.debtCategoryService.createDefaultCategories(user.id);
// Return user without password
const {password: _, ...userWithoutPassword} = user;
const {password: _password, ...userWithoutPassword} = user;
return userWithoutPassword;
}
@@ -62,7 +63,8 @@ export class AuthService {
const user = await this.userRepository.findById(id);
if (!user) return null;
const {password: _, ...userWithoutPassword} = user;
const {password: _password, ...userWithoutPassword} = user;
return userWithoutPassword;
}
}

View File

@@ -55,6 +55,7 @@ export class CashflowService {
const income = await this.incomeRepository.findById(id);
if (!income) throw new NotFoundError('Income source not found');
if (income.userId !== userId) throw new ForbiddenError('Access denied');
return income;
}
@@ -63,6 +64,7 @@ export class CashflowService {
if (data.amount !== undefined && data.amount <= 0) {
throw new ValidationError('Amount must be greater than 0');
}
return this.incomeRepository.update(id, data);
}
@@ -93,6 +95,7 @@ export class CashflowService {
const expense = await this.expenseRepository.findById(id);
if (!expense) throw new NotFoundError('Expense not found');
if (expense.userId !== userId) throw new ForbiddenError('Access denied');
return expense;
}
@@ -101,6 +104,7 @@ export class CashflowService {
if (data.amount !== undefined && data.amount <= 0) {
throw new ValidationError('Amount must be greater than 0');
}
return this.expenseRepository.update(id, data);
}
@@ -136,6 +140,7 @@ export class CashflowService {
const transaction = await this.transactionRepository.findById(id);
if (!transaction) throw new NotFoundError('Transaction not found');
if (transaction.userId !== userId) throw new ForbiddenError('Access denied');
return transaction;
}

View File

@@ -1,5 +1,5 @@
import {Client} from '@prisma/client';
import {ClientRepository} from '../repositories/ClientRepository';
import {ClientRepository, ClientWithStats} from '../repositories/ClientRepository';
import {NotFoundError, ValidationError, ForbiddenError, ConflictError} from '../utils/errors';
export interface CreateClientDTO {
@@ -60,7 +60,7 @@ export class ClientService {
/**
* Get clients with statistics
*/
async getWithStats(userId: string): Promise<any[]> {
async getWithStats(userId: string): Promise<ClientWithStats[]> {
return this.clientRepository.getWithStats(userId);
}
@@ -107,7 +107,7 @@ export class ClientService {
*/
async delete(id: string, userId: string): Promise<void> {
// Verify ownership
const client = await this.getById(id, userId);
await this.getById(id, userId);
// Check if client has invoices - we still allow deletion due to cascade
// but you might want to prevent deletion if there are invoices

View File

@@ -1,5 +1,5 @@
import {DebtAccount} from '@prisma/client';
import {DebtAccountRepository} from '../repositories/DebtAccountRepository';
import {DebtAccountRepository, DebtAccountWithStats} from '../repositories/DebtAccountRepository';
import {DebtCategoryRepository} from '../repositories/DebtCategoryRepository';
import {NotFoundError, ValidationError, ForbiddenError} from '../utils/errors';
@@ -79,7 +79,7 @@ export class DebtAccountService {
/**
* Get debt accounts with statistics
*/
async getWithStats(userId: string): Promise<any[]> {
async getWithStats(userId: string): Promise<DebtAccountWithStats[]> {
return this.accountRepository.getWithStats(userId);
}

View File

@@ -1,5 +1,5 @@
import {DebtCategory} from '@prisma/client';
import {DebtCategoryRepository} from '../repositories/DebtCategoryRepository';
import {DebtCategoryRepository, DebtCategoryWithStats} from '../repositories/DebtCategoryRepository';
import {NotFoundError, ValidationError, ForbiddenError, ConflictError} from '../utils/errors';
export interface CreateDebtCategoryDTO {
@@ -84,7 +84,7 @@ export class DebtCategoryService {
/**
* Get categories with statistics
*/
async getWithStats(userId: string): Promise<any[]> {
async getWithStats(userId: string): Promise<DebtCategoryWithStats[]> {
return this.categoryRepository.getWithStats(userId);
}
@@ -128,7 +128,7 @@ export class DebtCategoryService {
* Delete a category
*/
async delete(id: string, userId: string): Promise<void> {
const category = await this.getById(id, userId);
await this.getById(id, userId);
// Check if category has accounts
// Note: Cascade delete will remove all accounts and payments

View File

@@ -59,6 +59,7 @@ export class InvoiceService {
if (!invoice) {
throw new NotFoundError('Invoice not found');
}
return invoice as unknown as Invoice;
}
@@ -193,6 +194,7 @@ export class InvoiceService {
async getOverdueInvoices(userId: string): Promise<Invoice[]> {
const invoices = await this.invoiceRepository.findAllByUser(userId, {status: 'overdue'});
return invoices as unknown as Invoice[];
}

View File

@@ -1,5 +1,5 @@
import {NetWorthSnapshot} from '@prisma/client';
import {NetWorthSnapshotRepository} from '../repositories/NetWorthSnapshotRepository';
import {NetWorthSnapshotRepository, GrowthStats} from '../repositories/NetWorthSnapshotRepository';
import {AssetRepository} from '../repositories/AssetRepository';
import {LiabilityRepository} from '../repositories/LiabilityRepository';
import {NotFoundError, ValidationError, ForbiddenError} from '../utils/errors';
@@ -154,7 +154,7 @@ export class NetWorthService {
/**
* Get growth statistics
*/
async getGrowthStats(userId: string, limit?: number): Promise<any[]> {
async getGrowthStats(userId: string, limit?: number): Promise<GrowthStats[]> {
return this.snapshotRepository.getGrowthStats(userId, limit);
}