# PLAN FUNCIONAL Y TÉCNICO — ERP Créditos & Cobranzas

**Fecha:** 2026-03-28
**Stack:** NestJS + PostgreSQL + Prisma ORM + React + MUI
**Multiempresa:** `empresa_id` en todas las tablas
**Notificaciones:** Twilio ya integrado (SMS/WhatsApp disponible)

---

## 1. DIAGNÓSTICO — Estado real del sistema

| Requerimiento | Ya existe | Falta |
|---|---|---|
| Anulación de recibos | Lógica en `cobros.service.ts` | UI de anulación |
| Panel administración cobranzas | `CuentasCobrar.jsx` parcial | Sección recibos + admin |
| Cobrador separado de vendedor | Tabla única con `tipo` ✓ | `cobrador_id` directo en `factura_cab` |
| Fecha cobro / día fijo | `dia_fijo_pago` ya está en `solicitud_credito` | UI para gestionar + lógica de ajuste de fechas |
| Intereses en planes | `tasa_interes`, `tipo_interes` en `planes_cuotas` | Config mora independiente + cálculo al cobrar |
| Promesas de pago | Nada | Tabla + flujo + recordatorio Twilio |
| Autorización descuentos | Nada | Tabla + flujo supervisor/cobrador |
| Campo referencia domicilio | Nada | Migración + campo en `clientes` |
| Reporte cobros por cobrador | Nada | Página nueva |
| Modificar fecha recibo | Nada | PATCH endpoint + UI |

---

## 2. ARQUITECTURA FUNCIONAL

### Módulos a crear o extender

```
ERP
├── [EXTENDER] Recibos & Cobranzas
│   ├── AnularRecibo (UI + permiso COBROS.ANULAR)
│   ├── ModificarFechaRecibo (UI + permiso COBROS.EDITAR_FECHA)
│   └── PanelAdminCobranzas (vista supervisora)
│
├── [NUEVO] Panel Cobrador (mobile-first)
│   ├── CuotasProximasVencer
│   ├── RegistrarPromesaPago
│   └── VerDescuentoAutorizado (read-only)
│
├── [NUEVO] Autorizacion de Descuentos
│   ├── SolicitarDescuento (cobrador)
│   └── AprobarRechazarDescuento (supervisor)
│
├── [EXTENDER] SolicitudCredito
│   ├── CampoCobradorId
│   ├── CampoFechaCobro
│   ├── DiaSemanaFijo (con lógica de ajuste automático)
│   └── EditarFechaCobroDespuesDeAprobado
│
├── [EXTENDER] Facturas
│   └── Campos cobrador_id + vendedor_id directos en factura_cab
│
├── [NUEVO] Config Mora (por empresa)
│   ├── TasaDiariaOPorcentaje
│   ├── BaseCalculo (capital / cuota completa)
│   ├── PeriodoGracia
│   └── CalculoAlMomentoDelCobro
│
├── [EXTENDER] Clientes
│   └── CampoReferenciasDomicilio
│
└── [NUEVO] ReporteCobradoresCobros
    ├── MontosAsignadosVsCobrados
    ├── ProductividadPorDia
    ├── TasaMorosidadPorCobrador
    └── ExportarPDF/Excel
```

---

## 3. ENTIDADES / TABLAS NUEVAS Y MODIFICACIONES

### 3.1 Nueva tabla: `promesas_pago`

```sql
CREATE TABLE promesas_pago (
  id                   UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id           UUID NOT NULL REFERENCES empresas(id),
  factura_cab_id       UUID NOT NULL REFERENCES factura_cab(id),
  cliente_id           UUID NOT NULL REFERENCES clientes(id),
  cobrador_id          UUID REFERENCES vendedores_cobradores(id),
  cuota_id             UUID REFERENCES cuotas_calculadas_detalle(id), -- cuota específica o NULL = general
  fecha_prometida      DATE NOT NULL,
  monto_prometido      DECIMAL(19,4),
  estado               VARCHAR(20) NOT NULL DEFAULT 'pendiente',
  -- pendiente | cumplida | incumplida | cancelada
  notas                TEXT,
  recordatorio_enviado BOOLEAN DEFAULT false,
  fecha_recordatorio   DATE,   -- calculada: fecha_prometida - 1 día
  creado_por           UUID NOT NULL REFERENCES usuarios(id),
  created_at           TIMESTAMPTZ DEFAULT NOW(),
  updated_at           TIMESTAMPTZ DEFAULT NOW()
);
```

### 3.2 Nueva tabla: `autorizaciones_descuento`

```sql
CREATE TABLE autorizaciones_descuento (
  id                   UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id           UUID NOT NULL REFERENCES empresas(id),
  recibo_cobro_id      UUID REFERENCES recibos_cobro(id),
  factura_cab_id       UUID REFERENCES factura_cab(id),
  cliente_id           UUID NOT NULL REFERENCES clientes(id),
  cobrador_id          UUID NOT NULL REFERENCES vendedores_cobradores(id),
  solicitado_por       UUID NOT NULL REFERENCES usuarios(id),
  aprobado_por         UUID REFERENCES usuarios(id),
  descuento_monto      DECIMAL(19,4),
  descuento_porcentaje DECIMAL(5,2),
  motivo               TEXT,
  estado               VARCHAR(20) NOT NULL DEFAULT 'pendiente',
  -- pendiente | aprobado | rechazado | expirado
  notas_supervisor     TEXT,
  expires_at           TIMESTAMPTZ,   -- TTL para evitar autorizaciones zombie (default: +30 min)
  created_at           TIMESTAMPTZ DEFAULT NOW(),
  updated_at           TIMESTAMPTZ DEFAULT NOW()
);
```

### 3.3 Nueva tabla: `config_mora`

```sql
CREATE TABLE config_mora (
  id                   UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id           UUID NOT NULL UNIQUE REFERENCES empresas(id),
  activo               BOOLEAN DEFAULT false,
  tipo_calculo         VARCHAR(20) DEFAULT 'diario',
  -- diario | mensual | fijo_por_cuota
  tasa                 DECIMAL(8,4) DEFAULT 0,
  -- % diario o mensual, o monto fijo según tipo_calculo
  base_calculo         VARCHAR(20) DEFAULT 'capital',
  -- capital | cuota_completa (capital + interes original)
  periodo_gracia       SMALLINT DEFAULT 0,       -- días de gracia post-vencimiento
  monto_minimo_mora    DECIMAL(19,4) DEFAULT 0,  -- mora mínima a aplicar (0 = sin mínimo)
  monto_maximo_mora    DECIMAL(19,4),            -- mora máxima/cap (NULL = sin tope)
  descripcion_concepto VARCHAR(100) DEFAULT 'Interés por mora',
  -- Campo preparatorio para futura nota de débito fiscal
  tipo_fiscal          VARCHAR(20) DEFAULT 'interno',
  -- interno | nota_debito (implementación futura)
  created_at           TIMESTAMPTZ DEFAULT NOW(),
  updated_at           TIMESTAMPTZ DEFAULT NOW()
);
```

### 3.4 Modificaciones a tablas existentes

```sql
-- factura_cab: campos directos de vendedor y cobrador
-- NOTA: factura_cab ya puede tener cobrador via asignacion_facturas,
--       estos campos son la fuente de verdad al momento de emisión.
ALTER TABLE factura_cab
  ADD COLUMN IF NOT EXISTS vendedor_id UUID REFERENCES vendedores_cobradores(id),
  ADD COLUMN IF NOT EXISTS cobrador_id UUID REFERENCES vendedores_cobradores(id);

-- solicitud_credito: cobrador y configuración de fecha de cobro semanal
-- NOTA: dia_fijo_pago (1-28, día del mes) ya existe. El nuevo campo es
--       dia_cobro_semana (1-7, día de la semana). Son conceptos distintos.
ALTER TABLE solicitud_credito
  ADD COLUMN IF NOT EXISTS cobrador_id      UUID REFERENCES vendedores_cobradores(id),
  ADD COLUMN IF NOT EXISTS fecha_cobro      DATE,
  ADD COLUMN IF NOT EXISTS dia_cobro_semana SMALLINT;
  -- 1=Lunes, 2=Martes, ..., 7=Domingo

-- clientes: campo de referencia de domicilio libre
ALTER TABLE clientes
  ADD COLUMN IF NOT EXISTS referencia_domicilio VARCHAR(500);

-- recibos_cobro: separar fecha registro de fecha imputación fiscal (preparatorio)
ALTER TABLE recibos_cobro
  ADD COLUMN IF NOT EXISTS fecha_registro          DATE,
  ADD COLUMN IF NOT EXISTS fecha_imputacion_fiscal DATE;
  -- fecha_imputacion_fiscal para futuro módulo contable

-- recibo_cobro_detalle: desglose de mora por cuota cobrada
ALTER TABLE recibo_cobro_detalle
  ADD COLUMN IF NOT EXISTS monto_mora         DECIMAL(19,4) DEFAULT 0,
  ADD COLUMN IF NOT EXISTS dias_mora          SMALLINT DEFAULT 0,
  ADD COLUMN IF NOT EXISTS tasa_mora_aplicada DECIMAL(8,4) DEFAULT 0;
```

---

## 4. PERMISOS RECOMENDADOS

Agregar al sistema de permisos por módulo/acción:

```
COBRANZAS
  COBRANZAS.VER_PANEL
  COBRANZAS.REGISTRAR_COBRO
  COBRANZAS.ANULAR_RECIBO
  COBRANZAS.MODIFICAR_FECHA_RECIBO
  COBRANZAS.VER_TODAS_EMPRESAS       ← solo holding/admin SaaS

PANEL_COBRADOR
  COBRADOR.VER_CUOTAS_PROXIMAS
  COBRADOR.REGISTRAR_PROMESA_PAGO
  COBRADOR.SOLICITAR_DESCUENTO

DESCUENTOS
  DESCUENTOS.AUTORIZAR               ← solo supervisor/admin empresa
  DESCUENTOS.VER_PENDIENTES

CONFIG_MORA
  MORA.VER_CONFIG
  MORA.EDITAR_CONFIG                 ← solo admin empresa

CREDITOS
  CREDITOS.ASIGNAR_COBRADOR
  CREDITOS.MODIFICAR_FECHA_COBRO_POST_APROBACION

REPORTES
  REPORTES.COBROS_POR_COBRADOR
  REPORTES.EXPORTAR_PDF
  REPORTES.EXPORTAR_EXCEL
```

---

## 5. FLUJO DE DESCUENTO SUPERVISADO

```
Cobrador en campo
  │
  ├─ Identifica cuota a cobrar con descuento
  ├─ Llama a supervisor por teléfono
  │
  └─ Crea solicitud_descuento (POST /autorizaciones-descuento)
       Estado: "pendiente"
       TTL: 30 minutos (expires_at = NOW() + 30min)
       │
       Supervisor ve badge en PanelSupervisor (descuentos pendientes)
       │
       ├─ Aprueba → estado: "aprobado", descuento_monto/porcentaje fijado
       │    └─ Cobrador refresca vista → ve el descuento pre-aplicado (read-only)
       │         └─ Crea cobro → backend vincula autorización aprobada automáticamente
       │
       └─ Rechaza → estado: "rechazado" + notas_supervisor
            └─ Cobrador ve mensaje de rechazo en su vista

REGLA DE NEGOCIO (backend):
  Al crear recibo con descuento, el backend verifica que exista
  una autorizacion_descuento con estado="aprobado" para esa
  factura+cobrador, no expirada. Si no existe → 403 Forbidden.
  El cobrador NUNCA puede enviar descuento_monto directamente.
```

---

## 6. LÓGICA DE DÍA FIJO DE COBRO (SEMANAL)

```typescript
// Algoritmo de ajuste de fechas al día de la semana
// diaCobro: 1=Lunes, 2=Martes, ..., 7=Domingo
function ajustarAlDiaDeSemana(fecha: Date, diaCobro: number): Date {
  const diaActual = fecha.getDay() || 7; // getDay() retorna 0 para Domingo → convertir a 7
  let diff = diaCobro - diaActual;
  if (diff <= 0) diff += 7; // siempre avanzar hacia el próximo día indicado
  return addDays(fecha, diff);
}

// Para el cronograma semanal:
// cuota[0].fecha = ajustarAlDiaDeSemana(fechaSolicitud + diasGracia, diaCobro)
// cuota[n].fecha = cuota[n-1].fecha + 7 días
// El día de la semana se mantiene fijo independientemente del mes (4 o 5 semanas)
```

---

## 7. CÁLCULO DE MORA AL COBRAR

```typescript
function calcularMora(
  cuota: { fecha_vencimiento: Date; capital_pendiente: number; monto_cuota: number },
  configMora: ConfigMora,
  fechaPago: Date
): number {
  if (!configMora.activo) return 0;

  const diasVencido = differenceInDays(startOfDay(fechaPago), startOfDay(cuota.fecha_vencimiento));
  if (diasVencido <= configMora.periodo_gracia) return 0;

  const diasMora = diasVencido - configMora.periodo_gracia;
  const base = configMora.base_calculo === 'capital'
    ? cuota.capital_pendiente
    : cuota.monto_cuota;

  let mora = 0;
  switch (configMora.tipo_calculo) {
    case 'diario':
      mora = base * (configMora.tasa / 100) * diasMora;
      break;
    case 'mensual':
      mora = base * (configMora.tasa / 100) * (diasMora / 30);
      break;
    case 'fijo_por_cuota':
      mora = configMora.tasa; // monto fijo independiente de días
      break;
  }

  // Aplicar mínimo y máximo configurados
  if (configMora.monto_minimo_mora > 0 && mora < configMora.monto_minimo_mora) {
    mora = configMora.monto_minimo_mora;
  }
  if (configMora.monto_maximo_mora && mora > configMora.monto_maximo_mora) {
    mora = configMora.monto_maximo_mora;
  }

  return mora;
}

// NOTA IMPORTANTE — Timezone:
// Usar startOfDay() con la timezone de la empresa para evitar
// errores de un día por diferencia UTC vs UTC-4 (Paraguay).
```

---

## 8. ESTADO DE IMPLEMENTACIÓN Y ROADMAP

### ✅ YA IMPLEMENTADO

| Tarea | Estado |
|---|---|
| UI Anular recibo (modal + motivo) | ✅ |
| UI Modificar fecha recibo | ✅ |
| Config mora (tabla, endpoints, UI) | ✅ |
| Cálculo mora al cobrar | ✅ |
| Desglose mora en recibo PDF | ✅ |
| Tabla + endpoints `promesas_pago` | ✅ |
| Tabla + endpoints `autorizaciones_descuento` | ✅ |
| Job expiración autorizaciones (cron) | ✅ |
| Panel Cobrador mobile-first (página `PanelCobrador`) | ✅ |
| Reporte por cobrador (`ReporteCobrador.jsx`) | ✅ |
| RecibosPanel en Finanzas (tabla, filtros, Excel) | ✅ |
| Módulo `PRINT_MOBILE` + toggle en POS Config | ✅ |
| Módulo `PANEL_COBRADOR` (seed + ruta protegida) | ✅ |
| `fecha_anulacion` + `motivo_anulacion` en `recibos_cobro` | ✅ |
| `cobrador_id` en `recibos_cobro` (auto-detectado al cobrar) | ✅ |

---

### FASE 3 — Mejoras Panel Cobrador + Flujo Cobrador Externo
*Refinamientos operacionales basados en feedback de uso real.*

| # | Tarea | Descripción | Módulo/Permiso |
|---|---|---|---|
| F3.1 | Filtro "Ver todas" en Panel Cobrador | Toggle para ver cuotas de todos los cobradores + sin asignar, no solo las propias | `PANEL_COBRADOR` |
| F3.2 | Filtro por fecha manual en Panel Cobrador | Input desde/hasta además de los chips rápidos. Cuotas vencidas de días anteriores deben aparecer siempre | `PANEL_COBRADOR` |
| F3.3 | Detalle de cliente en Panel Cobrador | Al expandir cuota: dirección, `referencia_domicilio`, teléfono, RUC para identificación rápida | `PANEL_COBRADOR` |
| F3.4 | Historial últimos 5 cobros por cliente | Colapsado por defecto en Panel Cobrador. El cobrador lo expande si necesita | `PANEL_COBRADOR` |
| F3.5 | Chip promesa activa en tarjeta de cuota | Si la cuota tiene promesa pendiente, mostrar fecha prometida en la card | `PANEL_COBRADOR` |
| F3.6 | Registrar promesa desde Panel Cobrador | Botón inline en cada cuota para registrar/editar promesa de pago | `PANEL_COBRADOR` |
| F3.7 | Registrar promesa desde Gestión de Cobros | Dentro del detalle de cuotas de una factura, botón "Registrar promesa" | `COBROS` |
| F3.8 | Selección de cobrador al confirmar cobro | Campo cobrador (pre-rellena si usuario es cobrador) visible con permiso `COBRANZAS.ASIGNAR_COBRADOR` | `COBRANZAS.ASIGNAR_COBRADOR` |
| F3.9 | Campo fecha comprobante al confirmar cobro | `fecha_registro` editable en formulario de confirmación (para cobros externos) | `COBRANZAS.ASIGNAR_COBRADOR` |

### FASE 4 — Reportes y Excel
*Visibilidad gerencial y operativa.*

| # | Tarea | Descripción |
|---|---|---|
| F4.1 | Excel en RecibosPanel trae todos los resultados | ✅ Ya implementado (take: 10000) |
| F4.2 | Filtro cobrador en RecibosPanel | ✅ Ya implementado |
| F4.3 | Reporte por cobrador con exportación | ✅ Ya implementado en `ReporteCobrador.jsx` |

---

### DECISIONES TÉCNICAS CONFIRMADAS

| Decisión | Definición |
|---|---|
| `recibos_cobro.usuario_id` | Quién *registró* en el sistema (siempre = JWT user) |
| `recibos_cobro.cobrador_id` | Quién *cobró físicamente* (auto-detectado si usuario es cobrador; seleccionable con permiso) |
| `recibos_cobro.fecha_registro` | Fecha del comprobante físico (editable, solo con permiso `COBRANZAS.ASIGNAR_COBRADOR`) |
| Panel Cobrador | Protegido con módulo `PANEL_COBRADOR` (separado de `CUENTAS_COBRAR`) |
| Promesas de pago | Atadas a `factura_cab_id` + `cuota_id` específica. Visibles en Panel Cobrador y Gestión de Cobros |
| Flexibilidad de cuotas | Cobrador ve sus asignadas por defecto; toggle "Ver todas" para ver cuotas sin cobrador o de otros |

---

## 9. RIESGOS Y ERRORES COMUNES A EVITAR

### Riesgo 1 — Duplicación de lógica cobrador
`factura_cab` tendrá `cobrador_id` directo Y existe `asignacion_facturas`. Definir claramente:
- **`factura_cab.cobrador_id`** = fuente de verdad al momento de emisión (viene de solicitud de crédito o POS)
- **`asignacion_facturas`** = asignación posterior para facturas que no tenían cobrador al emitir
- El reporte usa `COALESCE(factura_cab.cobrador_id, asignacion_facturas.cobrador_id)`

### Riesgo 2 — Autorizaciones de descuento zombie
Sin el job de expiración, un cobrador puede reutilizar una autorización antigua. Implementar el cron de expiración **antes** de exponer el endpoint de cobro con descuento en la UI.

### Riesgo 3 — Mora mal calculada por timezone
`differenceInDays` debe usar `startOfDay` con timezone de Paraguay (UTC-4). Un cobro a las 23:00 local puede computar un día extra de mora si se usa UTC directamente.

### Riesgo 4 — `dia_fijo_pago` vs `dia_cobro_semana`
- `dia_fijo_pago` existente: rango 1-28, representa **día del mes** (ej: cobrar el día 15 de cada mes)
- `dia_cobro_semana` nuevo: rango 1-7, representa **día de la semana** (ej: cobrar todos los martes)
- Son conceptos distintos. No reutilizar el campo existente.

### Riesgo 5 — Modificar fecha de recibo sin auditoría
Cualquier cambio de `fecha_registro` debe registrarse en la tabla de auditoría existente (`AuditoriaService`). Incluir: fecha anterior, fecha nueva, usuario que modificó, IP. Obligatorio para detectar fraudes.

### Riesgo 6 — Vista cobrador sin paginación en mobile
Una lista de 200+ cuotas en mobile es inutilizable. El endpoint de cuotas del cobrador debe:
- Paginar (default: 20 por página)
- Ordenar por urgencia: vencidas primero, luego por fecha_vencimiento ASC
- Por defecto filtrar por cobrador_id del usuario autenticado; con flag `verTodas=true` traer todas

### Riesgo 7 — Cuotas vencidas de días anteriores no visibles
El filtro "Hoy" no debe ocultar cuotas vencidas previas. La lógica correcta:
- Chips de vencimiento (Hoy/Mañana/Semana/etc.) filtran la fecha máxima de vencimiento
- Las cuotas vencidas (dvenccuo < hoy) siempre se incluyen independientemente del chip seleccionado
- Solo el chip "Cobrado" oculta las ya cobradas

### Riesgo 8 — cobrador_id null en cobros registrados por admin
Si admin registra cobro sin seleccionar cobrador, `cobrador_id` queda null. Impacto:
- El RecibosPanel muestra "—" en columna Cobrador ✓ (ya manejado)
- El reporte por cobrador no lo contabiliza → aceptable, es cobro directo sin cobrador de campo
- **No** forzar cobrador obligatorio — hay empresas sin cobradores de campo

---

## 10. DEPENDENCIAS ENTRE FASES

```
F1.1 (cobrador_id en facturas)
  └─ F2.6 (badge supervisor)
       └─ F2.3 (autorizaciones descuento)

F1.2 (dia_cobro_semana en solicitud)
  └─ F2.2 (vista cobrador)
       └─ F2.1 (promesas de pago)
            └─ F2.5 (recordatorio Twilio)

F3.1 (config mora)
  └─ F3.3 (cálculo en cobro)
       └─ F3.4 (desglose en recibo PDF)
            └─ F4.1 (reporte con mora)
```

---

## 11. TAREAS FUTURAS (fuera del scope actual)

- [ ] Nota de débito por intereses de mora (con implicancia fiscal / IVA)
- [ ] Módulo contable: `fecha_imputacion_fiscal` en recibos (campo ya preparado)
- [ ] App móvil nativa para cobradores (actualmente usa sistema web responsive)
- [ ] Integración WhatsApp Business API para recordatorios (Twilio ya instalado)
- [ ] Dashboard de morosidad con predicción (ML básico sobre histórico de pagos)
