El motor de PDF ya existía entero para el staff — Chrome headless, text/template
con {{range .Items}}, y hasta el importador que convierte un Word en plantilla
con IA. Lo único que faltaba era que fueran de cada cliente.
PlantillaDocumento gana TenantID *uint: nulo = global del staff (lo de
siempre), con valor = del espacio. Mismo patrón exacto que AiConfig, el que ya
tiene su lección aprendida. Y GetPlantillaDocumentoActiva ahora filtra
tenant_id IS NULL explícitamente: sin eso, la plantilla que un cliente escribe
para su propio contrato podía salir en un documento de la empresa. Hay test.
Un cliente sin plantilla propia cae a la global, así puede emitir una
cotización desde el primer día y personalizarla cuando quiera. Guardar crea
versión nueva en vez de pisar la vieja: si la nueva sale mal, la anterior sigue
ahí. Y se valida que compile ANTES de guardar — una plantilla rota descubierta
al generar deja al cliente esperando un PDF que nunca llega.
La tool solo se le ofrece al modelo si hay alguna plantilla disponible:
prometerle una capacidad que después falla es peor que no tenerla.
El semáforo de tres: cada PDF levanta un Chrome entero, y cien clientes
generando a la vez son cien navegadores. Eso tira el servidor mucho antes que
cualquier consulta a la IA, así que va desde el día uno y no cuando se caiga.
El JSON de ítems mal formado no tumba la generación — sale el documento sin la
tabla, que todavía se puede corregir a mano. Y sin cantidad se asume 1: el
modelo la omite seguido, y un total en cero es peor que uno aproximado.
Cada documento se cobra (levanta un Chrome) y queda en los archivos del
espacio, descargable como cualquier otro.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dos piezas que se necesitan mutuamente.
ARCHIVOS — a nivel espacio y no de agente, porque los papeles son del negocio:
si mañana crea un segundo agente, sus contratos no se mudan. Subir, descargar,
borrar, cuota por plan, y un botón "usar como conocimiento" que lo manda por la
ingesta que ya existe — un clic, no volver a subir el mismo PDF.
Nunca se sirven por el estático: salen por su endpoint, que valida el tenant.
La cuota se mira antes de escribir, porque rechazar después de copiar 25 MB al
disco es cobrarle el espacio igual.
PENDIENTES — no es un motor de workflows, es una tabla y una regla: lo que sale
hacia afuera pedido por alguien que no es el dueño no se ejecuta, se encola.
El eje es quién está del otro lado, no qué tan peligrosa suena la acción. En el
widget público escribe cualquiera: ahí toda acción espera. En un canal interno
el dueño ya autorizó al escribirlo, y mandarlo a aprobar su propio pedido sería
fricción sin ninguna seguridad a cambio. (El plan decía aprobar siempre el
envío de correo; esto es más flojo a propósito y la propiedad que importa se
mantiene entera: desde un canal público no sale nada sin una persona.)
Cuatro decisiones que valen más que el esquema:
- Se guarda el payload EXACTO y se ejecuta eso. Aprobar no vuelve a llamar al
modelo: si regenerara, aprobarías una cosa y saldría otra, y la diferencia
aparecería recién en la mano del cliente. Hay un test que lo vigila.
- Dos personas mirando la misma bandeja pueden aprobar a la vez; el reclamo es
un UPDATE condicional, así la cotización no sale dos veces.
- Vencen a los 7 días. Una cotización aprobada tres semanas tarde llega con
precios de otro mes: es peor que ninguna.
- Al cliente nunca se le dice "rechazado" ni se le menciona una aprobación
interna — se le dice que le responde alguien del equipo. También con test.
El correo de aviso lleva un enlace al panel CON login: un enlace que ejecuta
algo irreversible sin autenticar es un enlace que reenviado por error firma.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base para que el cliente administre uMind desde su portal y para el cobro
por consumo. Todavía no cambia nada del comportamiento actual.
- UmindTenant gana ClienteID (punteros, sin not null: los tenants creados
antes del portal quedan sin asignar y el ALTER TABLE no falla sobre
datos existentes) y PlanID.
- GetUmindTenantsByClientes es fail-closed: sin clientes, no ve nada.
- UmindPlan define el máximo de agentes y los precios por 1k tokens, por
imagen OCR, por transcripción y la mensualidad, más un tope de consumo
que solo avisa. CRUD para staff en /app/umind-planes.
- El modelo va en AMBAS listas de AutoMigrate (main.go y migrations),
no repetir el error de agregarlo solo en una.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>