package controllers import ( "crypto/rand" "encoding/hex" "math" "strconv" "strings" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) var pasarelasValidas = map[string]bool{"bold": true, "dlocal": true, "paypal": true} // PagosExternosIndex renderiza el panel de administración de servicios de pago externos. func PagosExternosIndex(c *fiber.Ctx) error { return c.Render("pagos_externos", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } // GetServiciosPagoExterno devuelve la lista paginada. El token nunca se // devuelve completo, solo el preview guardado al crearlo/regenerarlo. func GetServiciosPagoExterno(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) if page < 1 { page = 1 } limit := 20 offset := (page - 1) * limit items, total, err := models.GetAllServiciosPagoExterno(limit, offset) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, }) } type servicioPagoReq struct { Nombre string `json:"nombre"` IPsPermitidas []string `json:"ips_permitidas"` PasarelasHabilitadas []string `json:"pasarelas_habilitadas"` NotificarWebhook bool `json:"notificar_webhook"` CallbackURL string `json:"callback_url"` NotificarTelegram bool `json:"notificar_telegram"` TelegramChatID string `json:"telegram_chat_id"` Activo bool `json:"activo"` } func (r servicioPagoReq) validar() (pasarelas []string, ips []string, err error) { if strings.TrimSpace(r.Nombre) == "" { return nil, nil, fiber.NewError(fiber.StatusBadRequest, "nombre es requerido") } for _, p := range r.PasarelasHabilitadas { p = strings.ToLower(strings.TrimSpace(p)) if p == "" { continue } if !pasarelasValidas[p] { return nil, nil, fiber.NewError(fiber.StatusBadRequest, "pasarela inválida: "+p) } pasarelas = append(pasarelas, p) } if len(pasarelas) == 0 { return nil, nil, fiber.NewError(fiber.StatusBadRequest, "selecciona al menos una pasarela habilitada") } for _, ip := range r.IPsPermitidas { ip = strings.TrimSpace(ip) if ip != "" { ips = append(ips, ip) } } if r.NotificarWebhook && strings.TrimSpace(r.CallbackURL) == "" { return nil, nil, fiber.NewError(fiber.StatusBadRequest, "callback_url es requerido si el webhook de notificación está activado") } if r.NotificarTelegram && strings.TrimSpace(r.TelegramChatID) == "" { return nil, nil, fiber.NewError(fiber.StatusBadRequest, "telegram_chat_id es requerido si la notificación por Telegram está activada") } return pasarelas, ips, nil } func generarSecretoHex(nbytes int) (string, error) { b := make([]byte, nbytes) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil } // CreateServicioPagoExternoHandler crea un nuevo servicio y devuelve el token // en texto plano — es la única respuesta donde vendrá completo. func CreateServicioPagoExternoHandler(c *fiber.Ctx) error { var req servicioPagoReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } pasarelas, ips, err := req.validar() if err != nil { return c.Status(err.(*fiber.Error).Code).JSON(fiber.Map{"error": err.Error()}) } item := &models.ServicioPagoExterno{ Nombre: strings.TrimSpace(req.Nombre), IPsPermitidas: strings.Join(ips, ","), PasarelasHabilitadas: models.JoinModulos(pasarelas), NotificarWebhook: req.NotificarWebhook, CallbackURL: strings.TrimSpace(req.CallbackURL), NotificarTelegram: req.NotificarTelegram, TelegramChatID: strings.TrimSpace(req.TelegramChatID), Activo: true, CreadoPorID: extraerUserID(c), } if req.NotificarWebhook { secret, err := generarSecretoHex(32) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo generar el secreto de firma"}) } item.CallbackSecret = secret } tokenPlano, err := models.CreateServicioPagoExterno(item) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "ok": true, "id": item.ID, "token": tokenPlano, "callback_secret": item.CallbackSecret, "aviso": "Guarda el token y el callback_secret ahora: no se volverán a mostrar completos.", }) } // UpdateServicioPagoExternoHandler actualiza un servicio existente (no toca el token). func UpdateServicioPagoExternoHandler(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) } var req servicioPagoReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } pasarelas, ips, verr := req.validar() if verr != nil { return c.Status(verr.(*fiber.Error).Code).JSON(fiber.Map{"error": verr.Error()}) } existente, err := models.GetServicioPagoExternoByID(uint(id)) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "servicio no encontrado"}) } updates := map[string]interface{}{ "nombre": strings.TrimSpace(req.Nombre), "ips_permitidas": strings.Join(ips, ","), "pasarelas_habilitadas": models.JoinModulos(pasarelas), "notificar_webhook": req.NotificarWebhook, "callback_url": strings.TrimSpace(req.CallbackURL), "notificar_telegram": req.NotificarTelegram, "telegram_chat_id": strings.TrimSpace(req.TelegramChatID), "activo": req.Activo, } // Si se activa el webhook y todavía no tenía secreto (se creó sin él), generarlo ahora. if req.NotificarWebhook && existente.CallbackSecret == "" { secret, err := generarSecretoHex(32) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo generar el secreto de firma"}) } updates["callback_secret"] = secret } if err := models.UpdateServicioPagoExterno(uint(id), updates); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // RegenerarTokenServicioPagoHandler invalida el token actual y devuelve uno nuevo. func RegenerarTokenServicioPagoHandler(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) } tokenPlano, err := models.RegenerarTokenServicioPago(uint(id)) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true, "token": tokenPlano, "aviso": "El token anterior dejó de funcionar. Guarda este ahora, no se volverá a mostrar."}) } func DeleteServicioPagoExternoHandler(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) } if err := models.DeleteServicioPagoExterno(uint(id)); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // GetSolicitudesPagoExternoHandler devuelve el historial de cobros, opcionalmente // filtrado por servicio, para trazabilidad/soporte. func GetSolicitudesPagoExternoHandler(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) if page < 1 { page = 1 } limit := 20 offset := (page - 1) * limit servicioID, _ := strconv.ParseUint(c.Query("servicio_id", "0"), 10, 64) items, total, err := models.GetAllSolicitudesPagoExterna(limit, offset, uint(servicioID)) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, }) }