# Sistema de Ofertas y Promociones

## Descripción General

Motor de ofertas flexible que soporta múltiples tipos de promociones con condiciones combinables, integrado con el sistema de facturación existente.

---

## Arquitectura por Fases

### Fase 1 — MVP
- Tipos: `descuento_porcentaje`, `descuento_monto`, `precio_especial`
- Aplicación: por producto, categoría, marca o todos
- Multi-sucursal: oferta puede limitarse a sucursales específicas
- Vigencia: fecha inicio/fin
- Evaluación automática al agregar items al carrito
- CRUD completo de ofertas
- Historial de uso por factura

### Fase 2
- Tipos: `nxm` (3x2, etc.), `combo`
- Condiciones por medio de pago
- Límites de uso (total y por cliente)
- Trazabilidad por ítem (`oferta_usos_detalle`)

### Fase 3
- Ofertas por día de semana / horario
- Condiciones avanzadas (banco, tarjeta, cliente nuevo, monto mínimo)
- Acumulabilidad entre ofertas
- Reportes y métricas de campañas

---

## Modelo de Datos — MVP (Fase 1)

```sql
-- Ofertas principales
CREATE TABLE ofertas (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    empresa_id UUID NOT NULL REFERENCES empresas(id),
    nombre VARCHAR(200) NOT NULL,
    descripcion TEXT,
    tipo VARCHAR(30) NOT NULL CHECK (tipo IN (
        'descuento_porcentaje',
        'descuento_monto',
        'precio_especial'
    )),
    valor DECIMAL(15,2) NOT NULL, -- Porcentaje, monto fijo o precio especial según tipo
    fecha_inicio TIMESTAMP NOT NULL,
    fecha_fin TIMESTAMP NOT NULL,
    prioridad INTEGER DEFAULT 0, -- Mayor = más prioridad
    active BOOLEAN DEFAULT true,
    created_by UUID REFERENCES usuario(id),
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- A qué productos/categorías/marcas aplica la oferta
CREATE TABLE oferta_aplicacion (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    oferta_id UUID NOT NULL REFERENCES ofertas(id) ON DELETE CASCADE,
    aplica_a VARCHAR(20) NOT NULL CHECK (aplica_a IN ('producto', 'categoria', 'marca', 'todos')),
    referencia_id UUID, -- ID del producto/categoría/marca. NULL si aplica_a = 'todos'
    excluir BOOLEAN DEFAULT false -- true = excluir este item de la oferta
);

-- En qué sucursales aplica (si no hay registros, aplica en todas)
CREATE TABLE oferta_sucursales (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    oferta_id UUID NOT NULL REFERENCES ofertas(id) ON DELETE CASCADE,
    sucursal_id UUID NOT NULL REFERENCES empresas_sucursales(id),
    UNIQUE(oferta_id, sucursal_id)
);

-- Historial de uso de ofertas (una entrada por factura donde se aplicó)
CREATE TABLE oferta_usos (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    oferta_id UUID NOT NULL REFERENCES ofertas(id),
    factura_id UUID NOT NULL REFERENCES factura_cab(id),
    cliente_id UUID REFERENCES clientes(id),
    monto_descuento DECIMAL(15,2) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Índices
CREATE INDEX idx_ofertas_empresa ON ofertas(empresa_id);
CREATE INDEX idx_ofertas_vigencia ON ofertas(fecha_inicio, fecha_fin);
CREATE INDEX idx_ofertas_active ON ofertas(empresa_id, active);
CREATE INDEX idx_oferta_aplicacion_oferta ON oferta_aplicacion(oferta_id);
CREATE INDEX idx_oferta_aplicacion_ref ON oferta_aplicacion(aplica_a, referencia_id);
CREATE INDEX idx_oferta_usos_oferta ON oferta_usos(oferta_id);
CREATE INDEX idx_oferta_usos_factura ON oferta_usos(factura_id);
```

---

## Modelo de Datos — Fase 2

```sql
-- Agregar tipos a ofertas
ALTER TABLE ofertas DROP CONSTRAINT ofertas_tipo_check;
ALTER TABLE ofertas ADD CONSTRAINT ofertas_tipo_check CHECK (tipo IN (
    'descuento_porcentaje',
    'descuento_monto',
    'precio_especial',
    'nxm',
    'combo'
));

-- Campos NxM en ofertas
ALTER TABLE ofertas ADD COLUMN valor_n INTEGER; -- Lleva N
ALTER TABLE ofertas ADD COLUMN valor_m INTEGER; -- Paga M

-- Límites de uso
ALTER TABLE ofertas ADD COLUMN limite_uso_total INTEGER;
ALTER TABLE ofertas ADD COLUMN limite_uso_cliente INTEGER;

-- Medios de pago válidos para la oferta (vinculado a medio_pago existente)
CREATE TABLE oferta_medios_pago (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    oferta_id UUID NOT NULL REFERENCES ofertas(id) ON DELETE CASCADE,
    medio_pago_id UUID NOT NULL REFERENCES medio_pago(id),
    UNIQUE(oferta_id, medio_pago_id)
);

-- Productos en combo
CREATE TABLE oferta_combo_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    oferta_id UUID NOT NULL REFERENCES ofertas(id) ON DELETE CASCADE,
    producto_id UUID NOT NULL REFERENCES productos(id),
    cantidad INTEGER DEFAULT 1,
    precio_combo DECIMAL(15,2)
);

-- Trazabilidad por ítem de factura
CREATE TABLE oferta_usos_detalle (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    oferta_uso_id UUID NOT NULL REFERENCES oferta_usos(id) ON DELETE CASCADE,
    factura_det_id UUID NOT NULL REFERENCES factura_det(id),
    producto_id UUID NOT NULL REFERENCES productos(id),
    cantidad_afectada INTEGER DEFAULT 1,
    descuento_unitario DECIMAL(15,2) NOT NULL,
    descuento_total DECIMAL(15,2) NOT NULL
);
```

---

## Modelo de Datos — Fase 3

```sql
-- Campos de horario/día en ofertas
ALTER TABLE ofertas ADD COLUMN dias_semana INTEGER[]; -- 0=domingo, 6=sábado
ALTER TABLE ofertas ADD COLUMN hora_inicio TIME;
ALTER TABLE ofertas ADD COLUMN hora_fin TIME;
ALTER TABLE ofertas ADD COLUMN acumulable BOOLEAN DEFAULT false;

-- Condiciones avanzadas de la oferta
CREATE TABLE oferta_condiciones (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    oferta_id UUID NOT NULL REFERENCES ofertas(id) ON DELETE CASCADE,
    tipo_condicion VARCHAR(30) NOT NULL CHECK (tipo_condicion IN (
        'cantidad_minima',
        'monto_minimo',
        'cliente_tipo',
        'cliente_nuevo',
        'cuotas_minimas'
    )),
    operador VARCHAR(10) DEFAULT '=' CHECK (operador IN ('=', '>=', '<=', '>', '<', 'in', 'not_in')),
    valor VARCHAR(200) NOT NULL,
    orden INTEGER DEFAULT 0
);
```

---

## Tipos de Ofertas Soportadas

| Tipo | Fase | Ejemplo |
|------|------|---------|
| Porcentaje | MVP | 20% OFF en lácteos |
| Monto fijo | MVP | Gs. 5.000 de descuento en shampoo X |
| Precio especial | MVP | Heladera a Gs. 500.000 (era Gs. 600.000) |
| NxM | F2 | 3x2 en bebidas |
| Combo | F2 | Shampoo + Acondicionador = Gs. 15.000 |
| Por cantidad | F3 | Llevando 6+ unidades, 15% OFF |
| Por pago | F2 | 10% con Visa, 15% efectivo |
| Por día | F3 | Martes de farmacia 25% OFF |
| Por horario | F3 | Happy Hour 18-20hs 30% OFF |

---

## Motor de Evaluación

### Flujo principal

```
1. Cajero agrega/modifica item en carrito
2. Motor busca ofertas activas y vigentes para la empresa/sucursal
3. Filtra por aplicabilidad (producto, categoría, marca, todos)
4. Filtra exclusiones
5. Ordena por prioridad (mayor primero)
6. Aplica la mejor oferta (o la de mayor descuento)
7. Retorna precio original vs precio con oferta para cada item
```

### Endpoint principal

```
POST /ofertas/evaluar
Body: { items: [{ producto_id, cantidad, precio_unitario }], sucursal_id }
Response: { items: [{ producto_id, oferta_id?, precio_original, precio_final, descuento, nombre_oferta? }] }
```

### Consideraciones
- En MVP no se acumulan ofertas: solo aplica la de mayor prioridad (o mayor descuento si misma prioridad)
- `precio_especial` tiene prioridad sobre `descuento_porcentaje` y `descuento_monto` si ambos aplican al mismo producto
- Las ofertas inactivas (`active = false`) o fuera de vigencia nunca se evalúan
- Si no hay `oferta_sucursales` para una oferta, aplica en todas las sucursales

---

## API — MVP

### CRUD Ofertas
| Método | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/ofertas` | Listar ofertas de la empresa (paginado, filtros) |
| GET | `/ofertas/:id` | Detalle de una oferta |
| POST | `/ofertas` | Crear oferta |
| PATCH | `/ofertas/:id` | Actualizar oferta |
| DELETE | `/ofertas/:id` | Eliminar oferta (soft delete: active=false) |
| POST | `/ofertas/evaluar` | Evaluar ofertas para un carrito de items |
| GET | `/ofertas/:id/usos` | Historial de uso de una oferta |
| GET | `/ofertas/activas` | Ofertas activas y vigentes para el POS |

---

## Integración con Facturación

Al confirmar una factura:
1. Se llama al motor de evaluación con los items finales
2. Se aplican los descuentos al `factura_det` (campo de descuento por item)
3. Se registra en `oferta_usos` la oferta aplicada con el monto de descuento
4. El recibo/factura muestra el descuento aplicado por oferta
