# Reseller Provisioning con JSON de contenido

> Estado: **Implementación parcial** — el lado Reseller (UI + storage del JSON + Airtable record) está listo. Falta el hook post-provisión en el orchestrator de VPS1.
>
> Última actualización: 2026-05-11

---

## Goal

Permitir que un Reseller provisione un proyecto **pre-cargado con contenido del cliente** (services, blog, faqs, gallery, team) — el cliente final recibe un sitio listo, no en blanco.

Versus el flujo manual actual: Reseller provisiona → tenant queda vacío → Reseller (o Leandro) entra al admin del tenant a cargar contenido.

---

## Arquitectura del flow

```
1. Reseller en bewpro.com/reseller/projects/new
   ├── Llena: customer email, project name, subdomain, core
   ├── Sube JSON opcional (services, blog, faqs, etc.)
   ↓
2. ResellerProjectsController::store()
   ├── Valida JSON (json_decode + estructura)
   ├── Crea Airtable Project record con:
   │   • Pipeline_Status = 'Required'
   │   • Has_Import_JSON = true (si subió)
   │   • Reseller = [reseller_airtable_id] (si linkeado)
   ├── Stash JSON en storage/app/reseller-imports/{airtable_id}.json
   ├── Crea TenantProject local con parent_reseller_id = reseller.id
   ↓
3. Cron VPS1 (process-airtable2.sh) detecta Required
   ├── Provisiona tenant normal (cPanel + DB + git + .env + bewpro:new)
   ├── ✨ HOOK POST-PROVISIÓN (a implementar):
   │   • Lee Airtable Project record por airtable_id
   │   • Si Has_Import_JSON == true:
   │     - Fetch JSON desde bewpro22 storage/app/reseller-imports/{id}.json
   │     - SCP al tenant: /home/{cpanel}/.../storage/app/incoming-import.json
   │     - su - {cpanel} -c "php artisan bewpro:import-content storage/app/incoming-import.json --mode=append"
   │     - Patchea Airtable: Import_Status = 'Completed' | 'Failed' (+ error message)
   │     - Limpia el JSON stash en bewpro22 (rm)
   ↓
4. Reseller + customer reciben email con credenciales + URL del sitio
   ↓
5. Customer entra al panel admin → sitio ya cargado con su contenido
```

---

## Implementación actual (cd-system)

### Files creados

| Path | Función |
|------|---------|
| `app/Modules/Clients/Controllers/Reseller/ResellerProjectsController.php` | `create()` form + `store()` provisión + `template()` descarga JSON modelo |
| `app/Modules/Clients/Controllers/Reseller/ResellerActivityController.php` | Vista de volumen + churn (sin USD) |
| `app/Services/CoreTemplateBuilder.php` | Ensambla el JSON modelo de un core desde los seeds canónicos |
| `resources/views/modules/clients/reseller/new-project.blade.php` | Form con select de cores + botón descarga template + upload JSON |
| `resources/views/modules/clients/reseller/activity.blade.php` | Vista de actividad |
| `routes/modules/clients.php` | Routes `reseller.projects.{create,store,template}`, `reseller.activity` |

### JSON modelo por core (descargable desde `/reseller/projects/template/{core}`)

Solo se exponen los **9 cores del marketplace** (los efectivamente disponibles
en bewpro.com/products-catalogue). El builder ensambla cada template
dinámicamente:

```
SITE (welcome/about/contact):
  database/schemas/demos/{demo}.json (schema canónico, paridad blade 100%)
       ↓
  Por cada view, derivar keys + defaults declarados
       ↓
  site.{view}.{key} = default

MÓDULOS (blog, services, menu, etc.):
  database/seeders/products/core/seeds/{module}-{core}.json
       ↓
  Normalizar keys legacy → canónicas (post_categories → blog_categories, etc.)
       ↓
  Merge top-level en el payload
```

**Paridad garantizada por CI**: `php artisan bewpro:validate-demo-parity --all`
falla si cualquier blade pide una key que el schema no declara. Resultado: el
JSON que descarga el reseller siempre tiene exactamente las keys que el demo
necesita renderear.

#### Mapping de keys por módulo

Los seeds usan keys legacy (plurales) que se normalizan al formato que `bewpro:seed` espera:

| Módulo | Key en seed | Key canónica |
|--------|-------------|--------------|
| blog | `post_categories` / `posts` | `blog_categories` / `blog` |
| faqs | `faqs_categories` | `faq_categories` |
| gallery | `galleries` | `gallery` |
| menu | `categories` / `products` | `menu_categories` / `menu` |
| menu | `menus` | `menus_meta` (no consumido, queda como referencia) |
| references | `categories` | `reference_categories` |
| testimonials | `categories` | `testimonial_categories` |
| projects | `categories` | `project_categories` |
| services | `categories` | `service_categories` |
| team-members | `team_members` | `team` |

#### Cobertura actual de seeds

24 cores disponibles. Cobertura promedio ~60-100% según core. `bp-dinamic` y
`construction` tienen 100%. Los gaps están listados en `_meta.missing_seeds`
del JSON modelo descargado.

Para mejorar cobertura: agregar el seed faltante en
`database/seeders/products/core/seeds/{module}-{core}.json` siguiendo el
patrón de otros cores. **No requiere código nuevo** — el builder lo recoge
automáticamente.

### Schema Airtable (Projects table — campos relevantes)

Hay que asegurar que estos 2 campos existan en Airtable Projects (`tblzCgJZCbbt5j13Q`):

| Campo | Tipo | Uso |
|-------|------|-----|
| `Has_Import_JSON` | Checkbox | Marca si el reseller subió JSON al provisionar |
| `Import_Status` | Single select (`Completed`, `Failed`, `Skipped`) | Resultado del import post-provisión |
| `Import_Error` | Long text | Mensaje de error si Import_Status=Failed |

> **TODO**: agregar estos campos a Airtable manualmente o vía API.

### Storage del JSON

Path: `storage/app/reseller-imports/{airtable_record_id}.json`

Permisos: `bewpro22:bewpro22` (default Laravel storage). El orchestrator de VPS1 corre como root, lee como root.

Cleanup: el orchestrator borra el JSON después del import exitoso. Si falla, queda para retry / debug manual.

---

## Pendiente — hook en el orchestrator (VPS1)

### Archivo a modificar
`/root/scripts/process-airtable2.sh` — agregar step entre "Provisionado OK" y "Email de credenciales".

### Pseudo-código

```bash
# Después del setup_cd_project4.sh exitoso, antes del send-welcome-email:

apply_content_import() {
  local airtable_id="$1"
  local cpanel_user="$2"
  local project_dir_name="$3"

  # 1. Check si Project tiene Has_Import_JSON=true
  local has_import
  has_import=$(airtable_get_field "$airtable_id" "Has_Import_JSON")
  if [ "$has_import" != "true" ] && [ "$has_import" != "checked" ]; then
    echo "  Skip content-import: sin JSON adjunto"
    return 0
  fi

  # 2. Fetch JSON desde bewpro22
  local json_path="/home/bewpro22/public_html/bewpro/storage/app/reseller-imports/${airtable_id}.json"
  if [ ! -f "$json_path" ]; then
    echo "  ERROR content-import: JSON no encontrado en $json_path"
    airtable_patch_field "$airtable_id" "Import_Status" "Failed"
    airtable_patch_field "$airtable_id" "Import_Error" "JSON file missing on bewpro22"
    return 1
  fi

  # 3. Copiar al tenant
  local tenant_path="/home/${cpanel_user}/public_html/git-files/${project_dir_name}/storage/app/incoming-import.json"
  cp "$json_path" "$tenant_path"
  chown "${cpanel_user}:${cpanel_user}" "$tenant_path"
  chmod 640 "$tenant_path"

  # 4. Ejecutar import dentro del tenant
  local import_log
  import_log=$(su - "$cpanel_user" -c "
    cd public_html/git-files/${project_dir_name} && \
    php artisan bewpro:import-content storage/app/incoming-import.json --mode=append
  " 2>&1)

  local rc=$?
  if [ $rc -eq 0 ]; then
    echo "  ✓ Content-import OK"
    airtable_patch_field "$airtable_id" "Import_Status" "Completed"
    # Cleanup: borrar el stash
    rm -f "$json_path"
  else
    echo "  ✗ Content-import FALLÓ (rc=$rc)"
    echo "$import_log" | tail -20
    airtable_patch_field "$airtable_id" "Import_Status" "Failed"
    airtable_patch_field "$airtable_id" "Import_Error" "$(echo "$import_log" | tail -5 | tr '\n' ' ')"
  fi
}

# Llamarlo después de provision exitoso:
if run_setup_on_server "$TARGET_IP" "$BASE_NAME" "$EMAIL" "$NAME_SAFE" "$SLUG" "$PASSWORD" "$DOMAIN"; then
  # ... código existente que actualiza Pipeline_Status=On Development ...

  # NUEVO:
  apply_content_import "$RECORD_ID" "$BASE_NAME" "$NAME_SAFE"
fi
```

### Funciones helper que necesitan existir

- `airtable_get_field <record_id> <field_name>` — GET el record + extrae el campo
- `airtable_patch_field <record_id> <field_name> <value>` — PATCH a Airtable

Estas funciones probablemente ya existen parcialmente en `process-airtable2.sh`. Hay que verificar y completarlas.

---

## Testing

### Pre-condition
- Reseller con rol `Reseller` y permission `reseller.dashboard`
- Acceso a `/reseller/projects/new` en bewpro.com

### Test manual (cuando esté el hook)
1. Login como reseller en bewpro.com
2. Ir a `/reseller/projects/new`
3. Llenar form con un subdomain único (ej. `demo-import-test-{timestamp}`)
4. Subir un JSON válido (copiar uno de `database/seeders/project-data/*.json` como sample)
5. Submit → debería redirigir a `/reseller/projects` con flash success
6. Esperar ~25 min al cron
7. Verificar:
   - Tenant existe en VPS1: `ls /home/<cpanel>/public_html/git-files/`
   - JSON aplicado: entrar al admin del tenant, verificar que hay services/blog/faqs cargados
   - Airtable Import_Status = `Completed`

---

## Roadmap de mejoras

- [ ] Implementar el hook post-provisión en el orchestrator (descrito arriba)
- [ ] Agregar Import_Status / Import_Error fields a Airtable (manual o via API)
- [ ] Vista `/reseller/projects/{id}/import-log` para que el reseller vea el log de su import
- [ ] Permitir re-importar (UI para subir JSON de nuevo si falló)
- [ ] Validación del JSON contra schema canónico (ContentImportService::analyze() ya existe)
- [ ] Wizard de generación de JSON desde un formulario web (para resellers sin habilidades técnicas)

---

## Referencias

- Comando CLI: `php artisan bewpro:import-content {path} [--mode=append|replace] [--sections=...]`
- Service: `app/Services/ContentImportService.php` (analyze + execute, 6 secciones: site/services/team/blog/faqs/references)
- Admin equivalente (Super Admin only, dentro del tenant): `/admin/content-import` (GET form + POST analyze + POST execute)
- Formato canónico del JSON: `database/seeders/project-data/*.json` (Compañía Digital templates)
