erp-construccion-backend/dist/modules/contracts/services/contract.service.js

307 lines
11 KiB
JavaScript

"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