# Plan: Módulo Ventas Mayoristas

**Fecha:** 2026-03-17
**Estado:** En desarrollo
**Stack:** React + styled-components (frontend) / NestJS + Prisma (backend)

---

## 1. Qué está construido

| Componente | Estado | Notas |
|---|---|---|
| `PedidosMayoristas.jsx` | ✅ | Layout, estado global del flujo |
| `ClienteSelector.jsx` | ✅ | Búsqueda con debounce, keyboard nav |
| `CatalogPanel.jsx` | ✅ | Búsqueda + frecuentes + top ventas + mapa de ofertas |
| `CartPanel.jsx` | ✅ | Carrito, edición inline, confirmar/cancelar |
| `ClienteInfoPanel.jsx` | ✅ | Crédito, últimas compras, ofertas, condiciones |
| `PedidoHeader.jsx` | ✅ | Estado del pedido, alerta crédito bloqueado |
| `pedidos.service.js` | ✅ | CRUD completo + flujo de estados + contexto negociación |

---

## 2. Modelos de datos clave (backend)

### `pedidos`
```
id, empresa_id, sucursal_id, cliente_id, vendedor_id
moneda_id, condicion_operacion_id, lista_precios_id
numero_pedido, estado (borrador→confirmado→en_caja→facturado/cancelado/vencido)
subtotal, total_descuento, total_iva, total
observaciones, referencia_interna, fecha_vencimiento
```

### `condiciones_pago`
```
id, descripcion ("Contado", "30 días", "60 días", "Cuotas", etc.)
dias_plazo, cuotas, intervalo_dias
cuota_inicial (bool) ← SI = el vendedor recibe una entrega parcial en el pedido
cod_condicion_venta (1=Contado, 2=Crédito)  ← SIFEN
es_default, activo, aplica_a (VENTAS / COMPRAS / AMBOS)
```

### `pedido_detalle`
```
cantidad, precio_lista, precio_negociado
descuento_porcentaje, descuento_monto
subtotal, monto_iva, observacion
```

> **Nota backend:** El modelo `pedidos` tiene `condicion_operacion_id` (código SIFEN genérico),
> pero NO tiene `condicion_pago_id`. Se necesita agregar esta FK al schema de Prisma y al
> servicio para vincular la condición de pago del negocio al pedido.

---

## 3. Escenarios de negociación a contemplar

| # | Escenario | Condición | Lo que debe verse en caja |
|---|-----------|-----------|--------------------------|
| A | Venta al contado | `cod_condicion_venta=1` | Cobrar 100% en el momento |
| B | Venta a crédito | `dias_plazo=30/60/90` | "Vence en X días" — no cobrar ahora |
| C | Crédito con entrega inicial | `cuota_inicial=true` | "Cobrar entrega inicial: ₲ X.XXX" |
| D | Venta en cuotas | `cuotas=3, intervalo_dias=30` | "3 cuotas de ₲ X" — primera al facturar |
| E | Moneda extranjera | `moneda_id=USD/BRL` | Mostrar cambio y equivalente en PYG |
| F | Descuento por volumen | `descuento_porcentaje>0` en ítem | Precio tachado + precio negociado |
| G | Descuento global | `%` sobre todo el pedido | Aplicar al total antes de IVA |
| H | Cliente con crédito bloqueado | `credito.bloqueado=true` | Solo contado habilitado |
| I | Cliente sin límite configurado | `credito.limite=null` | Ninguna restricción de monto |

---

## 4. Mejoras de UX al flujo de negociación existente

### 4.1 CartPanel — agregar sección "Condiciones del pedido"

Debajo de los ítems, antes de los totales, agregar un bloque colapsable:

```
┌─ CONDICIONES DEL PEDIDO ──────────────────────┐
│  Condición de pago:  [Contado ▼]              │
│  Moneda:             [Guaraní (₲) ▼]          │
│  Observaciones:      [campo de texto libre]   │
│  Referencia interna: [campo opcional]         │
└───────────────────────────────────────────────┘
```

- El selector de condición de pago carga de `/v1/condiciones-pago` (filtrado `aplica_a=VENTAS`)
- Si la condición tiene `cuota_inicial=true`, mostrar badge amarillo: **"Requiere entrega inicial"**
- Si la condición tiene `cod_condicion_venta=1` (contado) y el cliente tiene crédito bloqueado,
  ya estaba en solo-contado de todas formas — no mostrar opciones de crédito.

### 4.2 ClienteInfoPanel — indicador de crédito visual

Actualmente muestra texto plano. Mejorar con:

```
CRÉDITO
├─ Límite:     ₲ 5.000.000
├─ Usado:      ₲ 1.200.000  [████░░░░░░] 24%
├─ Disponible: ₲ 3.800.000
└─ Estado:     ● Activo
```

- Barra de progreso con colores: verde <60%, amarillo 60-85%, rojo >85%
- Si `bloqueado=true`: banner rojo con "Crédito bloqueado — solo contado"
- Si pedido actual supera disponible: advertencia inline en el carrito

### 4.3 PedidoHeader — mostrar condición seleccionada

Agregar chip pequeño junto al número de pedido:
```
PED-0000023 [BORRADOR] [30 días crédito] [Derlis Dacosta · 6264957-4]
```

### 4.4 CatalogPanel — mejoras menores

- Al agregar un producto que ya está en el carrito, hacer flash/highlight al ítem existente
  en lugar de agregar duplicado (o preguntar si sumar cantidad)
- Mostrar stock disponible en la card del producto (si el backend lo retorna)
- En la tab "Frecuentes" mostrar el precio negociado histórico si existe en el contexto

### 4.5 Cancelación con motivo

Actualmente `handleCancelar` siempre envía `"Cancelado por vendedor"`.
Agregar un mini-modal de confirmación con campo de motivo:

```
┌─ Cancelar pedido ─────────────────────┐
│  ¿Motivo de cancelación?              │
│  [campo de texto]                     │
│  [Cancelar] [Confirmar cancelación]   │
└───────────────────────────────────────┘
```

### 4.6 Descuento global al pedido

Agregar campo en la sección de totales del CartPanel:

```
Subtotal          ₲ 1.500.000
Desc. negociado:  [-5%] → -₲ 75.000
IVA incluido      ₲ 135.000
─────────────────────────────
TOTAL             ₲ 1.425.000
```

- El descuento global se aplica como `descuento_porcentaje` en el `PUT /pedidos/:id`
- Aplicar antes del IVA

---

## 5. Nuevas vistas / páginas

### 5.1 Lista de pedidos mayoristas (`/mayorista/pedidos`)

Vista en tabla/cards para el vendedor:

```
┌─────────────────────────────────────────────────────────────────┐
│ [Buscar por cliente/número...]  [Estado ▼]  [Fecha ▼]  [+ Nuevo] │
├──────────┬──────────────────┬──────────────┬───────┬────────────┤
│ Número   │ Cliente          │ Total        │ Estado│ Acciones   │
├──────────┼──────────────────┼──────────────┼───────┼────────────┤
│ PED-023  │ Derlis Dacosta   │ ₲ 1.425.000  │ CONF. │ [Ver][Caja]│
│ PED-022  │ Empresa XYZ      │ ₲ 3.200.000  │ BORRA.│ [Continuar]│
│ PED-021  │ ...              │ ...          │ FACT. │ [Ver]      │
└──────────┴──────────────────┴──────────────┴───────┴────────────┘
```

Filtros: estado, vendedor, fecha desde/hasta, cliente
Acciones rápidas:
- **Borrador**: "Continuar" → abre el flujo de negociación con ese pedido cargado
- **Confirmado**: "Enviar a caja" → `PATCH /pedidos/:id/en-caja`
- **Facturado**: "Ver factura"

### 5.2 Cargar pedido existente en el flujo

Al hacer clic en "Continuar" desde la lista, el flujo de `PedidosMayoristas` debe:
1. Pre-cargar el cliente
2. Pre-cargar el pedido (ítems, condición, observaciones)
3. Permitir seguir editando (si está en `borrador`)
4. Mostrar en modo solo lectura si está en otro estado

Requiere:
- Pasar `pedidoId` como query param o state en el router
- Agregar `useEffect` en `PedidosMayoristas` que llame a `getPedido(id)` si viene ese param

---

## 6. Flujo del cajero — integración con POS

### 6.1 Widget en POS: "Pedidos pendientes de caja"

Dentro del POS (ya existe `getPedidosPendientesCaja`), mostrar un panel/botón que indique:
```
⏳ 3 pedidos esperando en caja
```

Al abrirlo, listar los pedidos en estado `confirmado` o `en_caja` con:
- Número, cliente, total
- **Condición de pago** (importante para que el cajero sepa qué cobrar)
- Badge especial si tiene `cuota_inicial=true`: **"Cobrar entrega inicial"**

### 6.2 Resumen de cobro al procesar pedido en caja

Cuando el cajero abre un pedido para facturarlo, mostrar un resumen claro:

**Escenario contado:**
```
┌─ Pedido PED-023 ─ Derlis Dacosta ──────────┐
│ CONDICIÓN: Contado                          │
│ Total a cobrar ahora: ₲ 1.425.000          │
└─────────────────────────────────────────────┘
```

**Escenario crédito con entrega inicial:**
```
┌─ Pedido PED-024 ─ Empresa XYZ ─────────────┐
│ CONDICIÓN: 60 días crédito + entrega inicial│
│ Cobrar ahora (entrega inicial): ₲ 500.000  │
│ Saldo a crédito:               ₲ 2.700.000 │
│ Vence: 16 mayo 2026                        │
└─────────────────────────────────────────────┘
```

**Escenario crédito puro:**
```
┌─ Pedido PED-025 ─ Dist. Norte ─────────────┐
│ CONDICIÓN: 30 días crédito                  │
│ No cobrar ahora — registrar en cuenta       │
│ Vence: 16 abril 2026                       │
└─────────────────────────────────────────────┘
```

**Escenario cuotas:**
```
┌─ Pedido PED-026 ─ Comercial Sur ───────────┐
│ CONDICIÓN: 3 cuotas de 30 días              │
│ 1ra cuota (hoy):  ₲ 475.000               │
│ 2da cuota (abr):  ₲ 475.000               │
│ 3ra cuota (may):  ₲ 475.000               │
│ Total:            ₲ 1.425.000             │
└─────────────────────────────────────────────┘
```

### 6.3 Flujo completo en caja

```
Vendedor confirma pedido
       ↓
Cajero ve pedido en cola
       ↓
Cajero abre pedido → ve resumen de cobro (según condición de pago)
       ↓
    ┌──────────────────────────────┐
    │ ¿Tiene entrega inicial?      │
    │  SÍ → cobrar monto parcial   │
    │  NO + Contado → cobrar total │
    │  NO + Crédito → solo facturar│
    └──────────────────────────────┘
       ↓
Generar factura → PATCH /pedidos/:id/facturado
       ↓
Si es crédito: crear cuenta_cobrar automáticamente (backend)
```

---

## 7. Backend — cambios requeridos

| # | Cambio | Prioridad |
|---|--------|-----------|
| 1 | Agregar `condicion_pago_id` FK a `pedidos` (Prisma migration) | Alta |
| 2 | Agregar `condicion_pago_id` al `CreatePedidoDto` y `UpdatePedidoDto` | Alta |
| 3 | Retornar `condicion_pago` con sus campos en `GET /pedidos/:id` | Alta |
| 4 | `GET /condiciones-pago` ya existe — verificar que filtre por `aplica_a=VENTAS` | Media |
| 5 | Al `PATCH /facturado`: si `condicion_pago.cuota_inicial=true`, crear cuota inicial separada | Alta |
| 6 | Agregar `descuento_global_porcentaje` a pedidos (o calcularlo desde ítems) | Media |
| 7 | Retornar `saldo_credito_disponible` en `GET /contexto-negociacion/:clienteId` | Media |

---

## 8. Orden de implementación sugerido

### Sprint 1 — Completar el flujo de negociación (frontend)
1. [ ] Agregar servicio `condiciones-pago.service.js` en frontend
2. [ ] Agregar selector de condición de pago en `CartPanel` (sección condiciones)
3. [ ] Agregar campo observaciones en `CartPanel`
4. [ ] Mejorar indicador de crédito con barra en `ClienteInfoPanel`
5. [ ] Modal de cancelación con motivo
6. [ ] Mostrar condición de pago en `PedidoHeader`

### Sprint 2 — Soporte de variantes en el catálogo mayorista
7. [ ] Backend: actualizar `GET /productos/buscar` para incluir `es_padre`, variante_valores con color (V1)
8. [ ] Backend: crear `GET /productos/:id/variantes-disponibles` (V2)
9. [ ] Frontend: en `CatalogPanel`, detectar productos padre y bloquear adición directa
10. [ ] Frontend: crear componente `VarianteSelectorModal.jsx` — filtros por atributo, listado de variantes con stock/precio
11. [ ] Frontend: integrar selector de presentación después de elegir variante
12. [ ] Frontend: actualizar visualización de ítems en `CartPanel` — badges de atributos + presentación

### Sprint 3 — Lista y carga de pedidos
13. [ ] Crear `PedidosListaMayorista.jsx` (`/mayorista/pedidos`)
14. [ ] Agregar ruta en router
15. [ ] Cargar pedido existente en flujo de negociación (param en URL)

### Sprint 4 — Backend + flujo cajero
16. [ ] Migration: agregar `condicion_pago_id` a pedidos (backend)
17. [ ] Widget "Pedidos pendientes" en POS
18. [ ] Resumen de cobro según condición de pago en POS (contado / crédito / cuota inicial / cuotas)
19. [ ] Flujo de facturación desde pedido (con lógica de cuota inicial)

### Sprint 5 — Mejoras avanzadas
20. [ ] Descuento global al pedido
21. [ ] Edición masiva de variantes en inventario (V5)
22. [ ] Historial del pedido (UI para `GET /pedidos/:id/historial`)
23. [ ] Impresión / PDF del pedido
24. [ ] Validación de límite de crédito en tiempo real

---

## 9. Archivos a crear / modificar (frontend)

| Archivo | Acción | Sprint |
|---|---|---|
| `src/api/condiciones-pago.service.js` | Crear | 1 |
| `src/components/pedidos/CartPanel.jsx` | Modificar (condiciones, observaciones, modal cancelar, badges variante) | 1+2 |
| `src/components/pedidos/ClienteInfoPanel.jsx` | Modificar (barra crédito) | 1 |
| `src/components/pedidos/PedidoHeader.jsx` | Modificar (chip condición pago) | 1 |
| `src/components/pedidos/VarianteSelectorModal.jsx` | Crear (selector de variante con filtros por atributo) | 2 |
| `src/components/pedidos/CatalogPanel.jsx` | Modificar (detectar padre, abrir modal, selector presentación) | 2 |
| `src/pages/PedidosListaMayorista.jsx` | Crear | 3 |
| `src/pages/PedidosMayoristas.jsx` | Modificar (cargar pedido existente desde param) | 3 |
| `src/components/pedidos/PedidoResumenCaja.jsx` | Crear | 4 |
| `src/components/organismos/POSDesign/PedidosPendientes.jsx` | Crear | 4 |
| `src/routers/routes.jsx` | Modificar (nueva ruta lista mayorista) | 3 |

---

## 10. Variantes de productos — arquitectura y comportamiento en el módulo

### 10.1 Cómo está construido el sistema de variantes

El sistema **ya implementa variantes como productos independientes** con jerarquía padre-hijo:

```
productos (es_padre = true)          ← Producto base "Remera"
  └── productos (producto_padre_id)  ← Variante "Remera - Rojo / M"
  └── productos (producto_padre_id)  ← Variante "Remera - Rojo / L"
  └── productos (producto_padre_id)  ← Variante "Remera - Azul / M"
```

Cada variante hijo es un **registro completo en `productos`** con:
- `codigo_barra` propio → lectura de pistola de código de barras funciona directo
- `cod_producto` (SKU) propio → identificación interna independiente
- `precio` propio (puede diferir del padre)
- `precio_costo` propio → margen calculable por variante
- `maneja_inventario` → stock gestionado por variante

Los atributos se vinculan via `producto_variante_valores`:
```
producto_variante_valores
  ├── producto_id = id_de_la_variante
  └── atributo_valor_id → atributo_valor { valor: "Rojo", color_hex: "#FF0000" }
                                            atributo { nombre: "Color" }
```

### 10.2 Generación de variantes (estado actual)

El flujo en `VariantesSection.jsx` ya permite:
1. Seleccionar atributos (Color, Talle, Material, etc.) — globales por empresa
2. Elegir valores por atributo (múltiples chips)
3. Preview del producto cartesiano: 2 colores × 3 talles = 6 variantes
4. Editar precio, código de barras y SKU por variante antes de generar
5. Llamada a `POST /productos/:id/variantes` → crea N productos hijos

### 10.3 Gaps identificados a resolver

| # | Gap | Impacto | Prioridad |
|---|-----|---------|-----------|
| G1 | En el CatalogPanel mayorista, productos padre (`es_padre=true`) aparecen como seleccionables directamente — el vendedor podría agregar el producto base sin elegir variante | Pedido con producto "genérico" sin stock real | **Alta** |
| G2 | No hay edición masiva de variantes después de generadas (precio, código) | Para cambiar precio de todas las variantes rojas hay que editar 1 a 1 | Media |
| G3 | Al cambiar `es_padre` a false no se limpian/advierten los hijos | Variantes huérfanas en BD | Media |
| G4 | Duplicados: se puede generar el mismo combo dos veces | Stock y catálogo duplicado | Media |
| G5 | Sin restricción de unicidad en `cod_producto` entre variantes | Ambigüedad en búsqueda por SKU | Media |
| G6 | El flag `es_default` en presentaciones no tiene unicidad por producto | Comportamiento ambiguo | Baja |
| G7 | Presentaciones + variantes no están integradas (Gap 6 del análisis) | Ver sección 10.5 | Alta (mayorista) |

### 10.4 Comportamiento en el CatalogPanel mayorista

**Regla fundamental:**
> Si un producto tiene `es_padre = true`, **nunca se agrega directamente al carrito**.
> Se abre un selector de variante intermedio.

**Flujo propuesto:**

```
Vendedor busca "Remera" → aparece en resultados
  ↓
[Remera]  [● 3 variantes]  ₲ 45.000 base
  ↓  (click / Enter)
┌─ Seleccionar variante ──────────────────────┐
│  Color:  [● Rojo] [● Azul] [● Negro]        │
│  Talle:  [S] [M] [L] [XL]                  │
│                                             │
│  > Remera - Rojo / M    ₲ 45.000  [+ Agregar]│
│  > Remera - Rojo / L    ₲ 45.000  [+ Agregar]│
│  > Remera - Azul / M    ₲ 48.000  [+ Agregar]│
└─────────────────────────────────────────────┘
```

**Consideraciones de UX:**
- Mostrar stock disponible por variante (si `maneja_inventario = true`)
- Mostrar precio negociado histórico si el contexto lo tiene para esa variante
- Permitir agregar múltiples variantes del mismo padre en un solo paso (checkbox multi-select)
- Si el producto NO tiene variantes (es hijo o producto simple), se agrega directo como hoy

**En la búsqueda de resultados:**
- Producto simple → mostrar normal
- Producto padre → badge "N variantes", click abre selector
- Producto variante (si aparece por código de barras o SKU exacto) → agregar directo sin pasar por selector

### 10.5 Presentaciones + Variantes (Gap 7)

En ventas mayoristas es común: "quiero 5 cajas de Remera Roja M" donde:
- La variante es `Remera - Rojo / M`
- La presentación es `Caja x12` con `factor_conversion = 12`

El modelo `pedido_detalle` ya tiene `presentacion_id`. Lo que falta es:
1. En el CatalogPanel, después de elegir la variante, mostrar las presentaciones disponibles de ese producto (o del padre)
2. Actualizar el `factor_conversion` y `unidad_medida` en el ítem del pedido según la presentación

**Flujo completo:**
```
Producto padre → [selector de variante] → [selector de presentación] → agregar al carrito
```

Si el producto no tiene presentaciones configuradas, saltear ese paso.

### 10.6 Visualización en el carrito con variantes

En `CartPanel`, cada ítem con variante debe mostrar la jerarquía:

```
┌────────────────────────────────────────────────────┐
│ Remera                                             │
│ ● Rojo  |  M           [Caja x12]                 │
│                                                    │
│ [-] 3 [+]  CAJAS        ₲ 45.000   ₲ 135.000 sub │
└────────────────────────────────────────────────────┘
```

- Nombre del padre como título (o nombre de la variante completo si no hay padre visible)
- Badges de atributos en segunda línea (con color circle si tiene `color_hex`)
- Badge de presentación si aplica

### 10.7 Backend — cambios adicionales por variantes

| # | Cambio | Prioridad |
|---|--------|-----------|
| V1 | En `GET /productos/buscar`, incluir `es_padre`, `producto_padre_id`, y `producto_variante_valores` con atributo+valor+color en la respuesta | Alta |
| V2 | Nuevo endpoint `GET /productos/:id/variantes-disponibles` — retorna variantes activas con stock y precio | Alta |
| V3 | Agregar unicidad de `cod_producto` por empresa en Prisma (o al menos validación en servicio) | Media |
| V4 | Validar en `generateVariantes` que no exista ya una variante con el mismo set de atributo_valor_ids | Media |
| V5 | Endpoint de edición masiva de variantes: `PATCH /productos/:id/variantes/bulk` | Media |

---

## 11. UX — principios de diseño para el vendedor

- **Velocidad**: F2 para buscar, Tab para navegar, Enter para agregar — el vendedor no usa el mouse
- **Contexto visible siempre**: el panel derecho siempre muestra el historial del cliente para poder negociar con datos
- **Feedback inmediato**: cada cambio de precio/cantidad se guarda al instante (no hay botón "guardar")
- **Estado claro**: el cajero nunca debe adivinar qué cobrar — el pedido debe llegar con instrucciones de cobro explícitas
- **Jerarquía de producto natural**: el vendedor piensa en "Remera Roja M", no en IDs ni SKUs internos
- **Sin offline**: al perder conexión mostrar banner de alerta, deshabilitar acciones de escritura

---

*Ver también: `mejoras-ecommerce-mayorista.md`, `plan-cuotas-flujo-pos-admin.md`*
