Qué pasa cuando un cliente supera el tope de consumo de su plan: el servicio sigue funcionando y el excedente se factura en el próximo ciclo. Es la decisión de diseño y no cambia — cortarle el asistente a un negocio en medio de una conversación con un cliente suyo es peor que la factura. Lo que sí estaba mal es cómo se avisaba. El control de "a este ya le avisé" vivía en memoria, con un ponytail: que asumía reinicios raros. Con despliegue automático en cada push, ese flag se borra varias veces por día: el mismo cliente pasado de tope recibía el aviso de nuevo en el siguiente mensaje, y otra vez, y otra. Ahora se marca en la base, con un UPDATE condicionado que además lo hace atómico — dos mensajes que crucen el tope a la vez, o dos réplicas del proceso, avisan una sola vez. Y el aviso iba sólo al Telegram del staff. El dueño del negocio, que es el que va a recibir la factura con el excedente, no se enteraba por ningún lado: tenía que entrar al panel a mirar. Ahora le llega un correo que dice cuánto lleva consumido, cuánto incluye su plan y —lo más importante— que su asistente sigue funcionando con normalidad. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
110 lines
3.6 KiB
Go
110 lines
3.6 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
)
|
|
|
|
// 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")
|
|
|
|
// El marcado va en la base y de forma atómica: dos mensajes que crucen el
|
|
// tope al mismo tiempo (o dos réplicas del proceso) avisan una sola vez.
|
|
if !models.MarcarAvisoTope(tenantID, periodo) {
|
|
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))
|
|
|
|
avisarTopeAlCliente(tenantID, plan, total)
|
|
}
|
|
|
|
// avisarTopeAlCliente le manda el aviso al dueño del espacio. Enterarse recién
|
|
// en la factura de que se pasó del tope es la clase de sorpresa que hace
|
|
// perder un cliente, y hasta ahora sólo se avisaba al staff.
|
|
func avisarTopeAlCliente(tenantID uint, plan *models.UmindPlan, total float64) {
|
|
tenant, err := models.GetUmindTenantByID(tenantID)
|
|
if err != nil || tenant.ClienteID == nil {
|
|
return
|
|
}
|
|
usuarios, err := models.GetPortalUsersByClienteID(*tenant.ClienteID)
|
|
if err != nil || len(usuarios) == 0 {
|
|
return
|
|
}
|
|
|
|
asunto := "Tu asistente superó el consumo incluido en el plan"
|
|
cuerpo := fmt.Sprintf(`<p>Hola,</p>
|
|
<p>Tu asistente <strong>%s</strong> superó el consumo incluido en el plan %s este mes.</p>
|
|
<p>Consumo hasta ahora: <strong>%s %.2f</strong> — incluido en el plan: %s %.2f.</p>
|
|
<p><strong>El asistente sigue funcionando con normalidad.</strong> El excedente se factura en el próximo ciclo. Podés ver el detalle en tu panel, en Consumo.</p>`,
|
|
tenant.Nombre, plan.Nombre, plan.Moneda, total, plan.Moneda, plan.TopeConsumoMensual)
|
|
|
|
for _, u := range usuarios {
|
|
if u.Email == "" {
|
|
continue
|
|
}
|
|
destino := u.Email
|
|
go func() {
|
|
if err := soporteSendMail(destino, asunto, cuerpo); err != nil {
|
|
log.Printf("[UMIND_USO] No se pudo avisar del tope a %s: %v", destino, err)
|
|
}
|
|
}()
|
|
}
|
|
}
|