Yimaru-BackEnd/internal/web_server/handlers/chapa.go
Yared Yemane d3bbd8c95a Add backend Chapa payment success HTML page.
Serve /payment/success and /api/v1/payments/chapa/success to verify tx_ref on redirect and activate subscriptions, and share the payment success template with ArifPay.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 04:29:05 -07:00

165 lines
5.5 KiB
Go

package handlers
import (
"encoding/json"
"errors"
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/services/chapa"
"github.com/gofiber/fiber/v2"
)
// HandleChapaWebhook godoc
// @Summary Handle Chapa webhook
// @Description Processes payment notifications from Chapa (charge.success, etc.)
// @Tags payments
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Router /api/v1/payments/webhook [post]
func (h *Handler) HandleChapaWebhook(c *fiber.Ctx) error {
body := c.Body()
signature := c.Get("x-chapa-signature")
if signature == "" {
signature = c.Get("chapa-signature")
}
if err := h.chapaSvc.VerifyWebhookSignature(body, signature); err != nil {
h.logger.Error("Invalid Chapa webhook signature", "error", err)
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Message: "Invalid webhook signature",
})
}
var payload domain.ChapaWebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid webhook payload",
Error: err.Error(),
})
}
if err := h.chapaSvc.ProcessPaymentWebhook(c.Context(), payload); err != nil {
if errors.Is(err, chapa.ErrPaymentAlreadyPaid) {
return c.JSON(domain.Response{Message: "Webhook already processed"})
}
h.logger.Error("Failed to process Chapa webhook", "error", err, "tx_ref", payload.TxRef)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to process webhook",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Webhook processed successfully",
})
}
// HandleChapaCallback godoc
// @Summary Chapa payment callback
// @Description Verifies payment after Chapa redirects to callback_url
// @Tags payments
// @Produce json
// @Param trx_ref query string false "Transaction reference"
// @Param ref_id query string false "Chapa reference ID"
// @Param status query string false "Payment status"
// @Success 200 {object} domain.Response
// @Router /api/v1/payments/chapa/callback [get]
func (h *Handler) HandleChapaCallback(c *fiber.Ctx) error {
query := domain.ChapaCallbackQuery{
TrxRef: c.Query("trx_ref"),
RefID: c.Query("ref_id"),
Status: c.Query("status"),
}
if query.TrxRef == "" {
query.TrxRef = c.Query("tx_ref")
}
if query.TrxRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "trx_ref is required",
})
}
if err := h.chapaSvc.ProcessCallback(c.Context(), query); err != nil {
if errors.Is(err, chapa.ErrPaymentAlreadyPaid) {
return c.JSON(domain.Response{Message: "Payment already processed"})
}
h.logger.Error("Failed to process Chapa callback", "error", err, "trx_ref", query.TrxRef)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to process callback",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Callback processed successfully",
})
}
// HandleChapaSuccessPage godoc
// @Summary Chapa payment success page
// @Description Displays the Yimaru Academy success page after Chapa redirects the learner to return_url
// @Tags payments
// @Produce html
// @Param trx_ref query string false "Chapa transaction reference (tx_ref)"
// @Param tx_ref query string false "Chapa transaction reference"
// @Param ref_id query string false "Chapa reference ID"
// @Param status query string false "Payment status from Chapa redirect"
// @Success 200 {string} string "HTML success page"
// @Router /api/v1/payments/chapa/success [get]
// @Router /payment/success [get]
func (h *Handler) HandleChapaSuccessPage(c *fiber.Ctx) error {
txRef := firstNonEmpty(
c.Query("trx_ref"),
c.Query("tx_ref"),
)
page := defaultPaymentSuccessPage()
if txRef != "" {
payment, err := h.chapaSvc.VerifyPayment(c.Context(), txRef)
if err != nil {
h.logger.Warn("Failed to verify Chapa success redirect", "error", err, "tx_ref", txRef)
page.Body = "Thank you for your payment. We are confirming it with Chapa and will activate your subscription shortly."
page.Helper = "You can safely return to Yimaru Academy. If activation takes longer than expected, refresh the app in a moment."
page.Reference = txRef
} else {
page.Reference = txRef
page.PlanName = derefString(payment.PlanName)
if payment.Status == string(domain.PaymentStatusSuccess) {
page.StatusLabel = "Subscription active"
page.Body = "Your Yimaru Academy subscription is active. You now have access to your learning content."
} else {
page.Body = "Thank you for your payment. We received your success redirect and are finalizing subscription activation."
page.StatusLabel = "Processing confirmation"
}
}
} else {
page.Helper = "Return to Yimaru Academy and refresh your subscription status if you do not see access immediately."
}
html, err := renderPaymentSuccessPage(page)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Failed to render success page")
}
c.Type("html", "utf-8")
return c.SendString(html)
}
// GetChapaPaymentMethods godoc
// @Summary Get Chapa payment methods
// @Description Returns payment methods available on Chapa checkout
// @Tags payments
// @Produce json
// @Success 200 {object} domain.Response
// @Router /api/v1/payments/methods [get]
func (h *Handler) GetChapaPaymentMethods(c *fiber.Ctx) error {
return c.JSON(domain.Response{
Message: "Payment methods retrieved successfully",
Data: h.chapaSvc.GetPaymentMethods(),
})
}