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>
73 lines
2.8 KiB
Go
73 lines
2.8 KiB
Go
package models
|
|
|
|
import (
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// UmindPlan define qué puede hacer un tenant y a qué precio. Los precios se
|
|
// copian a cada UmindUso al momento de registrarlo, así que subir un precio
|
|
// nunca revalúa consumo ya facturado ni pendiente.
|
|
type UmindPlan struct {
|
|
gorm.Model
|
|
Nombre string `json:"nombre" gorm:"column:nombre;size:100;not null"`
|
|
MaxAgentes int `json:"max_agentes" gorm:"column:max_agentes;default:1"` // 0 = ilimitado
|
|
PrecioMensual float64 `json:"precio_mensual" gorm:"column:precio_mensual;default:0"`
|
|
PrecioPor1kTokens float64 `json:"precio_por_1k_tokens" gorm:"column:precio_por_1k_tokens;default:0"`
|
|
PrecioPorOCR float64 `json:"precio_por_ocr" gorm:"column:precio_por_ocr;default:0"`
|
|
PrecioPorTranscripcion float64 `json:"precio_por_transcripcion" gorm:"column:precio_por_transcripcion;default:0"`
|
|
Moneda string `json:"moneda" gorm:"column:moneda;size:3;default:'COP'"`
|
|
// TopeConsumoMensual solo dispara un aviso al superarse — no corta el
|
|
// servicio. 0 = sin tope.
|
|
TopeConsumoMensual float64 `json:"tope_consumo_mensual" gorm:"column:tope_consumo_mensual;default:0"`
|
|
// PrecioPorDocumento cobra cada PDF generado: cada uno levanta un Chrome
|
|
// headless, que es el recurso más caro del servidor.
|
|
PrecioPorDocumento float64 `json:"precio_por_documento" gorm:"column:precio_por_documento;default:0"`
|
|
// MaxAlmacenamientoMB acota el repositorio de archivos del espacio.
|
|
// 0 = sin límite.
|
|
MaxAlmacenamientoMB int `json:"max_almacenamiento_mb" gorm:"column:max_almacenamiento_mb;default:200"`
|
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
|
}
|
|
|
|
func (UmindPlan) TableName() string { return "umind_planes" }
|
|
|
|
func CreateUmindPlan(p *UmindPlan) error {
|
|
return app.Http.Database.DB.Create(p).Error
|
|
}
|
|
|
|
func GetUmindPlanes() ([]UmindPlan, error) {
|
|
var items []UmindPlan
|
|
err := app.Http.Database.DB.Order("id ASC").Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
func GetUmindPlanByID(id uint) (*UmindPlan, error) {
|
|
var p UmindPlan
|
|
if err := app.Http.Database.DB.First(&p, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
func UpdateUmindPlan(id uint, updates map[string]interface{}) error {
|
|
return app.Http.Database.DB.Model(&UmindPlan{}).Where("id = ?", id).Updates(updates).Error
|
|
}
|
|
|
|
func DeleteUmindPlan(id uint) error {
|
|
return app.Http.Database.DB.Delete(&UmindPlan{}, id).Error
|
|
}
|
|
|
|
// GetPlanDeTenant devuelve el plan del tenant, o nil si no tiene uno asignado
|
|
// (tenants viejos). Los llamadores tratan nil como "sin límites ni precios".
|
|
func GetPlanDeTenant(tenantID uint) *UmindPlan {
|
|
t, err := GetUmindTenantByID(tenantID)
|
|
if err != nil || t.PlanID == nil {
|
|
return nil
|
|
}
|
|
p, err := GetUmindPlanByID(*t.PlanID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return p
|
|
}
|