package controllers import ( "fmt" "log" "strconv" "time" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // Función para manejar la solicitud de creación de un plan func CreatePlan(c *fiber.Ctx) error { var plan models.PlanRequest // Parsear el cuerpo de la solicitud a la estructura PlanRequest if err := c.BodyParser(&plan); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": fmt.Sprintf("Error al parsear el cuerpo de la solicitud: %v", err), }) } // Obtener la configuración activa de DlocalApi dlocalConfig, err := models.GetLastActiveDlocalApi() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Error al obtener la configuración activa de DlocalApi.", }) } // Llamar al servicio para crear el plan response, err := services.CreatePlan(*dlocalConfig, plan) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fmt.Sprintf("Error al crear el plan: %v", err), }) } // Devolver la respuesta exitosa return c.Status(fiber.StatusOK).JSON(fiber.Map{ "message": "Plan creado exitosamente.", "response": string(response), }) } func SeePlan(c *fiber.Ctx) error { planID := c.Params("planID") // Verifica si se recibió correctamente if planID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "PlanID es requerido.", }) } // Obtener la configuración activa de DlocalApi dlocalConfig, err := models.GetLastActiveDlocalApi() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Error al obtener la configuración activa de DlocalApi.", }) } // Llamar al servicio para ver el plan response, err := services.SeePlan(*dlocalConfig, planID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fmt.Sprintf("Error al obtener el plan: %v", err), }) } // Devolver la respuesta exitosa return c.Status(fiber.StatusOK).JSON(fiber.Map{ "message": "Plan obtenido exitosamente.", "response": string(response), }) } func UpdatedPlan(c *fiber.Ctx) error { planID := c.Params("planID") var plan services.PlanUpdated // Parsear el cuerpo de la solicitud a la estructura PlanRequest if err := c.BodyParser(&plan); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": fmt.Sprintf("Error al parsear el cuerpo de la solicitud: %v", err), }) } // Obtener la configuración activa de DlocalApi dlocalConfig, err := models.GetLastActiveDlocalApi() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Error al obtener la configuración activa de DlocalApi.", }) } // Llamar al servicio para crear el plan response, err := services.UpdatedPlan(*dlocalConfig, plan, planID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fmt.Sprintf("Error al crear el plan: %v", err), }) } // Devolver la respuesta exitosa return c.Status(fiber.StatusOK).JSON(fiber.Map{ "message": "Plan actualizado exitosamente.", "response": string(response), }) } func DeactivatePlan(c *fiber.Ctx) error { planID := c.Params("planID") subscriptionId := c.Params("subscriptionId") // Obtener la configuración activa de DlocalApi dlocalConfig, err := models.GetLastActiveDlocalApi() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Error al obtener la configuración activa de DlocalApi.", }) } // Llamar al servicio para crear el plan response, err := services.DeactivatePlan(*dlocalConfig, planID, subscriptionId) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fmt.Sprintf("Error al crear el plan: %v", err), }) } // Devolver la respuesta exitosa return c.Status(fiber.StatusOK).JSON(fiber.Map{ "message": "Suscripción cancelada exitosamente.", "response": string(response), }) } func SeePlanes(c *fiber.Ctx) error { // Obtener la configuración activa de DlocalApi dlocalConfig, err := models.GetLastActiveDlocalApi() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Error al obtener la configuración activa de DlocalApi.", }) } // Llamar al servicio para ver el plan response, err := services.SeePlanes(*dlocalConfig) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fmt.Sprintf("Error al obtener los planes: %v", err), }) } // Devolver la respuesta exitosa return c.Status(fiber.StatusOK).JSON(fiber.Map{ "message": "Planes obtenido exitosamente.", "response": string(response), }) } func SeeSubscription(c *fiber.Ctx) error { subscriptionId := c.Params("subscriptionId") invoiceId := c.Params("invoiceId") // Obtener la configuración activa de DlocalApi dlocalConfig, err := models.GetLastActiveDlocalApi() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Error al obtener la configuración activa de DlocalApi.", }) } // Llamar al servicio para ver suscripción response, err := services.SeeSubscription(*dlocalConfig, subscriptionId, invoiceId) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fmt.Sprintf("Error al obtener la suscripción: %v", err), }) } // Devolver la respuesta exitosa return c.Status(fiber.StatusOK).JSON(fiber.Map{ "message": "Suscripción obtenida exitosamente.", "response": string(response), }) } func CreatePago(c *fiber.Ctx) error { var pago services.PagoRequest // Parsear el cuerpo de la solicitud a la estructura PagoRequest if err := c.BodyParser(&pago); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": fmt.Sprintf("Error al parsear el cuerpo de la solicitud: %v", err), }) } // Obtener la configuración activa de DlocalApi dlocalConfig, err := models.GetLastActiveDlocalApi() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Error al obtener la configuración activa de DlocalApi.", }) } // Llamar al servicio para crear el pago response, err := services.CreatePago(*dlocalConfig, pago) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fmt.Sprintf("Error al crear el pago: %v", err), }) } // Devolver la respuesta exitosa return c.Status(fiber.StatusOK).JSON(fiber.Map{ "message": "Pago creado exitosamente.", "response": string(response), }) } // ─── Webhook dLocal ─────────────────────────────────────────────────────────── // DlocalWebhook recibe notificaciones de pago de dLocal y las registra de forma idempotente. // dLocal espera HTTP 200; se responde inmediatamente y se procesa a continuación. func DlocalWebhook(c *fiber.Ctx) error { rawBody := c.Body() // ─── 1. Configuración activa ───────────────────────────────────────────── cfg, err := models.GetLastActiveDlocalApi() if err != nil { log.Println("[DLOCAL] Webhook: sin configuración activa") return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true}) } // ─── 2. Verificar firma HMAC-SHA256 (opcional según config) ────────────── signature := c.Get("X-dLocal-Signature") if signature != "" { if !services.VerifyDlocalSignature(rawBody, signature, *cfg) { log.Println("[DLOCAL] Webhook: firma inválida — descartado") return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true}) } } // ─── 3. Parsear notificación ───────────────────────────────────────────── notif, err := services.ParseDlocalWebhookNotification(rawBody) if err != nil { log.Printf("[DLOCAL] Webhook: error parseando body: %v", err) return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true}) } notificationID := notif.ID if notificationID == "" { log.Println("[DLOCAL] Webhook: notification_id vacío") return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true}) } // ─── 4. Idempotencia ───────────────────────────────────────────────────── if models.IsDlocalNotificationDuplicate(notificationID) { log.Printf("[DLOCAL] Webhook: notificación duplicada %s", notificationID) return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true}) } // ─── 5. Extraer campos (pago directo o suscripción) ────────────────────── paymentID := notif.Payment.ID orderID := notif.Payment.OrderID payerEmail := notif.Payment.Payer.Email monto := notif.Payment.Amount moneda := notif.Payment.Currency tipo := notif.Type estado := notif.Status // Si es suscripción, tomar amounts del invoice if tipo == "SUBSCRIPTION_CHARGE" || tipo == "SUBSCRIPTION_PAYMENT" { if notif.Invoice.Amount > 0 { monto = notif.Invoice.Amount moneda = notif.Invoice.Currency } if paymentID == "" { paymentID = notif.Invoice.ID } if orderID == "" { orderID = notif.Subscription.ID } } // ─── 6. Guardar log ────────────────────────────────────────────────────── logEntry := models.DlocalPaymentLog{ NotificationID: notificationID, Fuente: "webhook", Tipo: tipo, Estado: estado, PaymentID: paymentID, OrderID: orderID, Referencia: orderID, // order_id coincide con la referencia del contrato PayerEmail: payerEmail, Monto: monto, Moneda: moneda, Procesado: false, Raw: string(rawBody), } if err := models.SaveDlocalPaymentLog(logEntry); err != nil { log.Printf("[DLOCAL] Webhook: error guardando log: %v", err) return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true}) } // ─── 7. Procesar si el estado es PAID ──────────────────────────────────── if estado != "PAID" { log.Printf("[DLOCAL] Webhook: estado '%s' — registrado sin procesar", estado) return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true}) } log.Printf("[DLOCAL] Webhook: PAID — payment_id=%s order=%s email=%s monto=%.2f %s", paymentID, orderID, payerEmail, monto, moneda) // Intentar vincular con contrato si el order_id tiene formato "contrato-{id}" if orderID != "" { var contratoID uint if _, err := fmt.Sscanf(orderID, "contrato-%d", &contratoID); err == nil && contratoID > 0 { if err := models.MarcarContratoPagado(contratoID); err != nil { log.Printf("[DLOCAL] Webhook: error marcando contrato %d como pagado: %v", contratoID, err) } else { log.Printf("[DLOCAL] Webhook: contrato %d marcado como pagado", contratoID) // Notificar SaaS externos + enviar correo de confirmación (goroutine, no bloquea respuesta) go services.DispatchSaasPaymentNotification(contratoID, payerEmail, "dlocal", monto, moneda) go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal") } } } models.MarkDlocalPaymentProcessed(notificationID) return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": true}) } // ─── Registro manual de pago dLocal ────────────────────────────────────────── // DlocalRegistrarPago permite registrar manualmente un pago dLocal desde el backoffice // cuando la notificación no llegó por webhook (actualización directa, confirmación manual, etc.). func DlocalRegistrarPago(c *fiber.Ctx) error { type body struct { PaymentID string `json:"payment_id"` OrderID string `json:"order_id"` Referencia string `json:"referencia"` PayerEmail string `json:"payer_email"` Monto float64 `json:"monto"` Moneda string `json:"moneda"` Estado string `json:"estado"` // PAID, PENDING, REJECTED, … Tipo string `json:"tipo"` // PAYMENT, SUBSCRIPTION_CHARGE, manual, … Nota string `json:"nota"` } var b body if err := c.BodyParser(&b); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } if b.Estado == "" { b.Estado = "PAID" } if b.Tipo == "" { b.Tipo = "manual" } if b.Moneda == "" { b.Moneda = "USD" } // Generar un notification_id único para entradas manuales notifID := "manual-" + strconv.FormatInt(time.Now().UnixNano(), 36) if b.PaymentID != "" { notifID = "manual-" + b.PaymentID } // No deduplicar entradas manuales con el mismo payment_id — pueden ser actualizaciones // Se usa prefijo diferente por cada llamada si payment_id es el mismo if models.IsDlocalNotificationDuplicate(notifID) { notifID = "manual-" + strconv.FormatInt(time.Now().UnixNano(), 36) } referencia := b.Referencia if referencia == "" { referencia = b.OrderID } entry := models.DlocalPaymentLog{ NotificationID: notifID, Fuente: "manual", Tipo: b.Tipo, Estado: b.Estado, PaymentID: b.PaymentID, OrderID: b.OrderID, Referencia: referencia, PayerEmail: b.PayerEmail, Monto: b.Monto, Moneda: b.Moneda, Nota: b.Nota, Procesado: b.Estado == "PAID", } if err := models.SaveDlocalPaymentLog(entry); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } // Si el estado es PAID y referencia tiene formato "contrato-{id}", marcar contrato if b.Estado == "PAID" && referencia != "" { var contratoID uint if _, err := fmt.Sscanf(referencia, "contrato-%d", &contratoID); err == nil && contratoID > 0 { if err := models.MarcarContratoPagado(contratoID); err != nil { log.Printf("[DLOCAL] Registro manual: error marcando contrato %d: %v", contratoID, err) } else { log.Printf("[DLOCAL] Registro manual: contrato %d marcado como pagado", contratoID) } } } return c.JSON(fiber.Map{"message": "Pago registrado", "notification_id": notifID}) }