# ET-EST-003: Sistema de Anticipos y Retenciones **ID:** ET-EST-003 **Módulo:** MAI-008 **Relacionado con:** RF-EST-003 --- ## 📋 Tablas de Base de Datos ```sql -- Anticipos CREATE TABLE estimations.advances ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), contract_id UUID NOT NULL, tipo VARCHAR(20) NOT NULL, -- 'received', 'granted' monto BIGINT NOT NULL, porcentaje DECIMAL(5,2), fecha_pago DATE NOT NULL, referencia VARCHAR(100), -- Amortización porcentaje_amortizacion_por_estimacion DECIMAL(5,2), saldo_pendiente BIGINT DEFAULT 0, total_amortizado BIGINT DEFAULT 0, -- Garantías (si aplica) garantia_id UUID, status VARCHAR(20) DEFAULT 'active' ); -- Retenciones acumuladas CREATE TABLE estimations.retentions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), contract_id UUID NOT NULL, tipo VARCHAR(30) NOT NULL, -- 'fondo_garantia', 'fianza', 'isr', 'iva' porcentaje DECIMAL(5,2) NOT NULL, monto_acumulado BIGINT DEFAULT 0, monto_liberado BIGINT DEFAULT 0, saldo_pendiente BIGINT DEFAULT 0, fecha_liberacion_estimada DATE, condiciones_liberacion TEXT, status VARCHAR(20) DEFAULT 'active' ); ``` --- ## 🔧 Backend Services ### advance.service.ts ```typescript @Injectable() export class AdvanceService { async createAdvance(dto: CreateAdvanceDto): Promise { const advance = this.advancesRepo.create({ ...dto, saldoPendiente: dto.monto, totalAmortizado: 0 }); return this.advancesRepo.save(advance); } async amortizeAdvance( advanceId: string, estimacionMonto: number, estimacionNumero: number ): Promise { const advance = await this.advancesRepo.findOne(advanceId); if (advance.saldoPendiente <= 0) return 0; const amortizacion = Math.min( advance.saldoPendiente, estimacionMonto * (advance.porcentajeAmortizacionPorEstimacion / 100) ); advance.totalAmortizado += amortizacion; advance.saldoPendiente -= amortizacion; await this.advancesRepo.save(advance); return amortizacion; } async getAdvanceStatus(contractId: string): Promise { const advance = await this.advancesRepo.findOne({ where: { contractId, status: 'active' } }); if (!advance) return null; return { montoInicial: advance.monto, totalAmortizado: advance.totalAmortizado, saldoPendiente: advance.saldoPendiente, porcentajeAmortizado: (advance.totalAmortizado / advance.monto) * 100 }; } } ``` ### retention.service.ts ```typescript @Injectable() export class RetentionService { async acumularRetencion( contractId: string, tipo: string, monto: number ): Promise { const retention = await this.retentionsRepo.findOne({ where: { contractId, tipo } }); if (retention) { retention.montoAcumulado += monto; retention.saldoPendiente += monto; await this.retentionsRepo.save(retention); } } async liberarRetencion( retentionId: string, montoLiberar: number, razon: string ): Promise { const retention = await this.retentionsRepo.findOne(retentionId); if (retention.saldoPendiente < montoLiberar) { throw new BadRequestException('Monto a liberar excede saldo pendiente'); } retention.montoLiberado += montoLiberar; retention.saldoPendiente -= montoLiberar; await this.retentionsRepo.save(retention); // Audit log await this.auditService.log({ action: 'retention_released', entityId: retentionId, amount: montoLiberar, reason: razon }); } async getRetencionesAcumuladas(contractId: string): Promise { return this.retentionsRepo.find({ where: { contractId }, order: { tipo: 'ASC' } }); } } ``` --- **Generado:** 2025-11-20 **Estado:** ✅ Completo