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>
93 lines
2.8 KiB
Go
93 lines
2.8 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
)
|
|
|
|
// avisosTope recuerda a qué tenants ya se les avisó en el ciclo actual, para
|
|
// no mandar un aviso por cada mensaje una vez pasado el tope. Se pierde al
|
|
// reiniciar (y entonces se vuelve a avisar una vez): aceptable para un aviso,
|
|
// y mucho más simple que una columna de estado en el tenant.
|
|
//
|
|
// ponytail: en memoria y por proceso. Si algún día corren varias réplicas,
|
|
// cada una avisa una vez — mover el flag a la BD si eso molesta.
|
|
var avisosTope struct {
|
|
sync.Mutex
|
|
ultimo map[uint]string // tenantID → "2026-08" del último aviso
|
|
}
|
|
|
|
// RegistrarUso persiste el consumo y avisa si el tenant superó el tope de su
|
|
// plan. El aviso NO corta el servicio: el agente sigue respondiendo.
|
|
func RegistrarUso(agenteID uint, tipo string, cantidad float64, unidad string) {
|
|
if agenteID == 0 || cantidad <= 0 {
|
|
return
|
|
}
|
|
models.RegistrarUsoUmind(agenteID, tipo, cantidad, unidad)
|
|
|
|
agente, err := models.GetUmindAgenteByID(agenteID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
plan := models.GetPlanDeTenant(agente.TenantID)
|
|
if plan == nil || plan.TopeConsumoMensual <= 0 {
|
|
return
|
|
}
|
|
|
|
desde, hasta := cicloActual()
|
|
resumen, err := models.GetResumenUso(agente.TenantID, desde, hasta)
|
|
if err != nil {
|
|
return
|
|
}
|
|
total := 0.0
|
|
for _, r := range resumen {
|
|
total += r.Costo
|
|
}
|
|
if total < plan.TopeConsumoMensual {
|
|
return
|
|
}
|
|
avisarTopeConsumo(agente.TenantID, plan, total, desde)
|
|
}
|
|
|
|
// cicloActual devuelve el mes calendario en curso. Se usa el mes natural (no
|
|
// la fecha de alta del contrato) porque es lo que el cliente entiende al
|
|
// mirar "consumo de agosto" en su panel.
|
|
func cicloActual() (time.Time, time.Time) {
|
|
ahora := time.Now()
|
|
desde := time.Date(ahora.Year(), ahora.Month(), 1, 0, 0, 0, 0, ahora.Location())
|
|
return desde, desde.AddDate(0, 1, 0)
|
|
}
|
|
|
|
func avisarTopeConsumo(tenantID uint, plan *models.UmindPlan, total float64, desde time.Time) {
|
|
periodo := desde.Format("2006-01")
|
|
|
|
avisosTope.Lock()
|
|
if avisosTope.ultimo == nil {
|
|
avisosTope.ultimo = map[uint]string{}
|
|
}
|
|
yaAvisado := avisosTope.ultimo[tenantID] == periodo
|
|
avisosTope.ultimo[tenantID] = periodo
|
|
avisosTope.Unlock()
|
|
if yaAvisado {
|
|
return
|
|
}
|
|
|
|
tenant, err := models.GetUmindTenantByID(tenantID)
|
|
nombre := fmt.Sprintf("tenant %d", tenantID)
|
|
if err == nil {
|
|
nombre = tenant.Nombre
|
|
}
|
|
|
|
log.Printf("[UMIND_USO] %s superó el tope de consumo del plan %s: %.2f %s de %.2f",
|
|
nombre, plan.Nombre, total, plan.Moneda, plan.TopeConsumoMensual)
|
|
|
|
sendTelegramAdmin(fmt.Sprintf(
|
|
"📊 <b>Tope de consumo superado</b>\nCliente: <b>%s</b>\nPlan: %s\nConsumo del mes: <b>%s %.2f</b> (tope %s %.2f)\n\n<i>El servicio sigue activo — el excedente se cobra en el próximo ciclo.</i>",
|
|
escapeTelegramHTML(nombre), escapeTelegramHTML(plan.Nombre),
|
|
plan.Moneda, total, plan.Moneda, plan.TopeConsumoMensual))
|
|
}
|