package services import ( "fmt" "log" "net/url" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // ─── DispatchTicketNuevo ────────────────────────────────────────────────────── // Notifica al admin cuando un portal user crea un ticket. // portalUser puede ser nil: los tickets que entran por correo no tienen usuario // de portal detrás, y no por eso hay que dejar de avisar. func DispatchTicketNuevo(ticket *models.ProyectoTicket, portalUser *models.PortalUser, proyectoNombre string) { if ticket == nil { return } _ = portalUser cfg := models.GetNotifConfig("ticket_nuevo", "admin") if cfg == nil { return } ticketPath := adminTicketPath(ticket.ID) ticketURL := absAppURL(ticketPath) titulo := fmt.Sprintf("Nuevo ticket: %s", ticket.Titulo) cuerpo := fmt.Sprintf("Cliente: %s\nProyecto: %s\n%s", ticket.AutorNombre, proyectoNombre, ticket.Descripcion) if cfg.CanalSistema { _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ TipoUsuario: "admin", UsuarioID: 0, Titulo: titulo, Cuerpo: cuerpo, Url: ticketPath, Icono: "🎫", }) } if cfg.CanalEmail { if adminEmail := getAdminEmail(); adminEmail != "" { go SendTicketNuevoAdmin(adminEmail, proyectoNombre, ticket.AutorNombre, ticket.Titulo, ticket.Descripcion, ticketURL) } } if cfg.CanalTelegram { msg := fmt.Sprintf("🎫 Nuevo ticket\nProyecto: %s\nCliente: %s\nTítulo: %s\n\n%s\n\n🔗 %s", escapeTelegramHTML(proyectoNombre), escapeTelegramHTML(ticket.AutorNombre), escapeTelegramHTML(ticket.Titulo), escapeTelegramHTML(ticket.Descripcion), telegramHTMLLink(ticketURL, "Ver ticket")) sendTelegramAdmin(msg) } } // ─── DispatchTicketRespuestaCliente ────────────────────────────────────────── // Notifica al admin cuando el portal user responde un ticket. func DispatchTicketRespuestaCliente(ticket *models.ProyectoTicket, contenido string, proyectoNombre string) { if ticket == nil { return } cfg := models.GetNotifConfig("ticket_respuesta_cliente", "admin") if cfg == nil { return } ticketPath := adminTicketPath(ticket.ID) ticketURL := absAppURL(ticketPath) titulo := fmt.Sprintf("Respuesta de cliente en: %s", ticket.Titulo) cuerpo := fmt.Sprintf("Cliente: %s\nProyecto: %s\n%s", ticket.AutorNombre, proyectoNombre, contenido) if cfg.CanalSistema { _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ TipoUsuario: "admin", UsuarioID: 0, Titulo: titulo, Cuerpo: cuerpo, Url: ticketPath, Icono: "💬", }) } if cfg.CanalEmail { if adminEmail := getAdminEmail(); adminEmail != "" { go SendTicketRespuestaAdmin(adminEmail, proyectoNombre, ticket.AutorNombre, ticket.Titulo, contenido, ticketURL) } } if cfg.CanalTelegram { msg := fmt.Sprintf("💬 Respuesta de cliente\nProyecto: %s\nCliente: %s\n\n%s\n\n🔗 %s", escapeTelegramHTML(proyectoNombre), escapeTelegramHTML(ticket.AutorNombre), escapeTelegramHTML(contenido), telegramHTMLLink(ticketURL, "Ver ticket")) sendTelegramAdmin(msg) } } // ─── DispatchTicketRespuestaAdmin ───────────────────────────────────────────── // Notifica al portal user cuando el admin responde un ticket. func DispatchTicketRespuestaAdmin(ticket *models.ProyectoTicket, contenido string, proyectoNombre string) { if ticket == nil { return } proyectoSlug := "" if ticket.ProyectoID != nil { if proy, err := models.GetProyectoByID(*ticket.ProyectoID); err == nil { proyectoSlug = proy.Slug proyectoNombre = proy.Nombre } } // Si no está asociado a un portal user (ej: ticket por email), responder por email directo if ticket.PortalUserID == nil { if ticket.EmailFrom != "" { go SendTicketRespuestaPortalUser(ticket.EmailFrom, ticket.AutorNombre, proyectoNombre, ticket.Titulo, contenido, "") } return } portalUser, err := models.GetPortalUserByID(*ticket.PortalUserID) if err != nil || portalUser == nil { log.Printf("[Notif] PortalUser %d no encontrado: %v", *ticket.PortalUserID, err) return } cfg := models.GetNotifConfig("ticket_respuesta_admin", "portal_user") if cfg == nil { return } portalPath := portalTicketPath(proyectoSlug, ticket.ID) portalURL := absAppURL(portalPath) titulo := fmt.Sprintf("Respuesta en tu ticket: %s", ticket.Titulo) cuerpo := fmt.Sprintf("El equipo respondió: %s", contenido) if cfg.CanalSistema { _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ TipoUsuario: "portal_user", UsuarioID: portalUser.ID, Titulo: titulo, Cuerpo: cuerpo, Url: portalPath, Icono: "💬", }) } if cfg.CanalEmail && portalUser.Email != "" { go SendTicketRespuestaPortalUser(portalUser.Email, portalUser.Nombre, proyectoNombre, ticket.Titulo, contenido, portalURL) } if cfg.CanalTelegram && portalUser.TelegramChatID != "" { msg := fmt.Sprintf("💬 El equipo respondió tu ticket\nProyecto: %s\nTicket: %s\n\n%s\n\n🔗 %s", escapeTelegramHTML(proyectoNombre), escapeTelegramHTML(ticket.Titulo), escapeTelegramHTML(contenido), telegramHTMLLink(portalURL, "Ver en el portal")) if err := sendTelegramPortalUser(portalUser.TelegramChatID, msg); err != nil { log.Printf("[Notif] Error telegram portal_user %d: %v", portalUser.ID, err) } } } // ─── DispatchFacturaSubida ──────────────────────────────────────────────────── // Notifica a los portal_users del cliente cuando se sube una factura. func DispatchFacturaSubida(factura *models.Factura) { if factura == nil || factura.ClienteID == 0 { return } cfg := models.GetNotifConfig("factura_subida", "portal_user") if cfg == nil { return } portalUsers, err := models.GetPortalUsersByClienteID(factura.ClienteID) if err != nil || len(portalUsers) == 0 { return } clienteNombre := factura.Cliente.Empresa if clienteNombre == "" { clienteNombre = factura.Cliente.Nombre } portalPath := "/portal/dashboard" if factura.ProyectoID != nil && factura.Proyecto != nil && factura.Proyecto.Slug != "" { portalPath = fmt.Sprintf("/portal/proyecto/%s?tab=Facturas", url.PathEscape(factura.Proyecto.Slug)) } portalURL := absAppURL(portalPath) for _, u := range portalUsers { u := u titulo := fmt.Sprintf("Nueva factura disponible: %s", factura.Numero) cuerpo := fmt.Sprintf("Factura %s por %s %.2f", factura.Numero, factura.Moneda, factura.Monto) if cfg.CanalSistema { _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ TipoUsuario: "portal_user", UsuarioID: u.ID, Titulo: titulo, Cuerpo: cuerpo, Url: portalPath, Icono: "🧾", }) } if cfg.CanalEmail && u.Email != "" { go SendFacturaSubidaPortalUser(u.Email, u.Nombre, clienteNombre, factura.Numero, factura.Monto, factura.Moneda, portalURL) } if cfg.CanalTelegram && u.TelegramChatID != "" { msg := fmt.Sprintf("🧾 Nueva factura disponible\nNúmero: %s\nMonto: %s %.2f\n\n🔗 %s", escapeTelegramHTML(factura.Numero), escapeTelegramHTML(factura.Moneda), factura.Monto, telegramHTMLLink(portalURL, "Ver en el portal")) if err := sendTelegramPortalUser(u.TelegramChatID, msg); err != nil { log.Printf("[Notif] Error telegram portal_user %d: %v", u.ID, err) } } } log.Printf("[Notif] DispatchFacturaSubida factura=%d clientes notificados=%d", factura.ID, len(portalUsers)) } // ─── helpers ───────────────────────────────────────────────────────────────── func getAdminEmail() string { cfg, err := models.GetSmtpConfig() if err != nil || cfg == nil { return "" } return cfg.FromAddress } func getAdminBotToken() string { configs, err := models.GetAllTelegramConfigs() if err != nil { return "" } for _, c := range configs { if c.Activo { return c.BotToken } } return "" } // sendTelegramPortalUser intenta enviar usando todos los bots activos hasta que uno funcione. // Esto evita fallos cuando el usuario se vinculó con un bot distinto al primer bot activo. func sendTelegramPortalUser(chatID string, mensaje string) error { configs, err := models.GetAllTelegramConfigs() if err != nil { return err } var lastErr error for _, cfg := range configs { if !cfg.Activo || cfg.BotToken == "" { continue } ts := &TelegramService{BotToken: cfg.BotToken} if err := ts.SendMessage(chatID, mensaje); err != nil { lastErr = err continue } return nil } if lastErr != nil { return lastErr } return fmt.Errorf("no hay bots de Telegram activos para enviar") } // NotificarReniceAction envía Telegram cuando el agente aplica un renice automático. func NotificarReniceAction(servidorNombre, accion string) { msg := fmt.Sprintf("🔧 Renice automático aplicado\nServidor: %s\n%s\n\nCPU sostenida por encima del umbral.", escapeTelegramHTML(servidorNombre), escapeTelegramHTML(accion)) sendTelegramAdmin(msg) } // ─── Tareas ────────────────────────────────────────────────────────────────── func NotificarTareaAsignada(t *models.Tarea) { if t == nil || t.AsignadoID == nil { return } cfg := models.GetNotifConfig("tarea_asignada", "admin") if cfg == nil { return } asignado := "" if t.Asignado != nil { asignado = t.Asignado.Name } titulo := fmt.Sprintf("📋 Nueva tarea asignada: %s", t.Titulo) cuerpo := fmt.Sprintf("Asignado a: %s | Prioridad: %s\n%s", asignado, t.Prioridad, t.Descripcion) tareaPath := "/app/tareas" if cfg.CanalSistema { _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ TipoUsuario: "admin", UsuarioID: 0, Titulo: titulo, Cuerpo: cuerpo, Url: tareaPath, Icono: "📋", }) } if cfg.CanalEmail { tareaURL := absAppURL(tareaPath) if t.Asignado != nil && t.Asignado.Email != "" { go SendTareaAsignadaEmail(t.Asignado.Email, t.Asignado.Name, t.Titulo, t.Prioridad, t.Descripcion, tareaURL) } else if adminEmail := getAdminEmail(); adminEmail != "" { go SendTareaNotifAdmin(adminEmail, titulo, cuerpo, tareaURL) } } if cfg.CanalTelegram { msg := fmt.Sprintf("📋 Nueva tarea asignada\n%s\nAsignado a: %s\nPrioridad: %s\n\n%s", escapeTelegramHTML(t.Titulo), escapeTelegramHTML(asignado), escapeTelegramHTML(t.Prioridad), escapeTelegramHTML(t.Descripcion)) sendTelegramAdmin(msg) // También enviar al chat del usuario asignado si tiene TelegramChatID if t.Asignado != nil && t.Asignado.TelegramChatID != "" { go sendTelegramToUser(t.Asignado.TelegramChatID, msg) } } } func NotificarTareaEstado(t *models.Tarea) { if t == nil { return } cfg := models.GetNotifConfig("tarea_estado", "admin") if cfg == nil { return } etiquetas := map[string]string{ "por_hacer": "📥 Por hacer", "en_progreso": "🔄 En progreso", "revision": "🔍 En revisión", "hecho": "✅ Hecho", } label := etiquetas[t.Estado] if label == "" { label = t.Estado } asignado := "" asignadoSuffix := "" if t.Asignado != nil { asignado = t.Asignado.Name asignadoSuffix = "\nAsignado: " + asignado } titulo := fmt.Sprintf("🔀 Tarea → %s: %s", label, t.Titulo) cuerpo := fmt.Sprintf("Estado: %s%s", label, asignadoSuffix) tareaPath := "/app/tareas" if cfg.CanalSistema { _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ TipoUsuario: "admin", UsuarioID: 0, Titulo: titulo, Cuerpo: cuerpo, Url: tareaPath, Icono: "🔀", }) } if cfg.CanalEmail { if adminEmail := getAdminEmail(); adminEmail != "" { go SendTareaNotifAdmin(adminEmail, titulo, cuerpo, absAppURL(tareaPath)) } } if cfg.CanalTelegram { msg := fmt.Sprintf("🔀 Tarea movida → %s\n%s%s", label, escapeTelegramHTML(t.Titulo), escapeTelegramHTML(asignadoSuffix)) sendTelegramAdmin(msg) } } func NotificarTareaComentario(t *models.Tarea, contenido string) { if t == nil { return } cfg := models.GetNotifConfig("tarea_comentario", "admin") if cfg == nil { return } titulo := fmt.Sprintf("💬 Nuevo comentario en tarea: %s", t.Titulo) tareaPath := "/app/tareas" if cfg.CanalSistema { _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ TipoUsuario: "admin", UsuarioID: 0, Titulo: titulo, Cuerpo: contenido, Url: tareaPath, Icono: "💬", }) } if cfg.CanalEmail { if adminEmail := getAdminEmail(); adminEmail != "" { go SendTareaNotifAdmin(adminEmail, titulo, contenido, absAppURL(tareaPath)) } } if cfg.CanalTelegram { msg := fmt.Sprintf("💬 Nuevo comentario en tarea\n%s\n\n%s", escapeTelegramHTML(t.Titulo), escapeTelegramHTML(contenido)) sendTelegramAdmin(msg) } } func sendTelegramAdmin(mensaje string) { configs, err := models.GetAllTelegramConfigs() if err != nil { log.Printf("[Notif] Error obteniendo configs telegram: %v", err) return } // Un mismo chat puede estar cargado en más de una configuración (dos bots // apuntando al mismo grupo, o la misma config duplicada). Sin esto, cada // aviso llega repetido tantas veces como filas haya. yaEnviado := map[string]bool{} for _, cfg := range configs { if !cfg.Activo { continue } destino := cfg.BotToken + "→" + cfg.ChatID if yaEnviado[destino] { log.Printf("[Notif] Chat %s repetido en la config %d (%s), no se manda de nuevo", cfg.ChatID, cfg.ID, cfg.Nombre) continue } yaEnviado[destino] = true ts := &TelegramService{BotToken: cfg.BotToken} sendErr := ts.SendMessage(cfg.ChatID, mensaje) logEntry := &models.TelegramLog{ TelegramConfigID: cfg.ID, Titulo: "Notificación del sistema", Mensaje: mensaje, Estado: "ok", } if sendErr != nil { log.Printf("[Notif] Error enviando telegram admin (cfg %d): %v", cfg.ID, sendErr) logEntry.Estado = "failed" logEntry.ErrorMsg = sendErr.Error() } _ = models.CreateTelegramLog(logEntry) } } // sendTelegramToUser envía un mensaje al chat de un usuario usando el primer bot config activo. func sendTelegramToUser(chatID string, mensaje string) { configs, err := models.GetAllTelegramConfigs() if err != nil || len(configs) == 0 { log.Printf("[Notif] No hay configs telegram para enviar a usuario %s", chatID) return } for _, cfg := range configs { if !cfg.Activo { continue } ts := &TelegramService{BotToken: cfg.BotToken} if err := ts.SendMessage(chatID, mensaje); err != nil { log.Printf("[Notif] Error enviando telegram a %s: %v", chatID, err) } return } }