"use strict"; /** * ContractService - Servicio de gestión de contratos * * Gestión de contratos con workflow de aprobación. * * @module Contracts */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ContractService = void 0; const typeorm_1 = require("typeorm"); class ContractService { contractRepository; addendumRepository; constructor(contractRepository, addendumRepository) { this.contractRepository = contractRepository; this.addendumRepository = addendumRepository; } generateContractNumber(type) { const now = new Date(); const year = now.getFullYear().toString().slice(-2); const month = (now.getMonth() + 1).toString().padStart(2, '0'); const random = Math.random().toString(36).substring(2, 6).toUpperCase(); const prefix = type === 'client' ? 'CTR' : 'SUB'; return `${prefix}-${year}${month}-${random}`; } generateAddendumNumber(contractNumber, sequence) { return `${contractNumber}-ADD${sequence.toString().padStart(2, '0')}`; } async findWithFilters(ctx, filters = {}, page = 1, limit = 20) { const skip = (page - 1) * limit; const queryBuilder = this.contractRepository .createQueryBuilder('c') .leftJoinAndSelect('c.createdBy', 'createdBy') .leftJoinAndSelect('c.approvedBy', 'approvedBy') .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId }) .andWhere('c.deleted_at IS NULL'); if (filters.projectId) { queryBuilder.andWhere('c.project_id = :projectId', { projectId: filters.projectId }); } if (filters.fraccionamientoId) { queryBuilder.andWhere('c.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId: filters.fraccionamientoId, }); } if (filters.contractType) { queryBuilder.andWhere('c.contract_type = :contractType', { contractType: filters.contractType }); } if (filters.subcontractorId) { queryBuilder.andWhere('c.subcontractor_id = :subcontractorId', { subcontractorId: filters.subcontractorId, }); } if (filters.status) { queryBuilder.andWhere('c.status = :status', { status: filters.status }); } if (filters.expiringInDays) { const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + filters.expiringInDays); queryBuilder.andWhere('c.end_date <= :futureDate', { futureDate }); queryBuilder.andWhere('c.end_date >= :today', { today: new Date() }); queryBuilder.andWhere('c.status = :activeStatus', { activeStatus: 'active' }); } queryBuilder .orderBy('c.created_at', 'DESC') .skip(skip) .take(limit); const [data, total] = await queryBuilder.getManyAndCount(); return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } async findById(ctx, id) { return this.contractRepository.findOne({ where: { id, tenantId: ctx.tenantId, deletedAt: null, }, }); } async findWithDetails(ctx, id) { return this.contractRepository.findOne({ where: { id, tenantId: ctx.tenantId, deletedAt: null, }, relations: ['createdBy', 'approvedBy', 'addendums'], }); } async create(ctx, dto) { const contract = this.contractRepository.create({ tenantId: ctx.tenantId, createdById: ctx.userId, contractNumber: this.generateContractNumber(dto.contractType), projectId: dto.projectId, fraccionamientoId: dto.fraccionamientoId, contractType: dto.contractType, clientContractType: dto.clientContractType, name: dto.name, description: dto.description, clientName: dto.clientName, clientRfc: dto.clientRfc?.toUpperCase(), clientAddress: dto.clientAddress, subcontractorId: dto.subcontractorId, specialty: dto.specialty, startDate: dto.startDate, endDate: dto.endDate, contractAmount: dto.contractAmount, currency: dto.currency || 'MXN', paymentTerms: dto.paymentTerms, retentionPercentage: dto.retentionPercentage || 5, advancePercentage: dto.advancePercentage || 0, notes: dto.notes, status: 'draft', }); return this.contractRepository.save(contract); } async submitForReview(ctx, id) { const contract = await this.findById(ctx, id); if (!contract) { return null; } if (contract.status !== 'draft') { throw new Error('Can only submit draft contracts for review'); } contract.status = 'review'; contract.submittedAt = new Date(); contract.submittedById = ctx.userId || ''; contract.updatedById = ctx.userId || ''; return this.contractRepository.save(contract); } async approveLegal(ctx, id) { const contract = await this.findById(ctx, id); if (!contract) { return null; } if (contract.status !== 'review') { throw new Error('Can only approve contracts in review'); } contract.legalApprovedAt = new Date(); contract.legalApprovedById = ctx.userId || ''; contract.updatedById = ctx.userId || ''; return this.contractRepository.save(contract); } async approve(ctx, id) { const contract = await this.findById(ctx, id); if (!contract) { return null; } if (contract.status !== 'review') { throw new Error('Can only approve contracts in review'); } contract.status = 'approved'; contract.approvedAt = new Date(); contract.approvedById = ctx.userId || ''; contract.updatedById = ctx.userId || ''; return this.contractRepository.save(contract); } async activate(ctx, id, signedDocumentUrl) { const contract = await this.findById(ctx, id); if (!contract) { return null; } if (contract.status !== 'approved') { throw new Error('Can only activate approved contracts'); } contract.status = 'active'; contract.signedAt = new Date(); if (signedDocumentUrl) { contract.signedDocumentUrl = signedDocumentUrl; } contract.updatedById = ctx.userId || ''; return this.contractRepository.save(contract); } async complete(ctx, id) { const contract = await this.findById(ctx, id); if (!contract) { return null; } if (contract.status !== 'active') { throw new Error('Can only complete active contracts'); } contract.status = 'completed'; contract.updatedById = ctx.userId || ''; return this.contractRepository.save(contract); } async terminate(ctx, id, reason) { const contract = await this.findById(ctx, id); if (!contract) { return null; } if (contract.status !== 'active') { throw new Error('Can only terminate active contracts'); } contract.status = 'terminated'; contract.terminatedAt = new Date(); contract.terminationReason = reason; contract.updatedById = ctx.userId || ''; return this.contractRepository.save(contract); } async updateProgress(ctx, id, progressPercentage, invoicedAmount, paidAmount) { const contract = await this.findById(ctx, id); if (!contract) { return null; } contract.progressPercentage = progressPercentage; if (invoicedAmount !== undefined) { contract.invoicedAmount = invoicedAmount; } if (paidAmount !== undefined) { contract.paidAmount = paidAmount; } contract.updatedById = ctx.userId || ''; return this.contractRepository.save(contract); } async createAddendum(ctx, contractId, dto) { const contract = await this.findWithDetails(ctx, contractId); if (!contract) { throw new Error('Contract not found'); } if (contract.status !== 'active') { throw new Error('Can only add addendums to active contracts'); } const sequence = (contract.addendums?.length || 0) + 1; const addendum = this.addendumRepository.create({ tenantId: ctx.tenantId, createdById: ctx.userId, contractId, addendumNumber: this.generateAddendumNumber(contract.contractNumber, sequence), addendumType: dto.addendumType, title: dto.title, description: dto.description, effectiveDate: dto.effectiveDate, newEndDate: dto.newEndDate, amountChange: dto.amountChange || 0, newContractAmount: dto.amountChange ? Number(contract.contractAmount) + Number(dto.amountChange) : undefined, scopeChanges: dto.scopeChanges, notes: dto.notes, status: 'draft', }); return this.addendumRepository.save(addendum); } async approveAddendum(ctx, addendumId) { const addendum = await this.addendumRepository.findOne({ where: { id: addendumId, tenantId: ctx.tenantId, }, relations: ['contract'], }); if (!addendum) { return null; } if (addendum.status !== 'draft' && addendum.status !== 'review') { throw new Error('Can only approve draft or review addendums'); } addendum.status = 'approved'; addendum.approvedAt = new Date(); addendum.approvedById = ctx.userId || ''; addendum.updatedById = ctx.userId || ''; // Apply changes to contract if (addendum.newEndDate) { addendum.contract.endDate = addendum.newEndDate; } if (addendum.newContractAmount) { addendum.contract.contractAmount = addendum.newContractAmount; } await this.contractRepository.save(addendum.contract); return this.addendumRepository.save(addendum); } async softDelete(ctx, id) { const contract = await this.findById(ctx, id); if (!contract) { return false; } if (contract.status === 'active') { throw new Error('Cannot delete active contracts'); } await this.contractRepository.update({ id, tenantId: ctx.tenantId }, { deletedAt: new Date(), deletedById: ctx.userId || '' }); return true; } async getExpiringContracts(ctx, days = 30) { const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + days); return this.contractRepository.find({ where: { tenantId: ctx.tenantId, status: 'active', endDate: (0, typeorm_1.LessThan)(futureDate), deletedAt: null, }, order: { endDate: 'ASC' }, }); } } exports.ContractService = ContractService; //# sourceMappingURL=contract.service.js.map