import { Repository, IsNull, Like } from 'typeorm'; import { AppDataSource } from '../../../config/typeorm.js'; import { Partner, PartnerType } from '../entities/index.js'; import { NotFoundError, ValidationError, ForbiddenError } from '../../../shared/types/index.js'; import { logger } from '../../../shared/utils/logger.js'; // Re-export PartnerType for controller use export type { PartnerType }; // ===== Interfaces ===== export interface CreatePartnerDto { code: string; displayName: string; legalName?: string; partnerType?: PartnerType; email?: string; phone?: string; mobile?: string; website?: string; taxId?: string; taxRegime?: string; cfdiUse?: string; paymentTermDays?: number; creditLimit?: number; priceListId?: string; discountPercent?: number; category?: string; tags?: string[]; notes?: string; salesRepId?: string; } export interface UpdatePartnerDto { displayName?: string; legalName?: string | null; partnerType?: PartnerType; email?: string | null; phone?: string | null; mobile?: string | null; website?: string | null; taxId?: string | null; taxRegime?: string | null; cfdiUse?: string | null; paymentTermDays?: number; creditLimit?: number; priceListId?: string | null; discountPercent?: number; category?: string | null; tags?: string[]; notes?: string | null; isActive?: boolean; isVerified?: boolean; salesRepId?: string | null; } export interface PartnerFilters { search?: string; partnerType?: PartnerType; category?: string; isActive?: boolean; isVerified?: boolean; page?: number; limit?: number; } export interface PartnerWithRelations extends Partner { // Add computed fields if needed } // ===== PartnersService Class ===== class PartnersService { private partnerRepository: Repository; constructor() { this.partnerRepository = AppDataSource.getRepository(Partner); } /** * Get all partners for a tenant with filters and pagination */ async findAll( tenantId: string, filters: PartnerFilters = {} ): Promise<{ data: Partner[]; total: number }> { try { const { search, partnerType, category, isActive, isVerified, page = 1, limit = 20 } = filters; const skip = (page - 1) * limit; const queryBuilder = this.partnerRepository .createQueryBuilder('partner') .where('partner.tenantId = :tenantId', { tenantId }) .andWhere('partner.deletedAt IS NULL'); // Apply search filter if (search) { queryBuilder.andWhere( '(partner.displayName ILIKE :search OR partner.legalName ILIKE :search OR partner.email ILIKE :search OR partner.taxId ILIKE :search OR partner.code ILIKE :search)', { search: `%${search}%` } ); } // Filter by partner type if (partnerType !== undefined) { queryBuilder.andWhere('partner.partnerType = :partnerType', { partnerType }); } // Filter by category if (category) { queryBuilder.andWhere('partner.category = :category', { category }); } // Filter by active status if (isActive !== undefined) { queryBuilder.andWhere('partner.isActive = :isActive', { isActive }); } // Filter by verified status if (isVerified !== undefined) { queryBuilder.andWhere('partner.isVerified = :isVerified', { isVerified }); } // Get total count const total = await queryBuilder.getCount(); // Get paginated results const data = await queryBuilder .orderBy('partner.displayName', 'ASC') .skip(skip) .take(limit) .getMany(); logger.debug('Partners retrieved', { tenantId, count: data.length, total }); return { data, total }; } catch (error) { logger.error('Error retrieving partners', { error: (error as Error).message, tenantId, }); throw error; } } /** * Get partner by ID */ async findById(id: string, tenantId: string): Promise { try { const partner = await this.partnerRepository.findOne({ where: { id, tenantId, deletedAt: IsNull(), }, }); if (!partner) { throw new NotFoundError('Contacto no encontrado'); } return partner; } catch (error) { logger.error('Error finding partner', { error: (error as Error).message, id, tenantId, }); throw error; } } /** * Create a new partner */ async create( dto: CreatePartnerDto, tenantId: string, userId: string ): Promise { try { // Check if code already exists const existing = await this.partnerRepository.findOne({ where: { code: dto.code, tenantId }, }); if (existing) { throw new ValidationError('Ya existe un contacto con este código'); } // Create partner - only include defined fields const partnerData: Partial = { tenantId, code: dto.code, displayName: dto.displayName, partnerType: dto.partnerType || 'customer', paymentTermDays: dto.paymentTermDays ?? 0, creditLimit: dto.creditLimit ?? 0, discountPercent: dto.discountPercent ?? 0, tags: dto.tags || [], isActive: true, isVerified: false, createdBy: userId, }; // Add optional fields only if defined if (dto.legalName) partnerData.legalName = dto.legalName; if (dto.email) partnerData.email = dto.email.toLowerCase(); if (dto.phone) partnerData.phone = dto.phone; if (dto.mobile) partnerData.mobile = dto.mobile; if (dto.website) partnerData.website = dto.website; if (dto.taxId) partnerData.taxId = dto.taxId; if (dto.taxRegime) partnerData.taxRegime = dto.taxRegime; if (dto.cfdiUse) partnerData.cfdiUse = dto.cfdiUse; if (dto.priceListId) partnerData.priceListId = dto.priceListId; if (dto.category) partnerData.category = dto.category; if (dto.notes) partnerData.notes = dto.notes; if (dto.salesRepId) partnerData.salesRepId = dto.salesRepId; const partner = this.partnerRepository.create(partnerData); await this.partnerRepository.save(partner); logger.info('Partner created', { partnerId: partner.id, tenantId, code: partner.code, displayName: partner.displayName, createdBy: userId, }); return partner; } catch (error) { logger.error('Error creating partner', { error: (error as Error).message, tenantId, dto, }); throw error; } } /** * Update a partner */ async update( id: string, dto: UpdatePartnerDto, tenantId: string, userId: string ): Promise { try { const existing = await this.findById(id, tenantId); // Update allowed fields if (dto.displayName !== undefined) existing.displayName = dto.displayName; if (dto.legalName !== undefined) existing.legalName = dto.legalName as string; if (dto.partnerType !== undefined) existing.partnerType = dto.partnerType; if (dto.email !== undefined) existing.email = dto.email?.toLowerCase() || null as any; if (dto.phone !== undefined) existing.phone = dto.phone as string; if (dto.mobile !== undefined) existing.mobile = dto.mobile as string; if (dto.website !== undefined) existing.website = dto.website as string; if (dto.taxId !== undefined) existing.taxId = dto.taxId as string; if (dto.taxRegime !== undefined) existing.taxRegime = dto.taxRegime as string; if (dto.cfdiUse !== undefined) existing.cfdiUse = dto.cfdiUse as string; if (dto.paymentTermDays !== undefined) existing.paymentTermDays = dto.paymentTermDays; if (dto.creditLimit !== undefined) existing.creditLimit = dto.creditLimit; if (dto.priceListId !== undefined) existing.priceListId = dto.priceListId as string; if (dto.discountPercent !== undefined) existing.discountPercent = dto.discountPercent; if (dto.category !== undefined) existing.category = dto.category as string; if (dto.tags !== undefined) existing.tags = dto.tags; if (dto.notes !== undefined) existing.notes = dto.notes as string; if (dto.isActive !== undefined) existing.isActive = dto.isActive; if (dto.isVerified !== undefined) existing.isVerified = dto.isVerified; if (dto.salesRepId !== undefined) existing.salesRepId = dto.salesRepId as string; existing.updatedBy = userId; await this.partnerRepository.save(existing); logger.info('Partner updated', { partnerId: id, tenantId, updatedBy: userId, }); return await this.findById(id, tenantId); } catch (error) { logger.error('Error updating partner', { error: (error as Error).message, id, tenantId, }); throw error; } } /** * Soft delete a partner */ async delete(id: string, tenantId: string, userId: string): Promise { try { const partner = await this.findById(id, tenantId); // Soft delete using the deletedAt column partner.deletedAt = new Date(); partner.isActive = false; await this.partnerRepository.save(partner); logger.info('Partner deleted', { partnerId: id, tenantId, deletedBy: userId, }); } catch (error) { logger.error('Error deleting partner', { error: (error as Error).message, id, tenantId, }); throw error; } } /** * Get customers only */ async findCustomers( tenantId: string, filters: Omit ): Promise<{ data: Partner[]; total: number }> { return this.findAll(tenantId, { ...filters, partnerType: 'customer' }); } /** * Get suppliers only */ async findSuppliers( tenantId: string, filters: Omit ): Promise<{ data: Partner[]; total: number }> { return this.findAll(tenantId, { ...filters, partnerType: 'supplier' }); } } // ===== Export Singleton Instance ===== export const partnersService = new PartnersService();