/** * SubcontractorService - Servicio de gestión de subcontratistas * * Catálogo de subcontratistas con evaluaciones. * * @module Contracts */ import { Repository, FindOptionsWhere } from 'typeorm'; import { Subcontractor, SubcontractorStatus, SubcontractorSpecialty } from '../entities/subcontractor.entity'; import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; export interface CreateSubcontractorDto { businessName: string; tradeName?: string; rfc: string; address?: string; phone?: string; email?: string; contactName?: string; contactPhone?: string; primarySpecialty: SubcontractorSpecialty; secondarySpecialties?: string[]; bankName?: string; bankAccount?: string; clabe?: string; notes?: string; } export interface UpdateSubcontractorDto { tradeName?: string; address?: string; phone?: string; email?: string; contactName?: string; contactPhone?: string; secondarySpecialties?: string[]; bankName?: string; bankAccount?: string; clabe?: string; notes?: string; } export interface SubcontractorFilters { specialty?: SubcontractorSpecialty; status?: SubcontractorStatus; search?: string; minRating?: number; } export class SubcontractorService { constructor(private readonly subcontractorRepository: Repository) {} private generateCode(): string { const now = new Date(); const year = now.getFullYear().toString().slice(-2); const random = Math.random().toString(36).substring(2, 6).toUpperCase(); return `SC-${year}-${random}`; } async findWithFilters( ctx: ServiceContext, filters: SubcontractorFilters = {}, page: number = 1, limit: number = 20 ): Promise> { const skip = (page - 1) * limit; const queryBuilder = this.subcontractorRepository .createQueryBuilder('sc') .leftJoinAndSelect('sc.createdBy', 'createdBy') .where('sc.tenant_id = :tenantId', { tenantId: ctx.tenantId }) .andWhere('sc.deleted_at IS NULL'); if (filters.specialty) { queryBuilder.andWhere('sc.primary_specialty = :specialty', { specialty: filters.specialty }); } if (filters.status) { queryBuilder.andWhere('sc.status = :status', { status: filters.status }); } if (filters.search) { queryBuilder.andWhere( '(sc.business_name ILIKE :search OR sc.trade_name ILIKE :search OR sc.rfc ILIKE :search)', { search: `%${filters.search}%` } ); } if (filters.minRating !== undefined) { queryBuilder.andWhere('sc.average_rating >= :minRating', { minRating: filters.minRating }); } queryBuilder .orderBy('sc.business_name', 'ASC') .skip(skip) .take(limit); const [data, total] = await queryBuilder.getManyAndCount(); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } async findById(ctx: ServiceContext, id: string): Promise { return this.subcontractorRepository.findOne({ where: { id, tenantId: ctx.tenantId, deletedAt: null, } as unknown as FindOptionsWhere, }); } async findByRfc(ctx: ServiceContext, rfc: string): Promise { return this.subcontractorRepository.findOne({ where: { rfc: rfc.toUpperCase(), tenantId: ctx.tenantId, deletedAt: null, } as unknown as FindOptionsWhere, }); } async create(ctx: ServiceContext, dto: CreateSubcontractorDto): Promise { // Check for existing RFC const existing = await this.findByRfc(ctx, dto.rfc); if (existing) { throw new Error('A subcontractor with this RFC already exists'); } const subcontractor = this.subcontractorRepository.create({ tenantId: ctx.tenantId, createdById: ctx.userId, code: this.generateCode(), businessName: dto.businessName, tradeName: dto.tradeName, rfc: dto.rfc.toUpperCase(), address: dto.address, phone: dto.phone, email: dto.email, contactName: dto.contactName, contactPhone: dto.contactPhone, primarySpecialty: dto.primarySpecialty, secondarySpecialties: dto.secondarySpecialties, bankName: dto.bankName, bankAccount: dto.bankAccount, clabe: dto.clabe, notes: dto.notes, status: 'active', }); return this.subcontractorRepository.save(subcontractor); } async update(ctx: ServiceContext, id: string, dto: UpdateSubcontractorDto): Promise { const subcontractor = await this.findById(ctx, id); if (!subcontractor) { return null; } Object.assign(subcontractor, { ...dto, updatedById: ctx.userId || '', }); return this.subcontractorRepository.save(subcontractor); } async updateRating(ctx: ServiceContext, id: string, rating: number): Promise { const subcontractor = await this.findById(ctx, id); if (!subcontractor) { return null; } // Calculate new average rating const totalRatings = subcontractor.completedContracts; const currentTotal = subcontractor.averageRating * totalRatings; const newTotal = currentTotal + rating; subcontractor.averageRating = newTotal / (totalRatings + 1); subcontractor.updatedById = ctx.userId || ''; return this.subcontractorRepository.save(subcontractor); } async incrementContracts(ctx: ServiceContext, id: string, completed: boolean = false): Promise { const subcontractor = await this.findById(ctx, id); if (!subcontractor) { return null; } subcontractor.totalContracts += 1; if (completed) { subcontractor.completedContracts += 1; } subcontractor.updatedById = ctx.userId || ''; return this.subcontractorRepository.save(subcontractor); } async incrementIncidents(ctx: ServiceContext, id: string): Promise { const subcontractor = await this.findById(ctx, id); if (!subcontractor) { return null; } subcontractor.totalIncidents += 1; subcontractor.updatedById = ctx.userId || ''; return this.subcontractorRepository.save(subcontractor); } async deactivate(ctx: ServiceContext, id: string): Promise { const subcontractor = await this.findById(ctx, id); if (!subcontractor) { return null; } subcontractor.status = 'inactive'; subcontractor.updatedById = ctx.userId || ''; return this.subcontractorRepository.save(subcontractor); } async blacklist(ctx: ServiceContext, id: string, reason: string): Promise { const subcontractor = await this.findById(ctx, id); if (!subcontractor) { return null; } subcontractor.status = 'blacklisted'; subcontractor.notes = `${subcontractor.notes || ''}\n[BLACKLISTED] ${reason}`; subcontractor.updatedById = ctx.userId || ''; return this.subcontractorRepository.save(subcontractor); } async softDelete(ctx: ServiceContext, id: string): Promise { const subcontractor = await this.findById(ctx, id); if (!subcontractor) { return false; } await this.subcontractorRepository.update( { id, tenantId: ctx.tenantId } as FindOptionsWhere, { deletedAt: new Date(), deletedById: ctx.userId || '' } ); return true; } async getBySpecialty(ctx: ServiceContext, specialty: SubcontractorSpecialty): Promise { return this.subcontractorRepository.find({ where: { tenantId: ctx.tenantId, primarySpecialty: specialty, status: 'active' as SubcontractorStatus, deletedAt: null, } as unknown as FindOptionsWhere, order: { averageRating: 'DESC' }, }); } }