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 }