307 lines
11 KiB
JavaScript
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
|