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