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( "📊 Tope de consumo superado\nCliente: %s\nPlan: %s\nConsumo del mes: %s %.2f (tope %s %.2f)\n\nEl servicio sigue activo — el excedente se cobra en el próximo ciclo.", 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(`
Hola,
Tu asistente %s superó el consumo incluido en el plan %s este mes.
Consumo hasta ahora: %s %.2f — incluido en el plan: %s %.2f.
El asistente sigue funcionando con normalidad. El excedente se factura en el próximo ciclo. Podés ver el detalle en tu panel, en Consumo.
`, 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) } }() } }