package controllers import ( "os" "strconv" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/helpers" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // landingSecret verifica que el header X-Landing-Secret sea correcto. // Si LANDING_API_SECRET no está configurado, se rechaza por seguridad. func landingSecret(c *fiber.Ctx) bool { secret := os.Getenv("LANDING_API_SECRET") if secret == "" { return false } return c.Get("X-Landing-Secret") == secret } // ─── POST /landing/session ──────────────────────────────────────────────────── // LandingCreateSession crea una nueva sesión de chat para el generador de landing. func LandingCreateSession(c *fiber.Ctx) error { if !landingSecret(c) { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no autorizado"}) } const firstQuestion = "¡Hola! Soy tu asistente de landing pages. ¿Cuál es tu nombre o el nombre de tu negocio?" // Token simple: alfanumérico, 16 chars token := helpers.RandomString(16) session, err := models.CreateLandingSession(token, firstQuestion) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"id": session.ID, "token": session.Token}) } // ─── POST /landing/chat ─────────────────────────────────────────────────────── var landingQuestions = []struct { Key string Question string }{ {"user_name", "¡Hola! Soy tu asistente de landing pages. ¿Cuál es tu nombre o el nombre de tu negocio?"}, {"business_type", "¿A qué se dedica tu negocio? Descríbelo brevemente."}, {"target", "¿A quién va dirigida tu landing page? ¿Cuál es tu cliente ideal?"}, {"offer", "¿Qué producto o servicio principal quieres destacar?"}, {"value_prop", "¿Cuál es tu propuesta de valor? ¿Por qué deberían elegirte a ti y no a la competencia?"}, {"colors", "¿Tienes colores de marca? Descríbelos o dime el estilo visual que prefieres. (Ej: moderno, minimalista, corporativo, colorido)"}, {"user_phone", "¿Cuál es tu número de teléfono o WhatsApp de contacto?"}, {"user_email", "¿Cuál es tu correo electrónico de contacto?"}, {"user_website", "¿Cuál es tu sitio web o portafolio?"}, {"logo_url", "¿Tienes un logo? Comparte la URL de tu logo (o escribe \"no\" si no tienes)"}, {"social_media", "¿Qué redes sociales tienes? Comparte tus perfiles de Instagram, LinkedIn, YouTube, TikTok, etc. (Ej: Instagram: @tunombre, LinkedIn: /in/tunombre)"}, {"user_address", "¿Tienes una dirección física o ciudad donde operas?"}, {"extras", "¿Hay algo más que quieras incluir? Testimonios, precios, galería, FAQ... (O escribe \"listo\" para generar ya tu landing)"}, } // LandingChat recibe el mensaje del usuario y devuelve la siguiente pregunta. func LandingChat(c *fiber.Ctx) error { if !landingSecret(c) { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no autorizado"}) } type body struct { Token string `json:"token"` Message string `json:"message"` } var req body if err := c.BodyParser(&req); err != nil || req.Token == "" || req.Message == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "token y message son requeridos"}) } session, err := models.GetLandingSessionByToken(req.Token) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "sesión no encontrada"}) } if session.Status != "in_progress" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sesión ya finalizada"}) } msgs := session.GetAnswers() userAnswersCount := 0 for _, m := range msgs { if m.Role == "user" { userAnswersCount++ } } msgs = append(msgs, models.LandingMessage{Role: "user", Content: req.Message}) // Guardar campo estructurado si corresponde contactFields := map[string]bool{"user_name": true, "user_phone": true, "user_email": true, "user_website": true, "logo_url": true, "social_media": true, "user_address": true} if userAnswersCount < len(landingQuestions) { key := landingQuestions[userAnswersCount].Key if contactFields[key] { switch key { case "user_name": session.UserName = req.Message case "user_phone": session.UserPhone = req.Message case "user_email": session.UserEmail = req.Message case "user_website": session.UserWebsite = req.Message case "logo_url": session.LogoUrl = req.Message case "social_media": session.SocialMedia = req.Message case "user_address": session.UserAddress = req.Message } } } nextIndex := userAnswersCount + 1 var nextMessage string readyToGenerate := false if nextIndex < len(landingQuestions) { nextMessage = landingQuestions[nextIndex].Question } else { readyToGenerate = true nextMessage = "¡Perfecto! Ya tengo toda la información. ¿Listo para ver tu landing page generada por IA? 🚀" } msgs = append(msgs, models.LandingMessage{Role: "assistant", Content: nextMessage}) session.SetAnswers(msgs) if err := models.UpdateLandingSession(session); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "message": nextMessage, "readyToGenerate": readyToGenerate, "questionIndex": nextIndex, "totalQuestions": len(landingQuestions), }) } // ─── GET /landing/preview/:token ───────────────────────────────────────────── // LandingGetSession devuelve los datos públicos de una sesión (sin HTML si no está pagada). func LandingGetSession(c *fiber.Ctx) error { token := c.Params("token") session, err := models.GetLandingSessionByToken(token) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "no encontrado"}) } resp := fiber.Map{ "id": session.ID, "token": session.Token, "status": session.Status, "bold_paid": session.BoldPaid, "price_cop": session.PriceCOP, "user_name": session.UserName, "user_email": session.UserEmail, "user_phone": session.UserPhone, "user_company": session.UserCompany, "user_job": session.UserJob, "user_website": session.UserWebsite, "user_address": session.UserAddress, "social_media": session.SocialMedia, "logo_url": session.LogoUrl, "vcard_included": session.VcardIncluded, } // Solo exponer el HTML si la sesión está generada (preview con marca de agua siempre) if session.HTMLContent != "" { resp["html_content"] = session.HTMLContent } return c.JSON(resp) } // LandingGetAnswers devuelve el historial completo de mensajes (requiere secret). func LandingGetAnswers(c *fiber.Ctx) error { if !landingSecret(c) { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no autorizado"}) } token := c.Params("token") session, err := models.GetLandingSessionByToken(token) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "no encontrado"}) } return c.JSON(fiber.Map{ "answers": session.GetAnswers(), }) } // ─── POST /landing/generate ─────────────────────────────────────────────────── // LandingGenerate delega la generación del HTML a un servicio externo (Qwen). // Recibe el HTML ya generado desde el Next.js y lo persiste. func LandingGenerate(c *fiber.Ctx) error { if !landingSecret(c) { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no autorizado"}) } type body struct { Token string `json:"token"` HTMLContent string `json:"html_content"` VcardIncluded bool `json:"vcard_included"` } var req body if err := c.BodyParser(&req); err != nil || req.Token == "" || req.HTMLContent == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "token y html_content requeridos"}) } session, err := models.GetLandingSessionByToken(req.Token) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "sesión no encontrada"}) } session.HTMLContent = req.HTMLContent session.Status = "generated" session.VcardIncluded = req.VcardIncluded if err := models.UpdateLandingSession(session); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true, "token": session.Token}) } // ─── POST /landing/payment ──────────────────────────────────────────────────── // LandingCreatePayment genera el link de pago Bold para la sesión. func LandingCreatePayment(c *fiber.Ctx) error { if !landingSecret(c) { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no autorizado"}) } type body struct { Token string `json:"token"` CallbackURL string `json:"callback_url"` } var req body if err := c.BodyParser(&req); err != nil || req.Token == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "token requerido"}) } session, err := models.GetLandingSessionByToken(req.Token) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "sesión no encontrada"}) } if session.BoldPaid { return c.JSON(fiber.Map{"paid": true, "token": session.Token}) } if session.BoldLinkID != "" { boldURL := "https://checkout.bold.co/payment/" + session.BoldLinkID return c.JSON(fiber.Map{"url": boldURL, "link_id": session.BoldLinkID}) } cfg, err := models.GetBoldConfig() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "sin configuración Bold activa"}) } reference := "landing-" + session.Token callbackURL := req.CallbackURL result, err := services.CreateBoldPaymentLink(cfg, services.BoldPaymentLinkRequest{ AmountType: "CLOSE", Amount: services.BoldAmountField{Currency: "COP", TotalAmount: session.PriceCOP}, Description: "Landing Page Profesional", Reference: reference, PayerEmail: session.UserEmail, CallbackURL: callbackURL, }) if err != nil { return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) } session.BoldLinkID = result.Payload.PaymentLink _ = models.UpdateLandingSession(session) return c.JSON(fiber.Map{"url": result.Payload.URL, "link_id": result.Payload.PaymentLink}) } // ─── GET /landing/poll/:token ───────────────────────────────────────────────── // LandingPollPayment verifica el estado del pago (polling desde el cliente). func LandingPollPayment(c *fiber.Ctx) error { token := c.Params("token") session, err := models.GetLandingSessionByToken(token) if err != nil { return c.JSON(fiber.Map{"paid": false}) } if session.BoldPaid { return c.JSON(fiber.Map{"paid": true}) } // Consultar la API de Bold si hay un link_id if session.BoldLinkID != "" { cfg, err := models.GetBoldConfig() if err == nil { paid, paymentID, _, checkErr := services.CheckBoldLinkPaid(cfg, session.BoldLinkID) if checkErr == nil && paid { session.BoldPaid = true session.BoldPaymentID = paymentID session.Status = "paid" _ = models.UpdateLandingSession(session) return c.JSON(fiber.Map{"paid": true}) } } } return c.JSON(fiber.Map{"paid": false}) } // ─── GET /landing/download/:token ──────────────────────────────────────────── // LandingDownload entrega el HTML (o ZIP) solo si la sesión está pagada. func LandingDownload(c *fiber.Ctx) error { if !landingSecret(c) { return c.Status(fiber.StatusUnauthorized).SendString("no autorizado") } token := c.Params("token") session, err := models.GetLandingSessionByToken(token) if err != nil { return c.Status(fiber.StatusNotFound).SendString("no encontrado") } if !session.BoldPaid { return c.Status(fiber.StatusPaymentRequired).SendString("pago requerido") } if session.HTMLContent == "" { return c.Status(fiber.StatusNotFound).SendString("HTML no generado") } c.Set("Content-Type", "text/html; charset=utf-8") c.Set("Content-Disposition", "attachment; filename=\"landing.html\"") return c.SendString(session.HTMLContent) } // ─── GET /app/landing-pages (admin) ────────────────────────────────────────── // LandingAdminList lista todas las sesiones para el panel de administración. func LandingAdminList(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) perPage := 20 offset := (page - 1) * perPage statusFilter := c.Query("status", "") sessions, total, err := models.GetAllLandingSessions(perPage, offset, statusFilter) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "data": sessions, "total": total, "page": page, }) } // ─── GET /landing/ai-config ─────────────────────────────────────────────────── // LandingGetAiConfig devuelve la configuración de IA activa para el Landing Generator. // Protegido por X-Landing-Secret. func LandingGetAiConfig(c *fiber.Ctx) error { if !landingSecret(c) { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no autorizado"}) } config, err := models.GetAiConfigForService("landing") if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "no hay configuración de IA activa"}) } return c.JSON(fiber.Map{ "provider": config.Provider, "api_key": config.ApiKey, "base_url": config.BaseURL, "model_name": config.ModelName, }) }