Yimaru-BackEnd/internal/services/telebirr/service.go

407 lines
12 KiB
Go

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)
} else if transfer.Verified == true {
return fmt.Errorf("payment already verified")
}
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)
// }
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.PaymentStatusCompleted)); err != nil {
return fmt.Errorf("failed to update transfer status: %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)
}
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return fmt.Errorf("failed to update transfer verification: %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
}