# Infra Invisible — Health Check (2026-05-13)

> Frente 4 del Sprint Q2. Diagnostic de integraciones externas sin tocar tenants viejos.
>
> Realizado: 2026-05-13 14:30 ART

---

## TL;DR

✅ **Sistema sano operativamente**. 4 componentes auditados, 2 issues menores detectados y resueltos.

| Componente | Estado | Hallazgos |
|------------|--------|-----------|
| **Stripe webhooks** | ✅ OK | Procesando `customer.subscription.updated` + `invoice.payment_failed` correctamente. Emails de cancelación programada saliendo. |
| **Airtable sync** | ✅ OK | 2 crons activos (`process-airtable2.sh` + `apply-pending-imports.sh`), corriendo cada 1 min |
| **Email delivery** | ⚠️ Issue menor | Tickets de soporte tenían destinatario null en caso edge (fix `a45bb074`) |
| **Logging** | ⚠️ Issue menor | `sub_demo_*` subscriptions llenaban logs con warnings inútiles (fix `a45bb074`) |

---

## Detalle de auditoría

### 1 · Stripe webhooks

**Logs últimas 48hs**:
```
2026-05-10 19:15 customer.subscription.updated  cus_UJQADCuGIejqHT  status=active   ✅
2026-05-10 20:16 invoice.payment_failed         cus_UJQADCuGIejqHT  in_1TVfdIG...   ✅
2026-05-10 20:16 customer.subscription.updated  cus_UJQADCuGIejqHT  status=past_due ✅
2026-05-11 23:36 customer.subscription.updated  cus_USpoXNtXNbUBJs  status=trialing,cancel_at_period_end=true ✅
2026-05-11 23:36 Stripe Webhook: email cancelación programada enviado  lacompaniad@gmail.com  ✅
2026-05-12 14:16 invoice.payment_failed         cus_UJQADCuGIejqHT  ✅
```

**Conclusión**: webhook endpoint procesa todos los eventos esperados. Email lifecycle (cancelación programada, payment_failed) está saliendo correctamente al cliente final (Compañía Digital).

### 2 · Airtable sync

**Crons en VPS1**:
```
* * * * * /root/scripts/process-airtable2.sh >> /var/log/process-airtable.log 2>&1
* * * * * flock -n /var/lock/bewpro-apply-imports.lock /root/scripts/apply-pending-imports.sh >> /var/log/bewpro-apply-imports.log 2>&1
```

**Estado actual**:
- `process-airtable2.sh` → consulta Airtable cada 1 min. Si hay proyectos con `Pipeline_Status=Required`, provisiona.
- `apply-pending-imports.sh` → poller del filesystem para JSONs en `/storage/app/reseller-imports/`. Con `flock` para evitar concurrent runs.

**Output reciente** (sin proyectos pendientes — esperado, no hay compras nuevas):
```
PROCESS AIRTABLE TRI VPS — Provisioning
Consultando Airtable (Pipeline_Status = Required)...
No hay proyectos con Pipeline_Status = 'Required'. Nada que hacer.
```

### 3 · Email delivery

**Issues detectados**:

**3.1 Ticket de soporte con destinatario vacío** (fix `a45bb074`):
```
2026-05-11 23:35 [Support] No se pudo notificar staff (ticket nuevo)
  ticket_id: 4
  error: An email must have a "To", "Cc", or "Bcc" header.
```

Causa: `config('support.staff_email', config('mail.from.address', 'soporte@...'))` — el 2do argumento de `config()` no es fallback runtime, solo aplica si la KEY no existe. Si la key existía con value vacío, quedaba `$to = ''` → error en `Mail::to('')`.

Fix: Elvis operator (`?:`) que cae al default si el valor es falsy. Plus guard: si tras 3 fallbacks sigue vacío, log warning + return (no rompe el flujo del ticket).

**3.2 Emails operacionales OK** (sin issues):
- Stripe `email cancelación programada enviado` → llega ✅
- `FormReseller: solicitud enviada` → procesado ✅
- `[CustomersController] Roles actualizados` → log OK (no requiere email) ✅

### 4 · Logging

**Issue detectado: subscriptions demo llenan logs** (fix `a45bb074`):

Tenants demo (Leandro, Maxi, etc.) tienen `stripe_subscription_id` con prefijo `sub_demo_*` que son placeholders (no son IDs reales en Stripe). Cada vez que un cliente cargaba `/my-projects`, `ClientPortalService::resolveSubscriptionDetails()` intentaba consultar Stripe API → fallaba con "No such subscription" → log warning.

Múltiples ocurrencias diarias:
```
[ClientPortal] No se pudo resolver subscription Stripe  sub_demo_leandro_art_design
[ClientPortal] No se pudo resolver subscription Stripe  sub_demo_leandro_construction
[ClientPortal] No se pudo resolver subscription Stripe  sub_demo_leandro_law_firm_digital
[ClientPortal] No se pudo resolver subscription Stripe  sub_demo_maxi_personal_brand
```

Fix: skip temprano si el ID arranca con `sub_demo_*` o `sub_test_*`. Retorna defaults sin pegarle a la API. Misma estrategia que `hasMockStripeCustomer()` para los `cus_demo_*` / `cus_test_*`.

---

## Issues legacy ya resueltos (verificados en este audit)

- `LOG_SLACK_WEBHOOK_URL=null` → mitigado vía `array_filter` en `config/logging.php` (channel slack solo se incluye si la env var existe). El emergency log del 2026-05-11 fue antes de este fix.

---

## Commits del frente

| Commit | Qué resuelve |
|--------|--------------|
| `a45bb074` | SupportService: Elvis defensive en destinatario + ClientPortalService: skip Stripe API para sub_demo_* |

---

## Próximos chequeos preventivos sugeridos

### Cron diario opcional (no implementado)

Para detectar regresiones automáticamente:

```bash
#!/bin/bash
# /root/scripts/bewpro-health-check.sh — corre a las 09:00 ART daily
LOG=/home/bewpro22/public_html/bewpro/storage/logs/laravel.log

# 1. Stripe webhooks last 24hs
WEBHOOKS=$(grep "Stripe Webhook" "$LOG" --since="$(date -d 'yesterday')" | wc -l)
echo "Stripe webhooks last 24hs: $WEBHOOKS"

# 2. Tickets sin destinatario
NO_DEST=$(grep "sin destinatario configurado" "$LOG" | tail -10 | wc -l)
[ "$NO_DEST" -gt 0 ] && echo "⚠ Tickets con destinatario vacío: $NO_DEST"

# 3. Cron Airtable health
LAST_AIRTABLE=$(stat -c %Y /var/log/process-airtable.log)
AGE=$(( $(date +%s) - $LAST_AIRTABLE ))
[ "$AGE" -gt 300 ] && echo "🔴 process-airtable.log stale ($AGE seg)"
```

Esto avisa a Coke por email/Slack si algo se rompe sin requerir audit manual.

### Deferred:

- Refactor `SupportService` extracting `resolveStaffEmail()` helper (DRY entre new ticket + client reply)
- Validar que el cron `bewpro-health-check.sh` se inserta en crontab si decidimos implementarlo
- Audit Stripe Connect setup (cuando se active revenue split con Resellers)

---

## Conclusión

Las 4 integraciones críticas (Stripe / Airtable / email / logging) están sanas. Los 2 issues detectados eran ruido sin impacto funcional, ya resueltos con defensive code.

El frente "Infra Invisible" del sprint Q2 queda **cerrado para este ciclo** — la próxima auditoría debería ser pre-launch del Visitor panel o post-nueva integración (Stripe Connect / Connect Webhooks / SendGrid / etc.).
