package chapa import ( "context" "errors" "fmt" "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.WalletStore userStore user.UserStore cfg *config.Config chapaClient *Client } func NewService( transferStore wallet.TransferStore, walletStore wallet.WalletStore, 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 } // VerifyDeposit handles payment verification from webhook func (s *Service) VerifyDeposit(ctx context.Context, reference string) error { // Find payment by reference payment, err := s.transferStore.GetTransferByReference(ctx, reference) if err != nil { return ErrPaymentNotFound } // just making sure that the sender id is valid if !payment.SenderWalletID.Valid { return fmt.Errorf("sender wallet is invalid %v \n", payment.SenderWalletID) } // Skip if already completed if payment.Verified { return nil } // Verify payment with Chapa verification, err := s.chapaClient.VerifyPayment(ctx, reference) if err != nil { return fmt.Errorf("failed to verify payment: %w", err) } // Update payment status 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 verification.Status == domain.PaymentStatusCompleted { if err := s.walletStore.UpdateBalance(ctx, payment.SenderWalletID.Value, payment.Amount); err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) } } return nil } func (s *Service) ManualVerifyPayment(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) 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 }