# Data flow — bewpro.com

> Última actualización: 2026-05-07
> Cubre: 3 capas (BD local · Airtable · VPS3 HestiaCP) + dirección de sync.

## Las 3 capas

```
┌─────────────────────────────────────────────────────┐
│  bewpro.com (Laravel + bp-bewpro DB)               │
│  ───────────                                        │
│  · users (Spatie roles, perfil, billing)            │
│  · tenant_projects (proyección del estado real)     │
│  · permisos granulares                              │
│                                                     │
│  Es: la PROYECCIÓN del marketplace para el portal.  │
│  NO es source of truth de provisioning ni billing.  │
└──────────┬──────────────────────────────────────────┘
           │ ↑                              ↓ webhook Stripe
           │ │ sync cada 10min              │ + form-reseller
           ▼ │ (lectura)                    │ (escritura)
┌─────────────────────────────────────────────────────┐
│  Airtable (appRxvpzqCmNsw2JN)                       │
│  ───────────                                        │
│  · Projects (557 records, source of truth comercial)│
│  · Resellers (Compañía Digital + BewPro placeholder)│
│  · Subscriptions (Stripe lifecycle tracking)        │
│  · Shop Products + Copy + Cores                     │
│                                                     │
│  Es: source of truth COMERCIAL.                     │
│  Pipeline_Status, Cpanel_User, SERVER, Reseller     │
│  link son canónicos acá.                            │
└──────────┬──────────────────────────────────────────┘
           │ poll cada 5min
           │ process-airtable.sh
           ▼
┌─────────────────────────────────────────────────────┐
│  VPS3 (Donweb HestiaCP) + VPS1+VPS2 (legacy cPanel) │
│  ───────────                                        │
│  · Cuentas reales: usuarios + DBs + dominios + SSL  │
│  · /var/log de provisioning                         │
│                                                     │
│  Es: source of truth de la INFRAESTRUCTURA viva.    │
│  Si falta una cuenta acá, el sitio no responde.     │
└─────────────────────────────────────────────────────┘
```

## Dirección de sync por campo

| Campo | Source of truth | Dirección | Frecuencia |
|---|---|---|---|
| `users.email`, `first_name`, `last_name` | bp-bewpro | — (writeable solo desde portal) | inmediato |
| `users.password` | bp-bewpro | — | inmediato |
| `users.email_verified_at` | bp-bewpro | — | inmediato |
| `users.airtable_reseller_record_id` | bp-bewpro | — (manual con `bewpro:link-reseller`) | manual |
| `users.admin_notes` | bp-bewpro | — | inmediato |
| `tenant_projects.pipeline_status` | **Airtable** | Airtable → bp-bewpro | cada 10 min |
| `tenant_projects.domain`, `cpanel_user`, `server` | **Airtable** | Airtable → bp-bewpro | cada 10 min |
| `tenant_projects.amount_usd` | **Airtable** | Airtable → bp-bewpro | en sync inicial (no cambia) |
| `tenant_projects.parent_reseller_id` | **Airtable** (Reseller field) | Airtable → bp-bewpro vía mapping | cada 10 min |
| `tenant_projects.stripe_customer_id` | bp-bewpro (webhook Stripe) | bp-bewpro → Airtable | inmediato |
| Cuenta cPanel/HestiaCP | **VPS3/VPS1/VPS2** | — | manual |

## Modelo Reseller

### Conceptual

- **Compra directa**: el cliente final paga vía Stripe → recibe sus credenciales → administra su sitio.
  - En BD: `tenant_projects.parent_reseller_id` = `NULL`
  - En Airtable: `Reseller` field vacío o apunta al placeholder "BewPro" (`recQujQsn6xlJukO4`)
  - **Es la mayoría** y la vía deseada.
- **Compra vía reseller real** (ej: Compañía Digital): el reseller compra para un cliente. El cliente final tiene su propio panel; el reseller acompaña + tiene su panel multi-cliente.
  - En BD: `tenant_projects.parent_reseller_id` = `users.id` del reseller
  - En Airtable: `Reseller` field apunta al record del reseller en tabla Resellers
  - **Es el caso minoritario** que aporta servicio premium.

### Mapping Airtable → BD

La tabla Resellers de Airtable NO tiene campo Email canónico (sus campos: `Name`, `Slug`, `Type`, `Primary_Contact`, etc.). Por eso el mapeo a `users` en BD local se hace **manual** vía:

```bash
php artisan bewpro:link-reseller {airtable_record_id} {email_user_local}
```

**Mapeos actuales** (verificar con `bewpro:link-reseller --list`):

| Airtable record | User local | Tipo |
|---|---|---|
| `recJ0WzUEqWI6MthL` | `lacompaniad@gmail.com` | Reseller real (Compañía Digital) |
| `recQujQsn6xlJukO4` | _(no se mapea)_ | Placeholder "BewPro" = compra directa |

## Comandos de operación

### Audit (read-only)

```bash
# Estado completo de drift entre las 3 capas
php artisan bewpro:audit-data-consistency

# Audit específico de un reseller
php artisan bewpro:audit-data-consistency --reseller=lacompaniad@gmail.com

# Listar drift detallado (cuáles TPs no están sincronizados)
php artisan bewpro:audit-data-consistency --show-drift
```

### Mapping resellers

```bash
# Listar mapeos actuales
php artisan bewpro:link-reseller --list

# Asociar un user con un reseller record de Airtable
php artisan bewpro:link-reseller recJ0WzUEqWI6MthL lacompaniad@gmail.com

# Quitar el mapeo
php artisan bewpro:link-reseller --unlink=lacompaniad@gmail.com
```

### Sync inicial / backfill

```bash
# Dry-run primero (no persiste)
php artisan bewpro:sync-resellers --backfill --dry-run

# Real backfill (crea/upsert TPs desde Airtable, popula parent_reseller_id según mapping)
php artisan bewpro:sync-resellers --backfill

# Solo update de parent_reseller_id en TPs existentes (sin crear nuevos)
php artisan bewpro:sync-resellers
```

### Sync continuo (cron, ya configurado)

`bewpro:sync-tenant-projects` corre cada 10 min y actualiza:
- `pipeline_status` desde Airtable
- `server`, `cpanel_user`, `domain`
- **`parent_reseller_id`** (vía resolveParentResellerId — usa el mapping de `airtable_reseller_record_id`)

Si agregás un nuevo reseller real:
1. `bewpro:link-reseller {airtable_id} {email}`
2. Esperás max 10 min al próximo cron tick — los proyectos del reseller se mapean automáticamente

## Limitaciones conocidas

### Histórico pre-Stripe (89 proyectos de CD en Airtable)

De los 113 Projects con Reseller asignado en Airtable, solo 1 tiene `Stripe_Customer_ID`. Los 112 restantes (incluido los 89 de CD) son histórico **anterior a la integración Stripe** — no tienen el ID que el sync usa para matchear con BD local.

Estos proyectos están en Airtable con su Email vinculado al record de **tabla Clients** (linked record), no en el campo Email del Project directamente.

**Consecuencia**: el panel Reseller de CD hoy muestra solo proyectos vendidos POST-Stripe. Los históricos quedan en Airtable y son operados desde allá.

**Si en el futuro se quiere unificar**: comando dedicado que lea Airtable Clients table → matchee por email → cree TPs en bp-bewpro con `parent_reseller_id=CD`. Out-of-scope para Sprint 1.

### Drift potencial

- Si un Reseller cambia de mapping en Airtable manualmente sin actualizar BD local, los TPs quedan apuntando al user viejo hasta el próximo cron run que detecta el cambio.
- Si se borra un user que tenía `airtable_reseller_record_id`, los TPs apuntando a él pierden el `parent_reseller_id` (CASCADE NULL via FK).

## Validación de consistencia

Correr periódicamente (suggest: 1×/semana o ante cualquier issue de panel):

```bash
php artisan bewpro:audit-data-consistency
```

Si reporta drift > 5%, investigar antes de que se acumule.

## Anclajes

- Comandos: `app/Console/Commands/AuditDataConsistency.php`, `LinkReseller.php`, `SyncResellers.php`, `SyncTenantProjects.php`
- Modelos: `app/Models/User.php`, `app/Models/TenantProject.php`
- Service: `app/Services/AirtableService.php`
- Cron schedule: `app/Console/Kernel.php` (busca `bewpro:sync-tenant-projects`)
- Doc complementaria: [`infraestructura-operativa.md`](../infraestructura-operativa.md), [`roles-y-permisos.md`](roles-y-permisos.md)
