Yimaru-BackEnd/internal/web_server/handlers/chapa.go
Yared Yemane 1f7b38861e Integrate Chapa for learner subscription payments
Add Chapa checkout, verify, webhook, and callback flows so subscriptions activate only after confirmed payment. Route subscription checkout through Chapa while keeping ArifPay for direct payments. Include integration docs and a Postman collection.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 03:35:57 -07:00

115 lines
3.4 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",
})
}
// 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(),
})
}