diff --git a/cmd/main.go b/cmd/main.go index 8fbe580..b154765 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -51,6 +51,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/santimpay" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/telebirr" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -229,6 +230,7 @@ func main() { issueReportingSvc := issuereporting.New(issueReportingRepo) transferStore := wallet.TransferStore(store) + // walletStore := wallet.WalletStore(store) arifpaySvc := arifpay.NewArifpayService(cfg, transferStore, &http.Client{ Timeout: 30 * time.Second}) @@ -236,9 +238,11 @@ func main() { santimpayClient := santimpay.NewSantimPayClient(cfg) santimpaySvc := santimpay.NewSantimPayService(santimpayClient, cfg, transferStore) + telebirrSvc := telebirr.NewTelebirrService(cfg, transferStore, walletSvc) // Initialize and start HTTP server app := httpserver.NewApp( + telebirrSvc, arifpaySvc, santimpaySvc, issueReportingSvc, diff --git a/internal/config/config.go b/internal/config/config.go index a9400a0..3730309 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -72,6 +72,14 @@ type SANTIMPAYConfig struct { BaseURL string `mapstructure:"base_url"` } +type TELEBIRRConfig struct { + TelebirrFabricAppID string `mapstructure:"fabric_app_id"` + TelebirrAppSecret string `mapstructure:"appSecret"` + TelebirrBaseURL string `mapstructure:"base_url"` + TelebirrMerchantCode string `mapstructure:"merchant_code"` + TelebirrCallbackURL string `mapstructure:"callback_url"` +} + type Config struct { FIXER_API_KEY string FIXER_BASE_URL string @@ -103,6 +111,7 @@ type Config struct { VeliGames VeliConfig `mapstructure:"veli_games"` ARIFPAY ARIFPAYConfig `mapstructure:"arifpay_config"` SANTIMPAY SANTIMPAYConfig `mapstructure:"santimpay_config"` + TELEBIRR TELEBIRRConfig `mapstructure:"telebirr_config"` ResendApiKey string ResendSenderEmail string TwilioAccountSid string @@ -194,6 +203,13 @@ func (c *Config) loadEnv() error { return ErrInvalidLevel } + //Telebirr + c.TELEBIRR.TelebirrBaseURL = os.Getenv("TELEBIRR_BASE_URL") + c.TELEBIRR.TelebirrAppSecret = os.Getenv("TELEBIRR_APP_SECRET") + c.TELEBIRR.TelebirrAppSecret = os.Getenv("TELEBIRR_FABRIC_APP_ID") + c.TELEBIRR.TelebirrMerchantCode = os.Getenv("TELEBIRR_MERCHANT_CODE") + c.TELEBIRR.TelebirrCallbackURL = os.Getenv("TELEBIRR_CALLBACK_URL") + //Chapa c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY") c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY") diff --git a/internal/domain/telebirr.go b/internal/domain/telebirr.go new file mode 100644 index 0000000..02fc02d --- /dev/null +++ b/internal/domain/telebirr.go @@ -0,0 +1,60 @@ +package domain + +type TelebirrFabricTokenResponse struct { + Token string `json:"token"` + EffectiveDate string `json:"effectiveDate"` + ExpirationDate string `json:"expirationDate"` +} + +type TelebirrBizContent struct { + NotifyURL string `json:"notify_url"` + AppID string `json:"appid"` + MerchCode string `json:"merch_code"` + MerchOrderID string `json:"merch_order_id"` + TradeType string `json:"trade_type"` + Title string `json:"title"` + TotalAmount string `json:"total_amount"` + TransCurrency string `json:"trans_currency"` + TimeoutExpress string `json:"timeout_express"` + BusinessType string `json:"business_type"` + PayeeIdentifier string `json:"payee_identifier"` + PayeeIdentifierType string `json:"payee_identifier_type"` + PayeeType string `json:"payee_type"` + RedirectURL string `json:"redirect_url"` + CallbackInfo string `json:"callback_info"` +} + +type TelebirrPreOrderRequestPayload struct { + Timestamp string `json:"timestamp"` + NonceStr string `json:"nonce_str"` + Method string `json:"method"` + Version string `json:"version"` + BizContent TelebirrBizContent `json:"biz_content"` + SignType string `json:"sign_type"` + Sign string `json:"sign"` +} + +type TelebirrCheckoutParams struct { + AppID string `json:"appid"` + MerchCode string `json:"merch_code"` + NonceStr string `json:"nonce_str"` + PrepayID string `json:"prepay_id"` + Timestamp string `json:"timestamp"` +} + +type TelebirrPaymentCallbackPayload struct { + NotifyURL string `json:"notify_url"` // Optional callback URL + AppID string `json:"appid"` // App ID provided by Telebirr + NotifyTime string `json:"notify_time"` // Notification timestamp (UTC, in seconds) + MerchCode string `json:"merch_code"` // Merchant short code + MerchOrderID string `json:"merch_order_id"` // Order ID from merchant system + PaymentOrderID string `json:"payment_order_id"` // Order ID from Telebirr system + TotalAmount string `json:"total_amount"` // Payment amount + TransID string `json:"trans_id"` // Transaction ID + TransCurrency string `json:"trans_currency"` // Currency type (e.g., ETB) + TradeStatus string `json:"trade_status"` // Payment status (e.g., Completed, Failure) + TransEndTime string `json:"trans_end_time"` // Transaction end time (UTC seconds) + CallbackInfo string `json:"callback_info"` // Optional merchant-defined callback data + Sign string `json:"sign"` // Signature of the payload + SignType string `json:"sign_type"` // Signature type, e.g., SHA256WithRSA +} diff --git a/internal/services/telebirr/service.go b/internal/services/telebirr/service.go new file mode 100644 index 0000000..7d4d2bd --- /dev/null +++ b/internal/services/telebirr/service.go @@ -0,0 +1,397 @@ +package telebirr + +import ( + "bytes" + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" +) + +// TokenResponse is the expected response from Telebirr + +type TelebirrService struct { + // client TelebirrClient + cfg *config.Config + transferStore wallet.TransferStore + walletSvc *wallet.Service +} + +func NewTelebirrService(cfg *config.Config, transferStore wallet.TransferStore, walletSvc *wallet.Service) *TelebirrService { + return &TelebirrService{ + cfg: cfg, + transferStore: transferStore, + walletSvc: walletSvc, + } +} + +// GetFabricToken fetches the fabric token from Telebirr +func GetTelebirrFabricToken(s *TelebirrService) (*domain.TelebirrFabricTokenResponse, error) { + // Prepare the request body + bodyMap := map[string]string{ + "appSecret": s.cfg.TELEBIRR.TelebirrAppSecret, + } + bodyBytes, err := json.Marshal(bodyMap) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %v", err) + } + + // Prepare the HTTP request + req, err := http.NewRequest("POST", s.cfg.TELEBIRR.TelebirrBaseURL+"/payment/v1/token", bytes.NewBuffer(bodyBytes)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-APP-Key", s.cfg.TELEBIRR.TelebirrFabricAppID) + + // Perform the request + client := &http.Client{ + Timeout: 10 * time.Second, + } + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to perform request: %v", err) + } + defer resp.Body.Close() + + // Read and parse the response + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("non-200 response: %d, body: %s", resp.StatusCode, string(respBody)) + } + + var tokenResp domain.TelebirrFabricTokenResponse + if err := json.Unmarshal(respBody, &tokenResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %v", err) + } + + return &tokenResp, nil +} + +func (s *TelebirrService) CreateTelebirrOrder(title string, amount float32, userID int64) (string, error) { + // Step 1: Get Fabric Token + tokenResp, err := GetTelebirrFabricToken(s) + if err != nil { + return "", fmt.Errorf("failed to get token: %v", err) + } + fabricToken := tokenResp.Token + + // Step 2: Create request object + orderID := fmt.Sprintf("%d", time.Now().UnixNano()) + bizContent := domain.TelebirrBizContent{ + NotifyURL: s.cfg.TELEBIRR.TelebirrCallbackURL, // Replace with actual + AppID: s.cfg.TELEBIRR.TelebirrFabricAppID, + MerchCode: s.cfg.TELEBIRR.TelebirrMerchantCode, + MerchOrderID: orderID, + TradeType: "Checkout", + Title: title, + TotalAmount: fmt.Sprintf("%.2f", amount), + TransCurrency: "ETB", + TimeoutExpress: "120m", + BusinessType: "WalletRefill", + PayeeIdentifier: s.cfg.TELEBIRR.TelebirrMerchantCode, + PayeeIdentifierType: "04", + PayeeType: "5000", + RedirectURL: s.cfg.ARIFPAY.SuccessUrl, // Replace with actual + CallbackInfo: "From web", + } + + requestPayload := domain.TelebirrPreOrderRequestPayload{ + Timestamp: fmt.Sprintf("%d", time.Now().Unix()), + NonceStr: generateNonce(), + Method: "payment.preorder", + Version: "1.0", + BizContent: bizContent, + SignType: "SHA256WithRSA", + } + + // Sign the request + signStr := canonicalSignString(preOrderPayloadToMap(requestPayload)) + signature, err := signSHA256WithRSA(signStr, s.cfg.TELEBIRR.TelebirrAppSecret) + if err != nil { + return "", fmt.Errorf("failed to sign request: %v", err) + } + requestPayload.Sign = signature + + // Marshal to JSON + bodyBytes, _ := json.Marshal(requestPayload) + + // Step 3: Make the request + req, _ := http.NewRequest("POST", s.cfg.TELEBIRR.TelebirrBaseURL+"/payment/v1/merchant/preOrder", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-APP-Key", s.cfg.TELEBIRR.TelebirrFabricAppID) + req.Header.Set("Authorization", fabricToken) + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("telebirr preOrder request failed: %v", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("telebirr preOrder failed: %s", string(body)) + } + + var response map[string]interface{} + if err := json.Unmarshal(body, &response); err != nil { + return "", fmt.Errorf("telebirr response parse error: %v", err) + } + + biz := response["biz_content"].(map[string]interface{}) + prepayID := biz["prepay_id"].(string) + + // Step 4: Build checkout URL + checkoutURL, err := s.BuildTelebirrCheckoutURL(prepayID) + if err != nil { + return "", err + } + + SenderWallets, err := s.walletSvc.GetWalletsByUser(req.Context(), userID) + if err != nil { + return "", fmt.Errorf("failed to get user wallets: %v", err) + } + + s.transferStore.CreateTransfer(req.Context(), domain.CreateTransfer{ + Amount: domain.Currency(amount), + Verified: false, + Type: domain.DEPOSIT, + ReferenceNumber: orderID, + Status: string(domain.PaymentStatusPending), + SenderWalletID: domain.ValidInt64{ + Value: SenderWallets[0].ID, + Valid: true, + }, + Message: fmt.Sprintf("Telebirr order created with ID: %s and amount: %f", orderID, amount), + }) + + return checkoutURL, nil +} + +func (s *TelebirrService) BuildTelebirrCheckoutURL(prepayID string) (string, error) { + + // Convert params struct to map[string]string for signing + params := map[string]string{ + "app_id": s.cfg.TELEBIRR.TelebirrFabricAppID, + "merch_code": s.cfg.TELEBIRR.TelebirrMerchantCode, + "nonce_str": generateNonce(), + "prepay_id": prepayID, + "timestamp": fmt.Sprintf("%d", time.Now().Unix()), + } + signStr := canonicalSignString(params) + signature, err := signSHA256WithRSA(signStr, s.cfg.TELEBIRR.TelebirrAppSecret) + if err != nil { + return "", fmt.Errorf("failed to sign checkout URL: %v", err) + } + + query := url.Values{} + for k, v := range params { + query.Set(k, v) + } + query.Set("sign", signature) + query.Set("sign_type", "SHA256WithRSA") + query.Set("version", "1.0") + query.Set("trade_type", "Checkout") + + // Step 4: Build final URL + return s.cfg.TELEBIRR.TelebirrBaseURL + query.Encode(), nil +} + +func (s *TelebirrService) HandleTelebirrPaymentCallback(ctx context.Context, payload *domain.TelebirrPaymentCallbackPayload) error { + + transfer, err := s.transferStore.GetTransferByReference(ctx, payload.PaymentOrderID) + + if err != nil { + return fmt.Errorf("failed to fetch transfer by reference: %w", err) + } + + if transfer.Status != string(domain.PaymentStatusPending) { + return fmt.Errorf("payment not pending, status: %s", transfer.Status) + } + + if payload.TradeStatus != "Completed" { + return fmt.Errorf("payment not completed, status: %s", payload.TradeStatus) + } + + // 1. Validate the signature + // if err := s.VerifyCallbackSignature(payload); err != nil { + // return fmt.Errorf("invalid callback signature: %w", err) + // } + + // 4. Parse amount + amount, err := strconv.ParseFloat(payload.TotalAmount, 64) + if err != nil { + return fmt.Errorf("invalid amount format: %s", payload.TotalAmount) + } + + _, err = s.walletSvc.AddToWallet(ctx, transfer.SenderWalletID.Value, domain.Currency(amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet for Telebirr payment", amount)) + if err != nil { + return fmt.Errorf("failed to add amount to wallet: %w", err) + } + + return nil +} + +// Verifies the RSA-SHA256 signature of the payload +// func (s *TelebirrService) VerifyCallbackSignature(payload *domain.TelebirrPaymentCallbackPayload) error { +// // 1. Extract the signature from the payload +// signatureBase64 := payload.Sign +// signType := payload.SignType + +// if signType != "SHA256WithRSA" { +// return fmt.Errorf("unsupported sign_type: %s", signType) +// } + +// // 2. Convert the payload to map (excluding 'sign' and 'sign_type') +// payloadMap := map[string]string{ +// "notify_url": payload.NotifyURL, +// "appid": payload.AppID, +// "notify_time": payload.NotifyTime, +// "merch_code": payload.MerchCode, +// "merch_order_id": payload.MerchOrderID, +// "payment_order_id": payload.PaymentOrderID, +// "total_amount": payload.TotalAmount, +// "trans_id": payload.TransID, +// "trans_currency": payload.TransCurrency, +// "trade_status": payload.TradeStatus, +// "trans_end_time": payload.TransEndTime, +// } + +// // 3. Sort the keys and build the canonical string +// var keys []string +// for k := range payloadMap { +// keys = append(keys, k) +// } +// sort.Strings(keys) + +// var canonicalParts []string +// for _, k := range keys { +// canonicalParts = append(canonicalParts, fmt.Sprintf("%s=%s", k, payloadMap[k])) +// } +// canonicalString := strings.Join(canonicalParts, "&") + +// // 4. Hash the canonical string +// hashed := sha256.Sum256([]byte(canonicalString)) + +// // 5. Decode the base64 signature +// signature, err := base64.StdEncoding.DecodeString(signatureBase64) +// if err != nil { +// return fmt.Errorf("failed to decode signature: %w", err) +// } + +// // 6. Load the RSA public key (PEM format) +// pubKeyPEM := []byte(s.cfg.TELEBIRR.PublicKey) // Must be full PEM string + +// block, _ := pem.Decode(pubKeyPEM) +// if block == nil || block.Type != "PUBLIC KEY" { +// return errors.New("invalid public key PEM block") +// } + +// pubKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes) +// if err != nil { +// return fmt.Errorf("failed to parse RSA public key: %w", err) +// } + +// rsaPubKey, ok := pubKeyInterface.(*rsa.PublicKey) +// if !ok { +// return errors.New("not a valid RSA public key") +// } + +// // 7. Verify the signature +// err = rsa.VerifyPKCS1v15(rsaPubKey, crypto.SHA256, hashed[:], signature) +// if err != nil { +// return fmt.Errorf("RSA signature verification failed: %w", err) +// } + +// return nil +// } + +func generateNonce() string { + return fmt.Sprintf("telebirr%x", time.Now().UnixNano()) +} + +func canonicalSignString(data map[string]string) string { + keys := make([]string, 0, len(data)) + for k := range data { + if k != "sign" && k != "sign_type" { + keys = append(keys, k) + } + } + sort.Strings(keys) + + var b strings.Builder + for i, k := range keys { + value := data[k] + var valStr string + if k == "biz_content" { + jsonVal, _ := json.Marshal(value) + valStr = string(jsonVal) + } else { + valStr = fmt.Sprintf("%v", value) + } + b.WriteString(fmt.Sprintf("%s=%s", k, valStr)) + if i < len(keys)-1 { + b.WriteString("&") + } + } + return b.String() +} + +func signSHA256WithRSA(signStr, privateKeyPEM string) (string, error) { + block, _ := pem.Decode([]byte(privateKeyPEM)) + if block == nil { + return "", fmt.Errorf("invalid PEM private key") + } + + priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return "", fmt.Errorf("unable to parse private key: %v", err) + } + + hashed := sha256.Sum256([]byte(signStr)) + + sig, err := rsa.SignPKCS1v15(rand.Reader, priv.(*rsa.PrivateKey), crypto.SHA256, hashed[:]) + if err != nil { + return "", fmt.Errorf("signing failed: %v", err) + } + + return base64.StdEncoding.EncodeToString(sig), nil +} + +// Helper function to convert TelebirrPreOrderRequestPayload to map[string]string for signing +func preOrderPayloadToMap(payload domain.TelebirrPreOrderRequestPayload) map[string]string { + m := map[string]string{ + "timestamp": payload.Timestamp, + "nonce_str": payload.NonceStr, + "method": payload.Method, + "version": payload.Version, + "sign_type": payload.SignType, + } + // BizContent needs to be marshaled as JSON string + bizContentBytes, _ := json.Marshal(payload.BizContent) + m["biz_content"] = string(bizContentBytes) + return m +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index c06216d..7313199 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -24,6 +24,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/santimpay" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/telebirr" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -42,6 +43,7 @@ import ( ) type App struct { + telebirrSvc *telebirr.TelebirrService arifpaySvc *arifpay.ArifpayService santimpaySvc *santimpay.SantimPayService issueReportingSvc *issuereporting.Service @@ -80,6 +82,7 @@ type App struct { } func NewApp( + telebirrSvc *telebirr.TelebirrService, arifpaySvc *arifpay.ArifpayService, santimpaySvc *santimpay.SantimPayService, issueReportingSvc *issuereporting.Service, @@ -128,6 +131,7 @@ func NewApp( })) s := &App{ + telebirrSvc: telebirrSvc, arifpaySvc: arifpaySvc, santimpaySvc: santimpaySvc, issueReportingSvc: issueReportingSvc, diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 640c9ef..a59dbc9 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -24,6 +24,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/santimpay" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/telebirr" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -37,6 +38,7 @@ import ( ) type Handler struct { + telebirrSvc *telebirr.TelebirrService arifpaySvc *arifpay.ArifpayService santimpaySvc *santimpay.SantimPayService issueReportingSvc *issuereporting.Service @@ -72,6 +74,7 @@ type Handler struct { } func New( + telebirrSvc *telebirr.TelebirrService, arifpaySvc *arifpay.ArifpayService, santimpaySvc *santimpay.SantimPayService, issueReportingSvc *issuereporting.Service, @@ -106,6 +109,7 @@ func New( mongoLoggerSvc *zap.Logger, ) *Handler { return &Handler{ + telebirrSvc: telebirrSvc, arifpaySvc: arifpaySvc, santimpaySvc: santimpaySvc, issueReportingSvc: issueReportingSvc, diff --git a/internal/web_server/handlers/telebirr.go b/internal/web_server/handlers/telebirr.go new file mode 100644 index 0000000..c715484 --- /dev/null +++ b/internal/web_server/handlers/telebirr.go @@ -0,0 +1,98 @@ +package handlers + +import ( + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/gofiber/fiber/v2" +) + +// CreateTelebirrPaymentHandler initializes a payment session with Telebirr. +// +// @Summary Create Telebirr Payment Session +// @Description Generates a payment URL using Telebirr and returns it to the client. +// @Tags Telebirr +// @Accept json +// @Produce json +// @Param request body domain.GeneratePaymentURLInput true "Telebirr payment request payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/telebirr/payment [post] +func (h *Handler) CreateTelebirrPaymentHandler(c *fiber.Ctx) error { + var req domain.TelebirrPreOrderRequestPayload + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Invalid request payload", + }) + } + totalAmount, err := strconv.ParseFloat(req.BizContent.TotalAmount, 32) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "TotalAmount must be a valid number", + }) + } + userID, ok := c.Locals("user_id").(int64) + if !ok { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: "Invalid user_id type", + Message: "user_id must be an int64", + }) + } + paymentURL, err := h.telebirrSvc.CreateTelebirrOrder(req.BizContent.Title, float32(totalAmount), userID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to create Telebirr payment session", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Telebirr payment URL generated successfully", + Data: paymentURL, + Success: true, + StatusCode: fiber.StatusOK, + }) +} + +// HandleTelebirrCallbackHandler handles the Telebirr payment callback. +// +// @Summary Handle Telebirr Payment Callback +// @Description Processes the Telebirr payment result and updates wallet balance. +// @Tags Telebirr +// @Accept json +// @Produce json +// @Param payload body domain.TelebirrPaymentCallbackPayload true "Callback payload from Telebirr" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/telebirr/callback [post] +func (h *Handler) HandleTelebirrCallback(c *fiber.Ctx) error { + var payload domain.TelebirrPaymentCallbackPayload + + if err := c.BodyParser(&payload); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Invalid callback payload", + }) + } + + ctx := c.Context() + + err := h.telebirrSvc.HandleTelebirrPaymentCallback(ctx, &payload) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to handle Telebirr payment callback", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Telebirr payment processed successfully", + Data: nil, + Success: true, + StatusCode: fiber.StatusOK, + }) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 93d7bab..8311bb5 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -20,6 +20,7 @@ import ( func (a *App) initAppRoutes() { h := handlers.New( + a.telebirrSvc, a.arifpaySvc, a.santimpaySvc, a.issueReportingSvc, @@ -117,6 +118,10 @@ func (a *App) initAppRoutes() { groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) + //Telebirr + groupV1.Post("/telebirr/init-payment", a.authMiddleware, h.CreateTelebirrPaymentHandler) + groupV1.Post("/telebirr/callback", h.HandleTelebirrCallback) + //Santimpay groupV1.Post("/santimpay/init-payment", h.CreateSantimPayPaymentHandler) // groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.B2CTransferHandler)