Yimaru-BackEnd/internal/web_server/handlers/chapa.go
2025-11-06 16:37:41 +03:00

486 lines
15 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// InitiateDeposit godoc
// @Summary Initiate a deposit
// @Description Starts a new deposit process using Chapa payment gateway
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param request body domain.ChapaDepositRequestPayload true "Deposit request"
// @Success 200 {object} domain.ChapaDepositResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/deposit [post]
func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
// Get user ID from context (set by your auth middleware)
userID, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: "invalid user ID",
Message: "User ID is required to initiate a deposit",
})
}
var req domain.ChapaDepositRequestPayload
if err := c.BodyParser(&req); err != nil {
// fmt.Println("We first first are here init Chapa payment")
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to parse request body",
})
}
amount := domain.Currency(req.Amount)
fmt.Println("We are here init Chapa payment")
checkoutURL, err := h.chapaSvc.InitiateDeposit(c.Context(), userID, amount)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to initiate Chapa deposit",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa deposit process initiated successfully",
Data: checkoutURL,
StatusCode: 200,
Success: true,
})
}
// WebhookCallback godoc
// @Summary Chapa payment webhook callback (used by Chapa)
// @Description Handles payment and transfer notifications from Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Param request body domain.ChapaWebhookPayment true "Webhook payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/webhook/verify [post]
func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
body := c.Body()
// Retrieve signature headers
chapaSignature := c.Get("chapa-signature")
xChapaSignature := c.Get("x-chapa-signature")
// Verify webhook signature
valid, err := h.chapaSvc.VerifyWebhookSignature(c.Context(), body, chapaSignature, xChapaSignature)
if err != nil || !valid {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Chapa webhook signature",
Error: err.Error(),
})
}
// Try parsing as transfer webhook first
var transfer domain.ChapaWebhookTransfer
if err := json.Unmarshal(body, &transfer); err == nil &&
strings.EqualFold(transfer.Type, "payout") {
if err := h.chapaSvc.ProcessVerifyWithdrawWebhook(c.Context(), transfer); err != nil {
return domain.UnExpectedErrorResponse(c)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "Chapa withdrawal webhook processed successfully",
// Data: transfer,
Success: true,
})
}
// Otherwise, try as payment webhook
var payment domain.ChapaWebhookPayment
if err := json.Unmarshal(body, &payment); err != nil {
return domain.UnProcessableEntityResponse(c)
}
if err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), payment); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa deposit",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "Chapa deposit webhook processed successfully",
// Data: payment,
Success: true,
})
}
// CancelDeposit godoc
// @Summary Cancel a Chapa deposit transaction
// @Description Cancels an active Chapa transaction using its transaction reference
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.ChapaCancelResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transaction/cancel/{tx_ref} [put]
func (h *Handler) CancelDeposit(c *fiber.Ctx) error {
// Get user ID from context (set by your auth middleware)
userID, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: "invalid user ID",
Message: "User ID is required to cancel a deposit",
})
}
// Extract tx_ref from URL path
txRef := c.Params("tx_ref")
if txRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: "missing transaction reference",
Message: "Transaction reference is required in the path",
})
}
fmt.Printf("\n\nReceived request to cancel Chapa transaction: %s (User ID: %d)\n\n", txRef, userID)
// Call the service layer to cancel deposit
cancelResp, err := h.chapaSvc.CancelDeposit(c.Context(), userID, txRef)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to cancel Chapa deposit",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transaction cancelled successfully",
Data: cancelResp,
StatusCode: 200,
Success: true,
})
}
// FetchAllTransactions godoc
// @Summary Get all Chapa transactions
// @Description Retrieves all transactions from Chapa payment gateway
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {array} domain.ChapaTransaction
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transactions [get]
func (h *Handler) FetchAllTransactions(c *fiber.Ctx) error {
transactions, err := h.chapaSvc.FetchAllTransactions(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to fetch Chapa transactions",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transactions retrieved successfully",
Data: transactions,
StatusCode: 200,
Success: true,
})
}
// GetTransactionEvents godoc
// @Summary Fetch transaction events
// @Description Retrieve the timeline of events for a specific Chapa transaction
// @Tags Chapa
// @Accept json
// @Produce json
// @Param ref_id path string true "Transaction Reference"
// @Success 200 {array} domain.ChapaTransactionEvent
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transaction/events/{ref_id} [get]
func (h *Handler) GetTransactionEvents(c *fiber.Ctx) error {
refID := c.Params("ref_id")
if refID == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to fetch transaction events",
Error: "Transaction reference is required",
})
}
events, err := h.chapaSvc.FetchTransactionEvents(c.Context(), refID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch transaction events",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Transaction events fetched successfully",
Data: events,
StatusCode: 200,
Success: true,
})
}
// VerifyPayment godoc
// @Summary Verify a payment manually
// @Description Manually verify a payment using Chapa's API
// @Tags Chapa
// @Accept json
// @Produce json
// @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transaction/manual/verify/{tx_ref} [get]
func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error {
txRef := c.Params("tx_ref")
if txRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa transaction",
Error: "Transaction reference is required",
})
}
verification, err := h.chapaSvc.ManuallyVerify(c.Context(), txRef)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa transaction",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transaction verified successfully",
Data: verification,
StatusCode: 200,
Success: true,
})
}
// GetSupportedBanks godoc
// @Summary Get supported banks
// @Description Get list of banks supported by Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/banks [get]
func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error {
banks, err := h.chapaSvc.GetSupportedBanks(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to fetch banks",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Banks fetched successfully",
StatusCode: 200,
Success: true,
Data: banks,
})
}
// InitiateWithdrawal initiates a withdrawal request via Chapa payment gateway
// @Summary Initiate a withdrawal
// @Description Initiates a withdrawal request to transfer funds to a bank account via Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param request body domain.ChapaWithdrawalRequest true "Withdrawal request details"
// @Success 200 {object} domain.Response "Chapa withdrawal process initiated successfully"
// @Failure 400 {object} domain.ErrorResponse "Invalid request body"
// @Failure 401 {object} domain.ErrorResponse "Unauthorized"
// @Failure 422 {object} domain.ErrorResponse "Unprocessable entity"
// @Failure 500 {object} domain.ErrorResponse "Internal server error"
// @Router /api/v1/chapa/payments/withdraw [post]
func (h *Handler) InitiateWithdrawal(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok {
return domain.UnProcessableEntityResponse(c)
}
var req domain.ChapaWithdrawalRequest
if err := c.BodyParser(&req); err != nil {
return domain.UnProcessableEntityResponse(c)
}
withdrawal, err := h.chapaSvc.InitiateWithdrawal(c.Context(), userID, req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to initiate Chapa withdrawal",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa withdrawal process initiated successfully",
StatusCode: 200,
Success: true,
Data: withdrawal,
})
}
// GetPaymentReceipt godoc
// @Summary Get Chapa Payment Receipt URL
// @Description Retrieve the Chapa payment receipt URL using the reference ID
// @Tags Chapa
// @Accept json
// @Produce json
// @Param chapa_ref path string true "Chapa Reference ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/receipt/{chapa_ref} [get]
func (h *Handler) GetPaymentReceipt(c *fiber.Ctx) error {
chapaRef := c.Params("chapa_ref")
if chapaRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to get Chapa payment receipt",
Error: "Chapa reference ID is required",
})
}
receiptURL, err := h.chapaSvc.GetPaymentReceiptURL(chapaRef)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get Chapa payment receipt",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Payment receipt URL generated successfully",
Data: receiptURL,
StatusCode: 200,
Success: true,
})
}
// GetAllTransfers godoc
// @Summary Get all Chapa transfers
// @Description Retrieve all transfer records from Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transfers [get]
func (h *Handler) GetAllTransfers(c *fiber.Ctx) error {
transfers, err := h.chapaSvc.GetAllTransfers(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch Chapa transfers",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transfers retrieved successfully",
Data: transfers,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetAccountBalance godoc
// @Summary Get Chapa account balance
// @Description Retrieve Chapa account balance, optionally filtered by currency code (e.g., ETB, USD)
// @Tags Chapa
// @Accept json
// @Produce json
// @Param currency_code query string false "Currency code (optional)"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/balances [get]
func (h *Handler) GetAccountBalance(c *fiber.Ctx) error {
currencyCode := c.Query("currency_code", "")
balances, err := h.chapaSvc.GetAccountBalance(c.Context(), currencyCode)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch Chapa account balance",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa account balances retrieved successfully",
Data: balances,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// SwapCurrency godoc
// @Summary Swap currency using Chapa API
// @Description Convert an amount from one currency to another using Chapa's currency swap API
// @Tags Chapa
// @Accept json
// @Produce json
// @Param request body domain.SwapRequest true "Swap request payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/swap [post]
func (h *Handler) SwapCurrency(c *fiber.Ctx) error {
var reqBody domain.SwapRequest
// Parse request body
if err := c.BodyParser(&reqBody); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request payload",
Error: err.Error(),
})
}
// Validate input
if reqBody.From == "" || reqBody.To == "" || reqBody.Amount <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Missing or invalid swap parameters",
Error: "from, to, and amount are required fields",
})
}
// Call service
resp, err := h.chapaSvc.SwapCurrency(c.Context(), reqBody)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to perform currency swap",
Error: err.Error(),
})
}
// Success response
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Currency swapped successfully",
Data: resp,
StatusCode: fiber.StatusOK,
Success: true,
})
}