erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/especificaciones/ET-EST-003-anticipos-retenciones.md

167 lines
3.8 KiB
Markdown

# 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<Advance> {
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<number> {
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<AdvanceStatus> {
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<void> {
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<void> {
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<RetentionSummary[]> {
return this.retentionsRepo.find({
where: { contractId },
order: { tipo: 'ASC' }
});
}
}
```
---
**Generado:** 2025-11-20
**Estado:** ✅ Completo