# Plan de Trabajo — Módulo de Renovaciones y Contratos

> Sistema completo de gestión de servicios, clientes, contratos y notificaciones automáticas de vencimiento.

---

## Índice

1. [Visión General](#1-visión-general)
2. [Arquitectura de Datos](#2-arquitectura-de-datos)
3. [Módulos del Sistema](#3-módulos-del-sistema)
4. [Flujo de Trabajo](#4-flujo-de-trabajo)
5. [Sistema de Notificaciones](#5-sistema-de-notificaciones)
6. [Plantillas de Correo](#6-plantillas-de-correo)
7. [Configuración SMTP](#7-configuración-smtp)
8. [Plan de Implementación por Fases](#8-plan-de-implementación-por-fases)
9. [Estructura de Archivos](#9-estructura-de-archivos)
10. [API Endpoints](#10-api-endpoints)

---

## 1. Visión General

El módulo de **Renovaciones** permite a la empresa gestionar el ciclo de vida completo de sus servicios vendidos a clientes: creación de catálogo, asignación a clientes con fechas de vencimiento, envío automático y manual de correos de cobro/renovación, y configuración total de notificaciones programadas.

### Capacidades principales

| Capacidad | Descripción |
|-----------|-------------|
| Catálogo de servicios | Crear servicios con precio, tipo (renovable/único), periodicidad |
| Gestión de clientes | CRUD completo de clientes con datos de contacto |
| Contratos / Asignaciones | Relacionar clientes con servicios, definir fechas de inicio y vencimiento |
| Agrupación de correos | Si varios servicios vencen la misma fecha → un solo correo con el total |
| Notificaciones programadas | Configurar N días antes del vencimiento, con cron interno |
| Editor de plantillas | Editor visual con preview en tiempo real |
| Configuración SMTP | Panel para cambiar servidor de correo sin reiniciar |

---

## 2. Arquitectura de Datos

### Modelo Entidad-Relación

```
clientes ─────────── contratos ─────────── servicios
    │                    │                     │
    │                    ├── fecha_inicio       ├── tipo: renovable / unico
    │                    ├── fecha_vencimiento  ├── precio
    │                    ├── estado             ├── periodicidad (mensual/anual/etc)
    │                    └── notas              └── descripcion
    │
    └── email, telefono, empresa, ...

contratos ──── notificaciones_log
                    ├── fecha_envio
                    ├── tipo (aviso / vencimiento / recordatorio)
                    └── estado (enviado / fallido / pendiente)

smtp_config (tabla singleton)
plantillas_correo
notificacion_reglas (días antes, activo, plantilla_id)
```

### Tablas SQL

```sql
-- Servicios / Productos
CREATE TABLE servicios (
    id              BIGSERIAL PRIMARY KEY,
    created_at      TIMESTAMPTZ,
    updated_at      TIMESTAMPTZ,
    deleted_at      TIMESTAMPTZ,
    nombre          TEXT NOT NULL,
    descripcion     TEXT,
    precio          NUMERIC(12,2) NOT NULL DEFAULT 0,
    moneda          TEXT NOT NULL DEFAULT 'COP',
    tipo            TEXT NOT NULL DEFAULT 'renovable', -- 'renovable' | 'unico'
    periodicidad    TEXT,                              -- 'mensual' | 'trimestral' | 'semestral' | 'anual' | NULL
    activo          BOOLEAN NOT NULL DEFAULT true
);

-- Clientes
CREATE TABLE clientes (
    id              BIGSERIAL PRIMARY KEY,
    created_at      TIMESTAMPTZ,
    updated_at      TIMESTAMPTZ,
    deleted_at      TIMESTAMPTZ,
    nombre          TEXT NOT NULL,
    empresa         TEXT,
    email           TEXT NOT NULL,
    email_cc        TEXT,             -- correos adicionales separados por coma
    telefono        TEXT,
    documento       TEXT,
    notas           TEXT,
    activo          BOOLEAN NOT NULL DEFAULT true
);

-- Contratos / Asignaciones
CREATE TABLE contratos (
    id                  BIGSERIAL PRIMARY KEY,
    created_at          TIMESTAMPTZ,
    updated_at          TIMESTAMPTZ,
    deleted_at          TIMESTAMPTZ,
    cliente_id          BIGINT NOT NULL REFERENCES clientes(id),
    servicio_id         BIGINT NOT NULL REFERENCES servicios(id),
    fecha_inicio        DATE NOT NULL,
    fecha_vencimiento   DATE NOT NULL,
    precio_acordado     NUMERIC(12,2),  -- puede diferir del precio base
    estado              TEXT NOT NULL DEFAULT 'activo', -- 'activo' | 'vencido' | 'cancelado' | 'renovado'
    auto_renovar        BOOLEAN NOT NULL DEFAULT false,
    notas               TEXT
);

-- Reglas de notificación (configurable por días antes del vencimiento)
CREATE TABLE notificacion_reglas (
    id              BIGSERIAL PRIMARY KEY,
    created_at      TIMESTAMPTZ,
    updated_at      TIMESTAMPTZ,
    nombre          TEXT NOT NULL,          -- ej: "Aviso 30 días antes"
    dias_antes      INT NOT NULL,           -- ej: 30, 15, 7, 1
    plantilla_id    BIGINT REFERENCES plantillas_correo(id),
    activo          BOOLEAN NOT NULL DEFAULT true,
    aplica_a        TEXT NOT NULL DEFAULT 'todos' -- 'todos' | 'renovable' | 'unico'
);

-- Plantillas de correo
CREATE TABLE plantillas_correo (
    id              BIGSERIAL PRIMARY KEY,
    created_at      TIMESTAMPTZ,
    updated_at      TIMESTAMPTZ,
    nombre          TEXT NOT NULL,
    asunto          TEXT NOT NULL,
    cuerpo_html     TEXT NOT NULL,   -- soporta variables: {{.ClienteNombre}}, {{.Servicios}}, {{.Total}}, etc.
    tipo            TEXT NOT NULL DEFAULT 'renovacion' -- 'renovacion' | 'vencimiento' | 'pago' | 'personalizado'
);

-- Log de notificaciones enviadas
CREATE TABLE notificaciones_log (
    id              BIGSERIAL PRIMARY KEY,
    created_at      TIMESTAMPTZ,
    cliente_id      BIGINT REFERENCES clientes(id),
    regla_id        BIGINT REFERENCES notificacion_reglas(id),
    contratos_ids   TEXT,           -- JSON array de IDs agrupados
    fecha_envio     TIMESTAMPTZ,
    estado          TEXT NOT NULL DEFAULT 'pendiente', -- 'enviado' | 'fallido' | 'pendiente'
    error_msg       TEXT,
    asunto          TEXT,
    preview_html    TEXT            -- copia del correo enviado
);

-- Configuración SMTP (singleton, solo 1 fila activa)
CREATE TABLE smtp_config (
    id              BIGSERIAL PRIMARY KEY,
    updated_at      TIMESTAMPTZ,
    host            TEXT NOT NULL,
    port            INT NOT NULL DEFAULT 587,
    username        TEXT NOT NULL,
    password        TEXT NOT NULL,  -- almacenado cifrado (AES-256)
    encryption      TEXT NOT NULL DEFAULT 'tls', -- 'tls' | 'ssl' | 'none'
    from_address    TEXT NOT NULL,
    from_name       TEXT NOT NULL,
    activo          BOOLEAN NOT NULL DEFAULT true
);
```

---

## 3. Módulos del Sistema

### 3.1 Catálogo de Servicios (`/app/servicios`)

**Funcionalidades:**
- Listar servicios con filtro por tipo y estado
- Crear/editar/eliminar servicio
- Campos: nombre, descripción, precio, moneda, tipo (renovable | único), periodicidad
- Badge visual diferenciando renovables de únicos
- Indicador de cuántos contratos activos tiene cada servicio

**Campos del formulario:**

| Campo | Tipo | Requerido | Notas |
|-------|------|-----------|-------|
| Nombre | texto | ✅ | |
| Descripción | textarea | ❌ | |
| Precio | decimal | ✅ | |
| Moneda | select | ✅ | COP / USD / EUR |
| Tipo | radio | ✅ | Renovable / Único |
| Periodicidad | select | si renovable | Mensual / Trimestral / Semestral / Anual |
| Activo | toggle | ✅ | |

---

### 3.2 Clientes (`/app/clientes`)

**Funcionalidades:**
- CRUD completo de clientes
- Vista detalle del cliente con todos sus contratos activos/vencidos
- Historial de correos enviados al cliente
- Múltiples emails CC por cliente

**Campos del formulario:**

| Campo | Tipo | Requerido |
|-------|------|-----------|
| Nombre completo | texto | ✅ |
| Empresa / Razón social | texto | ❌ |
| Email principal | email | ✅ |
| Emails CC | texto (separados por coma) | ❌ |
| Teléfono / WhatsApp | texto | ❌ |
| Documento (NIT/CC) | texto | ❌ |
| Notas internas | textarea | ❌ |
| Activo | toggle | ✅ |

---

### 3.3 Contratos / Asignaciones (`/app/contratos`)

**Vista principal:**
- Tabla con columnas: Cliente, Servicio, Fecha inicio, Fecha vencimiento, Estado, Días restantes
- Filtros: por cliente, por servicio, por estado, por rango de fechas
- Indicador visual de urgencia (verde > 30 días, amarillo 1-30 días, rojo vencido)
- Botón "Renovar" que crea un nuevo contrato a partir del vencido

**Formulario de asignación:**

| Campo | Tipo | Notas |
|-------|------|-------|
| Cliente | select buscable | autocompletado |
| Servicio | select buscable | muestra precio base |
| Fecha inicio | date | default: hoy |
| Fecha vencimiento | date | calculada automáticamente según periodicidad |
| Precio acordado | decimal | editable, pre-rellena con precio del servicio |
| Auto-renovar | toggle | crea nuevo contrato automáticamente al vencer |
| Notas | textarea | |

**Vista detalle del contrato:**
- Historial de renovaciones anteriores
- Timeline de notificaciones enviadas
- Botón "Enviar correo manual"
- Botón "Renovar ahora"

---

### 3.4 Reglas de Notificación (`/app/notificaciones/reglas`)

Permite configurar cuándo y qué correo enviar antes del vencimiento.

**Ejemplos de reglas:**

| Regla | Días antes | Plantilla | Aplica a |
|-------|-----------|-----------|----------|
| Aviso temprano | 30 | Plantilla "Renovación próxima" | Renovables |
| Recordatorio | 7 | Plantilla "Vence en 7 días" | Todos |
| Último aviso | 1 | Plantilla "Vence mañana" | Todos |
| Vencido | 0 | Plantilla "Servicio vencido" | Todos |

**Campos:**

| Campo | Tipo | Notas |
|-------|------|-------|
| Nombre de la regla | texto | |
| Días antes del vencimiento | número | 0 = día del vencimiento |
| Plantilla de correo | select | |
| Aplica a | select | Todos / Solo renovables / Solo únicos |
| Activo | toggle | |

---

### 3.5 Plantillas de Correo (`/app/notificaciones/plantillas`)

**Editor visual con:**
- Editor de HTML (textarea con resaltado)
- Panel de variables disponibles (click to insert)
- Preview en tiempo real lado a lado
- Envío de correo de prueba a email específico

**Variables disponibles en plantillas:**

```
{{.ClienteNombre}}        → Nombre del cliente
{{.ClienteEmpresa}}       → Empresa del cliente
{{.Servicios}}            → Tabla HTML de servicios (nombre, vencimiento, precio)
{{.Total}}                → Total en moneda local
{{.FechaVencimiento}}     → Fecha de vencimiento (la más próxima si hay varias)
{{.DiasRestantes}}        → Días que faltan para vencer
{{.LinkPago}}             → URL de pago (configurable)
{{.EmpresaNombre}}        → Nombre de la empresa (desde config)
{{.FechaActual}}          → Fecha de hoy
```

**Tipo de plantillas:**

| Tipo | Uso |
|------|-----|
| `renovacion` | Aviso de renovación próxima |
| `vencimiento` | El servicio está a punto de vencer / vencido |
| `pago` | Confirmación o solicitud de pago |
| `personalizado` | Uso libre / envío manual |

---

### 3.6 Historial de Envíos (`/app/notificaciones/historial`)

- Tabla con todos los correos enviados
- Filtro por cliente, fecha, estado (enviado/fallido)
- Ver HTML del correo enviado (modal preview)
- Reenviar correo fallido con un clic

---

### 3.7 Configuración SMTP (`/app/configuracion/smtp`)

Panel para configurar el servidor de correo saliente sin tocar archivos.

**Campos:**

| Campo | Tipo | Default |
|-------|------|---------|
| Host SMTP | texto | smtp.gmail.com |
| Puerto | número | 587 |
| Usuario | email | |
| Contraseña | password (cifrada) | |
| Cifrado | select | TLS / SSL / Ninguno |
| Nombre remitente | texto | U-site |
| Email remitente | email | |

- Botón **"Probar conexión"** — envía correo de prueba y muestra resultado
- La config se guarda en BD (cifrada) y sobrescribe la del `config.yml` en runtime

---

## 4. Flujo de Trabajo

### Flujo de creación de un contrato

```
1. Crear servicio en catálogo
       ↓
2. Crear/seleccionar cliente
       ↓
3. Crear contrato (asignar servicio + fechas + precio)
       ↓
4. Sistema calcula automáticamente
   cuándo disparar notificaciones
   según reglas configuradas
       ↓
5. Cron diario evalúa contratos
   y dispara correos agrupados
```

### Lógica de agrupación de correos

```
Para cada cliente con contratos próximos a vencer:
  │
  ├── mismo_dia = contratos donde fecha_vencimiento == misma fecha
  │     └── → UN solo correo con tabla de servicios + total sumado
  │
  └── fechas_distintas = contratos con fechas diferentes
        └── → UN correo por cada fecha de vencimiento distinta
```

**Ejemplo práctico:**
- Cliente "Empresa ABC" tiene:
  - Dominio → vence 15 mayo → correo independiente
  - Hosting + SSL → ambos vencen 15 mayo → **un solo correo con total combinado**
  - Mantenimiento → vence 30 mayo → correo independiente

---

## 5. Sistema de Notificaciones

### Arquitectura del Cron

El sistema usa un **cron interno en Go** (librería `robfig/cron`) que se inicia al arrancar la aplicación.

```go
// Se ejecuta diariamente a las 8:00 AM
cron.AddFunc("0 8 * * *", jobs.ProcesarVencimientos)
```

### Job: `ProcesarVencimientos`

```
1. Obtener todas las reglas activas (ordenadas por dias_antes)
2. Para cada regla:
   a. Buscar contratos donde:
      - estado = 'activo'
      - fecha_vencimiento = HOY + dias_antes
      - NO exista ya un envío del mismo tipo en notificaciones_log
   b. Agrupar contratos encontrados por cliente + fecha_vencimiento
   c. Para cada grupo:
      - Renderizar plantilla con datos del grupo
      - Enviar correo (con CC si aplica)
      - Registrar en notificaciones_log
3. Si auto_renovar = true y estado = vencido:
   - Crear nuevo contrato con nueva fecha de vencimiento
   - Actualizar estado del anterior a 'renovado'
```

### Estados de un contrato

```
activo ──────────── (cron notifica) ──────────── vence
  │                                                 │
  ├── renovado (si auto_renovar o manual)            │
  │                                                  │
  └── cancelado (manual)                         vencido
```

---

## 6. Plantillas de Correo

### Variables en el cuerpo HTML

El motor de plantillas usa `html/template` de Go (ya usado en el proyecto).

### Plantilla base sugerida — Renovación

```html
<!DOCTYPE html>
<html>
<body style="font-family:Inter,sans-serif; background:#f8fafc; padding:20px">
  <div style="max-width:600px; margin:0 auto; background:white; border-radius:12px; overflow:hidden">
    <!-- Header -->
    <div style="background:#1e293b; padding:24px; text-align:center">
      <h1 style="color:#8eb02f; margin:0">U-site</h1>
    </div>
    <!-- Body -->
    <div style="padding:32px">
      <p>Hola <strong>{{.ClienteNombre}}</strong>,</p>
      <p>Te recordamos que los siguientes servicios vencen próximamente:</p>

      <!-- Tabla de servicios -->
      <table style="width:100%; border-collapse:collapse; margin:20px 0">
        <thead>
          <tr style="background:#f1f5f9">
            <th style="padding:10px; text-align:left">Servicio</th>
            <th style="padding:10px; text-align:right">Vencimiento</th>
            <th style="padding:10px; text-align:right">Valor</th>
          </tr>
        </thead>
        <tbody>
          {{range .Servicios}}
          <tr>
            <td style="padding:10px; border-bottom:1px solid #e2e8f0">{{.Nombre}}</td>
            <td style="padding:10px; border-bottom:1px solid #e2e8f0; text-align:right">{{.FechaVencimiento}}</td>
            <td style="padding:10px; border-bottom:1px solid #e2e8f0; text-align:right">{{.Precio}}</td>
          </tr>
          {{end}}
        </tbody>
        <tfoot>
          <tr>
            <td colspan="2" style="padding:10px; font-weight:bold">Total</td>
            <td style="padding:10px; font-weight:bold; text-align:right; color:#8eb02f">{{.Total}}</td>
          </tr>
        </tfoot>
      </table>

      <p style="text-align:center; margin:30px 0">
        <a href="{{.LinkPago}}" style="background:#8eb02f; color:white; padding:14px 28px; border-radius:8px; text-decoration:none; font-weight:bold">
          Renovar ahora →
        </a>
      </p>
    </div>
    <!-- Footer -->
    <div style="background:#f8fafc; padding:16px; text-align:center; font-size:12px; color:#64748b">
      © 2026 U-site — {{.EmpresaNombre}}
    </div>
  </div>
</body>
</html>
```

### Preview en tiempo real

- Frontend: Alpine.js + `<iframe srcdoc="...">` actualizado en cada keystroke con debounce de 500ms
- El preview sustituye las variables por datos de ejemplo configurables
- Botón "Enviar prueba" → POST `/app/notificaciones/plantillas/:id/test` con email destino

---

## 7. Configuración SMTP

### Almacenamiento seguro

La contraseña SMTP se cifra con AES-256-GCM antes de guardar en BD, usando el `app_jwt_secret` como clave de cifrado (ya disponible en `config.yml`).

```go
// Guardar: cifrar contraseña antes de INSERT
encrypted := utils.Encrypt(password, app.Http.Token.AppJwtSecret)

// Usar: descifrar antes de conectar
plain := utils.Decrypt(encrypted, app.Http.Token.AppJwtSecret)
```

### Override en runtime

Al guardar una nueva configuración SMTP el sistema:
1. Descifra la contraseña
2. Actualiza `app.Http.Mail.*` en memoria
3. **No requiere reiniciar** el servidor

---

## 8. Plan de Implementación por Fases

### Fase 1 — Modelos y migración de BD  *(~1-2 días)*
- [ ] Crear modelos Go: `Servicio`, `Cliente`, `Contrato`, `NotificacionRegla`, `PlantillaCorreo`, `NotificacionLog`, `SmtpConfig`
- [ ] Agregar al `Migrate()` todas las nuevas tablas
- [ ] Seed inicial: 1 plantilla por defecto, reglas básicas (30, 7, 1 día)

### Fase 2 — CRUD Servicios y Clientes  *(~2 días)*
- [ ] Controller + rutas para Servicios
- [ ] Controller + rutas para Clientes
- [ ] Vistas HTML: lista, formulario (modal), detalle
- [ ] Búsqueda y paginación

### Fase 3 — Contratos / Asignaciones  *(~2-3 días)*
- [ ] Controller + rutas para Contratos
- [ ] Select2 para cliente y servicio con autocompletado AJAX
- [ ] Cálculo automático de fecha de vencimiento según periodicidad
- [ ] Vista principal con indicadores de color por días restantes
- [ ] Botón renovar (clona contrato con nueva fecha)
- [ ] Historial de renovaciones

### Fase 4 — Plantillas de correo con preview  *(~2 días)*
- [ ] CRUD de plantillas
- [ ] Editor HTML con preview en `<iframe srcdoc>`
- [ ] API `/plantillas/:id/preview` → renderiza con datos de ejemplo
- [ ] Endpoint `/plantillas/:id/test` → envía correo de prueba

### Fase 5 — Reglas de notificación y Cron  *(~2 días)*
- [ ] CRUD de reglas
- [ ] Job `ProcesarVencimientos` con lógica de agrupación
- [ ] Inicializar cron al arrancar (`app.go`)
- [ ] Evitar doble envío (check en `notificaciones_log`)

### Fase 6 — Historial y reenvío  *(~1 día)*
- [ ] Vista historial de envíos con filtros
- [ ] Modal preview del correo enviado
- [ ] Botón reenviar

### Fase 7 — Configuración SMTP  *(~1 día)*
- [ ] Panel CRUD de configuración SMTP
- [ ] Cifrado AES de contraseña
- [ ] Override en runtime de `app.Http.Mail`
- [ ] Botón "Probar conexión"

### Fase 8 — Integración al menú y permisos  *(~0.5 días)*
- [ ] Agregar módulo "Renovaciones" al sidebar
- [ ] Asignar permisos RBAC (Casbin)
- [ ] Registrar módulo y submodules en BD

---

## 9. Estructura de Archivos

```
pkg/
  models/
    servicio.go
    cliente.go
    contrato.go
    notificacion_regla.go
    plantilla_correo.go
    notificacion_log.go
    smtp_config.go

rest/
  controllers/
    servicio_controller.go
    cliente_controller.go
    contrato_controller.go
    notificacion_controller.go
    plantilla_controller.go
    smtp_config_controller.go
  routes/
    renovaciones.go          ← agrupa todas las rutas del módulo

pkg/
  services/
    renovacion_service.go    ← lógica de agrupación y envío
    cron_service.go          ← job diario de vencimientos

resources/views/
  servicios/
    index.html
    form.html
  clientes/
    index.html
    form.html
    detalle.html
  contratos/
    index.html
    form.html
    detalle.html
  notificaciones/
    reglas.html
    plantillas.html
    historial.html
    editor.html              ← editor con preview
  configuracion/
    smtp.html
  emails/
    renovacion.html          ← plantilla base de correo
    vencimiento.html
    pago.html
```

---

## 10. API Endpoints

### Servicios
| Método | Ruta | Descripción |
|--------|------|-------------|
| GET | `/app/servicios` | Lista de servicios |
| GET | `/app/loadservicios` | JSON para DataTable |
| POST | `/app/servicios` | Crear servicio |
| PUT | `/app/servicios/:id` | Actualizar |
| DELETE | `/app/servicios/:id` | Eliminar |

### Clientes
| Método | Ruta | Descripción |
|--------|------|-------------|
| GET | `/app/clientes` | Lista de clientes |
| GET | `/app/loadclientes` | JSON para DataTable / Select2 |
| GET | `/app/clientes/:id` | Detalle con contratos |
| POST | `/app/clientes` | Crear |
| PUT | `/app/clientes/:id` | Actualizar |
| DELETE | `/app/clientes/:id` | Eliminar |

### Contratos
| Método | Ruta | Descripción |
|--------|------|-------------|
| GET | `/app/contratos` | Lista con filtros |
| GET | `/app/loadcontratos` | JSON para DataTable |
| POST | `/app/contratos` | Crear asignación |
| PUT | `/app/contratos/:id` | Actualizar |
| POST | `/app/contratos/:id/renovar` | Renovar contrato |
| POST | `/app/contratos/:id/correo` | Envío manual de correo |
| DELETE | `/app/contratos/:id` | Cancelar |

### Notificaciones / Plantillas
| Método | Ruta | Descripción |
|--------|------|-------------|
| GET | `/app/notificaciones/reglas` | Lista de reglas |
| POST | `/app/notificaciones/reglas` | Crear regla |
| PUT | `/app/notificaciones/reglas/:id` | Actualizar |
| DELETE | `/app/notificaciones/reglas/:id` | Eliminar |
| GET | `/app/notificaciones/plantillas` | Lista de plantillas |
| POST | `/app/notificaciones/plantillas` | Crear |
| PUT | `/app/notificaciones/plantillas/:id` | Actualizar |
| POST | `/app/notificaciones/plantillas/:id/preview` | Renderiza con datos de ejemplo |
| POST | `/app/notificaciones/plantillas/:id/test` | Enviar correo de prueba |
| GET | `/app/notificaciones/historial` | Log de envíos |
| POST | `/app/notificaciones/historial/:id/reenviar` | Reenviar |

### SMTP
| Método | Ruta | Descripción |
|--------|------|-------------|
| GET | `/app/configuracion/smtp` | Ver configuración actual |
| POST | `/app/configuracion/smtp` | Guardar configuración |
| POST | `/app/configuracion/smtp/test` | Probar conexión |

---

## Notas Técnicas

- **Cron:** usar `github.com/robfig/cron/v3` (agregar a `go.mod`)
- **Preview correo:** `<iframe srcdoc>` + Alpine `x-data` con debounce — sin backend para el preview inicial, JS reemplaza variables con datos de ejemplo en el cliente
- **Agrupación de correos:** lógica en `renovacion_service.go`, agrupa por `cliente_id + fecha_vencimiento` antes de enviar
- **Doble envío:** antes de enviar cada grupo, consultar `notificaciones_log` filtrando `DATE(created_at) = TODAY AND cliente_id = X AND regla_id = Y AND contratos_ids CONTAINS Z`
- **Moneda:** formatear con `fmt.Sprintf("$ %,.2f", valor)` o librería `golang.org/x/text`
- **Auto-renovar:** el job crea el nuevo contrato con `fecha_inicio = fecha_vencimiento_anterior + 1 día` y la nueva `fecha_vencimiento` calculada según periodicidad del servicio

---

*Documento generado el 29 de abril de 2026 — U-site Admin System*
