# Infraestructura Operativa — BewPro 2.0

> Última actualización: 2026-04-22

## Arquitectura Multi-VPS

```
                    ┌─────────────────┐
                    │   bewpro.com    │
                    │   (BewPro web)  │
                    └────────┬────────┘
                             │ Stripe Checkout
                             ▼
                    ┌─────────────────┐
                    │  Stripe Webhook │
                    │  → Airtable    │
                    │  → Slack       │
                    │  → Email       │
                    └────────┬────────┘
                             │ Pipeline_Status = "Required"
                             ▼
              ┌──────────────────────────────┐
              │  Cron: process-airtable.sh   │
              │  (corre en VPS Hostinger 1)  │
              │  cada 5 min                 │
              └──────────┬───────────────────┘
                         │ select_target_server()
                         │ (elige el VPS con menos cuentas)
                         │
              ┌──────────┴───────────┐
              ▼                      ▼
   ┌───────────────────┐  ┌───────────────────┐
   │ VPS Hostinger 1   │  │ VPS Donweb 1      │
   │ 72.61.45.136      │  │ 179.43.124.219    │
   │ Puerto SSH: 22    │  │ Puerto SSH: 5633  │
   │ ~27 proyectos     │  │ ~7 cuentas test   │
   └───────────────────┘  └───────────────────┘
```

### Servidores

| Server | IP | SSH | Rol | Proyectos |
|--------|----|----|-----|-----------|
| **VPS Hostinger 1** | 72.61.45.136 | Puerto 22 | Orquestador + hosting | ~36 cuentas |
| **VPS Donweb 1** | 179.43.124.219 | Puerto 5633 | Hosting solo | ~21 cuentas |
| **Hosting Rapido** | (legacy) | — | Legacy hosting | 6 proyectos viejos |

### Balanceo automático

`select_target_server()` en `process-airtable.sh` (solo en VPS1):
1. Cuenta cuentas cPanel en cada VPS via SSH
2. Elige el que tenga menos
3. Si VPS2 no responde SSH → fallback a VPS1
4. Escribe campo `SERVER` en Airtable

### Routing multi-VPS

```
process-airtable.sh (VPS1) → select_target_server()
  │
  ├── Target = VPS1 → TARGET_SERVER_IP=72.61.45.136 bash setup_cd_project4.sh (local)
  │
  └── Target = VPS2 → ssh -p 5633 root@179.43.124.219 \
                         "TARGET_SERVER_IP=179.43.124.219 bash setup_cd_project4.sh ..."
```

`TARGET_SERVER_IP` se pasa al setup script para que:
- Step 9 (DNS): cree el registro A apuntando a la IP correcta del VPS destino
- Step 10 (SSL): verifique que DNS resuelve a esa IP antes de emitir cert

### DNS

- Zona: `bewpro.com` gestionada en Hostinger (NS: orbit/horizon.dns-parking.com)
- `ns1.bewpro.com` / `ns2.bewpro.com` → 72.61.45.136 (delegación cPanel VPS1)
- Mail de bewpro.com → VPS1 (SPF solo autoriza 72.61.45.136)
- Subdominios: creados via Hostinger API en step 9 del setup script
- Dominios custom: DNS manual del cliente, apuntando al VPS asignado

### Scripts críticos

| Script | Ubicación | Función |
|--------|-----------|---------|
| `process-airtable.sh` | VPS1: `/root/scripts/` | Orquestador: detecta "Required", elige VPS, provisiona |
| `setup_cd_project4.sh` | Ambos VPS: `/root/scripts/` | Provisioning: cPanel + DB + DNS + SSL + bewpro:new |
| `fix-ssl.sh` | Ambos VPS: `/root/scripts/` | Retry SSL standalone: `fix-ssl.sh <username> [ip]` |
| `.airtable.env` | Ambos VPS: `/root/scripts/` | Credenciales Airtable + Slack + Hostinger token |
| `email_to_slack.php` | VPS1: `/home/lacompany/scripts/` | Pipe email → Slack |

---

## Pipeline de Provisioning

### Flujo completo (Stripe → sitio online)

```
1. Cliente paga en bewpro.com
   → Stripe Checkout (trial 15 días)
   → No se cobra durante trial

2. Webhook: checkout.session.completed
   → Crea Project en Airtable (Pipeline_Status: Required)
   → Extrae: email, nombre, producto, precio real, trial_end
   → Slack: "💰 Nueva compra"
   → Email: OrderReceivedMail (20-30 min para credenciales)

3. Cron (cada 5 min): process-airtable.sh
   → Detecta Pipeline_Status = Required
   → select_target_server() → elige VPS
   → setup_cd_project4.sh:
     [1/10] Crear cuenta cPanel
     [2/10] Crear DB + ALTER utf8mb4
     [3/10] SSH keys
     [4/10] Estructura git-files
     [5/10] Git clone cd-system
     [6/10] Composer install
     [7/10] bewpro:new (provisioning Laravel)
     [8/10] .htaccess
     [9/10] DNS (Hostinger API — usa TARGET_SERVER_IP del orquestador)
     [10/10] AutoSSL (foreground: clear exclusions → trigger → verify issuer Let's Encrypt)
   → Crea Subscription en Airtable
   → Actualiza Project: Pipeline_Status=On Development, SERVER, Domain
   → Slack: "✅ Provisioning completado"
   → Email: SiteProvisionedMail (credenciales)

4. Trial activo (15 días)
   → Día 8/12/14: alertas de vencimiento (email + Slack)
   → Día 15: Stripe cobra automáticamente

5. Post-cobro
   → payment_succeeded: actualiza Last_Payment_Date, Next_Payment_Date
   → payment_failed: incrementa Failed_Attempts, email de aviso
   → 3 fallos: suspensión + email + Slack
```

### Flujo Reseller (Compañía Digital)

```
1. Admin CD usa /form-reseller
   → Selecciona core (8 market-ready)
   → Opcionalmente: clean provision, colores de marca

2. FormResellerController → Airtable
   → Crea Project con Pipeline_Status: Required
   → Slack: "🚀 Provisioning solicitado"
   → Email al admin (CD) + email al cliente

3. Mismo cron → provisioning automático
```

---

## Billing y Suscripciones

### Modelo mixto

| Tipo | Cómo funciona |
|------|---------------|
| **Stripe** | Recurrente automático. Webhook actualiza Airtable. |
| **Manual** | Transferencia/MercadoPago. Fechas cargadas manualmente en Airtable. |
| **Trial** | 15 días gratis. Stripe cobra al día 16. |
| **Free** | Sin cobro (ej: Compañía Digital). |

### Airtable: tabla Subscriptions (`tblnpr52JhFBBi2Mg`)

Campos activos (20):

| Campo | Tipo | Función |
|-------|------|---------|
| Project | Text | Nombre del proyecto |
| Copia de Project | Link | Relación con Projects |
| Status | Select | Active/Suspended/Cancelled/Expired |
| Billing_Model | Select | Stripe/Manual/Free/Trial |
| Plan | Select | Monthly/Annual |
| Amount_USD | Currency | Monto por ciclo |
| Payment_Method | Select | Stripe/Mercadopago/Transferencia/Efectivo |
| Payment_Status | Select | Paid/Past Due/Failed/Suspended |
| Next_Payment_Date | Date | Próximo vencimiento |
| Last_Payment_Date | Date | Último pago |
| Grace_Period_End | Date | Fin de trial/gracia |
| Failed_Attempts | Number | Intentos de cobro fallidos |
| Stripe_Customer_ID | Text | ID en Stripe |
| Cpanel_User | Text | Usuario cPanel |
| App_URL | URL | URL del sitio |

### Alertas automáticas

| Evento | Email | Slack |
|--------|-------|-------|
| Nueva compra | OrderReceivedMail | 💰 cd-bewpro |
| Provisioning OK | SiteProvisionedMail | ✅ cd-bewpro |
| Provisioning fallido | — | ❌ cd-bewpro |
| Trial vence en 7/3/1 días | TrialExpiringMail | ⏰ cd-bewpro |
| Pago recibido | — | ✅ cd-bewpro |
| Pago fallido | PaymentFailedMail | ⚠️ cd-bewpro |
| Suspensión (3 fallos) | SubscriptionSuspendedMail | 🔴 cd-bewpro |
| Cancelación | — | ❌ cd-bewpro |

### Comandos de billing

```bash
# Verificar renovaciones y trials por vencer (diario 10:00)
php artisan bewpro:check-renewals [--dry-run]

# Verificar grace periods vencidos (diario 09:00)
php artisan bewpro:check-grace [--dry-run]

# Estado general de billing
php artisan bewpro:billing:status [--slack] [--model=Manual]
```

---

## Notificaciones Slack

### Webhooks configurados

| Canal | Webhook | Fuente |
|-------|---------|--------|
| cd-bewpro | `T081UKDS68P/B0AT45M5L0J/...` | Compras, provisioning, billing |
| cd-ventas | `T081UKDS68P/B0AUGUKCW49/...` | ventas@lacompaniadigital.com |
| cd-proyectos | `T081UKDS68P/B0ATXL99UNP/...` | proyectos@lacompaniadigital.com |
| cd-soporte | `T081UKDS68P/B0AU0KHLC77/...` | soporte@lacompaniadigital.com |
| cd-contacto | `T081UKDS68P/B0AU7NBC1SQ/...` | contacto@ + formulario web |
| juan (privado) | `T081UKDS68P/B0AU0S6EY5B/...` | juan@lacompaniadigital.com |
| coke (privado) | `T081UKDS68P/B0AU7S79SR2/...` | coke@lacompaniadigital.com |

### Email → Slack pipe

Script: `/home/lacompany/scripts/email_to_slack.php`
Configurado via cPanel Email Forwarders (pipe).

---

## Productos Market-Ready (8)

| Core | Demo | Precio | Tier |
|------|------|--------|------|
| law-firm-digital | demo-law-firm-2 | $35/mes | Pro |
| construction | demo-construction | $35/mes | Pro |
| real-estate | demo-real-estate | $40/mes | Premium |
| personal-brand | demo-accounting-2 | $25/mes | Pro |
| corporative | demo-marketing-1 | $30/mes | Standard |
| art-design | demo-architecture-2 | $35/mes | Pro |
| restaurant-bar | demo-restaurant | $30/mes | Standard |
| foundations-ong | demo-accounting-1 | $30/mes | Standard |

Cada core = DEMO + MÓDULOS + SEEDS + CONFIG + BRAND_DEFAULTS

173 variantes (shop products) en `catalog.json` mapean a estos 8 cores + 15 adicionales.

---

## SSL — Troubleshooting

### Flujo normal (step 10 de setup_cd_project4.sh)

1. Verifica que DNS apunte a `TARGET_SERVER_IP` (hasta 3 min)
2. Limpia exclusiones AutoSSL: `uapi --user SSL set_autossl_excluded_domains`
3. Dispara AutoSSL: `whmapi1 start_autossl_check_for_one_user`
4. Verifica en foreground que el issuer sea Let's Encrypt (hasta 4 min, 24 intentos)
5. Si detecta self-signed → re-limpia exclusiones + re-dispara AutoSSL

### Problemas comunes

| Problema | Causa | Solución |
|----------|-------|----------|
| Self-signed cert | DNS apunta a otro VPS | Corregir DNS → `fix-ssl.sh <user>` |
| No cert / timeout | DNS no propagó | Esperar propagación → `fix-ssl.sh <user>` |
| AutoSSL excluded | cPanel excluye por defecto | `uapi --user=X SSL set_autossl_excluded_domains` |
| Rate limit Let's Encrypt | >50 certs/semana por dominio | Esperar o usar staging |

### Comando de retry

```bash
# En el VPS donde está la cuenta:
/root/scripts/fix-ssl.sh <cpanel_user> [expected_ip]

# Ejemplo:
/root/scripts/fix-ssl.sh estudiolyr 72.61.45.136
```

### Verificar SSL de todas las cuentas

```bash
# Desde el propio VPS:
for user in $(whmapi1 listaccts | grep 'user:' | awk '{print $2}'); do
  domain=$(whmapi1 listaccts --output=json | python3 -c "import json,sys; [print(a['domain']) for a in json.load(sys.stdin)['data']['acct'] if a['user']=='$user']")
  issuer=$(echo | timeout 5 openssl s_client -connect "${domain}:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -issuer 2>/dev/null || echo 'NO_CERT')
  echo "$user | $domain | $issuer"
done
```

---

## Comandos operativos

```bash
# Provisioning
php artisan bewpro:new EMAIL "Título" SLUG [--db=...] [--clean] [--colors=...]
php artisan bewpro:products                    # listar cores
php artisan bewpro:delete CPANEL_USER          # borrar proyecto completo

# Billing
php artisan bewpro:billing:status [--slack]
php artisan bewpro:check-renewals [--dry-run]
php artisan bewpro:check-grace [--dry-run]

# Catálogo
php artisan bewpro:catalog:fill [--dry-run]    # crear productos faltantes en DB
php artisan bewpro:catalog:copy [--dry-run]    # generar marketing copy
php artisan bewpro:catalog:seed [--update]     # sincronizar desde Airtable
```

---

## Airtable: vistas recomendadas

### Projects
- **Pipeline** → Required, Processing (cola de provisioning)
- **Activos** → Active, sort by Name
- **Por servidor** → agrupado por SERVER
- **Por producto** → agrupado por Slug (from Product)

### Subscriptions
- **Dashboard** → Active, agrupado por Billing_Model
- **Próximos cobros** → Next_Payment_Date próximos 14 días
- **Trials** → Billing_Model = Trial
- **Morosos** → Payment_Status = Past Due / Failed

### Products
- **Market Ready** → Reseller_Ready = Ready
- **Por demo** → agrupado por Template
