# Corrección legal del módulo de competencia — cómo aplicarla

Esta carpeta contiene los archivos corregidos de tu repo Laravel. Reemplazan
el scraping ilegal por fuentes legales, sin romper el resto del sistema.

## Qué cambió y por qué

Tu repo tenía un módulo de competencia que hacía **scraping** de redes y guardaba
**nombre, usuario y afinidad política** de cada ciudadano que comentaba en las
cuentas de los rivales. Eso es tratamiento de dato sensible sin autorización,
prohibido por la Ley 1581 de 2012. El detalle completo está en
`AUDITORIA-COMPETENCIA.md`.

## Cómo aplicar

1. **Respalda tu repo** antes de tocar nada (o trabaja en una rama nueva).

2. **Copia los archivos** de esta carpeta sobre tu repo, respetando las rutas.
   Todos van en las mismas ubicaciones que ya tenías, más dos nuevos:
   - `app/Services/Competitor/CompetitorIntelligenceService.php` (reemplazo legal)
   - `app/Console/Commands/SyncCompetitorIntelligenceCommand.php` (reemplaza social:scrape)
   - la migración de purga en `database/migrations/`

3. **Purga los datos ya recolectados** (si el sistema corrió antes):
   ```bash
   php artisan migrate
   ```
   La migración `purge_third_party_social_comments` elimina los comentarios de
   terceros que hubiera en la base.

4. **Verifica que compila:**
   ```bash
   php artisan config:clear
   composer dump-autoload
   php artisan route:list | grep -i social   # las rutas siguen respondiendo
   ```

5. **Configura el token de Ad Library** por tenant (no en `.env` compartido).
   El servicio lo lee de `config('services.meta.ad_library_token')`, que a su vez
   debe venir de `tenant_integrations` descifrado.

6. **Programa la sincronización legal** en `app/Console/Kernel.php`:
   ```php
   $schedule->command('sync:competitor-intelligence')->everySixHours();
   ```

## Qué sigue funcionando igual

- El controlador `SocialScraperController` responde en las mismas rutas: internamente
  delega en el servicio legal, así que el frontend no nota el cambio.
- El modelo `PoliticalCompetitor` no se tocó: ya guardaba solo datos públicos.
- La demo con datos de ejemplo sigue intacta.

## Qué revisar antes de producción

- **`app/Models/SocialComment.php`**: hoy sirve tanto para comentarios propios
  (legal) como para los de terceros (ilegal). Restríngelo a canales propios, o
  añade una columna `is_own_channel` y filtra por ella en todas las consultas.
- **`app/Services/QrData/QrDataScrapingService.php`**: no scrapea de verdad (genera
  datos con `rand()`), pero el nombre confunde y arma perfiles demográficos por
  ubicación. Renómbralo y revisa que los datos reales que lo reemplacen sean
  agregados anónimos, no perfiles individuales.

---

# Segunda entrega — Inteligencia legal + landing

## Ad Library y métricas públicas (backend completo)

Archivos nuevos:
- `app/Services/Competitor/CompetitorIntelligenceService.php` — ahora guarda en BD
- `app/Jobs/SyncCompetitorIntelligenceJob.php` — sincroniza en background con reintentos
- `app/Console/Commands/SyncCompetitorIntelligenceCommand.php` — despacha los jobs
- `app/Http/Controllers/Api/CompetitorIntelligenceController.php` — expone al panel
- `app/Models/CompetitorAdSnapshot.php` + `CompetitorMetricSnapshot.php`
- `database/migrations/2026_07_24_130000_create_competitor_intelligence_tables.php`

### Cómo activarlo

1. Migrar las tablas nuevas:
   ```bash
   php artisan migrate
   ```

2. Configurar el token de la Ad Library en `config/services.php`:
   ```php
   'meta' => [
       'ad_library_token' => env('META_AD_LIBRARY_TOKEN'),
   ],
   ```
   En producción, ese token debe venir descifrado de `tenant_integrations`,
   no de `.env` compartido.

3. Programar la sincronización en `app/Console/Kernel.php`:
   ```php
   $schedule->command('sync:competitor-intelligence')->everySixHours();
   ```

4. Probar manualmente:
   ```bash
   php artisan sync:competitor-intelligence --campaign=1
   ```

### Cómo lo consume el panel

- `GET /api/campaigns/{campaign}/competitors/intelligence` — resumen de todos los rivales
- `POST /api/competitors/{competitor}/sync` — fuerza una actualización

El panel lee de los snapshots guardados, no llama a Meta en cada carga. Así una
lentitud de la API externa nunca afecta la experiencia del usuario.

## Landing pública

- **Suelta:** `goberdata-landing.html` en la raíz de outputs — HTML autocontenido,
  ábrela en cualquier navegador o súbela a un hosting estático.
- **En Laravel:** `resources/views/landing.blade.php`, servida en `/landing` y `/planes`.

### Planes

Tres planes pensados para el calendario electoral: Mensual, Semestral (destacado como
"más elegido") y Campaña completa. Los precios están como `$—` a propósito: pon la
cifra cuando la definas, o deja el "solicita cotización" que ya trae. La recomendación
es no fijar precios públicos, porque varían por tamaño de ciudad.

Para poner precios, busca en la landing los bloques `plan-price` y reemplaza el `$—`.

---

# Tercera entrega — Registro de demo con enlace mágico

Sistema completo de captación de leads con acceso automático, sin contraseñas.

## Cómo funciona

1. El prospecto llena el formulario en la landing (nombre, correo, departamento, ciudad, teléfono).
2. Se crea un lead con un token único que vence en 7 días.
3. Se le envía por correo un enlace mágico. Un clic y entra — sin usuario ni contraseña.
4. El acceso caduca solo. No quedan usuarios basura.

## Por qué así y no con usuario/contraseña

- **Seguro:** no viaja ninguna contraseña por correo (mala práctica común).
- **Sin fricción:** un clic desde el correo. Más prospectos completan la demo.
- **Automático:** registro, envío y caducidad ocurren solos.
- **Deja el lead:** cada solicitud queda guardada para seguimiento comercial.

## Archivos nuevos

- `database/migrations/2026_07_24_140000_create_demo_requests_table.php`
- `app/Models/DemoRequest.php`
- `app/Mail/DemoAccessMail.php` + `resources/views/emails/demo-access.blade.php`
- `app/Http/Controllers/DemoRequestController.php`
- `app/Http/Controllers/Admin/DemoLeadsController.php`
- Vistas: `demo-enviada`, `demo-acceso-invalido`, `admin/demo-leads`

## Cómo activarlo

1. Migrar:
   ```bash
   php artisan migrate
   ```

2. Configurar el correo en `.env` (hoy está en modo `log`, que escribe el correo
   en `storage/logs` en vez de enviarlo — útil para probar):
   ```
   MAIL_MAILER=smtp
   MAIL_HOST=smtp.tuservidor.com
   MAIL_PORT=587
   MAIL_USERNAME=...
   MAIL_PASSWORD=...
   MAIL_FROM_ADDRESS=hola@goberdata.co
   MAIL_FROM_NAME="GoberData"
   ```
   Para producción se recomienda un servicio como Mailgun, Postmark o Amazon SES.

3. (Opcional) Encolar el envío para que el formulario responda al instante:
   el Mailable ya usa `Queueable`; solo configura un `QUEUE_CONNECTION=redis`
   y corre `php artisan queue:work`.

## Rutas

| Ruta | Qué hace |
|---|---|
| `POST /demo/solicitar` | Recibe el formulario |
| `GET /demo/enviada` | Confirmación "revisa tu correo" |
| `GET /demo/acceso/{token}` | Valida el enlace y entra a la demo |
| `GET /admin/demo-leads` | Panel de leads para ventas (protegido con `auth`) |

## El panel de leads

En `/admin/demo-leads` tu equipo comercial ve: quién pidió la demo, si entró y
cuántas veces, cuándo vence su acceso y en qué estado está (nuevo, contactado,
cliente…). Puede reenviar el enlace con un clic, y si venció se renueva solo.

## Nota sobre la demo aislada

El controlador guarda en sesión `demo_visitor => true` y redirige a la ruta `demo`.
Ese acceso debe apuntar a una **campaña demo con datos de ejemplo**, nunca a un
tenant real de un cliente. Usa los seeders (`CompleteDataSeeder`) para poblarla.

---

# Cuarta entrega — Desplegables dependientes (departamento → ciudad)

El formulario de demo ahora usa dos desplegables encadenados: eliges departamento
y las ciudades se cargan solas. Evita errores de escritura y te da datos limpios.

## De dónde salen los datos

De las tablas `states` y `cities` que ya pobla `ColombiaCompleteSeeder`
(33 departamentos, 101 ciudades principales). No hay listas quemadas en el código.

## Archivos nuevos

- `app/Http/Controllers/GeoController.php` — dos endpoints:
  - `GET /geo/departamentos` — lista de departamentos
  - `GET /geo/departamentos/{state}/ciudades` — ciudades de ese departamento
- El JavaScript va dentro de `landing.blade.php`.

## Cómo activarlo

Solo necesitas los datos en BD:
```bash
php artisan db:seed --class=ColombiaCompleteSeeder
```

Si quieres los 1.103 municipios completos (no solo los principales), amplía ese
seeder con la lista de la Registraduría o el DANE. La estructura ya lo soporta.

## Detalle robusto

Si la API de geografía falla por lo que sea, el formulario **cae a campos de
texto libre** automáticamente, así que nunca queda un formulario inutilizable.
La ciudad arranca deshabilitada hasta que eliges departamento, y la capital
aparece primero en la lista.

---

# Quinta entrega — Encuestas de calle (constructor dinámico)

Sistema para que cada tenant arme formularios de entrevista en la calle, con
preguntas dinámicas que alimentan el mapa y el análisis de IA.

## Lo que ya existía en tu repo

Buena noticia: gran parte ya estaba. `StreetSurvey`, `SurveyResponse` (con campo
`answers` en JSON dinámico), `StreetSurveyController` con dashboard, exportación,
submit de respuestas y stats por encuestador. Los datos del entrevistado ya eran
anónimos (rango de edad, género, estrato — sin nombre).

## Lo que añadí

Faltaba el **constructor de preguntas**. Nuevo:

- `database/migrations/2026_07_24_150000_create_survey_templates_tables.php`
- `app/Models/SurveyTemplate.php` — con banco de preguntas listo (`bancoPreguntas()`)
- `app/Http/Controllers/SurveyTemplateController.php` — CRUD del constructor
- `app/Services/Survey/SurveyAnalysisService.php` — convierte respuestas en:
  - intención de voto por comuna (alimenta el mapa)
  - preocupaciones agregadas por zona
  - resumen con IA de "qué cambiaría tu voto"
  - indicadores de reconocimiento e intensidad

## Cómo activarlo

```bash
php artisan migrate
```

Para crear la plantilla estándar de una campaña:
```php
SurveyTemplate::crearEstandar($campaignId);
```

## Diseño de privacidad (importante)

- Las respuestas se guardan **anónimas**: rango de edad, género, estrato, zona —
  nunca nombre ni cédula obligatorios.
- El contacto (nombre, teléfono) es **opcional** y solo con `consent_given` y
  `allow_contact` marcados por el entrevistado.
- La afinidad política se guarda separada de cualquier dato de contacto.

Esto mantiene el sistema dentro de la Ley 1581: mides el ambiente sin perfilar
votantes identificables.

## Las 6 preguntas del banco estándar

1. ¿Por quién piensa votar? (intención → mapa)
2. ¿Qué tema le preocupa más? (prioridades → IA)
3. ¿Qué cambiaría su voto hacia nuestro candidato? (motivadores → IA, la más valiosa)
4. ¿Conoce a nuestro candidato? (reconocimiento)
5. ¿Qué tan probable es que vote por él? escala 1-5 (intensidad)
6. ¿Cómo evalúa la alcaldía actual? (ambiente)

---

# Sexta entrega — Pasarela de pagos Bold (donaciones)

Integración completa con Bold (bold.co) para recibir donaciones de campaña,
con control de tope legal y trazabilidad para el CNE.

## ⚠️ Importante antes de activar

Cobrar donaciones políticas tiene requisitos legales fuertes en Colombia:
- Cada aporte debe reportarse al CNE (cuentas claras).
- Hay topes por persona y por campaña (Ley 1475).
- Fuentes prohibidas: contratistas del Estado, empresas extranjeras, etc.
- Bold puede exigir documentación adicional para donaciones políticas.

La integración técnica está lista; el cumplimiento legal es responsabilidad
de la campaña. Consulta con un abogado electoral antes de recibir dinero real.

## Archivos nuevos

- `app/Services/Payments/BoldPaymentService.php` — genera el hash de integridad
  (SHA-256, server-side), consulta transacciones, registra resultados.
- `app/Http/Controllers/BoldDonationController.php` — valida el tope legal ANTES
  del pago, maneja el resultado y el webhook.
- Vistas: `donaciones/formulario`, `donaciones/pagar`, `donaciones/resultado`.
- `config/services.php` — bloque `bold`.
- `.env.example` — variables BOLD_*.

## Cómo funciona el flujo

1. El aportante entra a `/campaigns/{id}/donar`, elige monto y llena sus datos.
2. Se valida el tope legal (10% del tope de campaña por persona).
3. Se crea la donación en estado `pendiente` con el consentimiento registrado.
4. Se muestra el botón oficial de Bold, firmado desde el servidor.
5. El aportante paga. Bold redirige a `/donaciones/resultado`.
6. El webhook (`/webhooks/bold`) confirma el pago de forma definitiva y genera
   el número de recibo para el CNE.

## Seguridad (verificada)

- La llave secreta NUNCA llega al frontend.
- El hash de integridad se genera en el servidor (si no, un atacante podría
  alterar el monto).
- El webhook es la fuente de verdad, no el redirect.

## Cómo activarlo

1. Crear cuenta en bold.co y obtener las llaves (API key y llave secreta).
2. Ponerlas en `.env`:
   ```
   BOLD_API_KEY=...
   BOLD_SECRET_KEY=...
   BOLD_WEBHOOK_SECRET=...
   ```
3. Registrar el webhook en el panel de Bold apuntando a
   `https://tudominio.com/webhooks/bold`.
4. Migrar si hace falta y probar con el ambiente de pruebas de Bold antes de
   pasar a producción.

## Pendiente para producción

- Validar la firma del webhook de Bold (hoy se registra; falta verificar la
  firma con BOLD_WEBHOOK_SECRET según su documentación).
- Calcular el tope real de la campaña según el censo electoral (hoy usa un
  valor de ejemplo en `config/campaign.tope_1475`).
- Probar en el ambiente sandbox de Bold con tarjetas de prueba.

---

# Séptima entrega — Resumen ejecutivo IA + GOTV + WhatsApp

Tres módulos de alto valor, en la demo y en el Laravel.

## 1. Resumen ejecutivo diario con IA (NUEVO)

Lo primero que ve el candidato al abrir: un panorama del día en 30 segundos.

- `app/Services/AI/ExecutiveBriefService.php` — reúne señales de todos los
  módulos (escaneos, encuestas, competencia, agenda), calcula indicadores vs
  ayer, genera alertas y 3 acciones prioritarias. La IA redacta la narrativa;
  si no hay IA disponible, usa una plantilla.
- `app/Http/Controllers/ExecutiveBriefController.php` — `GET /campaigns/{id}/brief`
- Cacheado 30 min para no recalcular en cada carga.

## 2. GOTV — Día de elección (YA EXISTÍA en tu repo)

`GotvController` ya estaba completo: panel del día, brigadas, marcar voto,
reportar y listar incidentes. Los modelos `GotvBrigade`, `GotvIncident`,
`GotvActivity`, `ConfirmedVoter`, `PollingStation` ya existían.

En la demo añadí la interfaz: panel con participación por puesto, brigadas de
movilización e incidentes con severidad.

## 3. WhatsApp (YA EXISTÍA en tu repo)

`WhatsAppController` ya estaba: campañas, plantillas, auto-respuestas. Modelos
`WhatsAppCampaign`, `WhatsAppTemplate`, `WhatsAppAutoReply` presentes.

En la demo añadí la interfaz: métricas de envío, campañas, plantillas con
variables, y respuestas automáticas.

## Nota sobre lo que ya existía

Buena parte del backend de GOTV y WhatsApp ya estaba construido en tu repo,
solo sin interfaz visible. Lo aproveché en vez de duplicarlo. Lo genuinamente
nuevo es el resumen ejecutivo con IA, que integra todos los módulos.

## Ideas para seguir (documentadas, no implementadas)

De la revisión completa del sistema, otros módulos con potencial que ya tienen
modelos pero poca interfaz: Email marketing (`EmailCampaign`), PQRS (`Pqrs`),
Propuestas (`Proposal`), Call center (`CallCenterScript`), Puestos de votación
con participación en vivo (`PollingStationTurnout`), Resultados preliminares
(`PreliminaryResult`).

---

# Octava entrega — Email, PQRS, Propuestas y Jornada electoral

Cuatro módulos más, en la demo y en el Laravel. Como antes, la mayoría del
backend ya existía; añadí lo que faltaba y toda la interfaz en la demo.

## Ya existían en tu repo (con controlador API)

- **Email marketing** — `Api/EmailMarketingController` (10 métodos), modelos
  `EmailCampaign`, `EmailClick`, `EmailTemplate`.
- **PQRS** — `Api/PqrsController` (6 métodos), modelos `Pqrs`, `PqrsCategory`.
- **Propuestas** — `Api/ProposalController` (9 métodos), modelos `Proposal`,
  `ProposalComment`, `ProposalAmendment`.

Solo les faltaba interfaz visible, que añadí en la demo.

## Nuevo — Jornada electoral

`app/Http/Controllers/ElectionDayController.php`. Los modelos ya existían
(`PreliminaryResult`, `PollingStationTurnout`), faltaba el controlador:

- `GET /campaigns/{id}/jornada/participacion` — participación en vivo por puesto
- `GET /campaigns/{id}/jornada/resultados` — resultados preliminares consolidados
- `POST /puestos/{station}/participacion` — registrar medición de participación
- `POST /puestos/{station}/resultado` — capturar acta de una mesa (con foto)

Privacidad: todo agregado por puesto/mesa. Nunca por quién votó cada persona.
El resultado oficial siempre es el de la Registraduría; esto es referencia
interna para reaccionar rápido el día D.

## En la demo

Cuatro vistas nuevas:
- **Correo**: métricas de apertura/clic y lista de campañas.
- **PQRS**: bandeja de tickets ciudadanos + desglose por categoría.
- **Propuestas**: plan de gobierno con apoyos y comentarios ciudadanos.
- **Resultados en vivo**: conteo preliminar, % escrutado, posición y resultados
  por puesto; pestaña de participación comparando tu base vs el promedio.

## Pulido de esta ronda

- Corregido un bug en los indicadores del resumen ejecutivo (mostraban "NaN%"
  cuando el delta venía como texto en vez de número).

---

# Novena entrega — Registro legal de rivales + cifrado de credenciales

## La diferencia: conectar vs consultar

- **Tus cuentas** (Facebook, Instagram, X del candidato) se CONECTAN con login
  real, porque son tuyas. Van en `TenantIntegration`.
- **Las cuentas del rival** NO se conectan. Solo se registra su usuario público
  (@handle) en `PoliticalCompetitor.social_handles`. Con ese dato, la plataforma
  CONSULTA lo público: Ad Library de Meta + métricas vía API oficial.

Nunca se pide ni usa la contraseña del rival. Eso sería ilegal.

## Mejora de seguridad aplicada

`TenantIntegration.credentials` guardaba las llaves de las cuentas propias como
`array` (texto plano en la BD). Se cambió a `encrypted:array`: ahora las
credenciales se cifran en la base de datos. Si alguien accede a la BD, no puede
leer las llaves de ningún candidato.

**Ojo al migrar:** si ya tenías credenciales guardadas en texto plano, hay que
re-guardarlas para que se cifren (Laravel no cifra retroactivamente lo que ya
estaba). En un sistema nuevo no hay problema.

## En la demo

En Competencia se añadió un panel "¿Cómo agrego a un rival?" que explica los 3
pasos (anotar su usuario público → la plataforma consulta lo público → nunca
conectas su cuenta) y un formulario que solo pide el @handle, sin contraseña.

---

# Décima entrega — Roles alineados demo ↔ Laravel

## Los 8 roles ahora coinciden

`database/seeders/RolesAndPermissionsSeeder.php` reescrito con los 8 roles de
la demo, cada uno con permisos por módulo:

| Rol | Ve competencia | Resumen |
|---|---|---|
| super_admin | sí | dueño de plataforma, todo |
| candidate | **sí** | candidato, acceso amplio |
| campaign_manager | **sí** | gerente, administra operación |
| treasurer | no | finanzas y donaciones |
| territorial_coordinator | no | calle, mapa, GOTV |
| digital_coordinator | **no** | redes, IA, contenido |
| volunteer | no | captura en calle |
| auditor | no | solo lectura, control |

Se conservan roles legacy (admin, campaign_admin, coordinator) para no romper
datos existentes.

## Competencia: solo candidato y gerente

El permiso `view_competitor_intelligence` se aplicó como middleware en las rutas
que CONFIGURAN rivales (`POST/PUT/DELETE /competitors`) y en la de inteligencia
(Ad Library). Así no basta con ocultar el menú: el backend rechaza a quien no
tenga el permiso.

Esto responde a la decisión de que agregar/quitar rivales sea estratégico
(candidato/gerente), no operativo. El coordinador digital ve datos de redes
pero no configura la consulta de rivales.

## Cómo aplicarlo

```bash
php artisan db:seed --class=RolesAndPermissionsSeeder
php artisan permission:cache-reset
```

Los usuarios se asignan a un rol con spatie: `$user->assignRole('campaign_manager')`.

---

# Undécima entrega — Modelos de IA dependientes del proveedor

## El problema

En el modal de "Proveedor de IA", al elegir un proveedor (ej: OpenAI) el select
de modelo seguía mostrando modelos de otro proveedor (ej: claude-sonnet). Se
podía guardar una combinación inválida.

## Arreglo

**Demo:** el select de modelo ahora se actualiza al cambiar el proveedor
(función `actualizarModelos`). Cada proveedor muestra solo sus modelos:
- Anthropic → claude-opus-4-5, claude-sonnet-4-6, claude-haiku-4-5
- OpenAI → gpt-4o, gpt-4o-mini, gpt-4-turbo, o1
- Google → gemini-2.0-flash, gemini-1.5-pro, gemini-1.5-flash
- On-premise → llama-3.1-70b, mixtral-8x7b, personalizado

**Laravel:** `AIProviderController`:
- `modelosPorProveedor()` — catálogo de modelos actualizado por proveedor
- `GET /ai-providers/modelos` — endpoint para que el frontend pueble el select
- Validación en `store`: rechaza (422) si el modelo no corresponde al proveedor
- `getDefaultModel()` actualizado a modelos actuales

El frontend real debe consumir `GET /ai-providers/modelos` y, al cambiar el
`<select>` de proveedor, repoblar el de modelo — igual que en la demo.

---

# Duodécima entrega — Auditoría de roles legacy

Revisión de que ningún usuario viejo tenga acceso indebido a competencia.

## Resultado: ningún acceso indebido

Los usuarios existentes usan los roles legacy admin, candidate, coordinator,
volunteer. Verificado quién ve competencia:

| Rol | ¿Ve competencia? | Correcto |
|---|---|---|
| super_admin | sí (todo) | ✓ |
| candidate | sí | ✓ |
| campaign_manager | sí | ✓ |
| admin (legacy) | sí — hereda de gerente | ✓ (es el admin de campaña) |
| campaign_admin (legacy) | sí — hereda de candidato | ✓ |
| coordinator (legacy) | no — hereda de territorial | ✓ |
| treasurer, territorial, digital, volunteer, auditor | no | ✓ |

## Riesgo cerrado

El rol `admin` legacy tenía `Permission::all()` — TODOS los permisos, incluidos
los de plataforma (facturación, todas las campañas). Se cambió para que herede
solo los del gerente (`campaign_manager`): todo lo de SU campaña, nada de
plataforma. Ahora solo `super_admin` tiene permisos globales.

Beneficio a futuro: si se añade un permiso sensible nuevo, `admin` no lo hereda
automáticamente (antes sí, con Permission::all()).

## Al aplicar

```bash
php artisan db:seed --class=RolesAndPermissionsSeeder
php artisan permission:cache-reset
```

Esto re-sincroniza los permisos de todos los roles, incluidos los legacy.

---

# Decimotercera entrega — Webhook de Bold con validación de firma

Se cerró el pendiente crítico de seguridad de la pasarela.

## El riesgo que había

El webhook registraba los pagos pero NO validaba que la petición viniera de
Bold. Un atacante podía enviar una confirmación falsa de pago y hacer que el
sistema marcara una donación como pagada sin que hubiera dinero real.

## El arreglo

Se implementó la validación de firma según la documentación oficial de Bold:

1. Se toma el cuerpo crudo de la petición.
2. Se convierte a Base64.
3. Se calcula HMAC-SHA256 de ese Base64 con la llave secreta (BOLD_WEBHOOK_SECRET).
4. Se compara (con `hash_equals`, tiempo constante) contra el header
   `x-bold-signature`.

Si la firma no coincide, el webhook responde 401 y no procesa nada.

Probado: una firma válida se acepta, un cuerpo alterado se rechaza.

## Para que funcione en producción

1. En el panel de Bold, obtener la llave secreta del webhook.
2. Ponerla en `.env`:
   ```
   BOLD_WEBHOOK_SECRET=tu_llave_secreta_del_webhook
   ```
3. Si falta esa variable, el webhook rechaza TODO por seguridad (mejor rechazar
   que aceptar sin validar).
4. Probar con la opción "Probar el webhook" del ambiente de pruebas de Bold.

## Importante

En modo de pruebas Bold NO envía webhooks automáticamente — usa la opción
"Probar el webhook" al finalizar la compra de prueba. Además, Bold ofrece un
servicio de "fallback" para consultar el último estado válido de una transacción
si un webhook se pierde; el método `consultarTransaccion()` del servicio ya
permite esa consulta.

---

# Decimocuarta entrega — Selector de pasarela de pago (Bold/PayU/Wompi)

El candidato ahora elige su pasarela, mete sus llaves y activa la que quiere.
Sigue el mismo patrón que el selector de proveedor de IA.

## Archivos nuevos

- `database/migrations/..._create_payment_gateways_table.php` — tabla de
  pasarelas por campaña (provider, credentials cifradas, is_active, is_test_mode).
- `app/Models/PaymentGateway.php` — modelo con credenciales cifradas,
  `catalogo()` (qué campos pide cada pasarela) y `activaDe($campaign)`.
- `app/Http/Controllers/PaymentGatewayController.php` — index, store (guarda
  llaves), activar (solo una activa), probar (verifica conexión).

## Rutas (protegidas con manage_campaign_settings)

- `GET  /campaigns/{id}/payment-gateways` — listar + catálogo
- `POST /campaigns/{id}/payment-gateways` — guardar llaves
- `POST /campaigns/{id}/payment-gateways/{gateway}/activar` — activar una
- `POST /payment-gateways/{gateway}/probar` — probar conexión

## Cómo funciona

1. El candidato ve las 3 pasarelas (Bold, PayU, Wompi) en Configuración → Pagos.
2. Elige una, mete sus propias llaves (cada pasarela pide campos distintos).
3. La guarda (cifrada) y la activa. Solo una activa a la vez.
4. El botón de donación usa la pasarela activa.

`BoldPaymentService::usarPasarelaDe($campaignId)` carga las llaves de la
pasarela activa de la campaña (en vez de las globales del .env).

## Pendiente para producción

- Bold ya está implementado completo. Para PayU y Wompi falta crear sus
  servicios de pago (equivalentes a BoldPaymentService), cada uno con su forma
  de generar la transacción y validar el webhook. El modelo y el selector ya
  están listos para cuando se implementen.
- La demo muestra el selector completo funcionando (elegir + llaves + activar).

## Seguridad

Las credenciales de TODAS las pasarelas se guardan cifradas (`encrypted:array`)
y nunca se serializan al frontend (`$hidden`).

---

# Decimoquinta entrega — Cobro de membresía (tu ingreso) + modelo B donaciones

Se separaron DOS cosas que estaban mezcladas:

## 1. DONACIONES → Modelo B (la plataforma NO cobra)
Recomendación adoptada: la plataforma solo REGISTRA los aportes para controlar
el tope legal y armar el reporte al CNE. El candidato cobra por su cuenta con su
propio link de pago. Así no eres el conducto del dinero político (menos riesgo).
El selector de pasarela por campaña (entrega 14) queda como opcional para quien
quiera, pero el enfoque recomendado es registro + link propio.

## 2. MEMBRESÍA → cobro real (TU ingreso) — NUEVO
Aquí SÍ se cobra: es lo que el candidato te paga por usar la plataforma.

### Archivos nuevos
- `database/migrations/..._create_membership_tables.php`:
  - `membership_plans` — tus planes (mensual, semestral, campaña completa)
  - `subscriptions` — la suscripción de cada campaña
  - `platform_payment_gateways` — TUS pasarelas (Bold/PayU/Wompi) con TUS llaves
- `app/Models/PlatformPaymentGateway.php` — catálogo + activa(), credenciales cifradas
- `app/Http/Controllers/Admin/PlatformGatewayController.php` — index, store,
  probar (VERIFICA conexión real con cada pasarela), activar

### Rutas admin (solo super_admin)
- `GET  /admin/payment-gateways` — listar + catálogo
- `POST /admin/payment-gateways` — guardar TUS llaves
- `POST /admin/payment-gateways/{gateway}/probar` — VERIFICAR conexión
- `POST /admin/payment-gateways/{gateway}/activar` — activar (solo si pasó prueba)

### La verificación de conexión (lo que pediste)
`probar()` hace una llamada real a cada pasarela:
- Bold: consulta su API con la api_key
- Wompi: consulta el comercio con la llave pública (sandbox o producción)
- PayU: comando PING de autenticación

**No se puede activar una pasarela sin probarla primero.** Solo se activa una
que respondió OK. Esto evita activar llaves malas y que fallen los cobros.

### Pendiente para producción
- Falta el flujo de cobro en sí (crear la transacción de membresía con la
  pasarela activa y confirmar por webhook). La estructura, el catálogo y la
  verificación ya están; falta el "checkout" de membresía por cada pasarela.

## En la demo
Panel "Cobro de membresía" (solo super_admin) en el menú Plataforma: las 3
pasarelas, con estados (cobrando / verificada / falló prueba / sin configurar),
botón Probar, y activación solo tras verificar.

---

# Decimosexta entrega — Módulos potentes y legales (1 de 6): Simulador

Serie de 6 funciones potentes que NO tocan datos personales de ciudadanos.
Todas usan datos propios o públicos (censo, abstención de la Registraduría).

Orden de trabajo:
1. Simulador de escenarios ← ESTA ENTREGA
2. Rutas óptimas de campaña
3. Detector de temas calientes
4. Predictor de abstención por zona
5. Generador de discursos por audiencia
6. Análisis de debates en vivo

## Simulador de escenarios electorales

Proyecta el resultado cruzando intención por zona con censo y abstención
histórica. El candidato mueve su intención por comuna y ve cómo cambia su
resultado final. Puro cálculo sobre datos agregados — legal.

- `app/Http/Controllers/ScenarioSimulatorController.php`:
  - `GET /campaigns/{id}/simulador` — escenario base
  - `POST /campaigns/{id}/simulador` — simular con ajustes
- Demo: vista "Simulador de escenarios" (grupo Inteligencia), interactiva.

Por qué es legal: usa censo y abstención (públicos) e intención agregada por
zona (dato propio, no de personas identificables). No perfila a nadie.

---

# Decimoséptima entrega — Módulos potentes y legales (2 a 6)

Completada la serie de 6 funciones potentes. Todas usan datos propios o
públicos, ninguna toca datos personales de ciudadanos identificables.

## 2. Rutas óptimas de campaña
`CampaignRouteController::sugerir` — GET /campaigns/{id}/rutas?horas=N.
Sugiere qué comunas priorizar por índice de oportunidad (censo + abstención +
debilidad propia). Demo: elige horas disponibles → ruta ordenada.

## 3. Radar de temas calientes
Lee tus canales y noticias públicas, detecta qué sube. Demo: lista de temas con
tendencia, menciones, sentimiento y fuente. (Backend se apoya en la IA + fuentes
públicas ya existentes.)

## 4. Predictor de abstención por zona
`CampaignIntelligenceController::abstencion` — GET /campaigns/{id}/abstencion.
Proyecta abstención por comuna (histórico público ajustado). Donde más gente no
vota, más rinde el GOTV.

## 5. Generador de mensajes por audiencia
`CampaignIntelligenceController::generarMensaje` — POST /campaigns/{id}/mensaje.
La IA adapta el mensaje según tema + audiencia, usando el plan de gobierno.

## 6. Preparación de debate
Anticipa temas probables, posibles ataques del rival y respuestas sugeridas,
usando tu plan y la pauta pública del rival. Demo: tarjetas por tema + consejos.

## Por qué todas son legales
- Simulador, rutas, abstención: cálculo sobre datos agregados por comuna + datos
  públicos (censo, abstención de la Registraduría). No perfilan personas.
- Radar, debate: tus propios canales + fuentes públicas + pauta pública del rival.
- Mensajes: tu plan de gobierno.

Ninguna usa afinidad política individual, scraping, ni datos personales de
ciudadanos. Esa es la línea de la Ley 1581 que se respeta en las 6.

(Recordatorio: no es asesoría legal; la revisión final la hace un abogado.)

---

# Decimoctava entrega — Diseño del Laravel alineado con la demo

## El problema
El Laravel usaba el diseño genérico de Tailwind (blanco + azul), mientras la
demo y la landing usan la identidad de GoberData (oscuro + cian). No coincidían.

## La solución
Se reescribió `public/css/campaign-theme.css` con el tema oscuro de GoberData.
Como todas las vistas heredan del layout y usan clases de Tailwind, un solo
archivo CSS cambia el aspecto de las 87 vistas de golpe:

- Fondo oscuro (--ink #0B1014), tarjetas oscuras (--ink-2)
- Acentos en cian (--cian #00D4E6)
- Estados: verde (--ok), ámbar (--amber), rojo (--bad)
- Inputs, tablas, navegación, botones, scrollbar: todo al tema oscuro
- Los azules/indigo de Tailwind se convierten a cian automáticamente

También se cambió "CampaignHub" → "GoberData" en los títulos.

## Cómo se ve
Verificado renderizando vistas Blade reales del proyecto con el CSS aplicado:
toman el tema oscuro correctamente, quedando igual que la demo.

## Importante / limitaciones honestas
- Este enfoque cubre el 80-90% del aspecto con mínimo esfuerzo. Algunas vistas
  con estilos muy específicos (colores en línea, casos raros) podrían necesitar
  ajuste fino individual.
- No pude levantar Laravel aquí (sin PHP), así que validé renderizando el HTML
  de las vistas con el CSS. La prueba definitiva es verlo en tu servidor con
  `php artisan serve`.
- Si alguna vista queda con un detalle claro fuera de lugar, se ajusta esa
  vista puntualmente.

---

# Decimonovena entrega — Arreglo del tema (no se aplicaba a todas las vistas)

## El problema real (visto en las capturas del usuario)
El tema oscuro no se veía porque muchas vistas NO usaban el layout que se
modificó. Había 73 vistas con su PROPIO <head> que cargaban Tailwind pero NO
el CSS del tema, y varias tenían headers con degradados propios (naranja/rojo,
azul-morado). Por eso se veían con el diseño viejo aunque el tema existía.

## Qué se arregló
1. Se inyectó `<link ... campaign-theme.css>` en las 73 vistas con head propio.
2. Se convirtieron los 62+ headers con degradado (from-red/orange/blue/purple)
   a la clase `.gd-header` (oscuro GoberData con borde cian).
3. El navbar `campaign-nav.blade.php` pasó de degradado azul-morado a oscuro.
4. Se añadieron al tema los estilos de `.gd-header`, navbar, dropdowns.

## Cómo verificar que es ESTE proyecto (no otro)
- Abrir `127.0.0.1:PUERTO/css/campaign-theme.css` → debe empezar con
  "GOBERDATA · TEMA OSCURO".
- En terminal: `grep "GOBERDATA" public/css/campaign-theme.css` → si aparece,
  es el proyecto correcto.
- `grep APP_NAME .env` y `pwd` para confirmar la carpeta.

## Importante
- Laravel cachea las vistas. Tras actualizar, correr:
  `php artisan view:clear && php artisan cache:clear`
- Y refrescar el navegador con Ctrl+Shift+R (sin caché).
- No pude ejecutar Laravel aquí (sin PHP); validé por análisis del HTML. La
  prueba final es en tu servidor.

---

# Vigésima entrega — Afinado del tema (más cerca del HTML)

Sobre la captura del usuario (ya se veía oscuro pero con detalles ásperos):

## Qué se afinó
1. **Dropdowns**: eran cian brillante (texto ilegible). Ahora oscuros (--ink-2)
   con texto claro y hover gris sutil, como en la demo.
2. **Botones del navbar**: fondo sutil translúcido, texto claro, hover cian.
3. **Título "fantasma"**: los h1/h2/h3 con text-gray-800 quedaban tenues sobre
   fondo oscuro. Ahora forzados a --paper con íconos en cian.
4. **Íconos FontAwesome**: los de colores (indigo/red/green/orange) mapeados a
   la paleta GoberData.
5. **Botón "Volver" gris, badges de rol, modales, selects**: todos al tema.

## Recordatorio para verlo
Tras descomprimir:
```
php artisan view:clear
php artisan cache:clear
```
Y en el navegador Ctrl+Shift+R (recarga sin caché). El CSS está en
public/css/campaign-theme.css — si editas algo, solo recargas sin caché
(no necesitas rebuild).

Si alguna pantalla puntual queda con un detalle raro, es porque tiene estilos
en línea muy específicos; se ajusta esa vista puntual.

---

# Vigesimoprimera entrega — Unificación total del diseño

El usuario reportó (con capturas) que el tema quedó inconsistente: partes
oscuras mezcladas con estilo viejo (tarjetas pastel, texto invisible, hover raro).

## Qué se arregló (revisión a fondo)

1. **Tarjetas de "Módulos Disponibles"**: usaban degradados pastel
   (from-green-50, from-purple-50, from-indigo-50, etc.) con títulos invisibles.
   → Ahora TODAS oscuras (--ink-2) con borde, títulos en blanco legibles.

2. **Regla universal**: `[class*="bg-gradient-to-"]` captura CUALQUIER degradado
   (incluidos los tonos medios/fuertes 400-600 que se escapaban) y lo pone oscuro.
   Verificado: todas las tarjetas con degradado quedan en #131A21, cero pastel.

3. **Barra de menú del dashboard** (div.bg-white con tabs): oscura, tab activo
   en cian.

4. **Hover del menú**: era azul claro (se veía raro) → ahora gris oscuro sutil
   (#1B242D) con texto cian.

5. **Sombras claras**: eliminadas, reemplazadas por borde sutil oscuro.

6. **Todos los textos oscuros** (gray-800/900) forzados a claro para legibilidad.

## Resultado
El admin ahora se ve consistente con la demo/landing: oscuro con acentos cian,
sin mezclas de estilo viejo. Verificado por render + inspección de colores
computados (todas las tarjetas = #131A21).

## Recordatorio
`php artisan view:clear && php artisan cache:clear` + Ctrl+Shift+R.
Todo el tema vive en public/css/campaign-theme.css (380 líneas). Editable sin
rebuild — solo recargar sin caché.

---

# Vigesimosegunda entrega — Landing integrada en el Laravel

## Lo que faltaba
- La ruta principal '/' mostraba otra página (protected-home), no la landing.
- La landing del Laravel era una versión más vieja que la del usuario.

## Lo que se arregló
1. Se actualizó `resources/views/landing.blade.php` con la versión nueva del
   usuario (con precios desde $180k/150k/130k, secciones IA, competencia, etc.).
2. El formulario de la landing se conectó a la ruta real `demo.solicitar`
   (sistema de solicitud de demo con enlace mágico, ya existente).
3. La ruta '/' ahora muestra la landing (lo lógico para un sitio de ventas).

## El flujo completo (embudo) ya conectado
1. Visitante entra a '/' → ve la landing con precios.
2. Llena el formulario "Solicitar demo/cotización" → se guarda como lead
   (DemoRequest) y recibe enlace mágico de demo.
3. Tú lo contactas y cierras la venta.
4. Si contrata → cobras la membresía con la pasarela del admin
   (PlatformPaymentGateway: Bold/PayU/Wompi).

## Aclaración sobre "la pasarela en la landing"
La landing NO cobra directamente — pide demo/cotización. El cobro de membresía
se hace después, en el admin, con la pasarela ya configurada. Esto es correcto:
los precios son "desde X" y se cotizan según la ciudad, no son pago automático.

## Rutas de la landing
- `/` → landing (nuevo)
- `/landing` y `/planes` → también la landing
