# Vale — Estado de trabajo y roadmap

_Doc de trabajo interno (no es el doc de jueces). Última sesión: 7–8 jun 2026._

---

## Cómo se trabaja (principios que nos funcionaron)

1. **Conseguir el error real, nunca asumir.** Los fallbacks y `catch` silenciosos enmascaran la causa. Patrón ganador: instrumentar para ver el error → ver el error real → arreglar la causa → verificar que persiste en DB (no el mensaje de la UI).
2. **Instrumentar ≠ arreglar.** Agregar logs/excepciones da visibilidad; no resuelve. El fix viene después, según lo que el log revele.
3. **Un frente a la vez. Cero scope creep.** No abrir un frente nuevo sin cerrar el anterior.
4. **"Se ve bien en código" ≠ "ejecuta bien".** Verificar con hechos (git diff --stat, grep, queries SQL), no con lectura de diffs crudos (el canal VS Code llega vacío a Claude).

---

## Lo que se cerró hoy ✅

**Chat del dashboard del owner ("Hablar con Vale") — arreglado de raíz.** Cadena de 3 bugs:

- `tools.php` (~188, `listar_servicios`): `'properties' => []` → Gemini exige objeto, no array. Fix: `(object)[]`.
- `function_dispatcher.php` (~67, `agregar_servicio`): placeholder `:p` reusado en price_min/price_max → `HY093`. Fix: segundo placeholder `:pmax`.
- `gemini.php` (~168, buildRequestBody): `args` vacío `{}` se serializaba como `[]` → Gemini rechaza. Fix: normalizar `args === []` a `stdClass`. **(código compartido con el chat público)**

**Protección de historial contra FC huérfanos** en `dashboard.php` y `agent_service.php` (guardar `raw_candidate`/`lastCandidate` como null cuando se dispara el safety net, para no persistir historial corrupto en `$_SESSION`).

**Verificado en staging** (tenant Evergreen AI): agregar servicio ✓, editar+renombrar+precio ✓, listar ✓, horarios ✓, staff ✓, multi-turno sin corrupción ✓.

**Commits:**
- `167f673` — fix del chat (5 archivos: tools, function_dispatcher, gemini, agent_service, dashboard).
- `68528c1` — limpieza de logs DEBUG temporales de onboarding.

---

## Próximos pasos — EN ORDEN

### 🟩 Paso 1 — Cerrar el chat (casi hecho)
- [ ] Prueba en vivo del **chat público**: agendar como cliente desde el navegador. **Crítico** porque valida el cambio sin-probar de `agent_service.php` + `gemini.php` (código compartido).
- [ ] Verificar que impacta en DB **y** Vale devuelve texto.
- [ ] Si verde → `git tag` de release para congelar la victoria.

### 📅 Paso 2 — Frente Calendar / OAuth
**Orden: leer → instrumentar → reconectar → ver el error real → arreglar la causa.**

- [ ] **Leer primero** el flujo `google_auth.php → createCalendar()`: ¿se invoca siempre o está condicionado? ¿dónde debería guardar `vale_config.calendar_id` y con qué `tenant_id` (sesión o payload)?
- [ ] **Instrumentar** (mostrar diff, no commitear a ciegas):
  - `createCalendar` y `createEvent` (`google_calendar.php`): loguear el error HTTP real de Google y lanzar excepción en vez de `return null`.
  - `google_auth.php`: envolver las dos escrituras del token (`google_tokens` + `tenants`) en transacción PDO con try/catch.
- [ ] **Reconectar Calendar** de un tenant → ver el error real de por qué `calendar_id` queda NULL → fix de la causa.

> Las causas posibles del NULL universal son **tres** y cada una tiene fix distinto: createCalendar **falla** (HTTP), **no se llama**, o **se llama pero el UPDATE a `vale_config` no corre** (tenant_id vacío). No asumir cuál hasta ver el error.

### 🛠️ Paso 3 — Hardening transversal de escrituras silenciosas
- [ ] Verificar `rowCount()` antes de asumir éxito + try/catch en: `crearCita` (#1), `ensureLead` (#11), `ownerEditarServicio` (#10).

---

## Frente Calendar — lo que ya sabemos (hechos SQL)

- Los **7 tenants** tienen `calendar_id = NULL` en `vale_config` → `createCalendar()` **nunca** pobló para nadie (no es un tenant viejo).
- `google_tokens` (indexado por `user_id`): 3 tokens, **todos presentes, ninguno revocado** → el token NO es el problema (descarta la teoría del "token evaporado").
- El cambio de fallback `'primary'` → `null` fue de una sesión anterior y es correcto (el scope `calendar.app.created` no accede a 'primary'). Ese cambio es lo que **expone** el NULL: sin `calendar_id`, la cita se omite en silencio.

**Causa raíz = P0 #3** (`createCalendar` falla silencioso). Es el único incendio que rompe el core ahora mismo.

---

## Mapa de bugs (referencia, priorizado)

> No son 15 incendios sueltos: es **una enfermedad sistémica** (operaciones que fallan en silencio) concentrada en Calendar/OAuth.

| # | Prioridad | Archivo | Problema | Estado |
|---|---|---|---|---|
| 3 | P0 | google_calendar.php:186 | **createCalendar falla silencioso = causa raíz del calendar_id NULL** | causa raíz |
| 2 | P0 | google_calendar.php:84 | createEvent retorna null sin loguear | mitigado parcial (error_log hoy, sin deploy) |
| 6 | P0 | agent_service.php:143 | FC/FR intermedios no persistidos | mitigado parcial (safety net hoy) |
| 1 | P0 | function_dispatcher.php:638 | INSERT cita sin verificar rowCount | pendiente (Paso 3) |
| 4 | P0 | google_auth.php:145 | token en 2 lugares sin transacción | pendiente (Paso 2) |
| 5 | P0 | google_auth.php:268 | forceConsentOnce sigue sin error en el else | sospecha |
| 7 | P1 | function_dispatcher.php:585 | disponibilidad solo en DB, no en Calendar → doble-booking | pendiente (relevante para PWA) |
| 10 | P1 | function_dispatcher.php:97 | ownerEditarServicio sin try/catch | pendiente (Paso 3) |
| 11 | P1 | function_dispatcher.php | ensureLead sin verificar rowCount | pendiente (Paso 3) |
| 13 | P2 | google_auth.php:369 | refresh_token crudo sin cifrar en `tenants` | riesgo seguridad |
| 15 | P2 | agent_service.php:98 | historial LIMIT 11 hardcodeado | menor |

(También: #8 race condition contacto web, #9 media bypasea al agente, #12 refreshAccessToken sin excepción, #14 svcRows sin description.)

---

## Cabos sueltos a confirmar

- [ ] `grep -rn "DEBUG TEMP" app/` → debe dar **0** tras el commit `68528c1`.
- [ ] `google_calendar.php`: tenía un `error_log` de observabilidad en `createEvent` (~84) sin commitear. Confirmar si se descartó o se commiteó (sirve para el Paso 2).
- [ ] El cambio de `agent_service.php` se commiteó **sin probar el público** → lo resuelve el Paso 1.

---

## Pivote estratégico (PWA) — PARQUEADO

Visión propuesta: PWA (manifest + service worker), calendario híbrido con Google Calendar como visualizador, perfiles enriquecidos (`ai_metadata` JSON), "Vale Prospectadora" (IA predictiva), automatización de emails, niveles Free/Premium.

**Por qué está parqueado (timing, no la idea):**
1. Es de semanas, no una "Fase 1 de 3 hs".
2. **Depende de Calendar**, que está roto (calendar_id NULL).
3. Abriría un tercer frente sobre dos sin cerrar.
4. Tensión con XPRIZE: "Free desde día 1" choca con el requisito de tenants pagos reales.

**Plan:** documentar la visión como roadmap aparte (texto, no código) y **no ejecutar hasta cerrar chat + Calendar.** Si se hace doc para jueces, que sea visión honesta, no "ya construido".
