# Pipeline de Provisionado — BewPro

> Documentación del flujo completo desde que un proyecto entra en Airtable hasta que el cliente recibe sus credenciales por email.
>
> **Estado:** Producción operativa · **Versión:** Marzo 2026

---

## Índice

1. [Visión general](#1-visión-general)
2. [Arquitectura de componentes](#2-arquitectura-de-componentes)
3. [Flujo paso a paso](#3-flujo-paso-a-paso)
4. [Detalle de cada componente](#4-detalle-de-cada-componente)
5. [Estructura de datos en Airtable](#5-estructura-de-datos-en-airtable)
6. [Configuración del servidor](#6-configuración-del-servidor)
7. [Estado actual: qué funciona](#7-estado-actual-qué-funciona)
8. [Bugs corregidos](#8-bugs-corregidos)
9. [Oportunidades de mejora identificadas](#9-oportunidades-de-mejora-identificadas)
10. [Cuello de botella: DNS](#10-cuello-de-botella-dns)

---

## 1. Visión general

El pipeline convierte un registro en Airtable en un sitio web completamente funcional entregado al cliente, sin intervención manual más allá de crear el DNS.

```
Airtable (Required)
      ↓  cada 15 min (cron)
process-airtable.sh
      ↓
setup_cd_project2.sh  →  cPanel + DB + Git + Laravel + bewpro:new
      ↓
Airtable actualizado  (On Development + credenciales)
      ↓
Subscription record creado en Airtable
      ↓
Email de bienvenida al cliente  (noreply@bewpro.com)
```

**Tiempo total por proyecto:** ~5-8 minutos (dominado por `composer install` y `git clone`).

---

## 2. Arquitectura de componentes

### Servidor (WHM/cPanel · 72.61.45.136)

| Componente | Ruta | Rol |
|---|---|---|
| `process-airtable.sh` | `/root/scripts/` | Orquestador principal. Lee Airtable, llama al setup, actualiza vuelta. |
| `setup_cd_project2.sh` | `/root/scripts/` | Provisiona un proyecto: cPanel + DB + repo + Laravel. |
| `send-welcome-email.php` | `/root/scripts/` | Envía HTML via SMTP (`noreply@bewpro.com`). |
| `process-suspensions.sh` | `/root/scripts/` | Suspende/reactiva cuentas según pagos. Cron diario 8am. |
| `.airtable.env` | `/root/scripts/` | Credenciales Airtable (token, base_id, table_id). |
| Cron | `/etc/cron.d/bewpro-pipeline` | Dispara `process-airtable.sh` cada 15 min. |

### Repositorio (`cd-system`)

| Componente | Ruta | Rol |
|---|---|---|
| `bewpro:new` | `app/Console/Commands/ProvisionNew.php` | Artisan: migra DB, aplica preset, crea admin. |
| `bewpro:airtable:process` | `app/Console/Commands/AirtableProcess.php` | Versión artisan del pipeline (uso local/dev). |
| `AirtableService` | `app/Services/AirtableService.php` | HTTP client hacia Airtable API. |
| Catálogo de productos | `database/seeders/products/catalog.json` | 121 shop slugs → core slug mapping. |
| Core presets | `database/seeders/products/core/*.json` | 14 presets técnicos con demo + módulos. |

### Airtable (base: `appRxvpzqCmNsw2JN`)

| Tabla | ID | Contenido |
|---|---|---|
| Projects | `tblzCgJZCbbt5j13Q` | Registro maestro de cada proyecto/cliente. |
| Subscriptions | `tblnpr52JhFBBi2Mg` | Estado de pago, fechas, billing model. |
| Products | *(referenciada)* | Catálogo de productos con slugs. |

---

## 3. Flujo paso a paso

### Trigger
El operador crea o modifica un registro en la tabla **Projects** de Airtable y setea:
- `Pipeline_Status = Required`
- `Name` — nombre del proyecto/cliente
- `Cpanel_User` — username cPanel (max 15 chars, alfanumérico)
- `Email` — email del admin del cliente
- `Product` → linked record con `Slug (from Product)` como lookup
- `Domain` — URL del sitio (opcional; default: `{cpanel_user}.bewpro.com`)

### Paso 1 — Detección (cron cada 15 min)
```
/etc/cron.d/bewpro-pipeline:
*/15 * * * * root /root/scripts/process-airtable.sh >> /var/log/bewpro-pipeline.log 2>&1
```
`process-airtable.sh` consulta Airtable con el filtro `{Pipeline_Status}="Required"`, ordena por Name ascendente.

### Paso 2 — Parseo de registros
El script usa Python 3 inline para parsear el JSON de Airtable y generar una línea por registro:
```
record_id|name|base_name|email|slug|domain
```
- `base_name`: usa `Cpanel_User` si existe; sino genera desde `Name` (lowercase, alfanumérico, max 15 chars).
- `email`: fallback a `admin@{base_name}.bewpro.com` si vacío.
- `domain`: normaliza a `https://`; si está vacío o tiene `---`, usa `https://{base_name}.bewpro.com`.
- Si falta `Name` o `Slug`: el registro se salta con log en stderr.

### Paso 3 — Generación de password
```bash
PASSWORD=$(openssl rand -base64 12 | tr -d '/+=')
```
Se genera por cada proyecto antes de llamar al setup. Este mismo password es:
- Contraseña de la cuenta cPanel
- Contraseña del usuario de DB
- Contraseña del admin Laravel

### Paso 4 — Provisioning (`setup_cd_project2.sh`)

El script recibe: `base_name email "title" slug password domain`

#### [1/8] Crear cuenta cPanel
```bash
whmapi1 createacct username=... domain=... password=... contactemail=noreply@bewpro.com
```
- `contactemail` es interno (`noreply@bewpro.com`) para que cPanel no mande notificaciones al cliente.
- `whmapi1` siempre retorna exit 0 → se verifica `result: 1` en el log `/tmp/createacct_{user}.log`.
- Luego espera hasta 30s a que el usuario del sistema esté disponible (`id {username}`).

#### [2/8] Crear DB, usuario y permisos
```bash
uapi --user={username} Mysql create_database name={base_name}_bp
uapi --user={username} Mysql create_user name={base_name}_bpuser password={password}
uapi --user={username} Mysql set_privileges_on_database ...
```
Convención de nombres:
- DB: `{base_name}_bp`
- Usuario DB: `{base_name}_bpuser`

#### [3/8] Copiar claves SSH
Copia las claves SSH de root (`/root/.ssh/id_rsa*`, `id_ed25519*`) al nuevo usuario para que pueda hacer `git clone` desde GitHub.

#### [4/8] Preparar estructura git-files
```bash
su - {username} -c "mkdir -p public_html/git-files/{base_name}"
su - {username} -c "ssh-keyscan github.com >> ~/.ssh/known_hosts"
```

#### [5/8] Clonar repositorio
```bash
su - {username} -c "git clone --branch cd-system git@github.com:LACOMPANIADIGITAL/cd-system.git ."
```
El repo trae todo: `bewpro:new`, 14 core presets, `catalog.json`, módulos.

#### [6/8] Configurar Laravel
1. Copia `.env.example` → `.env`
2. Configura con `sed`: `APP_NAME`, `APP_ENV=production`, `APP_URL`, `DB_*`, `RUN_PROJECT_SEEDER=true`
3. Genera `APP_KEY` via `openssl rand -base64 32` → inyecta en `.env` (sin artisan, vendor no existe aún)
4. `composer install --no-scripts --no-interaction --ignore-platform-reqs`
5. `php artisan package:discover`

#### [7/8] Provisionar con bewpro:new
```bash
php artisan bewpro:new \
  '{email}' '{title}' '{slug}' \
  --db='{base_name}_bp' \
  --url='{app_url}' \
  --password='{password}' \
  --skip-assets \
  --no-interaction
```

`bewpro:new` internamente:
1. Resuelve shop slug → core slug via `catalog.json`
2. Carga el preset `database/seeders/products/core/{core}.json` (demo, módulos, config)
3. Escribe `users.json` temporal con email + password
4. Llama a `doProvision()` que ejecuta migrate + seeders + settings + módulos
5. Restaura `users.json` original

Luego: `php artisan storage:link` (separado, no incluido en bewpro:new).

#### [8/8] Crear .htaccess
```apache
RewriteRule ^(.*)$ git-files/{base_name}/public/$1 [L]
```
Redirige todas las requests desde `public_html/` hacia `public_html/git-files/{base_name}/public/`.

**Output:** Solo `echo "${APP_URL}"` va a stdout. Todo lo demás (`>&2`) para que `process-airtable.sh` capture únicamente la URL.

### Paso 5 — Actualizar Airtable
```bash
PATCH /Projects/{record_id}:
  Pipeline_Status  = "On Development"
  Cpanel_User      = {base_name}
  Provisioned_DB   = {base_name}_bp
  Provisioned_Password = {password}
  Domain           = {app_url}
```

### Paso 6 — Crear Subscription record
```bash
POST /tblnpr52JhFBBi2Mg:
  Project         = {name}
  Copia de Project = [{record_id}]  ← linked record
  Cpanel_User     = {base_name}
  App_URL         = {app_url}
  Billing_Model   = "Stripe"
  Status          = "Active"
  Plan            = "Monthly"
  Payment_Method  = "Stripe"
  Start_Date      = today
  Provisioned_At  = today
```

### Paso 7 — Email de bienvenida al cliente
```bash
php /root/scripts/send-welcome-email.php \
  {email} {name} {app_url} {password} {email}
```

Usa **PHPMailer** via SMTP:
- Host: `127.0.0.1:587` (servidor local)
- Auth: `noreply@bewpro.com` / `{password_mailbox}`
- SSL peer verification deshabilitada (cert Hostinger no matchea IP)
- Reply-To: `soporte@bewpro.com`
- HTML con: URL del sitio, URL del admin, email usuario, contraseña

### Paso 8 — Resumen
```
=============================================
  Resultado: N OK, M fallidos
=============================================
```
El script retorna exit code 1 si hay algún proyecto fallido.

---

## 4. Detalle de cada componente

### `bewpro:new` — Resolución de producto

```
shop slug (e.g. "yoga-instructor")
    ↓ catalog.json lookup
core slug (e.g. "nutritionist")
    ↓
database/seeders/products/core/nutritionist.json
    ↓
doProvision() → migrate + seeders
```

Si el slug no está en `catalog.json`, se asume que es un core slug directamente.

### Core presets disponibles (14)

| Core slug | Demo | Módulos principales |
|---|---|---|
| `insurance-advisor` | demo-insurance | services, faqs |
| `real-estate` | demo-real-estate | tokko, services, gallery, blog, faqs, projects |
| `business-catalogue` | demo-digital-agency-2 | services, products, faqs, gallery |
| `art-design` | demo-architecture-2 | services, gallery, projects, blog, faqs |
| `foundations-ong` | demo-accounting-1 | services, blog, gallery, faqs |
| `photography` | demo-photography-3 | gallery, blog, projects |
| `nutritionist` | demo-insurance | services, blog, gallery, faqs |
| `financial-wealth` | demo-insurance | services, blog, references, faqs, team |
| `bp-dinamic` | demo-business-consulting | services, faqs |
| `petite-website` | demo-business-consulting | services, faqs |
| `standard-website` | demo-digital-agency-2 | services, faqs, gallery, blog |
| `catalogue-ai` | demo-digital-agency-2 | products, faqs |
| `concierge` | demo-insurance | services, gallery, projects, blog, products |
| `agency` | demo-accounting-1 | services, blog, projects, gallery, team, references |

### Ciclo de vida de `Pipeline_Status`

```
[vacío / Manual]
      ↓  operador carga el proyecto
   Required
      ↓  process-airtable.sh detecta (cada 15 min)
  On Development
      ↓  cliente paga y activa
     Active      ← Stripe webhook (pendiente)
      ↓  falla pago
    Past_Due
      ↓  grace period (7 días) vence
    Suspended
      ↓  cliente paga
     Active
```

### Ciclo de vida de `Status` en Subscriptions

```
Active → Past_Due → Suspended → Active
                 ↘ (grace)
            (notificación email en grace period)
```

---

## 5. Estructura de datos en Airtable

### Tabla: Projects

| Campo | Tipo | Propósito |
|---|---|---|
| `Name` | Text | Nombre del proyecto |
| `Cpanel_User` | Text | Username cPanel (max 15 chars) |
| `Email` | Email | Admin email del cliente |
| `Product` | Linked → Products | Producto contratado |
| `Slug (from Product)` | Lookup | Slug técnico del producto |
| `Domain` | URL | URL del sitio (opcional) |
| `Pipeline_Status` | Single select | Required / On Development / Active / Suspended |
| `Provisioned_DB` | Text | DB asignada (`{user}_bp`) |
| `Provisioned_Password` | Text | Password del admin ⚠️ |
| `Launch_Date` | Date | Fecha de alta |
| `Launch_Ready` | Formula | Indica si está lista para salir |

### Tabla: Subscriptions

| Campo | Tipo | Propósito |
|---|---|---|
| `Project` | Text | Nombre del proyecto |
| `Copia de Project` | Linked → Projects | Relación al registro Projects |
| `Cpanel_User` | Text | Username cPanel |
| `App_URL` | URL | URL del sitio |
| `Billing_Model` | Select | Stripe / Manual |
| `Status` | Select | Active / Past_Due / Suspended |
| `Plan` | Select | Monthly / Annual |
| `Payment_Method` | Select | Stripe / Transfer |
| `Start_Date` | Date | Inicio de suscripción |
| `Provisioned_At` | Date | Fecha de provisioning |
| `Grace_Period_End` | Date | Vence el período de gracia |
| `Suspended_Date` | Date | Fecha de suspensión |

---

## 6. Configuración del servidor

### Archivos clave

```
/root/scripts/
├── .airtable.env              ← AIRTABLE_TOKEN, AIRTABLE_BASE_ID, AIRTABLE_TABLE_ID
├── process-airtable.sh        ← Orquestador principal
├── setup_cd_project2.sh       ← Setup completo de un proyecto
├── send-welcome-email.php     ← Email de bienvenida (PHPMailer)
├── process-suspensions.sh     ← Suspensiones/reactivaciones diarias
└── vendor/                    ← PHPMailer (composer install en /root/scripts/)

/etc/cron.d/bewpro-pipeline    ← Crons de pipeline y suspensiones
/var/log/bewpro-pipeline.log   ← Log del proceso de provisioning
/var/log/bewpro-suspensions.log ← Log de suspensiones
```

### Variables de entorno (`.airtable.env`)

```bash
AIRTABLE_TOKEN=pat...
AIRTABLE_BASE_ID=appRxvpzqCmNsw2JN
AIRTABLE_TABLE_ID=tblzCgJZCbbt5j13Q
```

### Cron schedule

```
*/15 * * * *  root  /root/scripts/process-airtable.sh      → /var/log/bewpro-pipeline.log
0 8  * * *    root  /root/scripts/process-suspensions.sh   → /var/log/bewpro-suspensions.log
```

### Email SMTP

- **Mailbox:** `noreply@bewpro.com` (cPanel usuario `bewpro22`)
- **SMTP:** `127.0.0.1:587` (local, misma máquina)
- **Library:** PHPMailer 7.x
- **Reply-To:** `soporte@bewpro.com`

---

## 7. Estado actual: qué funciona

| Funcionalidad | Estado |
|---|---|
| Lectura de proyectos Required desde Airtable | ✅ Operativo |
| Parseo y validación de campos | ✅ Operativo |
| Creación de cuenta cPanel automática | ✅ Operativo |
| Creación de DB + usuario MySQL | ✅ Operativo |
| Clone del repo cd-system (con reference local) | ✅ Operativo |
| Composer cache compartido (`/root/.composer-cache`) | ✅ Operativo |
| Configuración de .env + composer | ✅ Operativo |
| Provisioning via `bewpro:new` (14 cores, 121 productos) | ✅ Operativo |
| Generación de .htaccess | ✅ Operativo |
| **DNS automático via Hostinger API** | ✅ **Operativo** |
| Actualización de Airtable (On Development + credenciales) | ✅ Operativo |
| Creación de Subscription record | ✅ Operativo |
| Email HTML al cliente desde `noreply@bewpro.com` | ✅ Operativo |
| Cron cada 15 min | ✅ Activo |
| Proceso de suspensión/reactivación | ✅ Estructura lista |
| Manejo de múltiples proyectos en una corrida | ✅ Operativo |
| `--dry-run` para previsualizar | ✅ Disponible |
| Continuidad ante fallos (un error no detiene los demás) | ✅ Operativo |

---

## 8. Bugs corregidos

| Bug | Causa | Fix aplicado |
|---|---|---|
| `APP_URL` capturaba output de `uapi` | Los `uapi` y `su -` blocks no redirigían a `>&2` | Todos los blocks de setup ahora van a stderr; solo el `echo ${APP_URL}` final va a stdout |
| Domain en Airtable se guardaba con YAML de cPanel | Consecuencia del bug anterior: `APP_URL` contenía basura | Fix de stdout + validación de domain en el parser |
| Email llegaba como texto plano | `mail()` de PHP no enviaba MIME correcto | Reemplazado por PHPMailer + SMTP |
| AutoSSL de cPanel se enviaba al cliente | `contactemail` seteado al email del cliente | Cambiado a `noreply@bewpro.com` |
| `https://---` como domain | Parser no filtraba valores placeholder | Validación agregada para `---`, `-`, `N/A` |
| Cert mismatch en SMTP | CN del cert Hostinger no matchea el hostname | `SMTPOptions: verify_peer => false` |

---

## 9. Oportunidades de mejora identificadas

### Alta prioridad

#### 1. `Provisioned_Password` en texto plano en Airtable
**Riesgo:** Alta. Cualquier persona con acceso a Airtable puede ver las contraseñas de todos los clientes.
**Mejora:** Encriptar antes de guardar, o no guardarlo en Airtable (solo en un vault como 1Password/Bitwarden via API), o marcar el campo como restringido.

#### 2. Password único para cPanel + DB + Laravel admin
**Riesgo:** Si se compromete uno, se comprometen los tres.
**Mejora:** Generar passwords separados: uno para cPanel, uno para la DB, uno para el admin Laravel. El cliente solo recibe el de Laravel.

#### 3. Sin reintentos ni manejo de idempotencia
**Riesgo:** Si el script falla a mitad de provisioning, el registro queda en estado inconsistente (cuenta cPanel creada, pero Airtable sigue en `Required`).
**Mejora:** Un estado intermedio `Provisioning` en Airtable que se setea al inicio, y un mecanismo de rollback (`whmapi1 removeacct`) si falla.

#### 4. Email de bienvenida enviado desde el mismo servidor del cliente
**Riesgo:** Si el servidor tiene reputación afectada, los emails van a spam.
**Mejora:** Usar un proveedor transaccional dedicado (Postmark, SendGrid, Resend) que garantice deliverability y tracking de apertura.

#### 5. Sin notificación de fallo al equipo operativo
**Riesgo:** Si un proyecto falla, nadie se entera hasta revisar el log manualmente.
**Mejora:** Notificación por email/Slack al equipo cuando `FAILED > 0`.

### Media prioridad

#### 6. `users.json` temporal se escribe y restaura en proceso concurrente
Si dos proyectos se provisionan simultáneamente (paralelo), el archivo `users.json` se pisa. El script actual es secuencial, pero si se paraleliza en el futuro, esto rompe.
**Mejora:** Pasar email/password directamente como argumentos a `bewpro:new`, sin el archivo temporal.

#### 7. composer install descarga internet en cada proyecto
Cada nuevo proyecto hace un `composer install` completo (~200MB de dependencias).
**Mejora:** Usar un mirror local o un "composer cache" compartido en el servidor para acelerar.

#### 8. git clone en cada proyecto
Similar al punto anterior: clona el repo completo cada vez.
**Mejora:** `git clone --reference /ruta/al/clone/local --depth 1` para reutilizar objetos.

#### 9. Latencia del cron (hasta 15 min)
Un proyecto en `Required` puede esperar hasta 15 minutos antes de empezar a provisionarse.
**Mejora:** Webhook de Airtable → endpoint en el servidor para trigger inmediato.

#### 10. Sin log estructurado
Los logs van a un archivo de texto plano. Difícil de consultar para debugging o métricas.
**Mejora:** Salida JSON al log + integración con un servicio de log (Papertrail, Logtail).

### Baja prioridad

#### 11. `--no-interaction` en `bewpro:new` usa el default `true` para confirm DB
Si la DB ya existe, el artisan command pregunta y el default es "yes". Funciona, pero es frágil.
**Mejora:** Flag explícito `--db-exists-ok` en `bewpro:new`.

#### 12. Storage link se crea pero no se verifica
Si `storage:link` falla (por ejemplo, el symlink ya existe), el script lo ignora.
**Mejora:** Verificar existencia del symlink antes de crear.

---

## 10. DNS automático via Hostinger API

### Situación actual (resuelto)
~~El DNS se crea **manualmente**~~ El DNS se crea **automáticamente** al final del setup, sin intervención manual.

**Implementación:** Esto requiere:
1. Acceder a Hostinger
2. Crear registro A: `{base_name}.bewpro.com` → `72.61.45.136`
3. Esperar propagación (1-5 min en Hostinger, hasta 48h en general)

**Impacto:** El sitio está provisionado y el cliente recibe el email, pero la URL no resuelve hasta que el DNS esté creado. Si el operador tarda en crear el registro, el cliente intenta ingresar y no puede.

### Opciones para automatizar

#### Opción A — Hostinger API (recomendada)
Hostinger expone una API REST para gestión de DNS:
```bash
POST /api/dns/v1/zones/{zone}/records
  type: A
  name: {base_name}
  content: 72.61.45.136
  ttl: 3600
```
Se agregaría al final de `setup_cd_project2.sh` o en `process-airtable.sh` después del provisioning.
**Requiere:** API key de Hostinger.

#### Opción B — Cloudflare (si se migra DNS)
Si el dominio `bewpro.com` se mueve a Cloudflare como nameserver:
- API muy robusta y documentada
- Propagación casi instantánea (segundos)
- Soporte nativo de wildcards

#### Opción C — WHM DNS clustering
WHM tiene soporte nativo para crear zonas DNS automáticamente al crear una cuenta cPanel. Si el servidor también maneja el DNS de `bewpro.com`, el registro A se crea solo con `whmapi1 createacct`.
**Requiere:** Delegar DNS de `bewpro.com` al nameserver del servidor.

### Propuesta inmediata
Mientras no se automatiza: agregar el comando de creación DNS al log de `setup_cd_project2.sh` de forma que salga explícito en el output para el operador:
```
[DNS PENDIENTE] Crear registro A:
  Hostinger → bewpro.com → {base_name} → 72.61.45.136
```
Esto ya está en el script actual (línea de log al final). Se puede complementar con un email interno al equipo con las instrucciones del DNS.

---

*Última actualización: Marzo 2026*
