package chapa import ( "context" "errors" "fmt" "strconv" "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" ) var ( ErrPaymentNotFound = errors.New("payment not found") ErrPaymentAlreadyExists = errors.New("payment with this reference already exists") ErrInvalidPaymentAmount = errors.New("invalid payment amount") ) 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, chapaClient *Client, ) *Service { return &Service{ transferStore: transferStore, walletStore: walletStore, userStore: userStore, chapaClient: chapaClient, } } // 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 "", 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() senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID) if err != nil { return "", fmt.Errorf("failed to get sender wallets: %w", err) } for _, wallet := range senderWallets { if wallet.IsWithdraw { 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{ Amount: amount, Type: domain.DEPOSIT, PaymentMethod: domain.TRANSFER_CHAPA, ReferenceNumber: reference, // ReceiverWalletID: 1, SenderWalletID: domain.ValidInt64{ Value: senderWallet.ID, Valid: true, }, Verified: false, } if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { return "", fmt.Errorf("failed to save payment: %w", err) } // Initialize payment with Chapa response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{ Amount: amount, Currency: "ETB", Email: user.Email, FirstName: user.FirstName, LastName: user.LastName, TxRef: reference, CallbackURL: "https://fortunebet.com/api/v1/payments/callback", ReturnURL: "https://fortunebet.com/api/v1/payment-success", }) if err != nil { // Update payment status to failed // _ = s.transferStore.(payment.ID, domain.PaymentStatusFailed) return "", fmt.Errorf("failed to initialize payment: %w", err) } return response.CheckoutURL, nil } func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) { // Parse and validate amount amount, err := strconv.ParseInt(req.Amount, 10, 64) if err != nil || amount <= 0 { return nil, domain.ErrInvalidWithdrawalAmount } // Get user details // user, err := s.userStore.GetUserByID(ctx, userID) // if err != nil { // return nil, fmt.Errorf("failed to get user: %w", err) // } // Get user's wallet wallets, err := s.walletStore.GetWalletsByUser(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 withdrawWallet.Balance < domain.Currency(amount) { return nil, domain.ErrInsufficientBalance } // Generate unique reference reference := uuid.New().String() createTransfer := domain.CreateTransfer{ Amount: domain.Currency(amount), Type: domain.WITHDRAW, SenderWalletID: domain.ValidInt64{ Value: withdrawWallet.ID, 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("%d", amount), Currency: req.Currency, Reference: reference, // BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName), BankCode: req.BankCode, } success, err := s.chapaClient.InitiateTransfer(ctx, transferReq) if err != nil || !success { // Update withdrawal status to failed _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) return nil, fmt.Errorf("failed to initiate transfer: %w", err) } // 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) newBalance := withdrawWallet.Balance - domain.Currency(amount) if err := s.walletStore.UpdateBalance(ctx, withdrawWallet.ID, newBalance); err != nil { return nil, fmt.Errorf("failed to update wallet balance: %w", err) } return &transfer, nil } func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, 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) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { // First check if we already have a verified record transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) if err == nil && transfer.Verified { return &domain.ChapaVerificationResponse{ Status: string(domain.PaymentStatusCompleted), Amount: float64(transfer.Amount) / 100, // Convert from cents/kobo Currency: "ETB", }, nil } // just making sure that the sender id is valid if !transfer.SenderWalletID.Valid { return nil, fmt.Errorf("sender wallet id is invalid: %v \n", transfer.SenderWalletID) } // If not verified or not found, verify with Chapa verification, err := s.chapaClient.VerifyPayment(ctx, txRef) if err != nil { return nil, fmt.Errorf("failed to verify payment: %w", err) } // Update our records if payment is successful if verification.Status == domain.PaymentStatusCompleted { err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true) if err != nil { return nil, fmt.Errorf("failed to update verification status: %w", err) } // Credit user's wallet err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, transfer.Amount) if err != nil { return nil, fmt.Errorf("failed to update wallet balance: %w", err) } } return &domain.ChapaVerificationResponse{ Status: string(verification.Status), Amount: float64(verification.Amount), Currency: verification.Currency, }, nil } func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error { // Find payment by reference payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference) if err != nil { return 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 err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { return fmt.Errorf("failed to update payment status: %w", err) } // If payment is completed, credit user's wallet if transfer.Status == string(domain.PaymentStatusCompleted) { if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ Value: transfer.Reference, }, }); err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) } } return nil } func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error { // Find payment by reference transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference) if err != nil { return 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 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 wallet if payment.Status == string(domain.PaymentStatusFailed) { if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) } } return nil }