# Acceso demo por captura de lead + credenciales por correo — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Reemplazar el login demo passwordless (`/demo/{role}`) por una página `/demo` que captura el lead y le envía por correo las credenciales de 3 usuarios demo fijos (contraseñas cifradas, rotadas a diario); el prospecto inicia sesión normal en PWA + Filament.

**Architecture:** Backend Laravel 13. Las 3 contraseñas demo se generan, se guardan cifradas (`demo_credentials`, cast `encrypted`) y se aplican a los usuarios demo (cast `password => hashed`); `setup-demo` las crea si faltan, `reset-demo` las rota (nightly). Una página pública `/demo` captura el lead (reusa `Lead`) y encola `DemoCredentialsMail`. Se elimina todo el flujo passwordless.

**Tech Stack:** Laravel 13, Filament 5, PHPUnit, Blade, Mailable en cola, `Str::password`, cast `encrypted`.

---

## Estructura de archivos

- **Crear:** `database/migrations/xxxx_create_demo_credentials_table.php`, `app/Models/DemoCredential.php`, `app/Support/DemoCredentials.php`, `app/Http/Controllers/DemoRequestController.php`, `resources/views/demo-request.blade.php`, `app/Mail/DemoCredentialsMail.php`, `resources/views/emails/demo-credentials.blade.php`, `tests/Feature/Demo/DemoCredentialsTest.php`, `tests/Feature/Demo/DemoRequestTest.php`.
- **Modificar:** `app/Console/Commands/SetupDemoCompany.php`, `app/Console/Commands/ResetDemoCompany.php`, `app/Http/Requests/StoreLeadRequest.php`, `routes/web.php`, `resources/views/welcome.blade.php`, `resources/js/pwa/main.js`, `resources/js/pwa/stores/auth.js`, `CHANGELOG.md`.
- **Borrar:** `app/Http/Controllers/DemoAccessController.php`, `resources/views/demo-enter.blade.php`, `tests/Feature/Demo/DemoAccessTest.php`. Quitar `PwaAuthResponse::issueToken` si queda sin uso.

---

## Fase A — Credenciales demo (cifradas + rotación)

### Task 1: Tabla + modelo `DemoCredential` (cifrado en reposo)

**Files:**
- Create: `database/migrations/xxxx_create_demo_credentials_table.php`
- Create: `app/Models/DemoCredential.php`
- Test: `tests/Feature/Demo/DemoCredentialsTest.php`

- [ ] **Step 1: Crear la migración**

Run: `php artisan make:migration create_demo_credentials_table`

Contenido del `up()`:

```php
Schema::create('demo_credentials', function (Blueprint $table) {
    $table->id();
    $table->string('role')->unique(); // admin | supervisor | collector
    $table->text('password');         // texto plano cifrado en reposo (cast encrypted)
    $table->timestamps();
});
```

`down()`: `Schema::dropIfExists('demo_credentials');`

- [ ] **Step 2: Crear el modelo**

```php
<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

/**
 * Credenciales de los 3 usuarios demo compartidos. `password` guarda el texto
 * plano CIFRADO en reposo (cast `encrypted`) porque debe poder incluirse en el
 * correo al prospecto y rota a diario (credify:reset-demo). Son credenciales
 * desechables de un entorno que se reinicia cada noche; sin PII.
 */
class DemoCredential extends Model
{
    protected $fillable = ['role', 'password'];

    protected $casts = ['password' => 'encrypted'];
}
```

- [ ] **Step 3: Escribir el test (falla)**

```php
<?php

declare(strict_types=1);

namespace Tests\Feature\Demo;

use App\Models\DemoCredential;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class DemoCredentialsTest extends TestCase
{
    use DatabaseTransactions;

    #[Test]
    public function it_stores_the_password_encrypted_at_rest(): void
    {
        $cred = DemoCredential::create(['role' => 'admin', 'password' => 'secret-plain-123']);

        // El modelo descifra vía cast.
        $this->assertSame('secret-plain-123', $cred->fresh()->password);

        // En la BD está cifrado (no es el texto plano).
        $raw = DB::table('demo_credentials')->where('role', 'admin')->value('password');
        $this->assertNotSame('secret-plain-123', $raw);
    }
}
```

- [ ] **Step 4: Migrar y correr el test**

Run: `php artisan migrate && php artisan test --filter=DemoCredentialsTest`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add database/migrations app/Models/DemoCredential.php tests/Feature/Demo/DemoCredentialsTest.php
git commit -m "feat(demo): tabla demo_credentials (password cifrado en reposo)"
```

---

### Task 2: `DemoCredentials` — generar/rotar/leer y aplicar a los usuarios

**Files:**
- Create: `app/Support/DemoCredentials.php`
- Test: `tests/Feature/Demo/DemoCredentialsTest.php` (agregar casos)

- [ ] **Step 1: Escribir el support class**

```php
<?php

declare(strict_types=1);

namespace App\Support;

use App\Models\DemoCredential;
use App\Models\User;
use Illuminate\Support\Str;

/**
 * Gestiona las contraseñas de los 3 usuarios demo compartidos.
 *
 * - rotate(): genera 3 contraseñas nuevas (alfanuméricas, sin símbolos → fáciles
 *   de teclear en móvil), las guarda cifradas en `demo_credentials` y las aplica
 *   como password de cada usuario demo (el cast `hashed` del modelo User las
 *   hashea al asignarlas). Rotación (nightly vía reset-demo).
 * - ensure(): si faltan credenciales, rota; si ya existen las 3, no toca nada
 *   (para no invalidar credenciales vigentes en cada deploy que corre setup-demo).
 * - forEmail(): devuelve [ ['role','email','password'(plano)], ... ] para el correo.
 */
class DemoCredentials
{
    public static function rotate(): void
    {
        foreach (Demo::USERS as $email => $role) {
            $plain = Str::password(12, letters: true, numbers: true, symbols: false, spaces: false);

            DemoCredential::updateOrCreate(['role' => $role], ['password' => $plain]);

            $user = User::where('email', $email)->first();
            if ($user) {
                $user->password = $plain; // cast `hashed` en User lo hashea una vez
                $user->save();
            }
        }
    }

    public static function ensure(): void
    {
        $existing = DemoCredential::whereIn('role', array_values(Demo::USERS))->count();
        if ($existing < count(Demo::USERS)) {
            self::rotate();
        }
    }

    /** @return array<int, array{role: string, email: string, password: ?string}> */
    public static function forEmail(): array
    {
        $byRole = DemoCredential::pluck('password', 'role'); // descifrado por el cast

        $out = [];
        foreach (Demo::USERS as $email => $role) {
            $out[] = ['role' => $role, 'email' => $email, 'password' => $byRole[$role] ?? null];
        }

        return $out;
    }
}
```

- [ ] **Step 2: Agregar tests (fallan)**

Añadir a `DemoCredentialsTest`:

```php
    #[Test]
    public function rotate_sets_and_changes_the_demo_user_passwords(): void
    {
        \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
        \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'supervisor', 'guard_name' => 'web']);
        \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'collector', 'guard_name' => 'web']);
        $company = \App\Models\Company::factory()->create(['is_demo' => true]);
        foreach (\App\Support\Demo::USERS as $email => $role) {
            \App\Models\User::factory()->create(['email' => $email, 'company_id' => $company->id]);
        }

        \App\Support\DemoCredentials::rotate();

        $cred = DemoCredential::where('role', 'admin')->first();
        $user = \App\Models\User::where('email', 'demo-dueno@credifygo.com')->first();
        $this->assertTrue(\Illuminate\Support\Facades\Hash::check($cred->password, $user->password));

        $first = $cred->password;
        \App\Support\DemoCredentials::rotate();
        $this->assertNotSame($first, DemoCredential::where('role', 'admin')->first()->password);
    }

    #[Test]
    public function ensure_is_idempotent_when_credentials_exist(): void
    {
        DemoCredential::create(['role' => 'admin', 'password' => 'a']);
        DemoCredential::create(['role' => 'supervisor', 'password' => 'b']);
        DemoCredential::create(['role' => 'collector', 'password' => 'c']);

        \App\Support\DemoCredentials::ensure();

        $this->assertSame('a', DemoCredential::where('role', 'admin')->first()->password);
    }
```

- [ ] **Step 3: Correr los tests**

Run: `php artisan test --filter=DemoCredentialsTest`
Expected: PASS (3 tests).

- [ ] **Step 4: Commit**

```bash
git add app/Support/DemoCredentials.php tests/Feature/Demo/DemoCredentialsTest.php
git commit -m "feat(demo): DemoCredentials (generar/rotar/leer + aplicar a usuarios)"
```

---

### Task 3: Cablear en `setup-demo` (ensure) y `reset-demo` (rotate)

**Files:**
- Modify: `app/Console/Commands/SetupDemoCompany.php` (loop de usuarios + llamada a ensure)
- Modify: `app/Console/Commands/ResetDemoCompany.php` (rotate en el reset)
- Test: `tests/Feature/Demo/SetupDemoTest.php`, `tests/Feature/Demo/ResetDemoTest.php`

- [ ] **Step 1: Reemplazar el loop de usuarios en `SetupDemoCompany::handle`**

Sustituir el bloque `// 4. Los 3 usuarios fijos...` (el `foreach (Demo::USERS ...)` con `updateOrCreate([...'password'=>bcrypt(...)])`) por:

```php
            // 4. Los 3 usuarios fijos, uno por rol (ver Demo::USERS). La contraseña
            //    NO se fija aquí (updateOrCreate la reescribiría en cada corrida);
            //    la fija DemoCredentials::ensure() abajo (fuera de esta transacción).
            foreach (Demo::USERS as $email => $role) {
                $user = User::firstOrNew(['email' => $email]);
                $user->name = 'Demo '.ucfirst($role);
                $user->company_id = $company->id;
                if (! $user->exists) {
                    $user->password = Str::password(24); // placeholder; ensure() lo reemplaza
                }
                $user->save();

                if (! $user->hasRole($role)) {
                    $user->syncRoles([$role]);
                }
            }
```

Añadir el import `use Illuminate\Support\Str;` arriba.

- [ ] **Step 2: Llamar a `ensure()` tras la transacción de provisión**

Justo después del `DB::transaction(...)` (antes o después del `$this->info('Empresa demo lista...')`), añadir:

```php
        // Fija las contraseñas demo conocidas si aún no existen (no las rota en
        // cada deploy; solo credify:reset-demo rota). Ver App\Support\DemoCredentials.
        \App\Support\DemoCredentials::ensure();
```

- [ ] **Step 3: Rotar en `ResetDemoCompany::handle`**

En `ResetDemoCompany::handle`, justo después del `$this->call('db:seed', ['--class' => DemoDataSeeder::class, '--force' => true]);` (repoblado), añadir:

```php
        // Rota las contraseñas demo (credenciales filtradas expiran en ≤24 h).
        \App\Support\DemoCredentials::rotate();
        $this->info('Contraseñas demo rotadas.');
```

- [ ] **Step 4: Test — setup fija contraseñas (login funciona)**

Añadir a `SetupDemoTest`:

```php
    #[Test]
    public function it_sets_known_demo_passwords_that_authenticate(): void
    {
        $this->artisan('credify:setup-demo --no-seed')->assertSuccessful();

        $cred = \App\Models\DemoCredential::where('role', 'admin')->first();
        $this->assertNotNull($cred);
        $user = User::where('email', \App\Support\Demo::ROLE_SLUG_TO_EMAIL['dueno'])->first();
        $this->assertTrue(\Illuminate\Support\Facades\Hash::check($cred->password, $user->password));
    }
```

- [ ] **Step 5: Test — reset rota las contraseñas**

Añadir a `ResetDemoTest` (usa el setUp existente que hace setup-demo --no-seed + wipe):

```php
    #[Test]
    public function force_reset_rotates_demo_passwords(): void
    {
        $this->artisan('credify:setup-demo --no-seed')->assertSuccessful();
        $before = \App\Models\DemoCredential::where('role', 'admin')->first()->password;

        $this->artisan('credify:reset-demo', ['--force' => true])->assertSuccessful();

        $after = \App\Models\DemoCredential::where('role', 'admin')->first()->password;
        $this->assertNotSame($before, $after);
    }
```

- [ ] **Step 6: Correr los tests**

Run: `php artisan test tests/Feature/Demo/SetupDemoTest.php tests/Feature/Demo/ResetDemoTest.php`
Expected: PASS.

- [ ] **Step 7: Commit**

```bash
git add app/Console/Commands/SetupDemoCompany.php app/Console/Commands/ResetDemoCompany.php tests/Feature/Demo/SetupDemoTest.php tests/Feature/Demo/ResetDemoTest.php
git commit -m "feat(demo): setup fija contraseñas demo, reset las rota"
```

---

## Fase B — Página /demo + captura de lead + correo

### Task 4: Permitir `source = demo_request` en el lead

**Files:**
- Modify: `app/Http/Requests/StoreLeadRequest.php:34`
- Test: `tests/Feature/Demo/DemoRequestTest.php` (se crea en Task 6; aquí solo el cambio de validación)

- [ ] **Step 1: Añadir `demo_request` al `Rule::in`**

En `rules()`, cambiar:

```php
            'source' => ['sometimes', 'string', Rule::in(['sales_bot', 'account_request'])],
```

por:

```php
            'source' => ['sometimes', 'string', Rule::in(['sales_bot', 'account_request', 'demo_request'])],
```

- [ ] **Step 2: Commit**

```bash
git add app/Http/Requests/StoreLeadRequest.php
git commit -m "feat(demo): permitir source=demo_request en leads"
```

---

### Task 5: `DemoCredentialsMail` (mailable en cola) + plantilla

**Files:**
- Create: `app/Mail/DemoCredentialsMail.php`
- Create: `resources/views/emails/demo-credentials.blade.php`
- Test: `tests/Feature/Demo/DemoRequestTest.php` (se cubre en Task 6)

- [ ] **Step 1: Crear el mailable**

Run: `php artisan make:mail DemoCredentialsMail`

```php
<?php

declare(strict_types=1);

namespace App\Mail;

use App\Support\DemoCredentials;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class DemoCredentialsMail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    /** @param string $leadName */
    public function __construct(public string $leadName) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: 'Tus credenciales para probar Credify GO!');
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'emails.demo-credentials',
            with: [
                'leadName' => $this->leadName,
                'credentials' => DemoCredentials::forEmail(),
                'pwaUrl' => url('/pwa/login'),
                'adminUrl' => url('/admin'),
            ],
        );
    }
}
```

- [ ] **Step 2: Crear la plantilla markdown**

`resources/views/emails/demo-credentials.blade.php`:

```blade
@component('mail::message')
# ¡Hola {{ $leadName }}! 👋

Estas son tus credenciales para probar **Credify GO!** con datos de ejemplo.

@php
    $labels = ['admin' => 'Dueño', 'supervisor' => 'Supervisor', 'collector' => 'Cobrador'];
@endphp

@foreach ($credentials as $c)
**{{ $labels[$c['role']] ?? $c['role'] }}**
Usuario: `{{ $c['email'] }}`
Contraseña: `{{ $c['password'] }}`

@endforeach

@component('mail::button', ['url' => $pwaUrl])
Abrir la app móvil (PWA)
@endcomponent

@component('mail::button', ['url' => $adminUrl])
Abrir el panel de gestión
@endcomponent

- El **Dueño** puede entrar también al panel de gestión; los tres roles entran a la app móvil.
- Es un **entorno demo** con datos de ejemplo que **se reinicia cada día**; si las credenciales dejan de funcionar, vuelve a solicitarlas.

Gracias,
Equipo Credify GO!
@endcomponent
```

- [ ] **Step 3: Commit**

```bash
git add app/Mail/DemoCredentialsMail.php resources/views/emails/demo-credentials.blade.php
git commit -m "feat(demo): DemoCredentialsMail (credenciales por correo, en cola)"
```

---

### Task 6: Página `/demo` (show + store) + rutas + vista

**Files:**
- Create: `app/Http/Controllers/DemoRequestController.php`
- Create: `resources/views/demo-request.blade.php`
- Modify: `routes/web.php`
- Test: `tests/Feature/Demo/DemoRequestTest.php`

- [ ] **Step 1: Crear el controlador**

```php
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\StoreLeadRequest;
use App\Mail\DemoCredentialsMail;
use App\Models\Lead;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Mail;
use Illuminate\View\View;

/**
 * Página pública para probar la demo: captura el lead y le ENVÍA por correo las
 * credenciales de los 3 usuarios demo. NO inicia sesión (sin auto-login; el
 * prospecto entra por el login normal con las credenciales del correo).
 */
class DemoRequestController extends Controller
{
    public function show(): View
    {
        return view('demo-request');
    }

    public function store(StoreLeadRequest $request): RedirectResponse
    {
        $email = $request->string('email')->toString();

        Lead::create([
            'name' => $request->string('name')->toString(),
            'email' => $email,
            'phone' => $request->string('phone')->toString(),
            'message' => $request->filled('message') ? $request->string('message')->toString() : null,
            'source' => 'demo_request',
        ]);

        Mail::to($email)->queue(new DemoCredentialsMail($request->string('name')->toString()));

        return back()->with('demo_sent', true);
    }
}
```

- [ ] **Step 2: Registrar las rutas**

En `routes/web.php`, añadir (junto a las rutas públicas; el import `use App\Http\Controllers\DemoRequestController;` arriba):

```php
Route::get('/demo', [DemoRequestController::class, 'show'])->name('demo.request');
Route::post('/demo', [DemoRequestController::class, 'store'])
    ->middleware('throttle:6,1') // anti-spam de correos
    ->name('demo.request.store');
```

- [ ] **Step 3: Crear la vista**

`resources/views/demo-request.blade.php` (usa el layout público existente):

```blade
@extends('layouts.public')

@section('title', 'Probar Credify GO! — Demo con datos de ejemplo')

@section('meta')
    <meta name="robots" content="noindex, nofollow">
@endsection

@section('content')
<section class="hero-dark min-h-screen pt-32 pb-24 px-4">
    <div class="max-w-2xl mx-auto">
        <h1 class="text-4xl font-extrabold text-white mb-4">Prueba Credify GO! con datos de ejemplo</h1>
        <p class="text-slate-300 mb-8">
            Déjanos tus datos y te enviamos por correo las credenciales para entrar como
            <strong class="text-white">Dueño</strong>, <strong class="text-white">Supervisor</strong> y
            <strong class="text-white">Cobrador</strong>. Podrás probar la <strong class="text-white">app móvil</strong>
            y el <strong class="text-white">panel de gestión</strong> con una empresa demo ya poblada.
            Es un entorno de prueba que se reinicia a diario.
        </p>

        @if (session('demo_sent'))
            <div class="rounded-2xl bg-emerald-500/15 border border-emerald-400/30 text-emerald-200 p-5 mb-6">
                <p class="font-semibold">¡Listo! Te enviamos las credenciales a tu correo.</p>
                <p class="text-sm mt-1 text-emerald-200/80">Revisa tu bandeja (y spam). Con ellas inicias sesión en la app y el panel.</p>
            </div>
        @endif

        <form method="POST" action="{{ route('demo.request.store') }}" class="glass rounded-2xl p-6 space-y-4">
            @csrf
            <div>
                <label class="block text-sm text-slate-300 mb-1">Nombre</label>
                <input name="name" value="{{ old('name') }}" required maxlength="120"
                       class="w-full rounded-xl bg-white/5 border border-white/15 px-4 py-2.5 text-white">
                @error('name') <p class="text-red-400 text-sm mt-1">{{ $message }}</p> @enderror
            </div>
            <div>
                <label class="block text-sm text-slate-300 mb-1">Correo</label>
                <input type="email" name="email" value="{{ old('email') }}" required
                       class="w-full rounded-xl bg-white/5 border border-white/15 px-4 py-2.5 text-white">
                @error('email') <p class="text-red-400 text-sm mt-1">{{ $message }}</p> @enderror
            </div>
            <div>
                <label class="block text-sm text-slate-300 mb-1">WhatsApp</label>
                <input name="phone" value="{{ old('phone') }}" required
                       class="w-full rounded-xl bg-white/5 border border-white/15 px-4 py-2.5 text-white">
                @error('phone') <p class="text-red-400 text-sm mt-1">{{ $message }}</p> @enderror
            </div>
            <div>
                <label class="block text-sm text-slate-300 mb-1">Tu negocio (opcional)</label>
                <input name="message" value="{{ old('message') }}" maxlength="1000"
                       class="w-full rounded-xl bg-white/5 border border-white/15 px-4 py-2.5 text-white">
            </div>
            <button type="submit"
                    class="w-full px-6 py-3.5 bg-linear-to-r from-emerald-500 to-emerald-600 text-white font-bold rounded-2xl hover:brightness-110 transition-all cursor-pointer">
                Enviarme las credenciales
            </button>
        </form>
    </div>
</section>
@endsection
```

- [ ] **Step 4: Escribir el test (falla)**

`tests/Feature/Demo/DemoRequestTest.php`:

```php
<?php

declare(strict_types=1);

namespace Tests\Feature\Demo;

use App\Mail\DemoCredentialsMail;
use App\Models\Lead;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Mail;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class DemoRequestTest extends TestCase
{
    use DatabaseTransactions;

    #[Test]
    public function the_demo_page_renders(): void
    {
        $this->get('/demo')->assertOk()->assertSee('credenciales', false);
    }

    #[Test]
    public function submitting_captures_a_lead_and_queues_the_credentials_email_without_logging_in(): void
    {
        Mail::fake();

        $response = $this->post('/demo', [
            'name' => 'Prospecto Uno',
            'email' => 'prospecto@example.com',
            'phone' => '3001234567',
        ]);

        $response->assertRedirect();
        $response->assertSessionHas('demo_sent', true);
        $this->assertGuest(); // sin auto-login
        $this->assertDatabaseHas('leads', ['email' => 'prospecto@example.com', 'source' => 'demo_request']);
        Mail::assertQueued(DemoCredentialsMail::class, fn ($m) => $m->hasTo('prospecto@example.com'));
    }
}
```

- [ ] **Step 5: Correr el test**

Run: `php artisan test tests/Feature/Demo/DemoRequestTest.php`
Expected: PASS (2 tests).

- [ ] **Step 6: Commit**

```bash
git add app/Http/Controllers/DemoRequestController.php resources/views/demo-request.blade.php routes/web.php tests/Feature/Demo/DemoRequestTest.php
git commit -m "feat(demo): página /demo captura lead + envía credenciales por correo"
```

---

### Task 7: Landing — CTAs "Probar demo" apuntan a `/demo`

**Files:**
- Modify: `resources/views/welcome.blade.php` (hero CTA, CTA final, nav CTA)
- Modify: `resources/views/components/public-nav.blade.php` (CTA "Probar demo" del nav)
- Test: `tests/Feature/ExampleTest.php` (ya existe, solo se corre)

- [ ] **Step 1: Reemplazar el dropdown "Probar demo" del hero por un enlace a `/demo`**

En `welcome.blade.php`, el bloque del hero `<div class="relative dropdown" data-dropdown>` que contiene el botón "Probar demo" + el `<div class="dropdown-menu ...">` con los 3 enlaces `/demo/dueno|supervisor|cobrador`: reemplazarlo COMPLETO por:

```blade
                <a href="{{ route('demo.request') }}"
                   class="inline-flex items-center gap-2.5 px-7 py-4 bg-linear-to-r from-emerald-500 to-emerald-500 text-white font-bold text-base rounded-2xl shadow-xl shadow-emerald-500/40 hover:shadow-emerald-400/60 hover:brightness-110 transition-all">
                    Probar demo
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
                </a>
```

(La fila contenedora conserva la clase `dropdown-host`; es inocua sin dropdown.)

- [ ] **Step 2: Reemplazar el dropdown "Probar demo" del CTA final igual**

En `welcome.blade.php`, el segundo `<div class="relative dropdown" data-dropdown>` (CTA final, botón "Probar demo" grande + menú con `/demo/{role}`): reemplazarlo por:

```blade
                <a href="{{ route('demo.request') }}"
                   class="inline-flex items-center justify-center gap-3 px-8 py-5 bg-linear-to-r from-emerald-500 to-emerald-500 text-white font-extrabold text-lg rounded-2xl shadow-2xl shadow-emerald-500/30 hover:shadow-emerald-500/50 hover:brightness-110 transition-all">
                    Probar demo
                    <svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
                </a>
```

- [ ] **Step 3: Reemplazar el "Probar demo" del nav**

En `resources/views/components/public-nav.blade.php`, el `<div class="relative dropdown" data-dropdown>` cuyo botón dice "Probar demo" y cuyo menú lleva a `/demo/{role}`: reemplazarlo por:

```blade
            <a href="{{ route('demo.request') }}"
               class="flex items-center gap-2 px-4 py-2 text-sm font-bold text-white bg-linear-to-r from-emerald-500 to-emerald-600 rounded-xl shadow-md shadow-emerald-300/30 hover:shadow-emerald-300/50 hover:brightness-110 transition-all">
                Probar demo
            </a>
```

(El dropdown "Recursos" del nav se conserva tal cual.)

- [ ] **Step 4: Verificar render + que apunta a /demo**

Run: `php artisan view:clear && php artisan test --filter=ExampleTest`
Expected: PASS (renderiza `/`).

Run: `grep -c '/demo/' resources/views/welcome.blade.php resources/views/components/public-nav.blade.php`
Expected: 0 en ambos (ya no hay enlaces passwordless `/demo/{role}`).

- [ ] **Step 5: Commit**

```bash
git add resources/views/welcome.blade.php resources/views/components/public-nav.blade.php
git commit -m "feat(demo): CTAs 'Probar demo' de la landing → página /demo"
```

---

## Fase C — Eliminar el acceso passwordless

### Task 8: Borrar `DemoAccessController`, ruta, `demo-enter`, bootstrap SPA

**Files:**
- Delete: `app/Http/Controllers/DemoAccessController.php`, `resources/views/demo-enter.blade.php`, `tests/Feature/Demo/DemoAccessTest.php`
- Modify: `routes/web.php`, `resources/js/pwa/main.js`, `resources/js/pwa/stores/auth.js`, `app/Support/PwaAuthResponse.php`

- [ ] **Step 1: Borrar la ruta passwordless y su import**

En `routes/web.php`: eliminar la línea `use App\Http\Controllers\DemoAccessController;` y el bloque:

```php
Route::get('/demo/{role}', [DemoAccessController::class, 'enter'])
    ->where('role', '...')   // (la restricción existente)
    ->name('demo.enter');
```

- [ ] **Step 2: Borrar archivos del flujo passwordless**

```bash
git rm app/Http/Controllers/DemoAccessController.php resources/views/demo-enter.blade.php tests/Feature/Demo/DemoAccessTest.php
```

- [ ] **Step 3: Quitar el bootstrap por sessionStorage de `main.js`**

En `resources/js/pwa/main.js`, eliminar el IIFE `bootstrapDemoSession()` (el bloque que lee `window.__PWA_BOOTSTRAP__` / `sessionStorage.getItem('pwa_bootstrap')` y llama `useAuthStore(pinia).bootstrapFromInline(...)`) y el import `import { useAuthStore } from './stores/auth'` si queda sin uso.

- [ ] **Step 4: Quitar `bootstrapFromInline` de `stores/auth.js`**

En `resources/js/pwa/stores/auth.js`: eliminar la función `bootstrapFromInline(...)` y su entrada en el `return { ... }` del store.

- [ ] **Step 5: Quitar `issueToken` de `PwaAuthResponse` (quedó sin uso)**

En `app/Support/PwaAuthResponse.php`: eliminar el método `issueToken(...)` (solo lo usaba `DemoAccessController`). Conservar `pwaRole`, `permissions`, `userPayload` (los usa `Api\Pwa\AuthController`).

- [ ] **Step 6: Verificar**

Run: `php artisan test --filter='Auth|Demo'`
Expected: PASS (login PWA intacto; los tests demo nuevos pasan; DemoAccessTest ya no existe).

Run: `php artisan route:list | grep -c 'demo/{role}'`
Expected: 0.

Run: `curl -s -o /dev/null -w '%{http_code}' http://localhost:8123/demo/dueno` (con `credify-landing` levantado)
Expected: 404.

- [ ] **Step 7: Commit**

```bash
git add -A
git commit -m "refactor(demo): eliminar el acceso passwordless (/demo/{role}, demo-enter, bootstrap SPA)"
```

---

## Fase D — Verificación + CHANGELOG + build + cierre

### Task 9: Verificación holística, build, CHANGELOG y finish

**Files:**
- Modify: `CHANGELOG.md`

- [ ] **Step 1: PHPStan level 5 (preflight)**

Run: `vendor/bin/phpstan analyse --level=5 app/ --no-progress`
Expected: `[OK] No errors`. Si aparecen errores por el nuevo código, corregir (o ajustar baseline solo si es tipado laxo pre-existente que dejó de aplicar).

- [ ] **Step 2: Pint**

Run: `vendor/bin/pint`
Expected: PASS (o auto-fix + revisar).

- [ ] **Step 3: Suite completa de demo + leads + auth**

Run: `php artisan test tests/Feature/Demo tests/Feature/Filament/AdminResourcesUxTest.php --filter='Demo|Lead|Auth'`
Expected: PASS. (Además, correr `php artisan test` completo si el tiempo lo permite; ignorar flaky pre-existentes documentados en CHANGELOG.)

- [ ] **Step 4: Build de assets (cambió main.js/auth.js)**

Run: `npm run build`
Expected: bundle + service worker sin errores.

- [ ] **Step 5: CHANGELOG**

Añadir bajo `## [Sin publicar]` → `### Cambiado` una entrada describiendo: acceso demo por captura de lead + credenciales por correo (rotadas a diario, cifradas), eliminación del passwordless, dependencia de proveedor de correo (Resend, por `.env`). Referenciar `docs/superpowers/specs/2026-08-07-demo-lead-access-design.md`.

- [ ] **Step 6: Commit**

```bash
git add CHANGELOG.md
git commit -m "docs(demo): CHANGELOG acceso demo por lead + credenciales por correo"
```

- [ ] **Step 7: Finalizar la rama**

**REQUIRED SUB-SKILL:** Usar `superpowers:finishing-a-development-branch`. Verificar tests, presentar opciones y ejecutar la elección (PR → CI → merge → deploy full: incluye migración `demo_credentials` + `credify:setup-demo`; y **conectar el proveedor de correo** en el `.env` de prod con la API key del usuario — paso aparte).

---

## Notas de verificación final (contra el spec)

- Página `/demo` con explicación + form + `noindex` + rate-limit → Task 6.
- Lead `source=demo_request` + Filament Leads (reusa el resource existente) → Tasks 4/6.
- Correo con 3 credenciales + enlaces PWA/admin, en cola → Task 5.
- 3 usuarios fijos compartidos, contraseñas cifradas + rotadas a diario → Tasks 1/2/3.
- Acceso completo dentro de la empresa demo (sin cambios al modelo de datos/reset) → intacto.
- Eliminación del passwordless (controller, ruta, demo-enter, bootstrap SPA, issueToken) → Task 8.
- Proveedor de correo agnóstico (Resend por `.env`); dev con `log` → Tasks 5/9 + deploy.
- Rotación diaria vía `reset-demo` nightly → Task 3.
