721 lines
23 KiB
Go
721 lines
23 KiB
Go
package chapa
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type Service struct {
|
|
transferStore wallet.TransferStore
|
|
walletStore wallet.Service
|
|
userStore user.UserStore
|
|
cfg *config.Config
|
|
chapaClient *Client
|
|
}
|
|
|
|
func NewService(
|
|
transferStore wallet.TransferStore,
|
|
walletStore wallet.Service,
|
|
userStore user.UserStore,
|
|
cfg *config.Config,
|
|
chapaClient *Client,
|
|
|
|
) *Service {
|
|
return &Service{
|
|
transferStore: transferStore,
|
|
walletStore: walletStore,
|
|
userStore: userStore,
|
|
cfg: cfg,
|
|
chapaClient: chapaClient,
|
|
}
|
|
}
|
|
|
|
func (s *Service) VerifyWebhookSignature(ctx context.Context, payload []byte, chapaSignature, xChapaSignature string) (bool, error) {
|
|
secret := s.cfg.CHAPA_WEBHOOK_SECRET // or os.Getenv("CHAPA_SECRET_KEY")
|
|
if secret == "" {
|
|
return false, fmt.Errorf("missing Chapa secret key in configuration")
|
|
}
|
|
|
|
// Compute expected signature using HMAC SHA256
|
|
h := hmac.New(sha256.New, []byte(secret))
|
|
h.Write(payload)
|
|
expected := hex.EncodeToString(h.Sum(nil))
|
|
|
|
// Check either header
|
|
if chapaSignature == expected || xChapaSignature == expected {
|
|
return true, nil
|
|
}
|
|
|
|
// Optionally log for debugging
|
|
var pretty map[string]interface{}
|
|
_ = json.Unmarshal(payload, &pretty)
|
|
fmt.Printf("[Webhook Verification Failed]\nExpected: %s\nGot chapa-signature: %s\nGot x-chapa-signature: %s\nPayload: %+v\n",
|
|
expected, chapaSignature, xChapaSignature, pretty)
|
|
|
|
return false, fmt.Errorf("invalid webhook signature")
|
|
}
|
|
|
|
// InitiateDeposit starts a new deposit process
|
|
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
|
|
// Validate amount
|
|
if amount <= 0 {
|
|
return "", domain.ErrInvalidPaymentAmount
|
|
}
|
|
|
|
// Get user details
|
|
user, err := s.userStore.GetUserByID(ctx, userID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get user: %w", err)
|
|
}
|
|
|
|
// var senderWallet domain.Wallet
|
|
|
|
// Generate unique reference
|
|
// reference := uuid.New().String()
|
|
reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String())
|
|
|
|
senderWallet, err := s.walletStore.GetCustomerWallet(ctx, userID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get sender wallet: %w", err)
|
|
}
|
|
// for _, wallet := range senderWallets {
|
|
// if wallet.IsTransferable {
|
|
// senderWallet = wallet
|
|
// break
|
|
// }
|
|
// }
|
|
|
|
// Check if payment with this reference already exists
|
|
// if transfer, err := s.transferStore.GetTransferByReference(ctx, reference); err == nil {
|
|
|
|
// return fmt.Sprintf("%v", transfer), ErrPaymentAlreadyExists
|
|
// }
|
|
|
|
// Create payment record
|
|
transfer := domain.CreateTransfer{
|
|
Message: fmt.Sprintf("Depositing %v into wallet using chapa. Reference Number %v", amount.Float32(), reference),
|
|
Amount: amount,
|
|
Type: domain.DEPOSIT,
|
|
PaymentMethod: domain.TRANSFER_CHAPA,
|
|
ReferenceNumber: reference,
|
|
// ReceiverWalletID: 1,
|
|
SenderWalletID: domain.ValidInt64{
|
|
Value: senderWallet.RegularID,
|
|
Valid: true,
|
|
},
|
|
Verified: false,
|
|
Status: string(domain.STATUS_PENDING),
|
|
}
|
|
|
|
userPhoneNum := user.PhoneNumber[len(user.PhoneNumber)-9:]
|
|
|
|
if len(user.PhoneNumber) >= 9 {
|
|
userPhoneNum = "0" + userPhoneNum
|
|
}
|
|
|
|
payload := domain.ChapaInitDepositRequest{
|
|
Amount: amount,
|
|
Currency: "ETB",
|
|
Email: user.Email,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
TxRef: reference,
|
|
CallbackURL: s.cfg.CHAPA_CALLBACK_URL,
|
|
ReturnURL: s.cfg.CHAPA_RETURN_URL,
|
|
PhoneNumber: userPhoneNum,
|
|
}
|
|
|
|
// Initialize payment with Chapa
|
|
response, err := s.chapaClient.InitializePayment(ctx, payload)
|
|
|
|
fmt.Printf("\n\nChapa payload is: %+v\n\n", payload)
|
|
|
|
if err != nil {
|
|
// Update payment status to failed
|
|
// _ = s.transferStore.(payment.ID, domain.PaymentStatusFailed)
|
|
return "", fmt.Errorf("failed to initialize payment: %w", err)
|
|
}
|
|
|
|
tempTransfer, err := s.transferStore.CreateTransfer(ctx, transfer)
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to save payment: %w", err)
|
|
}
|
|
|
|
fmt.Printf("\n\nTemp transfer is: %v\n\n", tempTransfer)
|
|
|
|
return response.CheckoutURL, nil
|
|
}
|
|
|
|
func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, req domain.ChapaWebhookPayment) error {
|
|
// Find payment by reference
|
|
payment, err := s.transferStore.GetTransferByReference(ctx, req.TxRef)
|
|
if err != nil {
|
|
return domain.ErrPaymentNotFound
|
|
}
|
|
|
|
if payment.Verified {
|
|
return nil
|
|
}
|
|
|
|
// Verify payment with Chapa
|
|
// verification, err := s.chapaClient.VerifyPayment(ctx, transfer.Reference)
|
|
// if err != nil {
|
|
// return fmt.Errorf("failed to verify payment: %w", err)
|
|
// }
|
|
|
|
// Update payment status
|
|
// verified := false
|
|
// if transfer.Status == string(domain.PaymentStatusCompleted) {
|
|
// verified = true
|
|
// }
|
|
|
|
// If payment is completed, credit user's wallet
|
|
if req.Status == string(domain.PaymentStatusSuccessful) {
|
|
|
|
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
|
|
return fmt.Errorf("failed to update is payment verified value: %w", err)
|
|
}
|
|
|
|
if err := s.transferStore.UpdateTransferStatus(ctx, payment.ID, string(domain.DepositStatusCompleted)); err != nil {
|
|
return fmt.Errorf("failed to update payment status: %w", err)
|
|
}
|
|
|
|
if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{
|
|
ReferenceNumber: domain.ValidString{
|
|
Value: req.TxRef,
|
|
},
|
|
}, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil {
|
|
return fmt.Errorf("failed to credit user wallet: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) CancelDeposit(ctx context.Context, userID int64, txRef string) (domain.ChapaCancelResponse, error) {
|
|
// Validate input
|
|
if txRef == "" {
|
|
return domain.ChapaCancelResponse{}, fmt.Errorf("transaction reference is required")
|
|
}
|
|
|
|
// Retrieve user to verify ownership / context (optional but good practice)
|
|
user, err := s.userStore.GetUserByID(ctx, userID)
|
|
if err != nil {
|
|
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to get user: %w", err)
|
|
}
|
|
|
|
fmt.Printf("\n\nAttempting to cancel Chapa transaction: %s for user %s (%d)\n\n", txRef, user.Email, userID)
|
|
|
|
// Call Chapa API to cancel transaction
|
|
cancelResp, err := s.chapaClient.CancelTransaction(ctx, txRef)
|
|
if err != nil {
|
|
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to cancel transaction via Chapa: %w", err)
|
|
}
|
|
|
|
// Update transfer/payment status locally
|
|
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
|
|
if err != nil {
|
|
// Log but do not block cancellation if remote succeeded
|
|
fmt.Printf("Warning: unable to find local transfer for txRef %s: %v\n", txRef, err)
|
|
} else {
|
|
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.STATUS_CANCELLED)); err != nil {
|
|
fmt.Printf("Warning: failed to update transfer status for txRef %s: %v\n", txRef, err)
|
|
}
|
|
|
|
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, false); err != nil {
|
|
fmt.Printf("Warning: failed to update transfer status for txRef %s: %v\n", txRef, err)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("\n\nChapa cancellation response: %+v\n\n", cancelResp)
|
|
|
|
return cancelResp, nil
|
|
}
|
|
|
|
func (s *Service) FetchAllTransactions(ctx context.Context) ([]domain.ChapaTransaction, error) {
|
|
// Call Chapa API to get all transactions
|
|
resp, err := s.chapaClient.GetAllTransactions(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch transactions from Chapa: %w", err)
|
|
}
|
|
|
|
if resp.Status != "success" {
|
|
return nil, fmt.Errorf("chapa API returned non-success status: %s", resp.Status)
|
|
}
|
|
|
|
transactions := make([]domain.ChapaTransaction, 0, len(resp.Data.Transactions))
|
|
|
|
// Map API transactions to domain transactions
|
|
for _, t := range resp.Data.Transactions {
|
|
tx := domain.ChapaTransaction{
|
|
Status: t.Status,
|
|
RefID: t.RefID,
|
|
Type: t.Type,
|
|
CreatedAt: t.CreatedAt,
|
|
Currency: t.Currency,
|
|
Amount: t.Amount,
|
|
Charge: t.Charge,
|
|
TransID: t.TransID,
|
|
PaymentMethod: t.PaymentMethod,
|
|
Customer: domain.ChapaCustomer{
|
|
ID: t.Customer.ID,
|
|
Email: t.Customer.Email,
|
|
FirstName: t.Customer.FirstName,
|
|
LastName: t.Customer.LastName,
|
|
Mobile: t.Customer.Mobile,
|
|
},
|
|
}
|
|
transactions = append(transactions, tx)
|
|
}
|
|
|
|
return transactions, nil
|
|
}
|
|
|
|
func (s *Service) FetchTransactionEvents(ctx context.Context, refID string) ([]domain.ChapaTransactionEvent, error) {
|
|
if refID == "" {
|
|
return nil, fmt.Errorf("transaction reference ID is required")
|
|
}
|
|
|
|
// Call Chapa client to fetch transaction events
|
|
events, err := s.chapaClient.GetTransactionEvents(ctx, refID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch transaction events from Chapa: %w", err)
|
|
}
|
|
|
|
// Optional: Transform or filter events if needed
|
|
transformedEvents := make([]domain.ChapaTransactionEvent, 0, len(events))
|
|
for _, e := range events {
|
|
transformedEvents = append(transformedEvents, domain.ChapaTransactionEvent{
|
|
Item: e.Item,
|
|
Message: e.Message,
|
|
Type: e.Type,
|
|
CreatedAt: e.CreatedAt,
|
|
UpdatedAt: e.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
return transformedEvents, nil
|
|
}
|
|
|
|
func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) {
|
|
// Parse and validate amount
|
|
amount, err := strconv.ParseFloat(req.Amount, 64)
|
|
if err != nil || amount <= 0 {
|
|
return nil, domain.ErrInvalidWithdrawalAmount
|
|
}
|
|
|
|
// Get user's wallet
|
|
wallet, err := s.walletStore.GetCustomerWallet(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user wallets: %w", err)
|
|
}
|
|
|
|
// var withdrawWallet domain.Wallet
|
|
// for _, wallet := range wallets {
|
|
// if wallet.IsWithdraw {
|
|
// withdrawWallet = wallet
|
|
// break
|
|
// }
|
|
// }
|
|
|
|
// if withdrawWallet.ID == 0 {
|
|
// return nil, errors.New("withdrawal wallet not found")
|
|
// }
|
|
// Check balance
|
|
if float64(wallet.RegularBalance) < float64(amount) {
|
|
return nil, domain.ErrInsufficientBalance
|
|
}
|
|
|
|
// Generate unique reference
|
|
reference := uuid.New().String()
|
|
|
|
createTransfer := domain.CreateTransfer{
|
|
Message: fmt.Sprintf("Withdrawing %f into wallet using chapa. Reference Number %s", amount, reference),
|
|
Amount: domain.Currency(amount),
|
|
Type: domain.WITHDRAW,
|
|
SenderWalletID: domain.ValidInt64{
|
|
Value: wallet.RegularID,
|
|
Valid: true,
|
|
},
|
|
Status: string(domain.PaymentStatusPending),
|
|
Verified: false,
|
|
ReferenceNumber: reference,
|
|
PaymentMethod: domain.TRANSFER_CHAPA,
|
|
}
|
|
|
|
transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create transfer record: %w", err)
|
|
}
|
|
// Initiate transfer with Chapa
|
|
transferReq := domain.ChapaWithdrawalRequest{
|
|
AccountName: req.AccountName,
|
|
AccountNumber: req.AccountNumber,
|
|
Amount: fmt.Sprintf("%f", amount),
|
|
Currency: req.Currency,
|
|
Reference: reference,
|
|
// BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
|
|
BankCode: req.BankCode,
|
|
}
|
|
|
|
newBalance := float64(wallet.RegularBalance) - float64(amount)
|
|
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil {
|
|
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
|
|
}
|
|
|
|
success, err := s.chapaClient.InitializeTransfer(ctx, transferReq)
|
|
if err != nil {
|
|
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
|
|
newBalance := float64(wallet.RegularBalance) + float64(amount)
|
|
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil {
|
|
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
|
|
}
|
|
return nil, fmt.Errorf("failed to initiate transfer: %w", err)
|
|
}
|
|
|
|
if !success {
|
|
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
|
|
newBalance := float64(wallet.RegularBalance) + float64(amount)
|
|
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil {
|
|
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
|
|
}
|
|
return nil, errors.New("chapa rejected the transfer request")
|
|
}
|
|
|
|
// Update withdrawal status to processing
|
|
// if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil {
|
|
// return nil, fmt.Errorf("failed to update withdrawal status: %w", err)
|
|
// }
|
|
// Deduct from wallet (or wait for webhook confirmation depending on your flow)
|
|
|
|
return &transfer, nil
|
|
}
|
|
|
|
func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, req domain.ChapaWebhookTransfer) error {
|
|
// Find payment by reference
|
|
transfer, err := s.transferStore.GetTransferByReference(ctx, req.Reference)
|
|
if err != nil {
|
|
return domain.ErrPaymentNotFound
|
|
}
|
|
|
|
if transfer.Verified {
|
|
return nil
|
|
}
|
|
|
|
// Verify payment with Chapa
|
|
// verification, err := s.chapaClient.VerifyPayment(ctx, payment.Reference)
|
|
// if err != nil {
|
|
// return fmt.Errorf("failed to verify payment: %w", err)
|
|
// }
|
|
|
|
// Update payment status
|
|
// verified := false
|
|
// if transfer.Status == string(domain.PaymentStatusCompleted) {
|
|
// verified = true
|
|
// }
|
|
|
|
if req.Status == string(domain.PaymentStatusSuccessful) {
|
|
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
|
|
return fmt.Errorf("failed to update payment status: %w", err)
|
|
} // If payment is completed, credit user's walle
|
|
} else {
|
|
_, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{},
|
|
domain.TRANSFER_DIRECT, domain.PaymentDetails{},
|
|
fmt.Sprintf("Added %v to wallet by system because chapa withdraw is unsuccessful", transfer.Amount))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to credit user wallet: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) GetPaymentReceiptURL(refId string) (string, error) {
|
|
if refId == "" {
|
|
return "", fmt.Errorf("reference ID cannot be empty")
|
|
}
|
|
|
|
receiptURL := s.cfg.CHAPA_RECEIPT_URL + refId
|
|
return receiptURL, nil
|
|
}
|
|
|
|
func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (any, error) {
|
|
// Lookup transfer by reference
|
|
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err)
|
|
}
|
|
|
|
// If already verified, just return a completed response
|
|
if transfer.Verified {
|
|
return map[string]any{}, errors.New("transfer already verified")
|
|
}
|
|
|
|
// Validate sender wallet
|
|
if !transfer.SenderWalletID.Valid {
|
|
return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
|
|
}
|
|
|
|
var verification any
|
|
|
|
switch strings.ToLower(string(transfer.Type)) {
|
|
case string(domain.DEPOSIT):
|
|
// Verify Chapa payment
|
|
verification, err := s.chapaClient.ManualVerifyPayment(ctx, txRef)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err)
|
|
}
|
|
|
|
if strings.ToLower(verification.Data.Status) == "success" ||
|
|
verification.Status == string(domain.PaymentStatusCompleted) {
|
|
|
|
// Credit wallet
|
|
_, err := s.walletStore.AddToWallet(ctx,
|
|
transfer.SenderWalletID.Value,
|
|
transfer.Amount,
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_CHAPA,
|
|
domain.PaymentDetails{},
|
|
fmt.Sprintf("Added %.2f ETB to wallet using Chapa", transfer.Amount.Float32()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to credit wallet: %w", err)
|
|
}
|
|
|
|
// Mark verified in DB
|
|
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
|
|
return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err)
|
|
}
|
|
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.DepositStatusCompleted)); err != nil {
|
|
return nil, fmt.Errorf("failed to update deposit transfer status: %w", err)
|
|
}
|
|
}
|
|
|
|
case string(domain.WITHDRAW):
|
|
// Verify Chapa transfer
|
|
verification, err := s.chapaClient.ManualVerifyTransfer(ctx, txRef)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err)
|
|
}
|
|
|
|
if strings.ToLower(verification.Data.Status) == "success" ||
|
|
verification.Status == string(domain.PaymentStatusCompleted) {
|
|
|
|
// Deduct wallet
|
|
_, err := s.walletStore.DeductFromWallet(ctx,
|
|
transfer.SenderWalletID.Value,
|
|
transfer.Amount,
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_CHAPA,
|
|
fmt.Sprintf("Deducted %.2f ETB from wallet using Chapa", transfer.Amount.Float32()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to debit wallet: %w", err)
|
|
}
|
|
|
|
// Mark verified in DB
|
|
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
|
|
return nil, fmt.Errorf("failed to mark withdraw transfer as verified: %w", err)
|
|
}
|
|
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusCompleted)); err != nil {
|
|
return nil, fmt.Errorf("failed to update withdraw transfer status: %w", err)
|
|
}
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type)
|
|
}
|
|
|
|
return verification, nil
|
|
}
|
|
|
|
func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.BankData, error) {
|
|
banks, err := s.chapaClient.FetchSupportedBanks(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch banks: %w", err)
|
|
}
|
|
return banks, nil
|
|
}
|
|
|
|
func (s *Service) GetAllTransfers(ctx context.Context) (*domain.ChapaTransfersListResponse, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.cfg.CHAPA_BASE_URL+"/transfers", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
|
|
|
|
resp, err := s.chapaClient.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch transfers: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var result domain.ChapaTransfersListResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
// Return the decoded result directly; no intermediate dynamic map needed
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *Service) GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error) {
|
|
URL := s.cfg.CHAPA_BASE_URL + "/balances"
|
|
if currencyCode != "" {
|
|
URL = fmt.Sprintf("%s/%s", URL, strings.ToLower(currencyCode))
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create balance request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
|
|
|
|
resp, err := s.chapaClient.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute balance request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var result struct {
|
|
Status string `json:"status"`
|
|
Message string `json:"message"`
|
|
Data []domain.Balance `json:"data"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode balance response: %w", err)
|
|
}
|
|
|
|
return result.Data, nil
|
|
}
|
|
|
|
func (s *Service) SwapCurrency(ctx context.Context, reqBody domain.SwapRequest) (*domain.SwapResponse, error) {
|
|
URL := s.cfg.CHAPA_BASE_URL + "/swap"
|
|
|
|
// Normalize currency codes
|
|
reqBody.From = strings.ToUpper(reqBody.From)
|
|
reqBody.To = strings.ToUpper(reqBody.To)
|
|
|
|
// Marshal request body
|
|
body, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal swap payload: %w", err)
|
|
}
|
|
|
|
// Create HTTP request
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, bytes.NewBuffer(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create swap request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
|
|
|
|
// Execute request
|
|
resp, err := s.chapaClient.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute swap request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Handle unexpected status
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
// Decode response
|
|
var result domain.SwapResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode swap response: %w", err)
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// func (s *Service) InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error) {
|
|
// if amount < 1 {
|
|
// return nil, fmt.Errorf("amount must be at least 1 USD")
|
|
// }
|
|
// if strings.ToUpper(from) != "USD" || strings.ToUpper(to) != "ETB" {
|
|
// return nil, fmt.Errorf("only USD to ETB swap is supported")
|
|
// }
|
|
|
|
// payload := domain.SwapRequest{
|
|
// Amount: amount,
|
|
// From: strings.ToUpper(from),
|
|
// To: strings.ToUpper(to),
|
|
// }
|
|
|
|
// // payload := map[string]any{
|
|
// // "amount": amount,
|
|
// // "from": strings.ToUpper(from),
|
|
// // "to": strings.ToUpper(to),
|
|
// // }
|
|
|
|
// body, err := json.Marshal(payload)
|
|
// if err != nil {
|
|
// return nil, fmt.Errorf("failed to encode swap payload: %w", err)
|
|
// }
|
|
|
|
// req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.chapa.co/v1/swap", bytes.NewBuffer(body))
|
|
// if err != nil {
|
|
// return nil, fmt.Errorf("failed to create swap request: %w", err)
|
|
// }
|
|
|
|
// req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
|
|
// req.Header.Set("Content-Type", "application/json")
|
|
|
|
// resp, err := s.chapaClient.httpClient.Do(req)
|
|
// if err != nil {
|
|
// return nil, fmt.Errorf("failed to execute swap request: %w", err)
|
|
// }
|
|
// defer resp.Body.Close()
|
|
|
|
// if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
// bodyBytes, _ := io.ReadAll(resp.Body)
|
|
// return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
|
|
// }
|
|
|
|
// var result struct {
|
|
// Message string `json:"message"`
|
|
// Status string `json:"status"`
|
|
// Data domain.SwapResponse `json:"data"`
|
|
// }
|
|
|
|
// if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
// return nil, fmt.Errorf("failed to decode swap response: %w", err)
|
|
// }
|
|
|
|
// return &result.Data, nil
|
|
// }
|