telebirr service
This commit is contained in:
parent
d7fdd1aa7c
commit
49d0e73b6f
|
|
@ -51,6 +51,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/santimpay"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/santimpay"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
"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/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
|
@ -229,6 +230,7 @@ func main() {
|
||||||
issueReportingSvc := issuereporting.New(issueReportingRepo)
|
issueReportingSvc := issuereporting.New(issueReportingRepo)
|
||||||
|
|
||||||
transferStore := wallet.TransferStore(store)
|
transferStore := wallet.TransferStore(store)
|
||||||
|
// walletStore := wallet.WalletStore(store)
|
||||||
|
|
||||||
arifpaySvc := arifpay.NewArifpayService(cfg, transferStore, &http.Client{
|
arifpaySvc := arifpay.NewArifpayService(cfg, transferStore, &http.Client{
|
||||||
Timeout: 30 * time.Second})
|
Timeout: 30 * time.Second})
|
||||||
|
|
@ -236,9 +238,11 @@ func main() {
|
||||||
santimpayClient := santimpay.NewSantimPayClient(cfg)
|
santimpayClient := santimpay.NewSantimPayClient(cfg)
|
||||||
|
|
||||||
santimpaySvc := santimpay.NewSantimPayService(santimpayClient, cfg, transferStore)
|
santimpaySvc := santimpay.NewSantimPayService(santimpayClient, cfg, transferStore)
|
||||||
|
telebirrSvc := telebirr.NewTelebirrService(cfg, transferStore, walletSvc)
|
||||||
|
|
||||||
// Initialize and start HTTP server
|
// Initialize and start HTTP server
|
||||||
app := httpserver.NewApp(
|
app := httpserver.NewApp(
|
||||||
|
telebirrSvc,
|
||||||
arifpaySvc,
|
arifpaySvc,
|
||||||
santimpaySvc,
|
santimpaySvc,
|
||||||
issueReportingSvc,
|
issueReportingSvc,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,14 @@ type SANTIMPAYConfig struct {
|
||||||
BaseURL string `mapstructure:"base_url"`
|
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 {
|
type Config struct {
|
||||||
FIXER_API_KEY string
|
FIXER_API_KEY string
|
||||||
FIXER_BASE_URL string
|
FIXER_BASE_URL string
|
||||||
|
|
@ -103,6 +111,7 @@ type Config struct {
|
||||||
VeliGames VeliConfig `mapstructure:"veli_games"`
|
VeliGames VeliConfig `mapstructure:"veli_games"`
|
||||||
ARIFPAY ARIFPAYConfig `mapstructure:"arifpay_config"`
|
ARIFPAY ARIFPAYConfig `mapstructure:"arifpay_config"`
|
||||||
SANTIMPAY SANTIMPAYConfig `mapstructure:"santimpay_config"`
|
SANTIMPAY SANTIMPAYConfig `mapstructure:"santimpay_config"`
|
||||||
|
TELEBIRR TELEBIRRConfig `mapstructure:"telebirr_config"`
|
||||||
ResendApiKey string
|
ResendApiKey string
|
||||||
ResendSenderEmail string
|
ResendSenderEmail string
|
||||||
TwilioAccountSid string
|
TwilioAccountSid string
|
||||||
|
|
@ -194,6 +203,13 @@ func (c *Config) loadEnv() error {
|
||||||
return ErrInvalidLevel
|
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
|
//Chapa
|
||||||
c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY")
|
c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY")
|
||||||
c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY")
|
c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY")
|
||||||
|
|
|
||||||
60
internal/domain/telebirr.go
Normal file
60
internal/domain/telebirr.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
397
internal/services/telebirr/service.go
Normal file
397
internal/services/telebirr/service.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/santimpay"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/santimpay"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
"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/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
|
@ -42,6 +43,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
|
telebirrSvc *telebirr.TelebirrService
|
||||||
arifpaySvc *arifpay.ArifpayService
|
arifpaySvc *arifpay.ArifpayService
|
||||||
santimpaySvc *santimpay.SantimPayService
|
santimpaySvc *santimpay.SantimPayService
|
||||||
issueReportingSvc *issuereporting.Service
|
issueReportingSvc *issuereporting.Service
|
||||||
|
|
@ -80,6 +82,7 @@ type App struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(
|
func NewApp(
|
||||||
|
telebirrSvc *telebirr.TelebirrService,
|
||||||
arifpaySvc *arifpay.ArifpayService,
|
arifpaySvc *arifpay.ArifpayService,
|
||||||
santimpaySvc *santimpay.SantimPayService,
|
santimpaySvc *santimpay.SantimPayService,
|
||||||
issueReportingSvc *issuereporting.Service,
|
issueReportingSvc *issuereporting.Service,
|
||||||
|
|
@ -128,6 +131,7 @@ func NewApp(
|
||||||
}))
|
}))
|
||||||
|
|
||||||
s := &App{
|
s := &App{
|
||||||
|
telebirrSvc: telebirrSvc,
|
||||||
arifpaySvc: arifpaySvc,
|
arifpaySvc: arifpaySvc,
|
||||||
santimpaySvc: santimpaySvc,
|
santimpaySvc: santimpaySvc,
|
||||||
issueReportingSvc: issueReportingSvc,
|
issueReportingSvc: issueReportingSvc,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/santimpay"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/santimpay"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
"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/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
|
@ -37,6 +38,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
|
telebirrSvc *telebirr.TelebirrService
|
||||||
arifpaySvc *arifpay.ArifpayService
|
arifpaySvc *arifpay.ArifpayService
|
||||||
santimpaySvc *santimpay.SantimPayService
|
santimpaySvc *santimpay.SantimPayService
|
||||||
issueReportingSvc *issuereporting.Service
|
issueReportingSvc *issuereporting.Service
|
||||||
|
|
@ -72,6 +74,7 @@ type Handler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
|
telebirrSvc *telebirr.TelebirrService,
|
||||||
arifpaySvc *arifpay.ArifpayService,
|
arifpaySvc *arifpay.ArifpayService,
|
||||||
santimpaySvc *santimpay.SantimPayService,
|
santimpaySvc *santimpay.SantimPayService,
|
||||||
issueReportingSvc *issuereporting.Service,
|
issueReportingSvc *issuereporting.Service,
|
||||||
|
|
@ -106,6 +109,7 @@ func New(
|
||||||
mongoLoggerSvc *zap.Logger,
|
mongoLoggerSvc *zap.Logger,
|
||||||
) *Handler {
|
) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
|
telebirrSvc: telebirrSvc,
|
||||||
arifpaySvc: arifpaySvc,
|
arifpaySvc: arifpaySvc,
|
||||||
santimpaySvc: santimpaySvc,
|
santimpaySvc: santimpaySvc,
|
||||||
issueReportingSvc: issueReportingSvc,
|
issueReportingSvc: issueReportingSvc,
|
||||||
|
|
|
||||||
98
internal/web_server/handlers/telebirr.go
Normal file
98
internal/web_server/handlers/telebirr.go
Normal file
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
|
|
||||||
func (a *App) initAppRoutes() {
|
func (a *App) initAppRoutes() {
|
||||||
h := handlers.New(
|
h := handlers.New(
|
||||||
|
a.telebirrSvc,
|
||||||
a.arifpaySvc,
|
a.arifpaySvc,
|
||||||
a.santimpaySvc,
|
a.santimpaySvc,
|
||||||
a.issueReportingSvc,
|
a.issueReportingSvc,
|
||||||
|
|
@ -117,6 +118,10 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler)
|
groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler)
|
||||||
groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler)
|
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
|
//Santimpay
|
||||||
groupV1.Post("/santimpay/init-payment", h.CreateSantimPayPaymentHandler)
|
groupV1.Post("/santimpay/init-payment", h.CreateSantimPayPaymentHandler)
|
||||||
// groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.B2CTransferHandler)
|
// groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.B2CTransferHandler)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user