# Acceso demo por captura de lead + credenciales por correo (sin login passwordless)

- **Fecha:** 2026-08-07
- **Rama:** `feat/demo-lead-access`
- **Estado:** diseño aprobado en brainstorming; pendiente de plan.
- **Motivación:** el acceso demo actual (`GET /demo/{role}`) es un **login passwordless público** — cualquiera con la URL entra a la app sin credenciales. Es un hueco de seguridad y además no captura ningún dato del prospecto. Se reemplaza por: **capturar el lead → enviarle por correo las credenciales → que inicie sesión normal**. Objetivos: (a) capturar posibles clientes, (b) eliminar el auto-login desde la web, (c) que el prospecto pueda probar tanto la **PWA** como el **panel Filament**, (d) hacerlo lo más seguro posible.

## Cómo funciona hoy (auditado)

- **Empresa demo compartida** (`companies.is_demo`, invariante: exactamente 1), plan Profesional + suscripción permanente, **3 usuarios fijos** (`App\Support\Demo::USERS`: email → rol `dueno`/`supervisor`/`cobrador` = admin/supervisor/collector). Hoy sus contraseñas son `bcrypt(random)` (login passwordless, nunca se usan).
- **Acceso passwordless:** `DemoAccessController::enter($role)` (`GET /demo/{role}`) emite un Bearer token PWA (vía `App\Support\PwaAuthResponse::issueToken`) y lo entrega a la SPA por una **página intermedia `demo-enter`** → `sessionStorage['pwa_bootstrap']` → `/pwa/home`; `resources/js/pwa/main.js` + `stores/auth.js::bootstrapFromInline` lo consumen. **Sin contraseña.**
- **Provisión/datos:** `credify:setup-demo` (idempotente) crea empresa/plan/suscripción/usuarios y siembra `DemoDataSeeder` (clientes, créditos, socios + capital, enlace supervisor↔cobrador). `credify:reset-demo` (agendado nightly 04:00 `--force` + intra-día cada 3 h si uso ≥ 90 %) vacía lo tenant de la demo y resiembra. `ResetDemoCompany::wipeTenantData()` extraído y reusable.
- **Leads:** `POST /leads` (`LeadController::store`) + `Lead` (`source` ∈ {`sales_bot`, `account_request`}) + `StoreLeadRequest` + Filament `Leads` resource. La landing ya tiene "Solicitar cuenta" → lead `account_request`.
- **Landing:** el CTA "Probar demo" (hero, CTA final, y nav) es un dropdown con 3 enlaces a `/demo/{role}`. Los dropdowns usan la clase `.dropdown` + `.dropdown-host` (fix de z-index reciente). El nav tiene además un dropdown "Recursos" (contenido).
- **Correo:** `config/mail.php` default `env('MAIL_MAILER','log')`. En **prod hoy `MAIL_MAILER=log`** (los correos NO se envían; van al log). `MAIL_FROM_ADDRESS="no-reply@credifygo.com"`.

## Decisiones (del brainstorming)

1. **Entrega:** las credenciales se envían por **correo automático** al enviar el formulario (gate: correo válido).
2. **Permisos:** acceso **completo** dentro de la empresa demo (crear/editar/borrar); el multi-tenant los aísla y el reset nightly limpia.
3. **Credenciales:** **3 usuarios fijos compartidos** (los de `Demo::USERS`) con **contraseñas conocidas**, iguales para todos los prospectos.
4. **Proveedor de correo:** servicio API transaccional (recomendado **Resend**, nativo en Laravel 11). **El código es agnóstico del proveedor** (todo por `.env`): se implementa con `MAIL_MAILER=log` en dev y se conecta el proveedor real en prod cuando haya API key.
5. **Rotación diaria de contraseñas (incluida):** en cada `reset-demo` (nightly) se **regeneran** las contraseñas demo; el correo lleva la contraseña vigente al momento de la solicitud. Efecto de seguridad: credenciales filtradas expiran en ≤ 24 h.
6. **Se elimina el acceso passwordless.**

## Diseño

### A. Credenciales demo (fijas-compartidas, rotadas a diario)

- Las 3 contraseñas demo dejan de ser `random` inservible. Se **generan** (aleatorias, legibles ~12–16 chars) y se **guardan cifradas en reposo** para poder incluirlas en el correo (necesario porque rotan): nueva tabla `demo_credentials` (`role`, `password` con cast `encrypted`, `updated_at`), 3 filas (una por rol). Alternativa equivalente: columna JSON `encrypted` en la empresa demo. Se elige tabla dedicada por claridad y aislamiento.
- **`credify:setup-demo`**: si no existen, genera las 3 contraseñas, las guarda (cifradas) en `demo_credentials` y setea el `password` (hash) de cada usuario demo. Idempotente: si ya existen, NO las regenera (para no invalidar credenciales ya enviadas fuera del ciclo de reset).
- **`credify:reset-demo`** (en el mismo flujo que ya vacía+resiembra): **regenera** las 3 contraseñas (rotación), actualiza `demo_credentials` + el hash de cada usuario. Así rotan una vez al día (nightly) — y en cada reset intra-día por 90%.
- Fuente de verdad del texto plano para el correo: `demo_credentials` (descifrado por la app al construir el mailable). Documentado: son credenciales **compartidas y desechables** de un entorno que se reinicia a diario; cifradas en reposo; sin PII.

### B. Página dedicada `/demo` (explicación + captura de lead)

- **`GET /demo`** → `DemoRequestController::show` → `resources/views/demo-request.blade.php` (usa `layouts/public`, hereda nav/footer/estilos). Contenido:
  - Explicación: qué es la demo, que se prueba como **Dueño / Supervisor / Cobrador**, en la **app móvil (PWA)** y el **panel de gestión (Filament)**, con **datos de ejemplo**, y que **se reinicia a diario**.
  - Formulario (lead): **nombre**, **correo** (requerido — a él llegan las credenciales), **WhatsApp/teléfono**, **negocio** (opcional). `noindex` (meta robots) para no competir con SEO ni parecer registro.
- **`POST /demo`** → `DemoRequestController::store` (FormRequest `DemoRequestRequest`, reglas equivalentes a `StoreLeadRequest`):
  1. Crea `Lead` con `source = demo_request` (nuevo valor permitido).
  2. Encola `DemoCredentialsMail` al correo del prospecto.
  3. Redirige a la misma página con estado de éxito: "**Te enviamos las credenciales a tu correo**" (NO se muestran en pantalla).
  - **Rate-limit** (`throttle`) por IP/correo para evitar spam de correos.
- **Landing:** los CTA "Probar demo" (hero, CTA final, y el CTA del nav) dejan de ser dropdown passwordless y pasan a ser un **enlace/botón simple a `/demo`**. Se conserva el dropdown "Recursos" del nav (contenido) y la infraestructura `.dropdown`/`.dropdown-host` (sigue usándose por "Recursos").

### C. Correo `DemoCredentialsMail`

- Mailable **`ShouldQueue`** (no bloquea el request; hay worker supervisor + `queue:restart` en el deploy). Plantilla Blade/Markdown. Contenido:
  - Saludo + una línea de contexto.
  - **Las 3 credenciales** (Rol → usuario = correo del usuario demo, contraseña vigente leída de `demo_credentials`).
  - **Enlaces:** app móvil `https://credifygo.com/pwa/login` y panel de gestión `https://credifygo.com/admin`.
  - Notas: el **Dueño** entra también al panel Filament; los **3** entran a la PWA; datos de ejemplo; **entorno demo que se reinicia a diario** (las credenciales pueden cambiar tras el reinicio; si expiran, volver a solicitarlas en `/demo`).
  - `From: no-reply@credifygo.com`.

### D. Seguridad (se elimina el passwordless)

- **Borrar:** `DemoAccessController`, ruta `GET /demo/{role}`, vista `resources/views/demo-enter.blade.php`, y el bootstrap por `sessionStorage`: `stores/auth.js::bootstrapFromInline` + su consumo en `main.js`. (El `/demo` nuevo es `GET` a un slug fijo, patrón distinto — no colisiona.)
- **Conservar** `App\Support\PwaAuthResponse::{pwaRole, permissions, userPayload}` (los usa el login real `Api\Pwa\AuthController`). Quitar `issueToken` si queda sin uso tras borrar el demo passwordless.
- Acceso demo = **solo login normal** (PWA `POST /pwa/auth/login` con throttle `pwa-login`; Filament `/admin` con su login). Sin auto-login desde la web.
- Filament `/admin`: solo el **Dueño** (rol admin) entra (política existente `credify-filament-auth`); supervisor/collector no (correcto).
- `/demo` con `noindex` + rate-limit en el `POST`.

## No-objetivos / límites

- **No** credenciales únicas por prospecto (se eligió compartidas fijas rotadas a diario).
- **No** cambiar la empresa demo compartida, sus datos sembrados, ni la agenda del reset (se mantienen).
- **No** manejar el registro/pago real: "Solicitar cuenta" → lead `account_request` sigue igual y separado.
- **No** crear la cuenta del proveedor de correo ni manejar sus credenciales en texto (las provee el usuario; se cablean en el `.env` de prod).

## Riesgos / gotchas

- **Entregabilidad:** el correo real requiere el proveedor configurado + verificación de dominio (SPF/DKIM) para no caer en spam. Hasta entonces `MAIL_MAILER=log` (dev y prod inicial) → se verifica el **contenido** en el log; el flujo (lead + encolado) se prueba con `Mail::fake()`.
- **Credenciales en el correo:** mitigado por almacenamiento **cifrado en reposo** + **rotación diaria** + alcance **demo desechable** + sin PII. Aun así, cualquiera con un correo válido las obtiene (gate ligero, aceptado por diseño).
- **Ventana de rotación:** las credenciales solicitadas poco antes del reset (04:00) expiran pronto; aceptado — el correo indica que se pueden re-solicitar en `/demo`.
- **Idempotencia de setup vs rotación:** `setup-demo` NO regenera si ya existen (evita invalidar credenciales vigentes en cada deploy, que corre setup-demo); solo `reset-demo` rota. Blindar con tests.
- **Multi-tenant en consola** (`MultiTenantScope` es NO-OP en consola): los comandos/mailable filtran `company_id` de la demo explícitamente. Ver [[credify-filament-auth]].
- **PHPStan level 5** sobre el backend nuevo + **Pint** antes de push. Ver [[credify-ci-phpstan-preflight]].
- **Sin tests JS** de la PWA → `npm run build` (cambia `main.js`, `auth.js`) + revisión.
- **Deploy** (ver [[credify-prod-vm]]): cambia backend + assets (build por el cambio en `main.js`) + migración (`demo_credentials`) → deploy full; y **conectar el proveedor de correo** en el `.env` de prod (paso aparte con la API key del usuario).

## Verificación

- **Backend (tests, `Mail::fake()`):** `GET /demo` renderiza 200; `POST /demo` crea `Lead` (`source=demo_request`) **y** encola `DemoCredentialsMail` al correo; **no** crea sesión ni token (`assertGuest`, sin auto-login); rate-limit activo. Login normal con las credenciales demo funciona (PWA `/api/pwa/auth/me` 200; Filament accesible para el Dueño). `setup-demo` fija contraseñas; `reset-demo` **rota** (assert: cambian). El passwordless ya no existe (`GET /demo/dueno` → 404). Suite completa + PHPStan L5 + Pint.
- **Landing:** `ExampleTest` (renderiza `/`); el CTA "Probar demo" apunta a `/demo`; sin restos de `/demo/{role}`.
- **Build:** `npm run build` sin errores (bundle + SW).
- **Manual/prod (tras cablear el proveedor):** solicitar en `/demo` → recibir el correo → iniciar sesión como Dueño/Supervisor/Cobrador en la PWA y como Dueño en `/admin`.
- **CHANGELOG.**
