# Plan — Módulo de Rendiciones de Cobros

**Estado:** ✅ Completado · 2026-05-24
**Origen:** `docs/rendiciones.md` (Marcelo Palumbo, Code100) — adaptado a la arquitectura real de smartfactvoice
**Stack:** NestJS + Prisma + PostgreSQL (backend) · React + MUI (frontend pos-ventas)

## Cierre — 2026-05-24

Módulo en producción interna. Resumen de lo entregado en la última iteración (24/05):

### Multi-moneda (una rendición = una moneda)
- Migración `20260524_rendiciones_moneda` añade `moneda_id` a `rendiciones_cobranza` con FK.
- Migración `20260524_rendiciones_moneda_backfill` rellena rendiciones legacy con la moneda principal de la empresa.
- `create()` valida que todos los recibos compartan la misma moneda y la persiste.
- `findAll`, `findOne`, `getRecibosDisponibles` filtran/incluyen `moneda`.
- `getResumenDiario` devuelve `totales_por_moneda[]`.
- Frontend: selector de moneda always-on en `RendicionesPanel` (next to Fecha), auto-selección por principal → PYG → primera disponible. Solo monedas activas de `empresa_monedas`.
- Helpers de formato (`formatMonto`, `decimalesDe`, `simboloDe`) respetan decimales y símbolo por moneda; fallback hardcoded `MONEDAS_SIN_DECIMALES = ["PYG","CLP","JPY","KRW","ISK","VND"]`.
- `MonedaInput` (estándar) reemplaza inputs numéricos sueltos en verificación.

### Unificación MULTI / LEGACY
- Nuevo mapper `normalizarRendicion()` en `rendiciones.service.ts` expone por recibo:
  - `medios_pago_unificado: [{id, origen: 'MULTI'|'LEGACY', medio, monto, verificacion: {...}, cheque_numero, ...}]`
  - `facturas_numeros: ["001-001-0000014", ...]`
- Medios LEGACY se devuelven pre-verificados (`verificacion.estado='OK'`, `importe_verificado=monto`) → se consideran auto-verificados por el cobro original.
- Frontend usa una sola estructura: se eliminaron `MedioRowItem` y `LegacyMedioRow` (~120 LOC).

### Verificación agrupada
- `GroupedMedioRow`: agrupa por `(medio + cuenta_tesoreria_id)` a nivel rendición.
  - EFECTIVO / TRANSFERENCIA / DEPÓSITO / SALDO_FAVOR → 1 fila agregada.
  - CHEQUE → fila por cheque (Nº + banco).
  - TARJETA → fila por voucher (auth code).
- Verificación dispara N mutations en paralelo al endpoint existente `PATCH /rendiciones/:id/medios-pago/:mpId/verificar`, distribuyendo `importe_verificado` proporcional al `monto` de cada medio MULTI (última fila absorbe redondeo).
- En grupos mixtos MULTI+LEGACY, el input arranca con el total del grupo y se resta `legacyTotal` antes de distribuir → evita doble conteo en KPI.

### UX de verificación
- Botón **Observar** destacado (sólido naranja) cuando hay DIFERENCIAs; banner sugiere devolver al cobrador.
- Botón Aprobar deshabilitado con tooltip explicativo (diferencias / rechazados / pendientes).
- Flujo cobrador: tesorero observa → estado `OBSERVADO` → cobrador ajusta → `enviar-tesoreria` vuelve a `PENDIENTE`.

### Bugfixes asociados
- `fecha_registro` UTC al cobrar (`CobrosTemplateV2.jsx:441,569`): reemplazo de `toISOString().split("T")[0]` por componentes locales.
- `getRecibosDisponibles` ahora trae MULTI: `estado: { in: ['emitido', 'CONFIRMADO'] }` + include de `medios_pago_multi` + `facturas_multi`.
- Diferencia falsa de -0.34 PYG por mismatch de redondeo: `totalSeleccionado` redondea según decimales de la moneda.
- PYG con 8 decimales en DB: `UPDATE moneda SET decimales=0 WHERE codigo='PYG'`.

### Archivos finales relevantes
- Backend: `src/rendiciones/{rendiciones.service.ts, rendiciones.controller.ts, dto/rendiciones.dto.ts}`
- Backend: `prisma/migrations/20260524_rendiciones_moneda*/migration.sql`
- Frontend: `src/components/organismos/RendicionesDesign/{RendicionesPanel,CrearRendicionDialog,DetalleRendicionDialog,RevisarRendicionDialog}.jsx`
- Frontend: `src/api/rendiciones.service.js`, `src/tanstack/RendicionesStack.jsx`

---

---

## 0. Decisiones de diseño (alineadas con el equipo)

| # | Decisión | Elegido |
|---|---|---|
| 1 | Modelo de datos | **A** — El recibo es la fuente de verdad. La rendición agrupa N `recibos_cobro` existentes. |
| 2 | Momento de reducción de CxC | **A** — Saldo se reduce al crear el recibo en campo (estado `PENDIENTE_RENDICION`). Rechazo revierte. |
| 3 | Cartera de cobro previa | **B** — Sin tabla de carteras. Cobrador rinde sobre clientes/facturas que ya tiene asignados. |
| 4 | Verificación por Tesorería | **C** — Híbrida: efectivo por monto agregado, cheques uno por uno, transferencias/QR auto-validadas. |
| 5 | Caja destino al aprobar | **D** — Por medio de pago: efectivo → caja parametrizada, cheques → cartera de cheques, transferencias/QR → cuenta bancaria del recibo. |
| 6 | Devengamiento de comisión | **A** — Se devenga al APROBAR la rendición. Si se rechaza, la comisión nunca existió. |
| 7 | Tipo de cobrador | **A** — Solo EMPLEADO. Se reutiliza `vendedores_cobradores` → `rrhh_empleados`. |
| 8 | Diferencias y umbrales | Cero → APROBADO automático. Menor (≤ umbral) → APROBADO con asiento de ajuste a "Diferencias de Cobranza". Mayor (> umbral) → OBSERVADO o RECHAZADO manual. Umbral parametrizable. |
| 9 | Cadencia de rendición | **B** — Libre, una activa por cobrador. Alerta a 3 días sin rendir. |
| 10 | Puntos de entrada UI | **C** — Wizard responsive: cobrador desde mobile (`PanelCobrador`) y web. Verificación Tesorería solo web. |
| 11 | Numeración correlativa | **A** — `REN-000001`, único por empresa, sin timbrado fiscal. |
| 12 | Adjuntos | **A** — Sin adjuntos. Comprobantes físicos quedan en papel. |
| 13 | Alcance de reportes | Cobranzas-día · Pendientes (Tesorería) · Diferencias (anti-fraude) · Comisiones a liquidar. |

---

## 1. Estado actual del schema

Ya existe `rendiciones_cobranza` (línea 4812 de `prisma/schema.prisma`) con:
- `empresa_id`, `cobrador_id`, `ruta_id`, `fecha_rendicion`, `fecha_cobranza`
- `monto_esperado`, `monto_cobrado`, `monto_efectivo/cheque/transferencia/tarjeta`
- `diferencia`, `cantidad_recibos`
- `estado` (`pendiente`, `aprobada`, `rechazada`, `con_observacion`)
- `supervisor_id`, observaciones, relación `recibos_cobro[]`

Y `recibos_cobro` ya tiene FK `rendicion_id` (línea 3285).

**El plan es extender, no reemplazar.**

---

## 2. Cambios de base de datos

### 2.1 Extender `rendiciones_cobranza`

Migración SQL (`migrations/YYYYMMDD_rendiciones_extend.sql`):

```sql
ALTER TABLE rendiciones_cobranza
  ADD COLUMN numero SERIAL,                                       -- correlativo interno
  ADD COLUMN codigo VARCHAR(20) GENERATED ALWAYS AS
    ('REN-' || LPAD(numero::TEXT, 6, '0')) STORED,
  ADD COLUMN total_declarado NUMERIC(18,2) NOT NULL DEFAULT 0,    -- suma de pagos del cobrador
  ADD COLUMN total_verificado NUMERIC(18,2),                       -- suma confirmada por Tesorería (NULL hasta verificar)
  ADD COLUMN fecha_envio_tesoreria TIMESTAMPTZ,                    -- BORRADOR → PENDIENTE
  ADD COLUMN fecha_aprobacion TIMESTAMPTZ,
  ADD COLUMN aprobado_por UUID REFERENCES usuario(id),
  ADD COLUMN motivo_rechazo TEXT,
  ADD COLUMN asiento_id UUID,                                      -- FK lógica a cont_asientos al aprobar
  ADD COLUMN caja_destino_id UUID,                                 -- caja donde aterrizó el efectivo
  ADD COLUMN deleted_at TIMESTAMPTZ;

-- Estados: BORRADOR, PENDIENTE, OBSERVADO, APROBADO, RECHAZADO, ANULADO
-- (renombrar valores existentes para alinear)
UPDATE rendiciones_cobranza SET estado = 'PENDIENTE'   WHERE estado = 'pendiente';
UPDATE rendiciones_cobranza SET estado = 'APROBADO'    WHERE estado = 'aprobada';
UPDATE rendiciones_cobranza SET estado = 'RECHAZADO'   WHERE estado = 'rechazada';
UPDATE rendiciones_cobranza SET estado = 'OBSERVADO'   WHERE estado = 'con_observacion';

ALTER TABLE rendiciones_cobranza
  ADD CONSTRAINT chk_rendicion_estado
    CHECK (estado IN ('BORRADOR','PENDIENTE','OBSERVADO','APROBADO','RECHAZADO','ANULADO'));

CREATE UNIQUE INDEX idx_rendicion_codigo
  ON rendiciones_cobranza(empresa_id, numero)
  WHERE deleted_at IS NULL;
```

**Restricción de unicidad por cobrador:**
```sql
-- Solo una rendición activa (BORRADOR/PENDIENTE/OBSERVADO) por cobrador a la vez
CREATE UNIQUE INDEX idx_rendicion_activa_cobrador
  ON rendiciones_cobranza(empresa_id, cobrador_id)
  WHERE estado IN ('BORRADOR','PENDIENTE','OBSERVADO') AND deleted_at IS NULL;
```

### 2.2 Nueva tabla `rendicion_historial`

```sql
CREATE TABLE rendicion_historial (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  rendicion_id UUID NOT NULL REFERENCES rendiciones_cobranza(id) ON DELETE CASCADE,
  estado_anterior VARCHAR(20),
  estado_nuevo VARCHAR(20) NOT NULL,
  usuario_id UUID NOT NULL REFERENCES usuario(id),
  motivo TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_rend_hist_rendicion ON rendicion_historial(rendicion_id, created_at DESC);
```

### 2.3 Extender `recibo_cobro_medios_pago` (verificación granular)

```sql
ALTER TABLE recibo_cobro_medios_pago
  ADD COLUMN importe_verificado NUMERIC(18,2),     -- NULL = aún no verificado
  ADD COLUMN verificacion_estado VARCHAR(20),       -- OK | DIFERENCIA | RECHAZADO
  ADD COLUMN verificacion_observacion TEXT,
  ADD COLUMN verificado_por UUID REFERENCES usuario(id),
  ADD COLUMN verificado_at TIMESTAMPTZ;
```

### 2.3.1 Cobro diferido en `factura_cab` (factura contado cobrada en ruta)

Caso de uso: la empresa emite **factura contado** en mostrador pero el cobrador la cobra en campo y debe rendirla junto al resto de cobranzas.

```sql
ALTER TABLE factura_cab
  ADD COLUMN cobro_diferido BOOLEAN NOT NULL DEFAULT FALSE;

CREATE INDEX idx_factura_cab_cobro_diferido
  ON factura_cab(empresa_id, cobro_diferido)
  WHERE cobro_diferido = TRUE;
```

**Reglas de negocio:**

| Aspecto | Comportamiento |
|---|---|
| Fiscal / SIFEN | Sigue siendo CONTADO. SIFEN exige `gPaConEIni` con medio de pago → se declara igual (típicamente Efectivo). No cambia timbrado, IVA ni KuDE. |
| `factura_forma_pagos` | **SÍ se inserta** (es la fuente del XML SIFEN). |
| `movimiento_cajas` / `sesiones_caja` | **NO se actualiza** — la plata aún no entró. El cobro real lo hará el cobrador y moverá caja/banco al aprobar la rendición. |
| `saldo_pendiente` (cuentas_cobrar) | Igual al total general — la factura queda con saldo. Se crea fila en `cuentas_cobrar` para reusar la mecánica de cartera. |
| `asignacion_facturas` | Se crea automáticamente con `cobrador_id` (de la cabecera) para que la factura aparezca en cartera. |
| `cobrador_id` | **Obligatorio** cuando `cobro_diferido = TRUE`. Si falta, el backend rechaza con `BadRequest`. |
| Cobro real | El cobrador genera `recibos_cobro` normal en estado `PENDIENTE_RENDICION` y lo incluye en su rendición. |
| Reportes ventas | Se puede filtrar `cobro_diferido` para distinguir contado-mostrador de contado-en-ruta. La fuente de verdad del cobro real son los `recibos_cobro`, no `factura_forma_pagos`. |

**Por qué este enfoque (y no otros):**

- Convertirla a crédito de 1 día ensucia la estadística fiscal y los reportes de condición.
- Generar recibo en mostrador y "transferir" al cobrador duplica entradas contables.
- Un flujo paralelo dedicado duplicaría la maquinaria de rendiciones.
- Una bandera reusa **todo** el flujo de cartera + recibos + rendición ya existente.

### 2.4 Extender `recibos_cobro` — estado `PENDIENTE_RENDICION`

No requiere DDL: la columna `estado` ya es `VARCHAR(20)`. Sumar el nuevo valor a las validaciones del service:

| Estado | Significado | Reduce CxC? |
|---|---|---|
| `EMITIDO` | (legacy) Recibo confirmado sin pasar por rendición | ✅ |
| `PENDIENTE_RENDICION` | Creado por cobrador en campo, dentro de una rendición no aprobada | ✅ |
| `CONFIRMADO` | Rendición aprobada — el recibo es definitivo | ✅ |
| `ANULADO` | Anulado individualmente o por rechazo de rendición | ❌ (saldo restituido) |

### 2.5 Parámetros de empresa nuevos

Insertar en `parametros_empresa` (o equivalente) al instalar el módulo:

| Clave | Default | Descripción |
|---|---|---|
| `COBRO_UMBRAL_DIFERENCIA_PYG` | `1000` | Umbral en Gs. para diferencia menor (auto-aprobable) |
| `COBRO_UMBRAL_DIFERENCIA_USD` | `1` | Umbral en USD |
| `COBRO_CAJA_EFECTIVO_DEFAULT_ID` | `null` | Caja destino para efectivo al aprobar |
| `COBRO_CUENTA_DIFERENCIAS_ID` | `null` | Cuenta contable "Diferencias de Cobranza" |
| `COBRO_DIAS_ALERTA_RENDICION` | `3` | Días sin rendir antes de mostrar alerta |
| `COBRO_REINCIDENCIA_MES_UMBRAL` | `3` | Diferencias mayores por mes para gatillar alerta a RRHH |

### 2.6 Vistas de control

```sql
-- Rendiciones con diferencias (anti-fraude)
CREATE OR REPLACE VIEW vw_rendiciones_diferencias AS
SELECT r.empresa_id, r.codigo, r.cobrador_id, r.fecha_rendicion, r.estado,
       r.total_declarado, r.total_verificado,
       COALESCE(r.total_verificado, 0) - r.total_declarado AS diferencia,
       ABS(COALESCE(r.total_verificado, 0) - r.total_declarado) AS diferencia_abs,
       CASE
         WHEN r.total_verificado IS NULL THEN 'SIN_VERIFICAR'
         WHEN ABS(r.total_verificado - r.total_declarado) = 0 THEN 'SIN_DIFERENCIA'
         WHEN ABS(r.total_verificado - r.total_declarado) <= 1000 THEN 'MENOR'
         ELSE 'MAYOR'
       END AS nivel
FROM rendiciones_cobranza r
WHERE r.deleted_at IS NULL AND r.estado IN ('APROBADO','OBSERVADO','RECHAZADO');

-- Comisiones a liquidar (vinculadas a rendiciones aprobadas)
CREATE OR REPLACE VIEW vw_comisiones_a_liquidar AS
SELECT c.empresa_id, c.cobrador_id, r.codigo AS rendicion_codigo,
       r.fecha_aprobacion, c.monto, c.estado
FROM comisiones c
JOIN recibos_cobro rc ON rc.id = c.recibo_cobro_id
JOIN rendiciones_cobranza r ON r.id = rc.rendicion_id
WHERE r.estado = 'APROBADO' AND c.estado = 'DEVENGADA';
```

---

## 3. Estados y transiciones

```
        crear (cobrador)
              │
              ▼
        ┌──────────┐
        │ BORRADOR │── cobrador edita libremente
        └────┬─────┘
             │ enviar
             ▼
        ┌────────────┐
        │ PENDIENTE  │── tesorería verifica
        └─┬──┬───┬───┘
   apruebo│  │   │ rechazar
          │  │   │
          │  │observar
          │  ▼   │
          │┌──────────┐
          ││OBSERVADO │── cobrador regulariza → enviar de nuevo (vuelve a PENDIENTE)
          │└──────────┘
          ▼
        ┌──────────┐         ┌───────────┐
        │ APROBADO │         │ RECHAZADO │── recibos anulados, saldos restituidos
        └──────────┘         └───────────┘

Estado adicional: ANULADO (cobrador anula antes de enviar)
```

**Reglas:**
- Solo el cobrador dueño edita en `BORRADOR`. Solo Tesorería actúa sobre `PENDIENTE`/`OBSERVADO`.
- `OBSERVADO → PENDIENTE` no incrementa `numero` (es la misma rendición regularizada).
- `APROBADO`, `RECHAZADO`, `ANULADO` son finales.

---

## 4. Flujo de aprobación

```typescript
// Al APROBAR (estado PENDIENTE → APROBADO):
// 1. Persistir importe_verificado por cada medio de pago (Tesorería ya cargó vía pantalla de verificación)
// 2. Calcular diferencia = total_verificado - total_declarado
// 3. Si |diferencia| > umbral_empresa  →  bloquear: el tesorero debe OBSERVAR o RECHAZAR explícitamente
// 4. Para cada recibo del grupo:
//    a. Cambiar estado a CONFIRMADO
//    b. Devengar comisión (crear fila en `comisiones` con estado DEVENGADA)
// 5. Tesorería: registrar ingreso por medio de pago
//    - Efectivo → ingreso en caja_destino_id (parámetro o elegido en aprobación)
//    - Cheque → ingreso en cartera de cheques (estado RECIBIDO)
//    - Transferencia/QR → marcar como confirmado en la cuenta bancaria del recibo
// 6. Contabilidad: asiento automático vía contabilidad.integracion.service
//    - Db Caja/Banco                        total_verificado
//    - Db (o Cr) Diferencias de Cobranza    |diferencia| si hubo
//    - Cr Cuentas por Cobrar — Cliente      total_declarado
// 7. Guardar referencias: asiento_id, caja_destino_id
// 8. Insertar en rendicion_historial (PENDIENTE → APROBADO + usuario + observación)
```

**Al RECHAZAR (estado PENDIENTE → RECHAZADO):**
1. Para cada recibo: pasar a `ANULADO` con motivo "Rendición REN-XXXX rechazada".
2. Restituir saldos en CxC usando el flujo existente de anulación de recibo.
3. Liberar cheques/transferencias asociados (revertir lo que se haya provisorio).
4. Insertar en `rendicion_historial`.

**Al OBSERVAR (estado PENDIENTE → OBSERVADO):**
- Solo cambio de estado + motivo. Los recibos permanecen en `PENDIENTE_RENDICION` (saldos sin cambio).
- El cobrador puede editar pagos (agregar/corregir comprobantes, ajustar montos) y volver a enviar.

---

## 5. Backend (NestJS)

### 5.1 Estructura de módulo

```
src/rendiciones-cobranza/
  rendiciones-cobranza.module.ts
  rendiciones-cobranza.controller.ts
  rendiciones-cobranza.service.ts
  dto/
    crear-rendicion.dto.ts
    actualizar-rendicion.dto.ts
    verificar-rendicion.dto.ts          ← payload de Tesorería al aprobar
    observar-rendicion.dto.ts
  events/
    rendicion-aprobada.event.ts          ← dispara comisión + asiento + caja
    rendicion-rechazada.event.ts         ← dispara anulación en cascada
  guards/
    rendicion-owner.guard.ts             ← cobrador solo edita las suyas
```

### 5.2 Endpoints

| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| GET | `/rendiciones-cobranza` | COB_REN_LIST | Listar con filtros: `estado`, `cobrador_id`, `desde`, `hasta` |
| GET | `/rendiciones-cobranza/mias` | COBRADOR | Mis rendiciones (filtra por `cobrador_id = me`) |
| POST | `/rendiciones-cobranza` | COBRADOR | Crear rendición en `BORRADOR` con N recibos |
| GET | `/rendiciones-cobranza/:id` | COB_REN_VIEW | Detalle con items y pagos |
| PUT | `/rendiciones-cobranza/:id` | COBRADOR (owner) | Editar borrador (agregar/quitar recibos) |
| POST | `/rendiciones-cobranza/:id/enviar` | COBRADOR (owner) | `BORRADOR` → `PENDIENTE` |
| POST | `/rendiciones-cobranza/:id/aprobar` | TESORERO | Aprobar (acepta `verificarRendicionDto`) |
| POST | `/rendiciones-cobranza/:id/observar` | TESORERO | Observar con motivo |
| POST | `/rendiciones-cobranza/:id/rechazar` | TESORERO o SUPER_ADMIN | Rechazar — dispara anulación en cascada |
| POST | `/rendiciones-cobranza/:id/anular` | COBRADOR (owner) | Solo en `BORRADOR` |
| GET | `/rendiciones-cobranza/reportes/cobranzas-dia` | COB_REP | Reporte cobranzas del día |
| GET | `/rendiciones-cobranza/reportes/pendientes` | TESORERO | Cola Tesorería |
| GET | `/rendiciones-cobranza/reportes/diferencias` | COB_REP | Anti-fraude |
| GET | `/rendiciones-cobranza/reportes/comisiones` | RRHH_NOMINA | Comisiones a liquidar |

### 5.3 DTO clave — `VerificarRendicionDto`

```typescript
export class VerificarRendicionDto {
  efectivoVerificado: number;          // monto agregado verificado por Tesorería
  cajaDestinoEfectivoId?: string;       // override del parámetro de empresa
  chequesVerificados: Array<{
    medioPagoId: string;                // FK a recibo_cobro_medios_pago
    estado: 'OK' | 'RECHAZADO';
    observacion?: string;
  }>;
  // Las transferencias/QR se aprueban en bloque automáticamente
  observacionTesoreria?: string;
  forzarAprobacionConDiferencia?: boolean; // requerido si |diferencia| > umbral menor
}
```

### 5.4 Service — métodos críticos

```typescript
class RendicionesCobranzaService {
  /** El cobrador crea la rendición agrupando recibos que ya tiene en PENDIENTE_RENDICION */
  async crear(empresaId, cobradorId, dto: CrearRendicionDto): Promise<Rendicion>;

  /** Valida: 1 sola activa por cobrador, recibos pertenecen al cobrador y están sin rendir */
  private async validarUnicaActiva(empresaId, cobradorId);

  /** Cambia estado a PENDIENTE, calcula total_declarado desde la suma de los recibos */
  async enviarATesoreria(id, empresaId, usuarioId);

  /** Operación transaccional: verificación + comisión + tesorería + asiento */
  async aprobar(id, empresaId, dto: VerificarRendicionDto, usuarioId): Promise<Rendicion>;

  /** Cascada: anular cada recibo (reutiliza recibos.service.anular) */
  async rechazar(id, empresaId, motivo, usuarioId): Promise<void>;

  async observar(id, empresaId, motivo, usuarioId): Promise<void>;
}
```

### 5.5 Integraciones existentes a reusar

| Servicio | Para qué |
|---|---|
| `recibos-cobro.service` | Crear recibos en estado `PENDIENTE_RENDICION` desde el cobrador. Anular en cascada al rechazar. |
| `tesoreria.movimientos.service` | Registrar ingreso por medio de pago al aprobar. |
| `tesoreria.cheques.service` | Marcar cheques como `RECIBIDO` al aprobar (no `DEPOSITADO` — eso es paso posterior de Tesorería). |
| `contabilidad.integracion.service` | `integrarRendicionCobro(rendicionId)` — nuevo método que arma el asiento. |
| `comisiones.service` | Devengar comisión por cada recibo del grupo al aprobar. |
| `parametros-empresa.service` | Leer umbrales y cuentas/cajas default. |

---

## 6. Frontend (pos-ventas)

### 6.1 Componentes nuevos

```
src/components/rendiciones/
  RendicionWizard.jsx                    ← 4 pasos, responsive
  RendicionItemsStep.jsx                  ← selección de recibos no rendidos
  RendicionMediosPagoStep.jsx            ← cálculo de total declarado por medio
  RendicionResumenStep.jsx               ← preview + confirmar
  RendicionVerificacionPanel.jsx         ← pantalla de Tesorería (solo web)
  RendicionDetalle.jsx                   ← read-only, accesible desde listado
  RendicionEstadoBadge.jsx               ← semáforo de estados
  RendicionDiferenciasAlert.jsx          ← Alert MUI con detalle de diferencia detectada

src/components/rendiciones/reportes/
  CobranzasDiaPanel.jsx
  RendicionesPendientesPanel.jsx
  DiferenciasPanel.jsx
  ComisionesALiquidarPanel.jsx

src/screens/RendicionesPanel.jsx          ← nueva pestaña en Finanzas
```

### 6.2 Integración en pantallas existentes

| Pantalla | Cambio |
|---|---|
| `PanelCobrador` | Botón "Rendir cobros" abre `RendicionWizard` filtrado por mis recibos `PENDIENTE_RENDICION`. Badge con días desde el cobro más viejo. |
| `RecibosUnificadoPanel` | Columna "Rendición" con link al `RendicionDetalle`. Filtro por estado de rendición. |
| Finanzas (template) | Nueva pestaña **Rendiciones** que monta `RendicionesPanel`. |
| Tesorería (template) | Nueva pestaña **Verificar rendiciones** con cola `PENDIENTE`/`OBSERVADO`. |
| `POSAdminTemplate` (factura nueva) | Checkbox **"Cobro en ruta"** visible cuando `condicion=CONTADO` **y** la empresa tiene el módulo `RENDICIONES` activo (`hasModule("RENDICIONES")`). Si el módulo no está, el flujo se comporta como hoy. El checkbox queda habilitado solo si hay cobrador asignado. Al activarlo: oculta/deshabilita `PaymentPanel`, no envía `pagos`, envía `cobro_diferido=true`. |
| `CrearRendicionDialog` | Selector de cobrador usa `SearchableSelect` (con búsqueda). Campo "Total declarado" usa `MonedaInput` (formato PYG `1.250.000`). |

### 6.3 RendicionWizard — 4 pasos

| Paso | Contenido | Componentes `_standards` reusados |
|---|---|---|
| 1. Datos | Período (autocompleta con rango de fechas de recibos sin rendir), observación inicial | `FormShell`, `FieldHint` |
| 2. Recibos | Lista de mis recibos `PENDIENTE_RENDICION` con checkbox. Muestra: cliente, monto, medio de pago, fecha. CTA "Seleccionar todos" / "Solo de hoy". | Tabla MUI + `EmptyState` |
| 3. Resumen de medios | Calcula automáticamente totales por medio (efectivo/cheque/transferencia/QR) sumando los recibos seleccionados. Read-only — viene de los recibos. | `MonedaInput` (display) |
| 4. Confirmar | Preview completo. Botones: "Guardar borrador" / "Enviar a Tesorería". Confirmación adicional con `ConfirmDialog` antes de enviar. | `ConfirmDialog` |

**Nota:** como elegimos opción 1 (recibo es la fuente), no se editan montos por recibo en el wizard — los montos vienen ya cargados del cobro original. El wizard es esencialmente un **agrupador**.

### 6.4 Pantalla Verificación Tesorería

```
┌─ Rendición REN-000123 — Juan Pérez ────────────────────────┐
│ Período: 21/05/2026 — 23/05/2026     Estado: PENDIENTE     │
│ Total declarado: Gs. 4.500.000                              │
├──────────────────────────────────────────────────────────────┤
│ EFECTIVO                                                     │
│   Declarado:   Gs. 1.200.000                                 │
│   Verificado: [_______________]   Caja destino: [▼ Caja A]   │
├──────────────────────────────────────────────────────────────┤
│ CHEQUES                                                      │
│   [✓] Banco Itaú #45123  Gs. 1.500.000    [ OK | DIFER ]    │
│   [✓] Banco GNB  #98432  Gs.   800.000    [ OK | DIFER ]    │
├──────────────────────────────────────────────────────────────┤
│ TRANSFERENCIAS / QR                                          │
│   Auto-verificadas: Gs. 1.000.000  [✓ Validar bloque]        │
├──────────────────────────────────────────────────────────────┤
│ DIFERENCIA: Gs. 0  ✅ Aprobación directa                     │
│                                                              │
│ [ Observar ] [ Rechazar ]    [ APROBAR ]                     │
└──────────────────────────────────────────────────────────────┘
```

Si diferencia > umbral menor, el botón APROBAR queda deshabilitado y el banner pide observar o rechazar (o tickear "forzar aprobación con justificación").

---

## 7. Asiento contable

Generado al aprobar por `contabilidad.integracion.service.integrarRendicionCobro(rendicionId)`:

| # | Cuenta | Debe | Haber |
|---|---|---|---|
| 1 | Caja / Banco (según medio de pago verificado) | `total_verificado` | — |
| 2 | Diferencias de Cobranza (si hubo diferencia) | `|diferencia|` (si negativa) | `|diferencia|` (si positiva) |
| 3 | Cuentas por Cobrar — Cliente | — | `total_declarado` |

**Origen documental** del asiento: `origen_tipo = 'rendicion_cobranza'`, `origen_id = rendicion.id` (consistente con el patrón usado en `facturas`, `pagos_proveedor`, etc.).

---

## 8. Reportes

### 8.1 Cobranzas del día
Ruta: `/rendiciones-cobranza/reportes/cobranzas-dia?fecha=YYYY-MM-DD`
**Columnas:** Rendición · Cobrador · Cant. Recibos · Total Declarado · Total Verificado · Diferencia · Estado · Desglose por medio (Efectivo / Cheque / Transferencia / QR)
**Export:** PDF + Excel

### 8.2 Rendiciones pendientes (Tesorería)
Ruta: `/rendiciones-cobranza/reportes/pendientes`
**Filtro fijo:** `estado IN ('PENDIENTE','OBSERVADO')`
**Ordenado por:** antigüedad descendente
**Visual:** chip amarillo si >1 día, rojo si >3 días sin procesar
**Acción rápida:** botón "Verificar" → `/tesoreria/rendiciones/:id/verificar`

### 8.3 Diferencias (anti-fraude)
Ruta: `/rendiciones-cobranza/reportes/diferencias?desde=&hasta=&cobrador_id=`
**Agrupado por:** cobrador
**Indicadores:**
- Total diferencias menores
- Total diferencias mayores
- **Badge "REINCIDENTE"** si tiene ≥3 diferencias mayores en el mes (parámetro `COBRO_REINCIDENCIA_MES_UMBRAL`)
**Export:** Excel

### 8.4 Comisiones a liquidar
Ruta: `/rendiciones-cobranza/reportes/comisiones?desde=&hasta=`
**Origen:** vista `vw_comisiones_a_liquidar` (solo rendiciones aprobadas con comisión devengada y no pagada)
**Uso:** puente con RRHH para incluir en liquidación de nómina o pago a comisionistas

---

## 9. Plan de implementación por fases

| Fase | Alcance | Entregables | Estimación | Estado |
|---|---|---|---|---|
| **F1** | Migración + schema | DDL extendido, vistas, parámetros, regenerar Prisma client. | 1.5 día | ✅ |
| **F2** | Backend core | Module + service + controller + DTOs. Endpoints + transiciones. | 2.5 días | ✅ |
| **F3** | Aprobación transaccional | `aprobar()` con verificación granular, diferencia, comisión, asiento. | 3 días | ✅ |
| **F4** | Rechazo en cascada | `rechazar()` con anulación y restitución. | 1.5 día | ✅ |
| **F5** | UI cobrador | `CrearRendicionDialog` + `RendicionesPanel` web (responsive). | 2.5 días | ✅ |
| **F6** | UI verificación (Tesorería) | `RevisarRendicionDialog` con agrupación de medios y diferencias en tiempo real. | 2 días | ✅ |
| **F7** | Reportes (4) | Cobranzas-día, Pendientes, Diferencias, Comisiones a liquidar. | 3 días | ✅ |
| **F8** | Pulido + multi-moneda + unificación MULTI/LEGACY | Multi-moneda end-to-end, mapper unificado, agrupación visual, UX de observar. | 1.5 día | ✅ |
| | | **TOTAL** | **~17.5 días** | **Completado** |

---

## 10. Checklist de entrega

### Base de datos
- [ ] Migración `rendiciones_extend.sql` ejecutada en dev/staging/prod
- [ ] Constraint de única activa por cobrador validado con caso real
- [ ] Vistas `vw_rendiciones_diferencias` y `vw_comisiones_a_liquidar` testeadas
- [ ] Parámetros de empresa con defaults razonables

### Backend
- [ ] Módulo `rendiciones-cobranza` registrado en `AppModule`
- [ ] Endpoints protegidos con JWT + permisos `COB_REN_*`
- [ ] Guard `rendicion-owner` impide al cobrador ver/editar las de otros
- [ ] Tests de integración: crear → enviar → aprobar (happy path)
- [ ] Tests de integración: crear → enviar → rechazar (con verificación de saldos restituidos)
- [ ] Test de tope concurrencia: no se pueden crear 2 rendiciones activas para el mismo cobrador
- [ ] Asiento contable validado contra el plan de cuentas de una empresa real

### Frontend
- [ ] Wizard responsive (≤600px usable desde `PanelCobrador` mobile)
- [ ] Pantalla de verificación con cálculo de diferencia en tiempo real
- [ ] `EstadoBadge` con colores semánticos consistentes (`_standards/enums`)
- [ ] Sigue UI Standards (`MonedaInput`, `EmptyState`, `ScreenGuia`, `PrereqChecklist`)
- [ ] Tour/Onboarding implementado en `PanelCobrador` y verificación

### Integraciones
- [ ] CxC: saldos restituidos al rechazar (tests automatizados)
- [ ] Tesorería: ingresos por medio de pago correctamente distribuidos
- [ ] Cheques: estado `RECIBIDO` aplicado, no `DEPOSITADO`
- [ ] Comisiones: solo se devengan al aprobar; reincidencia detectada por reporte
- [ ] RRHH: comisiones a liquidar visibles en flujo de nómina (vista compartida)

---

## 11. Fuera de alcance

- ❌ Cobradores tipo TERCERO (terceros / agentes externos). Si surge, agregar `cobrador_tipo` posteriormente.
- ❌ Carteras explícitas con asignación de facturas por período.
- ❌ Adjuntos (fotos de recibos, vouchers).
- ❌ Reporte de antigüedad de deuda (se asume existente en módulo CxC).
- ❌ Rendiciones diarias forzadas o múltiples paralelas.
- ❌ Numeración por sucursal o por cobrador.
- ❌ Liquidación automática de comisiones a nómina (solo se expone el reporte; el flujo de liquidación queda en RRHH).
