# Demo compartida que reemplaza el trial — 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 trial self-serve (que crea una empresa por prospecto) por **una única empresa demo compartida** con acceso por rol (login passwordless), cuyos datos se **resetean** (nightly + al 90% de uso) borrando SOLO la empresa demo y re-sembrando un dataset realista; y canalizar el onboarding real por un lead "Solicitar cuenta".

**Architecture:** Un flag `is_demo` en `companies` es el **ancla de seguridad** (invariante: exactamente 1). Un comando `credify:setup-demo` provisiona la empresa demo (plan Profesional, suscripción permanente, 3 usuarios fijos). Un `DemoAccessController` loguea (sin password, acotado a usuarios `is_demo`) a la PWA. El comando `credify:reset-demo` borra solo lo tenant de la empresa demo (filtro `company_id` explícito + `withoutGlobalScopes()` — el scope está OFF en consola — + allowlist + preserve list) y llama a `DemoDataSeeder`. La landing pasa a "Probar demo" + "Solicitar cuenta" (Lead), y se deshabilita `POST /registro`.

**Tech Stack:** Laravel 13 / PHP 8.3, PHPUnit 11, Blade (landing), Vue PWA (destino del login). **Verificación:** phpunit + PHPStan level 5 + Pint (backend); `npm run build` (landing es Blade, no PWA — pero el deploy es full). Ver [[credify-test-db]], [[credify-ci-phpstan-preflight]].

**Spec:** `docs/superpowers/specs/2026-08-01-demo-company-design.md`

**Gotchas críticos:**
- **`MultiTenantScope` OFF en consola** (`app()->runningInConsole()`): TODO query en comandos/seeders debe filtrar `company_id` explícito + `withoutGlobalScopes()`. El comando de reset borra masivo → un fallo aquí toca empresas reales. El test que **más importa** es "una segunda empresa real NO se toca".
- Preservar en el reset: la fila `Company` demo, su `Subscription`, sus 3 usuarios demo (por email), su `CompanyFinancialSettings`. Todo lo demás tenant se borra.
- El seeder J&A **no** fija `plan_id`; el setup demo **sí** debe fijar el plan Profesional (para que `getUsageStats()` compute contra sus límites).
- `getUsageStats()` → `['users'=>['percentage'=>..], 'credits'=>[...], 'clients'=>[...], 'volume'=>[...]]`.

**Convención de emails demo (constante compartida):** `demo-dueno@credifygo.com` (admin), `demo-supervisor@credifygo.com` (supervisor), `demo-cobrador@credifygo.com` (collector). Definir como constante (p. ej. `App\Support\Demo::EMAILS` / `Demo::COMPANY_NAME`) usada por setup, access y reset.

---

## File Structure

```
app/Support/Demo.php                                  CREATE  constantes (emails, roles, company name) — fuente única
database/migrations/xxxx_add_is_demo_to_companies.php CREATE  is_demo bool + index
app/Models/Company.php                                MODIFY  scope isDemo / helper
app/Console/Commands/SetupDemoCompany.php             CREATE  credify:setup-demo (idempotente)
app/Http/Controllers/DemoAccessController.php         CREATE  login passwordless acotado a demo
routes/web.php                                        MODIFY  /demo/{role}; deshabilitar POST /registro; /solicitar-cuenta
database/seeders/DemoDataSeeder.php                   CREATE  reseed realista scoped a la demo
app/Console/Commands/ResetDemoCompany.php             CREATE  credify:reset-demo (guards + wipe + reseed)
bootstrap/app.php                                     MODIFY  schedule (nightly force + intra-día 90%)
resources/views/welcome.blade.php                     MODIFY  CTAs: Probar demo + Solicitar cuenta
app/Http/Controllers/LeadController.php               MODIFY  aceptar source 'account_request' (o ruta dedicada)
tests/Feature/Demo/*                                  CREATE  setup, access, reset (guard), reseed
CHANGELOG.md                                          MODIFY
```

Fases: **A** infraestructura demo (T1-T3) · **B** reset (T4-T6) · **C** reemplazo del trial (T7-T8) · **D** cierre (T9).

---

### Task 1: Flag `is_demo` + constantes demo

**Files:**
- Create: `app/Support/Demo.php`
- Create: `database/migrations/xxxx_add_is_demo_to_companies.php`
- Modify: `app/Models/Company.php`

- [ ] **Step 1: Constantes**

`app/Support/Demo.php`:
```php
<?php

namespace App\Support;

final class Demo
{
    public const COMPANY_NAME = 'Credify Demo';
    public const COMPANY_EMAIL = 'demo@credifygo.com';

    /** email => rol */
    public const USERS = [
        'demo-dueno@credifygo.com' => 'admin',
        'demo-supervisor@credifygo.com' => 'supervisor',
        'demo-cobrador@credifygo.com' => 'collector',
    ];

    /** slug del rol de acceso público (dueno|supervisor|cobrador) => email del usuario demo */
    public const ROLE_SLUG_TO_EMAIL = [
        'dueno' => 'demo-dueno@credifygo.com',
        'supervisor' => 'demo-supervisor@credifygo.com',
        'cobrador' => 'demo-cobrador@credifygo.com',
    ];
}
```

- [ ] **Step 2: Migración**

`database/migrations/xxxx_add_is_demo_to_companies.php` (usa la fecha real):
```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('companies', function (Blueprint $table) {
            $table->boolean('is_demo')->default(false)->index()->after('status');
        });
    }

    public function down(): void
    {
        Schema::table('companies', function (Blueprint $table) {
            $table->dropColumn('is_demo');
        });
    }
};
```

- [ ] **Step 3: Modelo `Company`**

Añadir `'is_demo'` al `$fillable` y `'is_demo' => 'boolean'` al `$casts`. Añadir un scope:
```php
    public function scopeDemo($query)
    {
        return $query->where('is_demo', true);
    }
```

- [ ] **Step 4: Migrar + build check + commit**

```bash
wsl bash -lc "cd /var/www/html/credify && php artisan migrate 2>&1 | tail -3 && vendor/bin/phpstan analyse --level=5 app/Support/Demo.php app/Models/Company.php 2>&1 | tail -3"
wsl bash -lc "cd /var/www/html/credify && git add app/Support/Demo.php database/migrations app/Models/Company.php && git commit -m 'feat(demo): flag is_demo + constantes demo'"
```

---

### Task 2: `credify:setup-demo` (provisión idempotente)

Crea/asegura la empresa demo (Profesional, suscripción permanente, settings, 3 usuarios). Idempotente. Sigue el patrón de `InitialSetupSeeder` **pero fijando `plan_id` Profesional y `is_demo=true`**.

**Files:**
- Create: `app/Console/Commands/SetupDemoCompany.php`
- Test: `tests/Feature/Demo/SetupDemoTest.php`

- [ ] **Step 1: Test (TDD)**

`tests/Feature/Demo/SetupDemoTest.php`:
```php
<?php

namespace Tests\Feature\Demo;

use App\Models\Company;
use App\Models\Plan;
use App\Models\User;
use App\Support\Demo;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

class SetupDemoTest extends TestCase
{
    use DatabaseTransactions;

    public function test_setup_demo_creates_exactly_one_demo_company_on_professional(): void
    {
        foreach (['admin', 'supervisor', 'collector'] as $r) {
            Role::firstOrCreate(['name' => $r, 'guard_name' => 'web']);
        }
        Plan::factory()->create(['slug' => 'professional', 'is_active' => true, 'max_clients' => 500]);

        $this->artisan('credify:setup-demo')->assertSuccessful();

        $demos = Company::query()->where('is_demo', true)->get();
        $this->assertCount(1, $demos);
        $demo = $demos->first();
        $this->assertNotNull($demo->currentSubscription);
        $this->assertSame('professional', $demo->currentSubscription->plan->slug);
        foreach (array_keys(Demo::USERS) as $email) {
            $this->assertDatabaseHas('users', ['email' => $email, 'company_id' => $demo->id]);
        }

        // Idempotente: re-correr no duplica
        $this->artisan('credify:setup-demo')->assertSuccessful();
        $this->assertCount(1, Company::query()->where('is_demo', true)->get());
    }
}
```

- [ ] **Step 2: Correr → falla** (comando no existe).
```bash
wsl bash -lc "cd /var/www/html/credify && php artisan test --filter=SetupDemoTest 2>&1 | grep -iE 'Tests:|FAIL|not found' | head"
```

- [ ] **Step 3: Implementar `SetupDemoCompany`**

`app/Console/Commands/SetupDemoCompany.php`:
```php
<?php

namespace App\Console\Commands;

use App\Models\Company;
use App\Models\CompanyFinancialSettings;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use App\Support\Demo;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class SetupDemoCompany extends Command
{
    protected $signature = 'credify:setup-demo';
    protected $description = 'Provisiona (idempotente) la empresa demo compartida en plan Profesional.';

    public function handle(): int
    {
        $plan = Plan::query()->where('slug', 'professional')->first();
        if (! $plan) {
            $this->error('No existe el plan Profesional (slug=professional).');
            return self::FAILURE;
        }

        DB::transaction(function () use ($plan) {
            $company = Company::query()->where('is_demo', true)->first()
                ?? Company::query()->firstOrCreate(
                    ['email' => Demo::COMPANY_EMAIL],
                    ['name' => Demo::COMPANY_NAME]
                );
            $company->forceFill(['name' => Demo::COMPANY_NAME, 'status' => 'active', 'is_demo' => true])->save();

            Subscription::query()->updateOrCreate(
                ['company_id' => $company->id],
                [
                    'plan_id' => $plan->id,
                    'billing_cycle' => 'annual',
                    'starts_at' => now()->startOfDay(),
                    'ends_at' => now()->addYears(74)->endOfYear(),
                    'status' => 'active',
                    'is_active' => true,
                ]
            );

            CompanyFinancialSettings::query()->firstOrCreate(['company_id' => $company->id]);

            foreach (Demo::USERS as $email => $role) {
                $user = User::query()->updateOrCreate(
                    ['email' => $email],
                    [
                        'name' => 'Demo '.ucfirst($role),
                        'company_id' => $company->id,
                        'password' => bcrypt(bin2hex(random_bytes(16))), // aleatoria; el acceso es passwordless
                    ]
                );
                if (! $user->hasRole($role)) {
                    $user->syncRoles([$role]);
                }
            }

            $this->info("Empresa demo #{$company->id} lista (plan {$plan->slug}).");
        });

        return self::SUCCESS;
    }
}
```
Nota: `CompanyFinancialSettings::firstOrCreate` — si el modelo exige `created_by_user_id` NOT NULL, crear primero el usuario dueño y pasar su id (ver `TrialRegistrationService`); el implementer ajusta según el esquema real.

- [ ] **Step 4: Correr → pasa + PHPStan + Pint + commit**
```bash
wsl bash -lc "cd /var/www/html/credify && php artisan test --filter=SetupDemoTest 2>&1 | grep -iE 'Tests:|FAIL'"
wsl bash -lc "cd /var/www/html/credify && vendor/bin/phpstan analyse --level=5 app/Console/Commands/SetupDemoCompany.php 2>&1 | tail -3 && vendor/bin/pint app/Console/Commands/SetupDemoCompany.php app/Support/Demo.php 2>&1 | tail -2"
wsl bash -lc "cd /var/www/html/credify && git add app/Console/Commands/SetupDemoCompany.php tests/Feature/Demo/SetupDemoTest.php && git commit -m 'feat(demo): comando credify:setup-demo (idempotente, plan Profesional)'"
```

---

### Task 3: Acceso "Probar demo" (login passwordless acotado)

**Files:**
- Create: `app/Http/Controllers/DemoAccessController.php`
- Modify: `routes/web.php`
- Test: `tests/Feature/Demo/DemoAccessTest.php`

- [ ] **Step 1: Test (TDD)** — `tests/Feature/Demo/DemoAccessTest.php`:
```php
<?php

namespace Tests\Feature\Demo;

use App\Support\Demo;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Models\Plan;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

class DemoAccessTest extends TestCase
{
    use DatabaseTransactions;

    private function seedDemo(): void
    {
        foreach (['admin', 'supervisor', 'collector'] as $r) {
            Role::firstOrCreate(['name' => $r, 'guard_name' => 'web']);
        }
        Plan::factory()->create(['slug' => 'professional', 'is_active' => true]);
        $this->artisan('credify:setup-demo')->assertSuccessful();
    }

    public function test_demo_role_logs_in_the_fixed_demo_user_and_redirects_to_pwa(): void
    {
        $this->seedDemo();
        $res = $this->get('/demo/cobrador');
        $res->assertRedirect('/pwa/home');
        $this->assertAuthenticated();
        $this->assertSame(Demo::ROLE_SLUG_TO_EMAIL['cobrador'], auth()->user()->email);
    }

    public function test_unknown_role_is_rejected(): void
    {
        $this->seedDemo();
        $this->get('/demo/hacker')->assertNotFound();
    }
}
```

- [ ] **Step 2: Correr → falla.**
```bash
wsl bash -lc "cd /var/www/html/credify && php artisan test --filter=DemoAccessTest 2>&1 | grep -iE 'Tests:|FAIL' | head"
```

- [ ] **Step 3: Controlador + rutas**

`app/Http/Controllers/DemoAccessController.php`:
```php
<?php

namespace App\Http\Controllers;

use App\Models\Company;
use App\Models\User;
use App\Support\Demo;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class DemoAccessController extends Controller
{
    public function enter(Request $request, string $role): RedirectResponse
    {
        abort_unless(array_key_exists($role, Demo::ROLE_SLUG_TO_EMAIL), 404);

        $demo = Company::query()->where('is_demo', true)->first();
        abort_if($demo === null, 503, 'Demo no disponible.');

        // SEGURIDAD: solo un usuario de la empresa demo, con el email demo esperado.
        $user = User::query()
            ->where('company_id', $demo->id)
            ->where('email', Demo::ROLE_SLUG_TO_EMAIL[$role])
            ->first();
        abort_if($user === null, 503, 'Demo no disponible.');

        Auth::login($user);
        $request->session()->regenerate();

        return redirect('/pwa/home');
    }
}
```

En `routes/web.php` (con throttle):
```php
use App\Http\Controllers\DemoAccessController;

Route::get('/demo/{role}', [DemoAccessController::class, 'enter'])
    ->middleware('throttle:20,1')
    ->name('demo.enter');
```

- [ ] **Step 4: Correr → pasa + PHPStan + Pint + commit**
```bash
wsl bash -lc "cd /var/www/html/credify && php artisan test --filter=DemoAccessTest 2>&1 | grep -iE 'Tests:|FAIL'"
wsl bash -lc "cd /var/www/html/credify && vendor/bin/phpstan analyse --level=5 app/Http/Controllers/DemoAccessController.php 2>&1 | tail -3 && vendor/bin/pint app/Http/Controllers/DemoAccessController.php routes/web.php 2>&1 | tail -2"
wsl bash -lc "cd /var/www/html/credify && git add app/Http/Controllers/DemoAccessController.php routes/web.php tests/Feature/Demo/DemoAccessTest.php && git commit -m 'feat(demo): acceso passwordless por rol a la empresa demo'"
```

---

### Task 4: `DemoDataSeeder` (reseed realista, scoped a la demo)

Siembra un dataset pequeño y realista **en la empresa demo** (resuelta por `is_demo`), con `company_id` explícito (scope OFF en consola). Adapta la lógica de `database/seeders/ClientSeeder.php` y `CreditSeeder.php` (léelos) pero apuntando SIEMPRE al `company_id` demo y al cobrador demo.

**Files:**
- Create: `database/seeders/DemoDataSeeder.php`
- Test: `tests/Feature/Demo/DemoSeederTest.php`

- [ ] **Step 1: Test (TDD)** — verifica que tras el seeder la demo tiene datos (>10 clientes, >10 créditos), TODOS con `company_id` de la demo:
```php
<?php

namespace Tests\Feature\Demo;

use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Plan;
use Database\Seeders\DemoDataSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

class DemoSeederTest extends TestCase
{
    use DatabaseTransactions;

    public function test_seeder_populates_only_the_demo_company(): void
    {
        foreach (['admin', 'supervisor', 'collector'] as $r) {
            Role::firstOrCreate(['name' => $r, 'guard_name' => 'web']);
        }
        Plan::factory()->create(['slug' => 'professional', 'is_active' => true, 'max_clients' => 500, 'max_active_credits' => 200]);
        $this->artisan('credify:setup-demo')->assertSuccessful();
        $demo = Company::query()->where('is_demo', true)->first();

        $this->seed(DemoDataSeeder::class);

        $this->assertGreaterThan(10, Client::withoutGlobalScopes()->where('company_id', $demo->id)->count());
        $this->assertGreaterThan(10, Credit::withoutGlobalScopes()->where('company_id', $demo->id)->count());
        // Nada fuera de la demo
        $this->assertSame(0, Client::withoutGlobalScopes()->where('company_id', '!=', $demo->id)->count());
    }
}
```

- [ ] **Step 2: Correr → falla.** `php artisan test --filter=DemoSeederTest`

- [ ] **Step 3: Implementar `DemoDataSeeder`**

Estructura: resolver el demo company + su cobrador; crear ~20 clientes con dirección, ~20 créditos en estados variados (al día/mora/pagado) con sus `installments`, y algunos `payments`/`collection_visits`. Reusa las utilidades de `ClientSeeder`/`CreditSeeder` (montos, amortización) pero con `company_id`/`created_by_user_id` demo explícitos. Debe dejar el uso < 10% del plan.
```php
<?php

namespace Database\Seeders;

use App\Models\Company;
use App\Models\User;
use App\Support\Demo;
use Illuminate\Database\Seeder;

class DemoDataSeeder extends Seeder
{
    public function run(): void
    {
        $demo = Company::query()->where('is_demo', true)->first();
        if ($demo === null) {
            $this->command?->warn('DemoDataSeeder: no hay empresa demo; se omite.');
            return;
        }
        $collector = User::query()->where('company_id', $demo->id)
            ->where('email', Demo::ROLE_SLUG_TO_EMAIL['cobrador'])->first();

        // ... crear clientes/créditos/pagos con company_id=$demo->id, created_by_user_id=$collector->id ...
        // (adaptar de ClientSeeder/CreditSeeder; montos realistas; ~20 clientes, ~20 créditos variados)
    }
}
```
(El cuerpo detallado de generación se redacta siguiendo `ClientSeeder`/`CreditSeeder`; requisito: todo con el `company_id` demo, dataset coherente, idempotente-tras-reset.)

- [ ] **Step 4: Correr → pasa + PHPStan + Pint + commit**
```bash
wsl bash -lc "cd /var/www/html/credify && php artisan test --filter=DemoSeederTest 2>&1 | grep -iE 'Tests:|FAIL'"
wsl bash -lc "cd /var/www/html/credify && vendor/bin/phpstan analyse --level=5 database/seeders/DemoDataSeeder.php 2>&1 | tail -3 && vendor/bin/pint database/seeders/DemoDataSeeder.php 2>&1 | tail -2"
wsl bash -lc "cd /var/www/html/credify && git add database/seeders/DemoDataSeeder.php tests/Feature/Demo/DemoSeederTest.php && git commit -m 'feat(demo): DemoDataSeeder (dataset realista scoped a la demo)'"
```

---

### Task 5: `credify:reset-demo` — guards + wipe + reseed (CRÍTICO)

El comando más delicado. **El test estrella:** una segunda empresa REAL sembrada en el test queda **intacta** tras el reset.

**Files:**
- Create: `app/Console/Commands/ResetDemoCompany.php`
- Test: `tests/Feature/Demo/ResetDemoTest.php`

- [ ] **Step 1: Test (TDD) — incluye el guard de no-tocar-empresas-reales**

`tests/Feature/Demo/ResetDemoTest.php`:
```php
<?php

namespace Tests\Feature\Demo;

use App\Models\Client;
use App\Models\Company;
use App\Models\Plan;
use App\Models\User;
use App\Support\Demo;
use Database\Seeders\DemoDataSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

class ResetDemoTest extends TestCase
{
    use DatabaseTransactions;

    private function seedDemo(): Company
    {
        foreach (['admin', 'supervisor', 'collector'] as $r) {
            Role::firstOrCreate(['name' => $r, 'guard_name' => 'web']);
        }
        Plan::factory()->create(['slug' => 'professional', 'is_active' => true, 'max_clients' => 500, 'max_active_credits' => 200]);
        $this->artisan('credify:setup-demo')->assertSuccessful();
        $this->seed(DemoDataSeeder::class);
        return Company::query()->where('is_demo', true)->first();
    }

    public function test_force_reset_wipes_demo_data_but_preserves_company_sub_users_and_reseeds(): void
    {
        $demo = $this->seedDemo();
        $before = Client::withoutGlobalScopes()->where('company_id', $demo->id)->count();
        $this->assertGreaterThan(0, $before);

        $this->artisan('credify:reset-demo --force')->assertSuccessful();

        // preservados
        $this->assertDatabaseHas('companies', ['id' => $demo->id, 'is_demo' => true]);
        $this->assertNotNull($demo->fresh()->currentSubscription);
        foreach (array_keys(Demo::USERS) as $email) {
            $this->assertDatabaseHas('users', ['email' => $email, 'company_id' => $demo->id]);
        }
        // re-sembrado (vuelve a haber datos)
        $this->assertGreaterThan(0, Client::withoutGlobalScopes()->where('company_id', $demo->id)->count());
    }

    public function test_reset_never_touches_a_real_company(): void
    {
        $demo = $this->seedDemo();

        // Empresa REAL con datos
        $real = Company::factory()->create(['is_demo' => false]);
        $realClient = Client::factory()->create(['company_id' => $real->id]);

        $this->artisan('credify:reset-demo --force')->assertSuccessful();

        // La empresa real y sus datos siguen intactos
        $this->assertDatabaseHas('companies', ['id' => $real->id]);
        $this->assertDatabaseHas('clients', ['id' => $realClient->id, 'company_id' => $real->id]);
    }

    public function test_without_force_below_threshold_does_not_reset(): void
    {
        $demo = $this->seedDemo();
        $this->artisan('credify:reset-demo --threshold=90')->assertSuccessful(); // dataset ~5% → no resetea
        $this->assertGreaterThan(0, Client::withoutGlobalScopes()->where('company_id', $demo->id)->count());
    }

    public function test_aborts_when_not_exactly_one_demo(): void
    {
        // sin demo
        Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
        $this->artisan('credify:reset-demo --force')->assertFailed();
    }
}
```

- [ ] **Step 2: Correr → falla.** `php artisan test --filter=ResetDemoTest`

- [ ] **Step 3: Implementar `ResetDemoCompany` con los guards**

`app/Console/Commands/ResetDemoCompany.php`. Estructura obligatoria (los guards en orden):
```php
<?php

namespace App\Console\Commands;

use App\Models\Company;
use App\Support\Demo;
use Database\Seeders\DemoDataSeeder;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class ResetDemoCompany extends Command
{
    protected $signature = 'credify:reset-demo {--force : Resetea sin chequear umbral} {--dry-run : Solo cuenta, no borra} {--threshold=90 : % de uso que dispara el reset}';
    protected $description = 'Borra SOLO los datos de la empresa demo (is_demo) y re-siembra el dataset. Seguro por diseño.';

    /** Modelos tenant con company_id propio a BORRAR. NO incluye: Company, Subscription, User, CompanyFinancialSettings (se preservan). */
    private const WIPE_COMPANY_SCOPED = [
        \App\Models\Payment::class,
        \App\Models\PaymentAuditLog::class,
        \App\Models\Income::class,
        \App\Models\Expense::class,
        \App\Models\FinancialOperation::class,
        \App\Models\CollectionVisit::class,
        \App\Models\CollectorCreditOrder::class,
        \App\Models\CompanyCollectorGoal::class,
        \App\Models\SupervisorCollector::class,
        \App\Models\CapitalContribution::class,
        \App\Models\PartnerWithdrawal::class,
        \App\Models\ProfitDistribution::class,
        \App\Models\Partner::class,
        \App\Models\ParSnapshot::class,
        \App\Models\DashboardSnapshot::class,
        \App\Models\PwaAuditLog::class,
        \App\Models\CreditAuditLog::class,
        \App\Models\CreditRestructureLog::class,
        \App\Models\Installment::class, // tiene company_id
        \App\Models\Credit::class,
        \App\Models\Client::class,
        \App\Models\SubscriptionRequest::class,
    ];

    public function handle(): int
    {
        // GUARD 1: exactamente 1 empresa demo
        $demos = Company::query()->where('is_demo', true)->get();
        if ($demos->count() !== 1) {
            $this->error("Abort: se esperaba exactamente 1 empresa demo, hay {$demos->count()}.");
            return self::FAILURE;
        }
        $demo = $demos->first();
        $id = $demo->id;

        // GUARD 2: umbral (si no --force)
        if (! $this->option('force')) {
            $stats = $demo->getUsageStats();
            $over = collect($stats)->contains(fn ($s) => (int) ($s['percentage'] ?? 0) >= (int) $this->option('threshold'));
            if (! $over) {
                $this->info("Demo #{$id} bajo el umbral. Sin reset.");
                return self::SUCCESS;
            }
        }

        if ($this->option('dry-run')) {
            foreach (self::WIPE_COMPANY_SCOPED as $model) {
                $n = $model::withoutGlobalScopes()->where('company_id', $id)->count();
                $this->line(sprintf('%-40s %d', class_basename($model), $n));
            }
            return self::SUCCESS;
        }

        DB::transaction(function () use ($id) {
            // Hijos sin company_id propio: borrar por subquery al padre ANTES del padre.
            // client_addresses (via client_id) y profit_distribution_lines (via profit_distribution_id):
            \App\Models\ClientAddress::query()->whereIn(
                'client_id', \App\Models\Client::withoutGlobalScopes()->where('company_id', $id)->select('id')
            )->delete();
            // (si ProfitDistributionLine existe: análogo antes de ProfitDistribution)

            // GUARD 3: cada delete filtra explícito por company_id (scope OFF en consola).
            foreach (self::WIPE_COMPANY_SCOPED as $model) {
                $model::withoutGlobalScopes()->where('company_id', $id)->delete();
            }

            // GUARD 4 (preserve): usuarios NO-demo de la empresa demo (por si acaso) se borran; los demo se conservan.
            \App\Models\User::query()->where('company_id', $id)
                ->whereNotIn('email', array_keys(Demo::USERS))->delete();
        });

        // Reseed
        $this->call('db:seed', ['--class' => DemoDataSeeder::class, '--force' => true]);

        $this->info("Demo #{$id} reseteada y re-sembrada.");
        return self::SUCCESS;
    }
}
```
Notas para el implementer:
- **Verificar la lista `WIPE_COMPANY_SCOPED`** contra los modelos reales (nombres/namespaces exactos) y contra qué tablas tienen `company_id` físico vs. dependen del padre (revisar migraciones). Ajustar el orden si hay FKs sin `onDelete cascade` (borrar hijos antes que padres: p. ej. `Installment`/`Payment` antes de `Credit`; `Client` al final tras sus dependientes). El test `test_force_reset_...` fallará si el orden viola una FK.
- Si `ProfitDistributionLine`/`FinancialAuditLog` u otros hijos existen sin `company_id`, añadir su borrado por subquery igual que `ClientAddress`.

- [ ] **Step 4: Correr → pasa (los 5 tests, incluido el guard de empresa real) + PHPStan + Pint + commit**
```bash
wsl bash -lc "cd /var/www/html/credify && php artisan test --filter=ResetDemoTest 2>&1 | grep -iE 'Tests:|FAIL'"
wsl bash -lc "cd /var/www/html/credify && vendor/bin/phpstan analyse --level=5 app/Console/Commands/ResetDemoCompany.php 2>&1 | tail -3 && vendor/bin/pint app/Console/Commands/ResetDemoCompany.php 2>&1 | tail -2"
wsl bash -lc "cd /var/www/html/credify && git add app/Console/Commands/ResetDemoCompany.php tests/Feature/Demo/ResetDemoTest.php && git commit -m 'feat(demo): credify:reset-demo con guards de seguridad + reseed'"
```

---

### Task 6: Agenda (nightly force + intra-día 90%)

**Files:**
- Modify: `bootstrap/app.php`

- [ ] **Step 1: Añadir al `->withSchedule(...)`** (junto a los demás `$schedule->command(...)`, mismo estilo `withoutOverlapping()->onOneServer()->appendOutputTo($log)`):
```php
// ── Demo ──
$schedule->command('credify:reset-demo --force')->dailyAt('04:00')->withoutOverlapping()->onOneServer()->appendOutputTo($log);
$schedule->command('credify:reset-demo --threshold=90')->cron('0 9,12,15,18,21 * * *')->withoutOverlapping()->onOneServer()->appendOutputTo($log);
```

- [ ] **Step 2: Verificar que la agenda parsea + commit**
```bash
wsl bash -lc "cd /var/www/html/credify && php artisan schedule:list 2>&1 | grep -iE 'reset-demo|error'"
wsl bash -lc "cd /var/www/html/credify && git add bootstrap/app.php && git commit -m 'feat(demo): agenda reset-demo (nightly force + intra-dia 90%)'"
```

---

### Task 7: Landing — "Probar demo" + "Solicitar cuenta"; deshabilitar registro

**Files:**
- Modify: `resources/views/welcome.blade.php` (CTAs en ~142, ~480, ~636)
- Modify: `routes/web.php` (deshabilitar `POST /registro`)

- [ ] **Step 1: CTA principal → "Probar demo" con selector de rol**

En `welcome.blade.php` ~línea 142, reemplazar el `<a href="{{ route('trial.register.form') }}">Empieza gratis · 14 días</a>` por un botón "Probar demo" que abra un selector de rol (3 links a `/demo/dueno`, `/demo/supervisor`, `/demo/cobrador`). Patrón mínimo (details/summary o un pequeño menú; sin JS pesado):
```html
                <div class="relative group">
                    <button type="button" 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: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="M19 9l-7 7-7-7"/></svg>
                    </button>
                    <div class="absolute left-0 mt-2 w-56 rounded-2xl bg-[#0b1f17] border border-white/10 shadow-xl p-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition">
                        <a href="/demo/dueno" class="block px-4 py-2.5 rounded-lg text-slate-200 hover:bg-white/5">Entrar como Dueño</a>
                        <a href="/demo/supervisor" class="block px-4 py-2.5 rounded-lg text-slate-200 hover:bg-white/5">Entrar como Supervisor</a>
                        <a href="/demo/cobrador" class="block px-4 py-2.5 rounded-lg text-slate-200 hover:bg-white/5">Entrar como Cobrador</a>
                    </div>
                </div>
```
(Ajustar el estilo al del hero; el objetivo es 3 enlaces a `/demo/{role}`.)

- [ ] **Step 2: CTAs de precios (~480, ~636) → "Solicitar cuenta"**

Cambiar esos 2 `<a href="{{ route('trial.register.form') }}">` por un botón "Solicitar cuenta" que abra el bot de ventas existente (`data-open-chat="lead"`) o enlace a un ancla de contacto. (Reusa el lead del bot; el origin lo maneja Task 8.)

- [ ] **Step 3: Deshabilitar el registro self-serve**

En `routes/web.php`, comentar/eliminar la ruta `POST /registro` (`trial.register`) y la `GET /registro` (`trial.register.form`) — o dejar el GET redirigiendo a `/` para no romper enlaces viejos. El self-serve que crea empresa queda retirado.

- [ ] **Step 4: Build + render check + commit**
```bash
wsl bash -lc "cd /var/www/html/credify && php artisan view:clear && php artisan test --filter=ExampleTest 2>&1 | grep -iE 'Tests:|FAIL|ParseError'"
wsl bash -lc "cd /var/www/html/credify && git add resources/views/welcome.blade.php routes/web.php && git commit -m 'feat(demo): landing Probar demo + Solicitar cuenta; retirar registro self-serve'"
```

---

### Task 8: Lead "Solicitar cuenta" (reusa `Lead`)

**Files:**
- Modify: `app/Http/Controllers/LeadController.php` (o el widget del bot) para marcar origen.

- [ ] **Step 1: Distinguir el origen**

El `LeadController::store` ya hace `Lead::create([... 'source' => 'sales_bot'])`. Para "Solicitar cuenta", permitir un `source` distinto (`account_request`) cuando el request lo indique (p. ej. un campo `source` validado en `StoreLeadRequest` con allowlist `['sales_bot','account_request']`, default `sales_bot`). Así el equipo distingue prospectos que piden cuenta. (Si prefieres no tocar el bot, basta con dejar "Solicitar cuenta" apuntando al mismo bot — en cuyo caso este task es no-op y se documenta.)

- [ ] **Step 2: Verificar + commit** (si hubo cambio)
```bash
wsl bash -lc "cd /var/www/html/credify && vendor/bin/phpstan analyse --level=5 app/Http/Controllers/LeadController.php 2>&1 | tail -3"
wsl bash -lc "cd /var/www/html/credify && git add app/Http/Controllers/LeadController.php app/Http/Requests/StoreLeadRequest.php && git commit -m 'feat(demo): lead source account_request para Solicitar cuenta'"
```

---

### Task 9: Verificación holística + CHANGELOG

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

- [ ] **Step 1: Suite + PHPStan + Pint + build**
```bash
wsl bash -lc "cd /var/www/html/credify && php artisan test 2>&1 | tail -5"
wsl bash -lc "cd /var/www/html/credify && vendor/bin/phpstan analyse --level=5 app/ 2>&1 | tail -5"
wsl bash -lc "cd /var/www/html/credify && vendor/bin/pint --test 2>&1 | tail -5"
wsl bash -lc "cd /var/www/html/credify && npm run build 2>&1 | grep -iE 'error|built in' | tail -3"
```
Expected: todo verde (revisar flaky por fecha si aplica).

- [ ] **Step 2: CHANGELOG** — bajo `## [Sin publicar]` → `### Cambiado`: reemplazo del trial self-serve por una **empresa demo compartida** (`is_demo`) con acceso por rol (passwordless) a la PWA, **reset seguro** (`credify:reset-demo`: solo la demo, nightly + al 90%, con reseed realista `DemoDataSeeder`), provisión `credify:setup-demo`, y onboarding real vía lead "Solicitar cuenta". Ref al spec.

- [ ] **Step 3: Commit**
```bash
wsl bash -lc "cd /var/www/html/credify && git add CHANGELOG.md && git commit -m 'docs(demo): CHANGELOG demo compartida que reemplaza el trial'"
```

---

## Deploy (post-merge, flujo — **full** + migración + setup)

Tras merge a `main` y realineo de `dev`:
1. `npm run build` (WSL) → assets (la landing cambió).
2. scp assets + swap; `git pull --ff-only origin main`; **`php artisan migrate --force`** (añade `is_demo`); `php artisan optimize:clear && optimize && filament:optimize`; `sudo -n systemctl reload php8.3-fpm`.
3. **Una vez:** `php artisan credify:setup-demo` en prod (crea la empresa demo). Verificar `credify:reset-demo --dry-run`.
4. Validación en dispositivo: los 3 accesos `/demo/{role}` entran a la PWA; el dataset demo se ve; la landing muestra "Probar demo"/"Solicitar cuenta"; `/registro` ya no crea empresas.
Ver [[credify-prod-vm]].

---

## Self-Review (cobertura vs. spec)

- **Flag `is_demo` (ancla, invariante 1):** T1 + guard en T5. ✅
- **Setup demo (Profesional, permanente, 3 usuarios, idempotente):** T2 (con `plan_id`, a diferencia de J&A). ✅
- **Acceso passwordless por rol → PWA, acotado a `is_demo`:** T3. ✅
- **Reset seguro (guards: 1-demo, company_id explícito, withoutGlobalScopes, allowlist, preserve, dry-run/force, umbral):** T5 + su test estrella "no toca empresa real". ✅
- **Reseed realista scoped:** T4. ✅
- **Agenda nightly force + intra-día 90%:** T6. ✅
- **Reemplazar trial → Probar demo + Solicitar cuenta (Lead); retirar /registro:** T7 + T8. ✅
- **Tenancy gotcha (scope OFF en consola):** guards en T5, `withoutGlobalScopes()` + company_id explícito en T4/T5. ✅
- **Verificación (tests + PHPStan + Pint + build) + deploy full + setup en prod:** T9 + Deploy. ✅

**Placeholder scan:** el cuerpo generador de `DemoDataSeeder` (T4.3) y la verificación de la lista `WIPE_COMPANY_SCOPED`/orden FK (T5.3) se especifican como "adaptar de X / verificar contra migraciones" — legítimo para código que adapta patrones existentes; los guards, firmas y tests están completos y son el núcleo verificable. Sin `TBD` vagos.

**Consistencia de tipos/nombres:** `App\Support\Demo::{USERS, ROLE_SLUG_TO_EMAIL, COMPANY_NAME, COMPANY_EMAIL}` usados idénticos en T1-T5; `is_demo` (columna+cast+scope) consistente; `credify:setup-demo`/`credify:reset-demo` consistentes entre comandos, agenda y deploy; `getUsageStats()` shape (`['x'=>['percentage'=>..]]`) usado en T5. ✅
