3 Commits
Author SHA1 Message Date
Lizandro GuarnizoandClaude Opus 5 b125ceef95 fix(umind): la config de IA de un cliente podía atender tareas del sistema
Auditando el aislamiento apareció el agujero al revés del que se buscaba: no
un cliente leyendo datos de otro, sino la cuenta de IA de un cliente pagando
trabajo nuestro.

GetAiConfigForService recorre las configs activas y devuelve la primera sin
módulo asignado. Las configs de cliente no llevan módulo — ninguna lo lleva, es
parte del diseño — así que caían justo en ese fallback. Con un cliente que
hubiera conectado su cuenta, su clave terminaba clasificando correos de
soporte, importando plantillas o atendiendo la vCard. Ninguno de los dos se
enteraba: la respuesta llegaba igual y la factura le llegaba a él.

Todos los resolvedores globales filtran ahora tenant_id IS NULL. Un test lo
verifica sobre el código de cada uno, porque son consultas a base y acá no hay
una.

El de embeddings además no podía ser de cliente por otra razón: los vectores de
todos los agentes tienen que salir del mismo modelo o la similitud coseno entre
ellos no significa nada. Un cliente con su propio modelo de embeddings rompía
su propia búsqueda sin un solo error visible.

Del alcance entre clientes, que era lo que se auditaba: los 39 handlers de
uMind validan, y el CRUD de espacios y planes ni siquiera se monta en las rutas
del portal. Lo que faltaba era prueba: UmindScopeDe distingue "staff" de
"cliente sin espacios" por nil contra slice vacío, y esa diferencia no tenía
un solo test. Ahora la cubre uno que además falla si se invierte el fail-closed
de una ruta sin scope — probado inyectando las dos fugas.

Y dos cosas que quedaban colgando:

En /app/ai-config toda config de cliente se mostraba como "Global", que es
justo lo que no es. Ahora dice de qué espacio es, por nombre.

El consumo de una cuenta propia se registraba con el costo del plan. Se sigue
midiendo —el cliente quiere ver cuánto usa su asistente— pero con costo cero y
marcado como cuenta propia: cobrarlo también sería cobrar dos veces lo mismo.
En la pantalla de consumo aparece "va por tu cuenta de IA" en vez de un "$0"
que parecería un error. El OCR y la transcripción siguen costando: son
servicios nuestros, los use quien los use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 23:15:28 -05:00
Lizandro GuarnizoandClaude Sonnet 5 023a8af494 feat(cobro): suma el consumo de uMind al ciclo de cobro existente
El cobro recurrente (link de pago, webhook con firma, renovación
automática de vencimiento) ya funcionaba de punta a punta; lo único que
le faltaba para cobrar por uso era monto variable.

- MontoACobrar = PrecioAcordado + consumo pendiente del cliente. Si el
  cliente no usa uMind el consumo es 0 y el monto queda idéntico al de
  antes, así que los contratos existentes no cambian.
- El consumo se agrega por Cliente, no por tenant: un contrato es de un
  cliente y un cliente puede tener varios tenants.
- El desglose (cuántos tokens, imágenes y transcripciones, y a cuánto)
  va como líneas de servicio, así las plantillas de correo existentes lo
  muestran sin tocarlas. Cobrar un monto variable sin decir de dónde sale
  es pedir una disputa.
- Al confirmarse el pago se cierra el consumo (facturado_at). Solo si el
  UPDATE del contrato afectó filas: si el webhook llega dos veces, la
  segunda no vuelve a cerrar nada.
- Test de que el detalle del correo suma exactamente lo que se cobra.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:53:40 -05:00
Lizandro GuarnizoandClaude Sonnet 5 08265510ea feat(umind): mide el consumo de IA, OCR y transcripción por tenant
Es la base del cobro por uso: hasta ahora no había ninguna medición de
consumo en todo el repo.

- UmindUso registra cada evento facturable con el costo YA calculado al
  precio vigente del plan. Congelarlo evita que subir un precio revalúe
  consumo pasado, que haría indefendible una factura ante un reclamo.
- callAI devuelve los tokens que reportó el proveedor (campo usage, igual
  en todos los OpenAI-compatibles; input+output en Anthropic). Se mide
  cada ronda de tool-calling, no solo la última: todas gastan tokens.
- ExtraerTextoOCR y TranscribirAudioSelfHosted reciben agenteID; 0 = no
  medir, que es lo que pasan los botones "Probar" del panel de staff.
- Aviso al superar el tope del plan, una vez por mes y sin cortar el
  servicio. El flag de "ya avisé" es en memoria a propósito.
- GET /app/umind/uso con filtros de fecha: resumen por tipo + detalle.
- Test del cálculo de costo por tipo, incluida fracción de 1k tokens y
  tenant sin plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:41:23 -05:00