workspace-v1/projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/especificaciones/ET-EST-003-anticipos-retenciones.md
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
Sistema NEXUS v3.4 migrado con:

Estructura principal:
- core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles)
- core/catalog: Catalogo de funcionalidades reutilizables
- shared/knowledge-base: Base de conocimiento compartida
- devtools/scripts: Herramientas de desarrollo
- control-plane/registries: Control de servicios y CI/CD
- orchestration/: Configuracion de orquestacion de agentes

Proyectos incluidos (11):
- gamilit (submodule -> GitHub)
- trading-platform (OrbiquanTIA)
- erp-suite con 5 verticales:
  - erp-core, construccion, vidrio-templado
  - mecanicas-diesel, retail, clinicas
- betting-analytics
- inmobiliaria-analytics
- platform_marketing_content
- pos-micro, erp-basico

Configuracion:
- .gitignore completo para Node.js/Python/Docker
- gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git)
- Sistema de puertos estandarizado (3005-3199)

Generated with NEXUS v3.4 Migration System
EPIC-010: Configuracion Git y Repositorios
2026-01-04 03:37:42 -06:00

3.8 KiB

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

-- 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

@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

@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