# NOVASIS ERP

## MÓDULO DE COBRANZAS

## ADICIÓN: MÓDULO DE RENDICIONES DE COBROS

#### Facturas Crédito y Contado — Especificación Funcional, Técnica y Reportes de Control

##### Para: Equipo de Desarrollo

##### Responsable: Marcelo Palumbo — mpalumbopy@gmail.com

##### Versión: v1.0 — Mayo 2026

##### Módulos nuevos: M30–M35 (Cobranzas)

##### Módulos afectados: Ventas · CxC · Tesorería · Contabilidad · RRHH

## 1. Alcance y Contexto del Módulo

#### Este documento especifica la adición del Módulo de Rendiciones de Cobros al sistema Novasis ERP.

#### Cubre el ciclo completo: asignación de cartera de facturas a un cobrador, registro de cobros en

#### campo, rendición formal ante Tesorería, conciliación de valores e impacto automático en Cuentas por

#### Cobrar, Tesorería y Contabilidad.

#### Aplica a facturas al crédito (cobro diferido) y facturas al contado cuando el cobrador recibe el dinero

#### en nombre de la empresa.

### 1.1 Objetivos

- Gestionar carteras de cobro asignadas por cobrador y período.
- Permitir registro de cobros parciales o totales por factura.
- Generar la rendición formal con medios de pago (efectivo, cheque, transferencia, QR/billetera).
- Conciliar importes declarados vs. verificados por Tesorería.
- Detectar y alertar diferencias (control anti-fraude).
- Impactar automáticamente CxC, Tesorería y Contabilidad al aprobar.
- Proveer reportes de control: cartera, cobranzas del día, antigüedad de deuda, diferencias.
- Soporte multi-empresa, multi-moneda (PYG / USD) y validación CDC SIFEN.

### 1.2 Definiciones Clave

##### Término Definición

##### Rendición Documento formal donde el cobrador declara los cobros realizados en un

##### período ante Tesorería.

##### Cobrador Empleado o agente externo que visita clientes y recauda pagos.

##### Cartera de cobro Conjunto de facturas asignadas a un cobrador para gestionar su cobro.

##### Factura crédito Factura con plazo de pago diferido; puede tener cuotas pendientes.

##### Factura contado Cobrada en el acto; entra al módulo si el cobrador recibe el dinero por la

##### empresa.

##### Conciliación Verificación por Tesorería de que el dinero físico entregado coincide con lo

##### declarado.

### 1.3 Relación con Módulos Existentes

##### Módulo Código Interacción

##### Ventas / Facturación MOD-VENTAS Fuente de facturas; se actualiza el saldo al conciliar.

##### Cuentas por Cobrar MOD-CXC Fuente de la cartera. Se aplican pagos sobre saldos

##### pendientes.

##### Tesorería / Caja MOD-TES Recibe fondos conciliados; registra ingreso en

##### caja/banco.

##### Contabilidad MOD-CONT Asiento automático al aprobar: Db Caja/Banco, Cr

##### CxC.

##### RRHH / Empleados MOD-RRHH Catastro de cobradores internos (lectura).

##### SIFEN SET-PY Validación de CDC del comprobante electrónico al

##### cargar cobros.

## 2. Flujo de Proceso General

##### # Etapa Descripción

##### 1 Asignación de Cartera El supervisor selecciona facturas crédito/contado pendientes y las

##### asigna a un cobrador con fecha límite de gestión.

##### 2 Cobro en Campo El cobrador visita al cliente y registra el cobro (parcial o total)

##### indicando el medio de pago y adjuntando comprobante.

##### 3 Apertura de Rendición El cobrador crea la rendición del período, agrega los cobros realizados

##### y declara el total por medio de pago.

##### 4 Envío a Tesorería Cierra y envía la rendición. Estado → PENDIENTE. Bloqueada para

##### edición.

##### 5 Verificación Tesorería recibe el efectivo/cheques/comprobantes y confirma o ajusta

##### los importes declarados.

##### 6 Aprobación /

##### Observación

##### Sin diferencias → APROBADO. Con diferencias → OBSERVADO (el

##### cobrador regulariza). Fraude/error grave → RECHAZADO.

##### 7 Impacto Contable Al aprobar: actualización de CxC, registro en Tesorería y asiento

##### contable automático.

### 2.1 Estados de la Rendición

##### Estado Transiciones Descripción

##### BORRADOR → PENDIENTE,

##### ANULADO

##### El cobrador carga datos. Solo él puede editar.

##### PENDIENTE → APROBADO,

##### OBSERVADO,

##### RECHAZADO

##### Enviada a Tesorería. Bloqueada.

##### OBSERVADO → PENDIENTE

##### (reenvío)

##### Tesorería encontró diferencias. Cobrador regulariza.

##### APROBADO (estado final) Conciliada y contabilizada.

##### RECHAZADO (estado final) Anulada por error grave o incumplimiento.

##### ANULADO (estado final) Cobrador anuló antes de enviar.

## 3. Modelo de Base de Datos — PostgreSQL

#### Todas las tablas siguen las convenciones de Novasis ERP: UUID como PK (gen_random_uuid()),

#### soft delete con deleted_at, timestamps automáticos, y empresa_id para multi-empresa.

### 3.1 Tabla: cobro_carteras

```
CREATE TABLE cobro_carteras (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
empresa_id UUID NOT NULL REFERENCES empresas(id),
cobrador_id UUID NOT NULL,
cobrador_tipo VARCHAR(10) NOT NULL CHECK (cobrador_tipo IN
('EMPLEADO','TERCERO')),
periodo_desde DATE NOT NULL,
periodo_hasta DATE NOT NULL,
descripcion TEXT,
estado VARCHAR(20) NOT NULL DEFAULT 'ACTIVA'
CHECK (estado IN ('ACTIVA','CERRADA','ANULADA')),
creado_por UUID NOT NULL REFERENCES usuarios(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT chk_periodo CHECK (periodo_hasta >= periodo_desde)
);
CREATE INDEX idx_carteras_cobrador ON cobro_carteras(empresa_id, cobrador_id,
deleted_at);
CREATE INDEX idx_carteras_periodo ON cobro_carteras(empresa_id, periodo_desde) WHERE
deleted_at IS NULL;
```

### 3.2 Tabla: cobro_cartera_items

```
CREATE TABLE cobro_cartera_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cartera_id UUID NOT NULL REFERENCES cobro_carteras(id),
empresa_id UUID NOT NULL REFERENCES empresas(id),
factura_id UUID NOT NULL,
tipo_documento VARCHAR(20) NOT NULL CHECK (tipo_documento IN
('CREDITO','CONTADO')),
cliente_id UUID NOT NULL,
moneda CHAR(3) NOT NULL DEFAULT 'PYG',
monto_original NUMERIC(18,2) NOT NULL,
monto_saldo NUMERIC(18,2) NOT NULL,
monto_cobrado NUMERIC(18,2) NOT NULL DEFAULT 0,
fecha_vencimiento DATE,
estado VARCHAR(20) NOT NULL DEFAULT 'PENDIENTE'
CHECK (estado IN
('PENDIENTE','COBRADO_PARCIAL','COBRADO_TOTAL','DEVUELTO')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_cartera_items_factura ON cobro_cartera_items(factura_id) WHERE
deleted_at IS NULL;
CREATE INDEX idx_cartera_items_cliente ON cobro_cartera_items(empresa_id,
cliente_id, estado) WHERE deleted_at IS NULL;
```

### 3.3 Tabla: cobro_rendiciones (cabecera)

```
CREATE TABLE cobro_rendiciones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
empresa_id UUID NOT NULL REFERENCES empresas(id),
numero SERIAL,
codigo VARCHAR(20) GENERATED ALWAYS AS ('REN-' ||
LPAD(numero::TEXT,6,'0')) STORED,
cobrador_id UUID NOT NULL,
```

```
cobrador_tipo VARCHAR(10) NOT NULL CHECK (cobrador_tipo IN
('EMPLEADO','TERCERO')),
fecha_desde DATE NOT NULL,
fecha_hasta DATE NOT NULL,
fecha_rendicion DATE NOT NULL DEFAULT CURRENT_DATE,
estado VARCHAR(20) NOT NULL DEFAULT 'BORRADOR'
CHECK (estado IN
('BORRADOR','PENDIENTE','OBSERVADO','APROBADO','RECHAZADO','ANULADO')),
total_declarado NUMERIC(18,2) NOT NULL DEFAULT 0,
total_verificado NUMERIC(18,2),
diferencia NUMERIC(18,2) GENERATED ALWAYS AS
(COALESCE(total_verificado,0) - total_declarado) STORED,
observacion_cobrador TEXT,
observacion_tesoreria TEXT,
aprobado_por UUID REFERENCES usuarios(id),
fecha_aprobacion TIMESTAMPTZ,
asiento_id UUID,
caja_ingreso_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX idx_rendicion_codigo ON cobro_rendiciones(empresa_id, numero)
WHERE deleted_at IS NULL;
CREATE INDEX idx_rendicion_cobrador ON cobro_rendiciones(empresa_id,
cobrador_id, estado) WHERE deleted_at IS NULL;
CREATE INDEX idx_rendicion_fecha ON cobro_rendiciones(empresa_id,
fecha_rendicion DESC) WHERE deleted_at IS NULL;
```

### 3.4 Tabla: cobro_rendicion_items

```
CREATE TABLE cobro_rendicion_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rendicion_id UUID NOT NULL REFERENCES cobro_rendiciones(id),
empresa_id UUID NOT NULL REFERENCES empresas(id),
factura_id UUID NOT NULL,
tipo_documento VARCHAR(20) NOT NULL CHECK (tipo_documento IN
('CREDITO','CONTADO')),
nro_factura VARCHAR(30),
cdc_sifen VARCHAR(44),
cliente_id UUID NOT NULL,
cliente_ruc VARCHAR(20),
cliente_nombre VARCHAR(200),
fecha_factura DATE NOT NULL,
fecha_vencimiento DATE,
moneda CHAR(3) NOT NULL DEFAULT 'PYG',
tipo_cambio NUMERIC(12,4) NOT NULL DEFAULT 1,
monto_factura NUMERIC(18,2) NOT NULL,
saldo_anterior NUMERIC(18,2) NOT NULL,
monto_cobrado NUMERIC(18,2) NOT NULL,
saldo_posterior NUMERIC(18,2) GENERATED ALWAYS AS (saldo_anterior - monto_cobrado)
STORED,
cartera_item_id UUID REFERENCES cobro_cartera_items(id),
es_cobro_campo BOOLEAN NOT NULL DEFAULT FALSE,
observacion TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_cobrado_positivo CHECK (monto_cobrado > 0),
CONSTRAINT chk_cobrado_saldo CHECK (monto_cobrado <= saldo_anterior)
);
CREATE INDEX idx_rend_items_rendicion ON cobro_rendicion_items(rendicion_id);
CREATE INDEX idx_rend_items_factura ON cobro_rendicion_items(factura_id);
```

### 3.5 Tabla: cobro_rendicion_pagos

```
CREATE TABLE cobro_rendicion_pagos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rendicion_id UUID NOT NULL REFERENCES cobro_rendiciones(id),
```

```
medio_pago VARCHAR(30) NOT NULL
CHECK (medio_pago IN
('EFECTIVO','CHEQUE','TRANSFERENCIA','QR_BILLETERA','OTRO')),
banco_id UUID,
numero_cheque VARCHAR(50),
fecha_cheque DATE,
referencia VARCHAR(100),
moneda CHAR(3) NOT NULL DEFAULT 'PYG',
tipo_cambio NUMERIC(12,4) NOT NULL DEFAULT 1,
importe_moneda NUMERIC(18,2) NOT NULL,
importe_pyg NUMERIC(18,2) NOT NULL,
importe_verificado NUMERIC(18,2),
diferencia NUMERIC(18,2) GENERATED ALWAYS AS
(COALESCE(importe_verificado,0) - importe_moneda) STORED,
observacion TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_rend_pagos ON cobro_rendicion_pagos(rendicion_id);
```

### 3.6 Tabla: cobro_rendicion_historial

```
CREATE TABLE cobro_rendicion_historial (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rendicion_id UUID NOT NULL REFERENCES cobro_rendiciones(id),
estado_anterior VARCHAR(20),
estado_nuevo VARCHAR(20) NOT NULL,
usuario_id UUID NOT NULL REFERENCES usuarios(id),
motivo TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_rend_historial ON cobro_rendicion_historial(rendicion_id, created_at
DESC);
```

### 3.7 Vistas de Control

```
-- Cartera por cobrador con saldos
CREATE OR REPLACE VIEW vw_cartera_cobrador AS
SELECT cc.empresa_id, cc.cobrador_id, cc.periodo_desde, cc.periodo_hasta,
COUNT(ci.id) AS
total_facturas,
SUM(ci.monto_saldo) AS
total_asignado,
SUM(ci.monto_cobrado) AS total_cobrado,
SUM(ci.monto_saldo - ci.monto_cobrado) AS
saldo_pendiente,
ROUND(SUM(ci.monto_cobrado)/NULLIF(SUM(ci.monto_saldo),0)*100, 2) AS pct_cobrado
FROM cobro_carteras cc
JOIN cobro_cartera_items ci ON ci.cartera_id = cc.id AND ci.deleted_at IS NULL
WHERE cc.deleted_at IS NULL
GROUP BY cc.id, cc.empresa_id, cc.cobrador_id, cc.periodo_desde, cc.periodo_hasta;
```

```
-- Rendiciones con diferencias (control Tesorería)
CREATE OR REPLACE VIEW vw_rendiciones_diferencias AS
SELECT empresa_id, codigo, cobrador_id, fecha_rendicion, estado,
total_declarado, total_verificado, diferencia,
ABS(diferencia) AS diferencia_abs,
CASE WHEN ABS(diferencia) = 0 THEN 'SIN_DIFERENCIA'
WHEN ABS(diferencia) <= 1000 THEN 'DIFERENCIA_MENOR'
ELSE 'DIFERENCIA_MAYOR' END AS nivel
FROM cobro_rendiciones
WHERE deleted_at IS NULL AND estado IN ('APROBADO','OBSERVADO')
ORDER BY empresa_id, fecha_rendicion DESC;
```

```
-- Antigüedad de deuda por cliente (tramos estándar Paraguay)
CREATE OR REPLACE VIEW vw_antiguedad_deuda AS
SELECT empresa_id, cliente_id,
```

SUM(CASE WHEN CURRENT_DATE - fecha_vencimiento <= 0 THEN monto_saldo -
monto_cobrado ELSE 0 END) AS corriente,
SUM(CASE WHEN CURRENT_DATE - fecha_vencimiento BETWEEN 1 AND 30 THEN monto_saldo -
monto_cobrado ELSE 0 END) AS venc_30,
SUM(CASE WHEN CURRENT_DATE - fecha_vencimiento BETWEEN 31 AND 60 THEN monto_saldo -
monto_cobrado ELSE 0 END) AS venc_60,
SUM(CASE WHEN CURRENT_DATE - fecha_vencimiento BETWEEN 61 AND 90 THEN monto_saldo -
monto_cobrado ELSE 0 END) AS venc_90,
SUM(CASE WHEN CURRENT_DATE - fecha_vencimiento > 90 THEN monto_saldo -
monto_cobrado ELSE 0 END) AS venc_mas
FROM cobro_cartera_items
WHERE deleted_at IS NULL AND estado != 'COBRADO_TOTAL'
GROUP BY empresa_id, cliente_id;

## 4. Tipos TypeScript

#### Crear en: src/types/cobranzas.ts

```
export type EstadoCartera = 'ACTIVA' | 'CERRADA' | 'ANULADA';
export type EstadoItemCartera= 'PENDIENTE' | 'COBRADO_PARCIAL' | 'COBRADO_TOTAL' |
'DEVUELTO';
export type EstadoRendicion = 'BORRADOR' | 'PENDIENTE' | 'OBSERVADO' | 'APROBADO' |
'RECHAZADO' | 'ANULADO';
export type TipoDocumento = 'CREDITO' | 'CONTADO';
export type MedioPago = 'EFECTIVO' | 'CHEQUE' | 'TRANSFERENCIA' |
'QR_BILLETERA' | 'OTRO';
export type CobradorTipo = 'EMPLEADO' | 'TERCERO';
```

```
export interface CobroCartera {
id: string; empresaId: string; cobradorId: string; cobradorTipo: CobradorTipo;
periodoDesde: string; periodoHasta: string; descripcion?: string;
estado: EstadoCartera; creadoPor: string; createdAt: string; updatedAt: string;
}
```

```
export interface CobroCarteraItem {
id: string; carteraId: string; empresaId: string; facturaId: string;
tipoDocumento: TipoDocumento; clienteId: string; moneda: string;
montoOriginal: number; montoSaldo: number; montoCobrado: number;
fechaVencimiento?: string; estado: EstadoItemCartera;
}
```

```
export interface CobroRendicion {
id: string; empresaId: string; numero: number; codigo: string;
cobradorId: string; cobradorTipo: CobradorTipo;
fechaDesde: string; fechaHasta: string; fechaRendicion: string;
estado: EstadoRendicion;
totalDeclarado: number; totalVerificado?: number; diferencia?: number;
observacionCobrador?: string; observacionTesoreria?: string;
aprobadoPor?: string; fechaAprobacion?: string;
asientoId?: string; cajaIngresoId?: string;
items: CobroRendicionItem[]; pagos: CobroRendicionPago[];
createdAt: string; updatedAt: string;
}
```

```
export interface CobroRendicionItem {
id: string; rendicionId: string; facturaId: string;
tipoDocumento: TipoDocumento; nroFactura?: string; cdcSifen?: string;
clienteId: string; clienteRuc?: string; clienteNombre?: string;
fechaFactura: string; fechaVencimiento?: string;
moneda: string; tipoCambio: number;
montoFactura: number; saldoAnterior: number;
montoCobrado: number; saldoPosterior: number;
carteraItemId?: string; esCobrador: boolean; observacion?: string;
}
```

```
export interface CobroRendicionPago {
id: string; rendicionId: string; medioPago: MedioPago;
bancoId?: string; numeroCheque?: string; fechaCheque?: string;
referencia?: string; moneda: string; tipoCambio: number;
importeMoneda: number; importePyg: number;
importeVerificado?: number; diferencia?: number; observacion?: string;
}
```

```
// ── DTOs ──────────────────────────────────────────────────────────
export interface CrearRendicionDTO {
cobradorId: string; cobradorTipo: CobradorTipo;
fechaDesde: string; fechaHasta: string;
observacionCobrador?: string;
items: { facturaId: string; tipoDocumento: TipoDocumento;
montoCobrado: number; carteraItemId?: string; observacion?: string }[];
pagos: { medioPago: MedioPago; bancoId?: string; numeroCheque?: string;
fechaCheque?: string; referencia?: string;
moneda: string; tipoCambio: number; importeMoneda: number }[];
}
```

export interface VerificarRendicionDTO {
observacionTesoreria?: string;
pagosVerificados: { pagoId: string; importeVerificado: number; observacion?: string
}[];
}

## 5. API Endpoints — Next.js App Router

#### Todas las rutas bajo src/app/api/cobranzas/. Requieren JWT Bearer. El empresaId se extrae del

#### token de sesión.

##### Ruta Método Descripción

##### /api/cobranzas/carteras GET Listar carteras. Filtros: cobrador_id, estado,

##### periodo.

##### /api/cobranzas/carteras POST Crear cartera y asignar facturas.

##### /api/cobranzas/carteras/[id] GET Detalle de cartera con items y estado de cobro.

##### /api/cobranzas/carteras/[id]/cerrar POST Cerrar cartera; devuelve facturas sin cobrar.

##### /api/cobranzas/rendiciones GET Listar rendiciones. Filtros: estado, cobrador,

##### fechas.

##### /api/cobranzas/rendiciones POST Crear rendición en BORRADOR.

##### /api/cobranzas/rendiciones/[id] GET Detalle con items y pagos.

##### /api/cobranzas/rendiciones/[id] PUT Editar rendición (solo BORRADOR).

##### /api/cobranzas/rendiciones/[id]/enviar POST Enviar a Tesorería (BORRADOR →

##### PENDIENTE).

##### /api/cobranzas/rendiciones/[id]/aprobar POST Aprobar con importes verificados. Rol:

##### TESORERO.

##### /api/cobranzas/rendiciones/[id]/observar POST Observar con motivo. Rol: TESORERO.

##### /api/cobranzas/rendiciones/[id]/rechazar POST Rechazar. Rol: TESORERO / SUPER_ADMIN.

##### /api/cobranzas/reportes/cartera GET Reporte de cartera por cobrador.

##### /api/cobranzas/reportes/cobranzas-dia GET Cobranzas y rendiciones del día.

##### /api/cobranzas/reportes/antiguedad-

##### deuda

##### GET Antigüedad de deuda por cliente (tramos).

##### /api/cobranzas/reportes/diferencias GET Rendiciones con diferencias declarado vs.

##### verificado.

### 5.1 Ejemplo: POST /api/cobranzas/rendiciones

```
// src/app/api/cobranzas/rendiciones/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { cobranzasServicio } from '@/lib/servicios/cobranzasServicio';
import { getSessionOrThrow } from '@/lib/auth/session';
```

```
const itemSchema = z.object({
facturaId: z.string().uuid(),
tipoDocumento: z.enum(['CREDITO', 'CONTADO']),
montoCobrado: z.number().positive(),
carteraItemId: z.string().uuid().optional(),
observacion: z.string().optional(),
});
const pagoSchema = z.object({
medioPago: z.enum(['EFECTIVO','CHEQUE','TRANSFERENCIA','QR_BILLETERA','OTRO']),
bancoId: z.string().uuid().optional(),
numeroCheque: z.string().optional(),
fechaCheque: z.string().optional(),
referencia: z.string().optional(),
moneda: z.string().length(3),
tipoCambio: z.number().positive(),
importeMoneda: z.number().positive(),
```

###### });

const bodySchema = z.object({
cobradorId: z.string().uuid(),
cobradorTipo: z.enum(['EMPLEADO','TERCERO']),
fechaDesde: z.string(),
fechaHasta: z.string(),
observacionCobrador: z.string().optional(),
items: z.array(itemSchema).min(1),
pagos: z.array(pagoSchema).min(1),
});

export async function POST(req: NextRequest) {
try {
const session = await getSessionOrThrow(req);
const body = await req.json();
const validated = bodySchema.parse(body);
const rendicion = await cobranzasServicio.crearRendicion(
session.empresaId, validated, session.userId
);
return NextResponse.json({ data: rendicion }, { status: 201 });
} catch (err: any) {
if (err.name === 'ZodError')
return NextResponse.json({ error: err.errors }, { status: 400 });
return NextResponse.json({ error: err.message }, { status: 500 });
}
}

## 6. Servicio — Lógica de Negocio

#### Crear en: src/lib/servicios/cobranzasServicio.ts

```
import { cobranzasRepositorio } from '@/lib/db/repositorios/cobranzasRepositorio';
import { contabilidadServicio } from '@/lib/servicios/contabilidadServicio';
import { tesoreriaServicio } from '@/lib/servicios/tesoreriaServicio';
import { cxcServicio } from '@/lib/servicios/cxcServicio';
import type { CrearRendicionDTO, VerificarRendicionDTO } from '@/types/cobranzas';
```

```
export const cobranzasServicio = {
```

```
/** Validar datos antes de crear la rendición */
async validarCreacion(empresaId: string, data: CrearRendicionDTO) {
const errores: string[] = [];
for (const item of data.items) {
const saldo = await cxcServicio.obtenerSaldo(empresaId, item.facturaId);
if (!saldo)
errores.push(`Factura ${item.facturaId} no encontrada.`);
else if (item.montoCobrado > saldo.saldoPendiente)
errores.push(`Factura ${item.facturaId}: monto cobrado supera saldo
pendiente.`);
}
// Suma de cobros debe coincidir con suma de pagos (tolerancia 1 Gs.)
const totalItems = data.items.reduce((s, i) => s + i.montoCobrado, 0);
const totalPagos = data.pagos.reduce((s, p) => s + p.importeMoneda *
p.tipoCambio, 0);
if (Math.abs(totalItems - totalPagos) > 1)
errores.push(`Diferencia entre cobros (${totalItems}) y medios de pago ($
{totalPagos}).`);
return errores;
},
```

```
/** Crear rendición en estado BORRADOR */
async crearRendicion(empresaId: string, data: CrearRendicionDTO, usuarioId: string)
{
const errores = await this.validarCreacion(empresaId, data);
if (errores.length) throw new Error(errores.join(' | '));
return cobranzasRepositorio.crearRendicion(empresaId, data, usuarioId);
},
```

###### /\*_ BORRADOR → PENDIENTE _/

```
async enviarATesoreria(rendicionId: string, empresaId: string, usuarioId: string) {
const r = await cobranzasRepositorio.obtenerRendicion(rendicionId, empresaId);
if (!r || r.estado !== 'BORRADOR') throw new Error('La rendición debe estar en
BORRADOR.');
await cobranzasRepositorio.cambiarEstado(rendicionId, 'PENDIENTE', usuarioId,
'Enviada a Tesorería');
},
```

```
/** PENDIENTE → APROBADO — genera impactos en CxC, Tesorería y Contabilidad */
async aprobarRendicion(rendicionId: string, empresaId: string,
data: VerificarRendicionDTO, usuarioId: string) {
const r = await cobranzasRepositorio.obtenerRendicion(rendicionId, empresaId);
if (!r || r.estado !== 'PENDIENTE') throw new Error('Estado inválido para
aprobación.');
// 1. Registrar importes verificados
let totalVerificado = 0;
for (const pv of data.pagosVerificados) {
await cobranzasRepositorio.verificarPago(pv.pagoId, pv.importeVerificado,
pv.observacion);
totalVerificado += pv.importeVerificado;
}
await cobranzasRepositorio.actualizarVerificacion(rendicionId, totalVerificado,
data.observacionTesoreria, usuarioId);
// 2. Reducir saldo CxC por cada item
for (const item of r.items)
await cxcServicio.aplicarPago(empresaId, item.facturaId, item.montoCobrado,
rendicionId);
// 3. Registrar ingreso en Tesorería
const cajaId = await tesoreriaServicio.registrarIngreso({
```

empresaId, concepto: `Rendición ${r.codigo}`,
importe: totalVerificado, referenciaId: rendicionId,
referenciaTipo: 'RENDICION', usuarioId, pagos: r.pagos,
});
// 4. Asiento contable automático
const asientoId = await contabilidadServicio.generarAsientoRendicion(
{ empresaId, rendicionId, rendicion: r, cajaId, usuarioId }
);
// 5. Estado final
await cobranzasRepositorio.cambiarEstado(rendicionId, 'APROBADO', usuarioId,
'Aprobada por Tesorería');
await cobranzasRepositorio.actualizarReferencias(rendicionId, asientoId, cajaId);
},

###### /\*_ PENDIENTE → OBSERVADO _/

async observarRendicion(rendicionId: string, empresaId: string,
motivo: string, usuarioId: string) {
const r = await cobranzasRepositorio.obtenerRendicion(rendicionId, empresaId);
if (!r || r.estado !== 'PENDIENTE') throw new Error('Solo PENDIENTE puede
observarse.');
await cobranzasRepositorio.cambiarEstado(rendicionId, 'OBSERVADO', usuarioId,
motivo);
},
};

## 7. Reportes de Control

#### Cuatro reportes obligatorios para la gestión y auditoría del proceso de cobranzas. Se implementan

#### como endpoints + componentes React con exportación a PDF y Excel.

### 7.1 Reporte de Cartera por Cobrador

##### Campo Descripción

##### Cobrador Nombre y código del cobrador.

##### Período Rango de fechas de la cartera asignada.

##### Total asignado Suma de saldos de todas las facturas asignadas.

##### Total cobrado Suma de montos cobrados en rendiciones aprobadas.

##### Saldo pendiente Total asignado − Total cobrado.

##### % Efectividad (Total cobrado / Total asignado) × 100.

##### Facturas sin gestión Facturas asignadas sin ningún cobro registrado.

### 7.2 Reporte de Cobranzas del Día

##### Campo Descripción

##### Fecha Fecha de la rendición (por defecto hoy, parametrizable).

##### Cobrador Identificación del cobrador.

##### Nro. Rendición Código correlativo (REN-000001).

##### Facturas cobradas Cantidad de facturas incluidas.

##### Total declarado Importe total declarado por el cobrador (Gs.).

##### Total verificado Importe confirmado por Tesorería.

##### Diferencia Verificado − Declarado. Señalizado en rojo si supera umbral.

##### Estado Estado de la rendición con semáforo visual.

##### Desglose medio de pago Subtotales por efectivo, cheque, transferencia, QR.

### 7.3 Reporte de Rendiciones Pendientes

#### Lista todas las rendiciones en estado PENDIENTE u OBSERVADO con su antigüedad en el estado

#### actual para que Tesorería priorice la verificación.

- Días desde el envío (alerta amarilla > 1 día, roja > 3 días sin procesar).
- Totales por cobrador y por estado.
- Botón de acción rápida: ir directo a la pantalla de verificación.

### 7.4 Reporte de Diferencias — Control Anti-fraude

##### Indicador Nivel Acción

##### Diferencia = 0 Sin diferencia Sin acción. Rendición limpia.

##### 0 < Diferencia <= 1.000 Gs. Menor Nota en historial. Tesorería puede aprobar con

##### observación.

##### Diferencia > 1.000 Gs. Mayor Regularización obligatoria. Estado

##### OBSERVADO.

##### 3+ diferencias mayores/mes

##### por cobrador

##### Reiterancia Notificación automática a RRHH y supervisor.

##### INFO: El umbral es configurable por empresa en: parametros_empresa donde clave =

##### 'COBRO_UMBRAL_DIFERENCIA_PYG'.

### 7.5 Reporte de Antigüedad de Deuda

#### Estándar en sistemas de cobranzas de Paraguay. Muestra la deuda por cliente en tramos de

#### vencimiento:

##### Cliente Corriente 1-30 días 31-60 días 61-90 días +90 días

##### (Ejemplo) Cliente S.A. Gs.

##### 1.500.

##### Gs.

##### 3.200.

##### Gs. 800.000 Gs. 500.000 Gs.

##### 2.100.

## 8. Asiento Contable Automático

#### Al aprobar la rendición, contabilidadServicio.generarAsientoRendicion() genera el asiento estándar

#### (adaptable al plan de cuentas de cada empresa):

##### # Cuenta Debe (Gs.) Haber (Gs.)

##### 1 Caja / Banco (según medio de pago verificado) Importe verificado —

##### 2 Cuentas por Cobrar — Cliente — Importe verificado

##### INFO: Si existe diferencia: el delta se contabiliza en la cuenta 'Diferencias de Cobranza' (configurable).

##### Diferencia a favor empresa: Db Caja, Cr CxC + Cr Diferencias. Diferencia en contra: Db Caja + Db

##### Diferencias, Cr CxC.

## 9. Componentes Frontend — Next.js

### 9.1 Estructura de Carpetas

```
src/
app/cobranzas/
carteras/
page.tsx ← Listado de carteras activas
nueva/page.tsx ← Crear cartera + asignar facturas
[id]/page.tsx ← Detalle de cartera con items
rendiciones/
page.tsx ← Listado de rendiciones (cobrador)
nueva/page.tsx ← Wizard de creación (4 pasos)
[id]/page.tsx ← Detalle de rendición
[id]/verificar/page.tsx ← Pantalla exclusiva de Tesorería
reportes/
cartera/page.tsx
cobranzas-dia/page.tsx
antiguedad-deuda/page.tsx
diferencias/page.tsx
components/modulos/cobranzas/
CarteraTable.tsx
RendicionWizard.tsx ← Wizard 4 pasos
RendicionItemsTable.tsx
RendicionPagosForm.tsx
VerificacionTesoreriaForm.tsx
EstadoBadge.tsx ← Semáforo visual de estados
ReporteCartera.tsx
ReporteCobranzasDia.tsx
ReporteAntiguedad.tsx
ReporteDiferencias.tsx
```

### 9.2 RendicionWizard — 4 Pasos

##### Paso Nombre Contenido

##### 1 Datos generales Cobrador (selector), período desde/hasta, observación inicial.

##### 2 Selección de facturas Tabla paginada con facturas de la cartera (o búsqueda libre).

##### Check para incluir; campo editable de monto cobrado. Muestra

##### saldo disponible en tiempo real.

##### 3 Medios de pago Agregar líneas por medio de pago. Suma debe coincidir con total

##### cobros; validación en tiempo real con indicador de diferencia.

##### 4 Resumen y confirmación Preview completo de la rendición. Botones: Guardar borrador /

##### Enviar a Tesorería (con confirmación).

### 9.3 Pantalla de Verificación (Tesorería)

#### Ruta: /cobranzas/rendiciones/[id]/verificar — Solo roles TESORERO / SUPER_ADMIN.

- Muestra el detalle completo de la rendición junto a campos de importe verificado (uno por

##### pago).

- El sistema calcula la diferencia declarado vs. verificado en tiempo real con señal visual.
- Botones de acción: Aprobar / Observar (requiere motivo obligatorio) / Rechazar.
- Al aprobar se ejecutan automáticamente: actualización CxC, ingreso Tesorería y asiento

##### contable.

## 10. Plan de Implementación por Fases

##### Fase Alcance Entregables principales Estimación

##### FASE 1 Base de datos y tipos Migración SQL completa, vistas, tipos

##### TypeScript, repositorio base.

##### 2 días

##### FASE 2 Carteras de cobro CRUD carteras, asignación masiva de

##### facturas, cierre de cartera.

##### 3 días

##### FASE 3 Rendiciones (cobrador) RendicionWizard, creación, edición en

##### borrador, envío a Tesorería.

##### 4 días

##### FASE 4 Verificación Tesorería Pantalla verificación, aprobación,

##### observación, rechazo.

##### 3 días

##### FASE 5 Impactos contables Integración CxC, Tesorería, asiento

##### automático, historial.

##### 3 días

##### FASE 6 Reportes de control 4 reportes con filtros, paginación,

##### exportación PDF y Excel.

##### 4 días

##### TOTAL ~19 días

## 11. Checklist de Entrega

### Base de datos

- Migración SQL ejecutada y validada en entorno dev.
- Índices verificados con EXPLAIN ANALYZE en dataset de prueba.
- Vistas de control creadas y testeadas.
- Parámetro COBRO_UMBRAL_DIFERENCIA_PYG configurado en parametros_empresa.

### Backend

- Tipos TypeScript en src/types/cobranzas.ts.
- Repositorio con todas las operaciones CRUD + cambio de estado.
- Servicio con validaciones de negocio (saldo, suma cobros = pagos, transiciones de estado).
- Todos los endpoints implementados, protegidos con JWT + roles.
- Validación de entrada con Zod en todos los POST / PUT.
- Manejo de errores estandarizado: { error: string }.
- Tests de integración para el flujo completo: crear → enviar → aprobar.

### Frontend

- RendicionWizard con 4 pasos y validaciones progresivas.
- Pantalla de verificación con cálculo de diferencias en tiempo real.
- EstadoBadge con semáforo de colores por estado.
- 4 reportes implementados con filtros, paginación y exportación PDF / Excel.

### Integraciones

- CxC: saldo reducido correctamente al aprobar rendición.
- Tesorería: ingreso de fondos registrado por medio de pago.
- Contabilidad: asiento generado con cuentas correctas del plan de cuentas.
- SIFEN: CDC validado al incorporar factura electrónica a la rendición.

_Documento generado por Code100 / Novasis ERP — Contacto: Marcelo Palumbo — mpalumbopy@gmail.com_
