# Audit + Linkeo de Proyectos del Reseller

> Cómo validar que cada proyecto vivo de un Reseller esté correctamente
> registrado end-to-end (Airtable + DB local + Users + linkeo). Para el barrido
> de proyectos legacy que se hicieron antes que el sistema de
> `tenant_projects` + `parent_reseller_id` existieran.

**Audiencia**: founder, equipo de soporte BewPro.
**Vivo desde**: 2026-05-19 (a partir del barrido de CD).

---

## 🎯 Por qué existe este flow

Cuando el sistema arrancó (antes de 2026-05), los proyectos del reseller solo
vivían en:
- **Airtable** (tabla Projects)
- **VPS** (carpeta + DB del tenant)

A partir de 2026-05-05 con la implementación del User Portal, se agregó la
tabla `tenant_projects` en `bewpro22_bp` como **proyección local de Airtable +
bridge a Stripe customer**. Sin un row en esta tabla:

- **Customer no ve su sitio** en `/my-projects` (query es `forUser(user_id)`)
- **Reseller no ve el proyecto** en `/reseller/*` (queries usan `parent_reseller_id`)
- **Cron `bewpro:check-billing` falla silently** (no encuentra DB matcheante)
- **Observer auto-upgrade tier no se dispara** (no hay transición a active)

Los proyectos legacy de CD nunca tuvieron ese TP local. Hay que crearlos
manualmente, linkeándolos contra Airtable como source of truth.

---

## 🛠 Comando: `bewpro:audit-reseller-project`

```bash
php artisan bewpro:audit-reseller-project --airtable-id=recXXXXXXX [--auto] [--dry-run]
```

**Opciones:**
| Opción | Descripción |
|---|---|
| `--airtable-id=` | Required. ID del Project en Airtable (`recXXX`). |
| `--auto` | No pide confirmación — crea User + TP faltantes automáticamente. |
| `--dry-run` | Solo reporta diffs, no escribe nada. |

**Lo que hace:**

1. Fetch Airtable Project + verifica campos clave (Name, Email, Domain, Cpanel_User, Pipeline_Status, Provisioned_DB, Reseller).
2. Resuelve el Reseller User a partir del `Reseller` record en Airtable
   (mapeo conocido: `recJ0WzUEqWI6MthL → lacompaniad@gmail.com` para CD).
3. Resuelve/crea el Customer User a partir del `Email` del Project. Asigna rol Spatie `Client`.
4. Verifica si existe `TenantProject` local linkeado por `airtable_record_id`.
   - **Si existe**: reconciliá diffs (pipeline_status, domain, user_id, parent_reseller_id).
   - **Si no existe**: crea con todos los campos correctos.
5. Backfill `Airtable.Provisioned_DB` si está UNSET (sugiere `{Cpanel_User}_db`).
6. HTTP probe al `https://{domain}/` y reporta status.

**Salida ejemplo:**
```
=== Airtable Project recCzvrAXdFZJQsU0 ===
  Name: Coke Colombres
  Email: coke_colombres@hotmail.com
  Domain: cokecolombres.com
  Cpanel_User: cokecolombres
  Pipeline_Status: Active
  Provisioned_DB: cokecolombres_db
  Reseller: ["recJ0WzUEqWI6MthL"]
  → Reseller resuelto: #132 lacompaniad@gmail.com
  → Customer resuelto: #135 coke_colombres@hotmail.com
  → TP local ya existe: #243 status=active
    ✓ TP en sync con Airtable
  → Sitio https://cokecolombres.com/ responde HTTP 200
✓ Audit completado para recCzvrAXdFZJQsU0
```

---

## ✅ Checklist canónico — qué debe estar al día para considerar un proyecto E2E "100%"

Aplicar este checklist a cada proyecto del barrido. Si algo falla, fix antes de
pasar al siguiente.

### 1. Airtable Project record
- [ ] `Name` populated
- [ ] `Email` del cliente real
- [ ] `Cpanel_User` populated
- [ ] `Provisioned_DB` populated (convención: `{Cpanel_User}_db` o `_bp`)
- [ ] `Domain` populated
- [ ] `Pipeline_Status = Active` (si el sitio está vivo)
- [ ] `Reseller` linkeado (recJ0WzUEqWI6MthL para CD)
- [ ] `Subscriptions` linkeada (al menos 1)
- [ ] `Product` + `Slug (from Product)` populated
- [ ] `SERVER` populated (VPS Hostinger 1 / VPS Donweb 1 / etc)
- [ ] `Country` (informativo)
- [ ] `GA_Tracking_ID` (si aplica)
- [ ] Sin registros duplicados (1 record por proyecto físico)

### 2. Airtable Subscription record
- [ ] `Billing_Model` definido (Free / Stripe / Manual)
- [ ] `Status` correcto (Active / Past Due / Suspended / Cancelled / Pending)
- [ ] `Project` linkeado bidireccional
- [ ] `Reseller (from Copia de Project)` herencia OK
- [ ] Si Billing_Model=Stripe: `Stripe_Customer_ID` + `Stripe_Subscription_ID` populated
- [ ] Sin subscriptions orphan (sin Project linkeado)

### 3. TenantProject local (`bewpro22_bp.tenant_projects`)
- [ ] Row existe linkeada por `airtable_record_id`
- [ ] `user_id` apunta al Customer User correcto
- [ ] `parent_reseller_id` apunta al Reseller User (132=CD)
- [ ] `project_name`, `slug`, `domain`, `cpanel_user`, `server` consistentes con Airtable
- [ ] `product_name` + `tier` populated
- [ ] `pipeline_status` refleja realidad (active si site live)
- [ ] `stripe_customer_id` real **o** mock detectable (`cus_demo_*`, `cus_test_*`, `cus_pending_*`, `cus_local_*`, `cus_mock_*`, `cus_free_*`, `reseller_*`)
- [ ] Para Free: `amount_usd = 0`, `grace_period_end = NULL`
- [ ] Para Stripe: `amount_usd > 0`, `grace_period_end` populated por webhook

### 4. User Customer
- [ ] Existe en `users` table con email correcto
- [ ] Rol Spatie `Client` asignado
- [ ] `email_verified_at` populated
- [ ] `password` set (cliente debe poder loguearse)

### 5. User Reseller
- [ ] Rol Spatie `Reseller` (+ `Client` si compra para sí mismo)
- [ ] `reseller_tier` definido (bronze/silver/gold)
- [ ] `affiliate_code` set (CD para Compañía Digital)
- [ ] `closes_count` refleja la cantidad real de proyectos active provisionados

### 6. Infraestructura del tenant (en su VPS — verificar SERVER de Airtable)
- [ ] cPanel user existe (`/var/cpanel/users/{user}` legible por root)
- [ ] Home dir con código (`/home/{user}/public_html/git-files/{user}/`)
- [ ] DB existe (`SHOW DATABASES LIKE '{user}_%'`)
- [ ] `.env` configurado:
  - [ ] `DB_DATABASE` apunta a la DB correcta
  - [ ] `APP_URL` apunta al dominio
  - [ ] `AIRTABLE_TOKEN` + `AIRTABLE_BASE_ID` + `AIRTABLE_SUBSCRIPTIONS_TABLE` presentes (sino `bewpro:check-billing` falla)
  - [ ] `STRIPE_KEY` + `STRIPE_SECRET` presentes (para subs reales)
- [ ] Cron configurado: `* * * * * php artisan schedule:run`
- [ ] Clone de git actualizado (HEAD reciente — debe tener `bewpro:check-billing` registrado en `php artisan list`)

### 7. Sitio público
- [ ] `curl -sI https://{domain}/` → HTTP 200 (o 503 si está deliberadamente suspendido)
- [ ] DNS resuelve al IP del VPS correcto
- [ ] SSL válido (certificado no vencido — `openssl s_client | x509 -noout -dates`)

### 8. Flujo de usuario (smoke test manual)
- [ ] Coke entra a `https://bewpro.com/login` → puede loguear con su email/password
- [ ] Tras login → `/my-projects` muestra solo el proyecto correcto
- [ ] Click en el proyecto → detalle con badge status correcto
- [ ] CD entra a `/reseller/customers` → ve al Customer
- [ ] CD entra a `/reseller/projects` → ve el proyecto

### 9. Stripe (solo si Billing_Model=Stripe)
- [ ] Subscription existe en Stripe LIVE
- [ ] Status del sub coincide con Subscription.Status en Airtable
- [ ] Customer_ID y Subscription_ID matchean entre Stripe ↔ Airtable ↔ TenantProject

---

## 📋 Playbook del barrido

### Paso 1 — Listar Airtable IDs del Reseller a auditar

En Airtable Projects, filtrar por `Reseller = CD`:
```
filterByFormula: SEARCH("recJ0WzUEqWI6MthL", ARRAYJOIN({Reseller}))
```
Anotá los record IDs (`recXXX`).

### Paso 2 — Correr el comando por cada uno (en bewpro22)

```bash
# Dry run primero — ver qué cambiaría
php artisan bewpro:audit-reseller-project --airtable-id=recXXX --dry-run

# Si todo OK, aplicar
php artisan bewpro:audit-reseller-project --airtable-id=recXXX
```

### Paso 3 — Verificación visual

Desde el panel del Reseller (`/reseller/projects`):
- El proyecto aparece en la lista
- Status badge refleja Pipeline_Status de Airtable
- Cliente aparece en `/reseller/customers`

Desde el panel del Customer (`/my-projects`):
- El customer ve su sitio
- Click en el TP → detalle con domain link

### Paso 4 — Resetear password del customer (si nunca se logueó)

```bash
php artisan tinker --execute="
\$u = \App\Models\User::find(135);
\$u->password = bcrypt('TEMP_PASS_AQUI');
\$u->save();
echo 'Password reseteado. Pasale las credenciales al cliente.' . PHP_EOL;
"
```

O mejor: mandarle email de "reset password" desde `https://bewpro.com/password/reset`.

---

## 🚨 Hallazgos comunes durante el barrido

### A. Duplicados en Airtable
Algunos proyectos tienen 2 records (el viejo del dev local + el nuevo del tenant prod). Convención:
- **El record correcto**: el que tiene `Email` apuntando al customer real y `Cpanel_User` matcheando el tenant físico.
- **El obsoleto**: dejar `Pipeline_Status=Archived` + prefijar Notes con `[DEPRECATED YYYY-MM-DD] Reemplazado por recXXX (motivo)`.

### B. `Provisioned_DB` UNSET
Backfill automático: `{Cpanel_User}_db` es la convención cPanel. El comando lo sugiere y aplica si confirmás.

### C. Reseller field vacío en Airtable
El record no tiene Reseller asignado. Hay que agregarlo manualmente en Airtable apuntando al record del User Reseller (CD: `recJ0WzUEqWI6MthL`).

### D. Customer User no existe
El comando lo crea con:
- `password = bcrypt(random_16_chars)` → cliente debe usar "reset password"
- Rol Spatie `Client` asignado
- `email_verified_at = now()` (autoassigned, asume que Coke ya verifica email por otro lado)

### E. Stripe placeholder según Billing_Model
- `Billing_Model = Free` → `stripe_customer_id = cus_free_{customer_id}`
- `Billing_Model = Manual` → `cus_pending_{customer_id}`
- `Billing_Model = Stripe` con sub real → debería sincronizar con el ID real cuando el cliente compre

Todos esos prefijos son detectados por `TenantProject::hasMockStripeCustomer()` y ocultan el botón "Gestionar pago" hasta que haya billing real.

---

## 🔗 Mapeo Airtable Reseller IDs → User emails

| Airtable Reseller record_id | User email | User ID en `bewpro22_bp` |
|---|---|---|
| `recJ0WzUEqWI6MthL` | `lacompaniad@gmail.com` | #132 (Compañía Digital) |
| (futuros) | ... | ... |

El comando `bewpro:audit-reseller-project` tiene este mapeo hardcoded en `guessResellerEmail()`. Cuando se agreguen más resellers, actualizar.

---

## 📜 Historial de barridos

| Fecha | Reseller | Proyectos auditados | Notas |
|---|---|---|---|
| 2026-05-19 | CD | 1 (Coke Colombres) | Primer caso. Encontré 2 records duplicados en Airtable + TP local faltante. Comando creado en este día. |
| ... | ... | ... | ... |

---

---

## 🗑️ Eliminar un proyecto end-to-end

El comando `bewpro:delete {cpanel_user}` borra cPanel + Airtable pero **NO toca
Stripe ni la tabla `tenant_projects` local**. Para una limpieza completa hay
que orquestar 5 fases en orden:

### Fase 1 — Cancel Stripe sub (LIVE)

Si tiene subscripción recurrente real:

```bash
ssh vps1-claude "su -s /bin/bash bewpro22 -c \
  'cd /home/bewpro22/public_html/bewpro && php -r \"
require \\\"vendor/autoload.php\\\";
\\\$app = require \\\"bootstrap/app.php\\\";
\\\$app->make(\\\"Illuminate\\\\Contracts\\\\Console\\\\Kernel\\\")->bootstrap();
\Stripe\Stripe::setApiKey(env(\\\"STRIPE_SECRET\\\"));
\\\$s = \Stripe\Subscription::retrieve(\\\"sub_XXXXX\\\");
echo \\\$s->cancel()->status;
\"'"
```

### Fase 2 — Borrar TenantProject local en `bewpro22_bp`

```bash
ssh vps1-claude "su -s /bin/bash bewpro22 -c \
  'cd /home/bewpro22/public_html/bewpro && php -r \"
require \\\"vendor/autoload.php\\\";
\\\$app = require \\\"bootstrap/app.php\\\";
\\\$app->make(\\\"Illuminate\\\\Contracts\\\\Console\\\\Kernel\\\")->bootstrap();
\App\Models\TenantProject::where(\\\"airtable_record_id\\\", \\\"recXXX\\\")->delete();
\"'"
```

### Fase 3 — Borrar Airtable records

`bewpro:delete` borra el Project + 1 Subscription (la primera que matchee por
nombre). Si hay subs duplicadas u orphan (sin Project linkeado), hay que
borrarlas vía API directo:

```bash
# Subs primero (child records)
curl -X DELETE -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  "https://api.airtable.com/v0/$BASE_ID/$SUBS_TABLE/recXXX"

# Project después
curl -X DELETE -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  "https://api.airtable.com/v0/$BASE_ID/$PROJECTS_TABLE/recXXX"
```

### Fase 4 — Borrar cPanel account en su VPS

⚠️ **Detectar el VPS correcto primero**: Airtable Project tiene `SERVER`
(VPS Hostinger 1 = vps1 / VPS Donweb 1 = vps2 / etc).

```bash
ssh vps2-claude "/scripts/removeacct {cpanel_user} --force"
```

Esto borra: cuenta cPanel, DB, DNS zone, home dir.

### Fase 5 — Limpieza homedir residual (si quedó)

```bash
ssh vps2-claude "rm -rf /home/{cpanel_user}"
```

### Verificación post-delete

```bash
# Sitio ya no responde (DNS puede tardar minutos en limpiarse)
curl -sI https://{domain}/ | head -1

# Coke (o el customer que veía mal el TP) ya no lo ve en /my-projects
ssh vps1-claude "su -s /bin/bash bewpro22 -c 'cd /home/bewpro22/public_html/bewpro && \
  php -r \"... \App\Models\TenantProject::forUser({user_id})->count() ...\"'"
```

### ⚠️ Atención al cancelar Stripe

`$subscription->cancel()` cancela **inmediatamente** (no espera al fin de período).
Si querés que el cliente use lo que pagó hasta el fin del mes:

```php
$subscription->cancel_at_period_end = true;
$subscription->save();
```

Para proyectos de prueba (como el caso Guido/Yastim 2026-05-19), el cancel
inmediato es correcto.

---

## 📜 Historial extendido

| Fecha | Acción | Detalle |
|---|---|---|
| 2026-05-19 | Audit Coke Colombres | Encontrado duplicados Airtable + TP local faltante. Creado comando + docs. |
| 2026-05-19 | Delete Guido Abogados + Yastim Restaurant | 2 proyectos de prueba que Coke pagó vía Stripe Checkout en bewpro.com. Cancel sub LIVE + TP local + 4 subs Airtable + 2 Projects Airtable + 2 cPanel accounts en VPS2 Donweb. Resultado: Coke ve solo Coke Colombres en /my-projects. |
| 2026-05-19 | Coke Colombres 100% E2E | Aplicados 5 fixes: git pull tenant (HEAD viejo → último), append AIRTABLE_*/STRIPE_*/CASHIER_CURRENCY a .env del tenant (faltaban, igual que Gemafi), CD `closes_count` 0→1, password reset de Coke. Checklist canónico documentado en este doc. Validado con `bewpro:check-billing` runeado OK en el tenant. |

---

## 🔗 Docs relacionados

- [`panel-reseller.md`](panel-reseller.md) — qué ve el reseller en su panel
- [`provisión-de-tenants.md`](provisión-de-tenants.md) — cómo se provisiona uno nuevo
- [`../fase-5-spec-completo.md`](../fase-5-spec-completo.md) — spec completo del sistema comercial
- [`../../billing-lifecycle.md`](../../billing-lifecycle.md) — lifecycle de Stripe + Airtable + cron suspend
