# Demo compartida que reemplaza el trial self-serve

- **Fecha:** 2026-08-01
- **Rama:** `feat/demo-company`
- **Estado:** diseño aprobado en brainstorming, pendiente de plan.
- **Motivación:** hoy cada prospecto que prueba la app se auto-registra y **crea su propia empresa** (`/registro`), acumulando decenas de empresas-trial y carga en el servidor. Se reemplaza por **una única empresa demo compartida** en la que los visitantes entran (por rol) a probar el plan Profesional; sus datos se **resetean** (nightly + al llegar al 90% de uso) para que no crezcan sin control.

## Contexto (auditado)

- **Trial actual:** `POST /registro` → `TrialRegistrationService::register()` crea `Company` + `User` (rol admin) + `CompanyFinancialSettings` + `Subscription` (plan `professional`, trial 14 días), en una transacción; luego `Auth::login` + redirect `/admin`. `unique:users,email` = 1 trial por email. CTA de la landing (`welcome.blade.php`) apunta a `route('trial.register.form')`.
- **Límites/uso — YA EXISTE el primitivo:** `Company::getUsageStats()` devuelve `current/max/percentage` para usuarios, créditos, clientes y volumen contra los límites del plan (Profesional: clientes 500, créditos activos 200, usuarios 10, colectores 5, volumen 100M/mes). `Subscription::getUsagePercentage($field, $count)`. `PlanLimitService` aplica los límites en creación (Filament + API PWA), **exento** el registro de trial.
- **Suscripción/gate:** `EnsureActiveSubscription` redirige a `/suscripcion` a admins sin suscripción activa. `InitialSetupSeeder` ya crea una empresa dev con suscripción que no expira (`ends_at = now()->addYears(74)`).
- **Tenancy — el gotcha crítico:** `MultiTenantScope` (trait por-modelo, keyed en `Auth::user()->company_id`) **se desactiva en consola** (`app()->runningInConsole()`) y `super_admin` lo bypassa. → un comando Artisan ve TODAS las empresas; hay que filtrar por `company_id` explícito en cada tabla.
- **`Company`:** **sin** flag `is_demo`, **sin** `SoftDeletes`. Tablas tenant a resetear (~23): `clients`, `client_addresses`, `credits`, `installments`, `payments`, `collection_visits`, `incomes`, `expenses`, `financial_operations`, `partners`, `capital_contributions`, `partner_withdrawals`, `profit_distributions`(+lines), `par_snapshots`, `dashboard_snapshots`, `pwa_audit_logs`, `credit_audit_logs`, `payment_audit_logs`, `collector_credit_orders`, `company_collector_goals`, `supervisor_collectors`, `credit_restructure_logs`, `subscription_requests` (+ los audit/log). **Preservar:** la fila `Company` demo, su `Subscription`, sus usuarios demo y su `CompanyFinancialSettings`.
- **Seeders:** `ClientSeeder`/`CreditSeeder` (deshabilitados) generan clientes/créditos hardcoded — base a adaptar para el reseed. `bootstrap/app.php` tiene el scheduler (convención `credify:*` + `withoutOverlapping()->onOneServer()`).

## Decisiones (del brainstorming)

1. **Solo demo compartida, reemplaza el trial.** Una empresa demo; todos comparten datos.
2. **Acceso por selector de rol** (Dueño/Supervisor/Cobrador) → login sin registro a **usuarios demo fijos**.
3. **Reset:** disparador = **cualquier** métrica de `getUsageStats()` ≥ 90% (intra-día) **+ reset nightly base** (higiene). Tras borrar → **reseed** de un dataset realista.
4. **Onboarding real:** se retira `/registro` self-serve → formulario **"Solicitar cuenta"** (lead) → provisión **manual** por super_admin. **Nota de diseño:** el lead reusa el **mecanismo de `Lead` existente** (el del bot de ventas, `POST /leads`), que sí está pensado para prospectos **sin empresa** — NO `SubscriptionRequest` (que requiere una empresa existente). El plan confirma el modelo `Lead` y sus campos.

## Diseño

### 1. Empresa demo (ancla de seguridad)
- **Migración:** añadir `is_demo` (bool, default false, indexado) a `companies`. **Invariante: exactamente una** empresa con `is_demo=true`.
- **Provisión** (seeder/comando idempotente `credify:setup-demo`): empresa demo (`is_demo=true`, `status=active`) en plan **Profesional** con `Subscription` **permanente** (`status=active`, `ends_at = now()->addYears(74)`) + `CompanyFinancialSettings` + **3 usuarios demo fijos**: `demo-dueno@credifygo.com` (admin), `demo-supervisor@…` (supervisor), `demo-cobrador@…` (collector), con `company_id` fijado explícito (request sin sesión) y contraseña interna (no expuesta). Idempotente (`updateOrCreate` por email/company).

### 2. Acceso — "Probar demo" con selector de rol
- **Landing:** el CTA principal pasa de "Empieza gratis" a **"Probar demo"** → abre selector "Entrar como Dueño / Supervisor / Cobrador".
- **Backend `DemoAccessController`** (ruta pública, p. ej. `GET /demo/{role}` con `role ∈ {dueno,supervisor,cobrador}`, throttle):
  - Resuelve la empresa `is_demo=true` (aborta si no hay exactamente 1) y su usuario demo del rol pedido.
  - `Auth::login($demoUser)` + `session()->regenerate()` + redirect a la **PWA** (`/pwa/home`).
  - **Seguridad (hardcoded):** solo puede loguear un usuario **cuya empresa tenga `is_demo=true`** y cuyo rol esté en el set demo; jamás un usuario real. Rate-limit. Es una entrada "passwordless" acotada al sandbox.

### 3. Reset — `credify:reset-demo` (máxima seguridad)
Comando Artisan, convención `credify:*`.
- **Guards (invariantes, en orden):**
  1. `$demo = Company::where('is_demo', true)->get();` → **abortar** si `count() !== 1`.
  2. `$id = $demo->first()->id;` — todas las operaciones filtran **explícito** `where('company_id', $id)` + `withoutGlobalScopes()` (el scope está OFF en consola).
  3. **Allowlist** de modelos/tablas tenant a borrar (la lista del contexto). Ningún `delete` sin `where company_id = $id`. Nunca `companies`/`plans`/`roles`/otras empresas.
  4. **Preservar** por id/rol: la `Company` demo, su `Subscription`, los 3 usuarios demo (por email), su `CompanyFinancialSettings`. (Los usuarios NO-demo de la empresa demo, si los hubiera, se borran; pero por diseño solo existen los 3.)
  5. Todo en **transacción**; log de conteos por tabla; `--dry-run` (previsualiza sin borrar) y `--force` (salta el chequeo de umbral).
  6. Tras el wipe → invoca el **reseed** (§4).
- **Trigger de umbral:** sin `--force`, el comando lee `getUsageStats()` de la demo; resetea **solo si** alguna métrica `percentage ≥ 90` (umbral configurable `--threshold=90`). Con `--force`, resetea siempre.
- **Agenda** (`bootstrap/app.php`):
  - Nightly base: `credify:reset-demo --force` a las **04:00** (higiene diaria).
  - Intra-día: `credify:reset-demo` (condicional 90%) cada ~3 h en horario activo (p. ej. 09/12/15/18/21). `withoutOverlapping()->onOneServer()`.

### 4. Reseed — `DemoDataSeeder` (dataset realista)
Seeder/servicio **scoped a la empresa demo** (recibe el `company_id` demo; no depende del scope de consola). Adapta `ClientSeeder`/`CreditSeeder`:
- ~15-25 clientes con dirección; ~20 créditos en varios estados (al día, en mora, pagado) con sus `installments`; algunos `payments`/`collection_visits`; asignados al cobrador demo (ruta poblada). Montos realistas.
- Idempotente/repetible: siempre parte de cero (el reset ya borró) y crea el set fijo. Deja el uso a ~5% del plan (margen amplio).

### 5. Reemplazar el trial self-serve por "Solicitar cuenta" (lead)
- **Retirar** el flujo self-serve `/registro` que crea empresa (deshabilitar la ruta POST / la creación). El CTA de la landing ya no apunta ahí.
- **Nuevo "Solicitar cuenta":** formulario simple (nombre, empresa, email, teléfono/mensaje) → crea un **`Lead`** reusando la infraestructura existente del bot de ventas (`LeadController`/`POST /leads`), marcándolo con un `source`/tipo "solicitud de cuenta" para distinguirlo; notifica al equipo. La provisión de la empresa real la hace un humano (super_admin) desde Filament. Throttle anti-spam. (Si el modelo `Lead` no tiene un campo para distinguir origen, el plan añade uno mínimo.)
- La landing queda con 2 CTAs: **"Probar demo"** (primario) + **"Solicitar cuenta"** (secundario).

## No-objetivos / límites
- **No** onboarding self-serve automático de clientes reales (pasa a manual vía lead).
- **No** SoftDeletes en `Company` (el reset borra datos hijos, no la empresa).
- **No** múltiples empresas demo (invariante: exactamente 1).
- **No** exponer credenciales demo (el acceso es por endpoint, passwordless acotado).
- **No** tocar el cálculo de límites/uso ni `PlanLimitService` (se reusa `getUsageStats()`).

## Riesgos / gotchas
- **Borrado masivo mal acotado (el riesgo #1):** mitigado con ancla `is_demo` (exactamente 1) + filtro `company_id` explícito + `withoutGlobalScopes()` + allowlist + `--dry-run` + tests que verifican que una **segunda empresa (real) NO se toca**.
- **Scope OFF en consola:** todos los queries del comando y del reseed asumen sin-scope y filtran a mano.
- **Reset durante uso:** un visitante puede perder lo que hizo si cae un reset — aceptable (es demo); el nightly corre 04:00 (bajo tráfico).
- **Login passwordless:** acotado a usuarios `is_demo`; rate-limited; sin escalar a super_admin.
- **PHPStan level 5** sobre el backend nuevo; **tests** del comando de reset (incluido el guard de no-tocar-empresas-reales) y del acceso demo. Ver [[credify-ci-phpstan-preflight]], [[credify-test-db]].
- **Deploy full** (assets + backend + **migración**): build + ship assets + `git pull` + `migrate --force` + `optimize` + reload + **correr `credify:setup-demo` una vez** en prod. Ver [[credify-prod-vm]].

## Verificación
- Tests backend: `credify:reset-demo` borra solo la demo (una empresa real sembrada en el test permanece intacta), preserva empresa/suscripción/usuarios/settings, respeta el umbral 90% y `--force`; `DemoAccessController` solo loguea usuarios demo; el reseed deja el dataset esperado. Suite completa + PHPStan + Pint.
- `npm run build` sin errores; la landing muestra "Probar demo" + "Solicitar cuenta" y ya no el registro self-serve.
- Validación en dispositivo: entrar por los 3 roles a la PWA; ver el dataset demo; (en staging) forzar `--dry-run` y `--force` y confirmar el reseed.
- CHANGELOG.
