Files
personal-finance/backend-api/src/services/ClientService.ts
Alexander Zinn df2cf418ea 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.
2025-12-11 02:19:05 -05:00

149 lines
3.9 KiB
TypeScript

import {Client} from '@prisma/client';
import {ClientRepository, ClientWithStats} from '../repositories/ClientRepository';
import {NotFoundError, ValidationError, ForbiddenError, ConflictError} from '../utils/errors';
export interface CreateClientDTO {
name: string;
email: string;
phone?: string;
address?: string;
notes?: string;
}
export interface UpdateClientDTO {
name?: string;
email?: string;
phone?: string;
address?: string;
notes?: string;
}
/**
* Service for Client business logic
* Implements Single Responsibility Principle - handles only business logic
* Implements Dependency Inversion - depends on repository abstraction
*/
export class ClientService {
constructor(private clientRepository: ClientRepository) {}
/**
* Create a new client
*/
async create(userId: string, data: CreateClientDTO): Promise<Client> {
this.validateClientData(data);
// Check for duplicate email
const existing = await this.clientRepository.findByEmail(userId, data.email);
if (existing) {
throw new ConflictError('A client with this email already exists');
}
return this.clientRepository.create({
name: data.name,
email: data.email,
phone: data.phone,
address: data.address,
notes: data.notes,
user: {
connect: {id: userId}
}
});
}
/**
* Get all clients for a user
*/
async getAllByUser(userId: string): Promise<Client[]> {
return this.clientRepository.findAllByUser(userId);
}
/**
* Get clients with statistics
*/
async getWithStats(userId: string): Promise<ClientWithStats[]> {
return this.clientRepository.getWithStats(userId);
}
/**
* Get a single client by ID
*/
async getById(id: string, userId: string): Promise<Client> {
const client = await this.clientRepository.findById(id);
if (!client) {
throw new NotFoundError('Client not found');
}
// Ensure user owns this client
if (client.userId !== userId) {
throw new ForbiddenError('Access denied');
}
return client;
}
/**
* Update a client
*/
async update(id: string, userId: string, data: UpdateClientDTO): Promise<Client> {
// Verify ownership
await this.getById(id, userId);
if (data.email) {
this.validateClientData(data as CreateClientDTO);
// Check for duplicate email (excluding current client)
const existing = await this.clientRepository.findByEmail(userId, data.email);
if (existing && existing.id !== id) {
throw new ConflictError('A client with this email already exists');
}
}
return this.clientRepository.update(id, data);
}
/**
* Delete a client
*/
async delete(id: string, userId: string): Promise<void> {
// Verify ownership
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
// Uncomment below to prevent deletion:
// if (client.invoices && client.invoices.length > 0) {
// throw new ValidationError('Cannot delete client with existing invoices');
// }
await this.clientRepository.delete(id);
}
/**
* Get total revenue from all clients
*/
async getTotalRevenue(userId: string): Promise<number> {
return this.clientRepository.getTotalRevenue(userId);
}
/**
* Validate client data
*/
private validateClientData(data: CreateClientDTO | UpdateClientDTO): void {
if (data.email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
throw new ValidationError('Invalid email format');
}
}
if (data.phone) {
// Basic phone validation - at least 10 digits
const phoneDigits = data.phone.replace(/\D/g, '');
if (phoneDigits.length < 10) {
throw new ValidationError('Phone number must contain at least 10 digits');
}
}
}
}