package chapa import ( "context" "database/sql" "errors" "fmt" // "log/slog" "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/google/uuid" "github.com/shopspring/decimal" ) type Service struct { transactionStore transaction.TransactionStore walletStore wallet.WalletStore userStore user.UserStore referralStore referralservice.ReferralStore branchStore branch.BranchStore chapaClient ChapaClient config *config.Config // logger *slog.Logger store *repository.Store } func NewService( txStore transaction.TransactionStore, walletStore wallet.WalletStore, userStore user.UserStore, referralStore referralservice.ReferralStore, branchStore branch.BranchStore, chapaClient ChapaClient, store *repository.Store, ) *Service { return &Service{ transactionStore: txStore, walletStore: walletStore, userStore: userStore, referralStore: referralStore, branchStore: branchStore, chapaClient: chapaClient, store: store, } } func (s *Service) HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error { _, tx, err := s.store.BeginTx(ctx) if err != nil { return err } defer tx.Rollback(ctx) // Use your services normally (they don’t use the transaction, unless you wire `q`) referenceID, err := strconv.ParseInt(req.Reference, 10, 64) if err != nil { return fmt.Errorf("invalid reference ID: %w", err) } txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("transaction with ID %d not found", referenceID) } return err } if txn.Verified { return nil } webhookAmount, _ := decimal.NewFromString(req.Amount) storedAmount, _ := decimal.NewFromString(txn.Amount.String()) if !webhookAmount.Equal(storedAmount) { return fmt.Errorf("amount mismatch") } txn.Verified = true if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, txn.Verified, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil { return err } return tx.Commit(ctx) } func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error { _, tx, err := s.store.BeginTx(ctx) if err != nil { return err } defer tx.Rollback(ctx) if req.Status != "success" { return fmt.Errorf("payment status not successful") } // 1. Parse reference ID referenceID, err := strconv.ParseInt(req.TxRef, 10, 64) if err != nil { return fmt.Errorf("invalid tx_ref: %w", err) } // 2. Fetch transaction txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("transaction with ID %d not found", referenceID) } return err } if txn.Verified { return nil // already processed } webhookAmount, _ := strconv.ParseFloat(req.Amount, 32) if webhookAmount < float64(txn.Amount) { return fmt.Errorf("webhook amount is less than expected") } // 4. Fetch wallet wallet, err := s.walletStore.GetWalletByID(ctx, txn.ID) if err != nil { return err } // 5. Update wallet balance newBalance := wallet.Balance + txn.Amount if err := s.walletStore.UpdateBalance(ctx, wallet.ID, newBalance); err != nil { return err } // 6. Mark transaction as verified if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, true, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil { return err } // 7. Check & Create Referral stats, err := s.referralStore.GetReferralStats(ctx, strconv.FormatInt(wallet.UserID, 10)) if err != nil { return err } if stats == nil { if err := s.referralStore.CreateReferral(ctx, wallet.UserID); err != nil { return err } } return tx.Commit(ctx) } func (s *Service) WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error { _, tx, err := s.store.BeginTx(ctx) if err != nil { return err } defer tx.Rollback(ctx) // Get the requesting user user, err := s.userStore.GetUserByID(ctx, userID) if err != nil { return fmt.Errorf("user not found: %w", err) } branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID) if err != nil { return err } wallets, err := s.walletStore.GetWalletsByUser(ctx, userID) if err != nil { return err } var targetWallet *domain.Wallet for _, w := range wallets { if w.ID == req.WalletID { targetWallet = &w break } } if targetWallet == nil { return fmt.Errorf("no wallet found with the specified ID") } if !targetWallet.IsWithdraw || !targetWallet.IsActive { return fmt.Errorf("wallet not eligible for withdrawal") } if targetWallet.Balance < domain.Currency(req.Amount) { return fmt.Errorf("insufficient balance") } txID := uuid.New().String() payload := domain.ChapaTransferPayload{ AccountName: req.AccountName, AccountNumber: req.AccountNumber, Amount: strconv.FormatInt(req.Amount, 10), Currency: req.Currency, BeneficiaryName: req.BeneficiaryName, TxRef: txID, Reference: txID, BankCode: req.BankCode, } ok, err := s.chapaClient.IssuePayment(ctx, payload) if err != nil || !ok { return fmt.Errorf("chapa transfer failed: %v", err) } // Create transaction using user and wallet info _, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{ Amount: domain.Currency(req.Amount), Type: domain.TransactionType(domain.TRANSACTION_CASHOUT), ReferenceNumber: txID, AccountName: req.AccountName, AccountNumber: req.AccountNumber, BankCode: req.BankCode, BeneficiaryName: req.BeneficiaryName, PaymentOption: domain.PaymentOption(domain.BANK), BranchID: req.BranchID, BranchName: branch.Name, BranchLocation: branch.Location, // CashierID: user.ID, // CashierName: user.FullName, FullName: user.FirstName + " " + user.LastName, PhoneNumber: user.PhoneNumber, CompanyID: branch.CompanyID, }) if err != nil { return fmt.Errorf("failed to create transaction: %w", err) } newBalance := domain.Currency(req.Amount) err = s.walletStore.UpdateBalance(ctx, targetWallet.ID, newBalance) if err != nil { return fmt.Errorf("failed to update wallet balance: %w", err) } return tx.Commit(ctx) } func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error) { _, tx, err := s.store.BeginTx(ctx) if err != nil { return "", err } defer tx.Rollback(ctx) user, err := s.userStore.GetUserByID(ctx, userID) if err != nil { return "", err } branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID) if err != nil { return "", err } txID := uuid.New().String() _, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{ Amount: req.Amount, Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT), ReferenceNumber: txID, BranchID: req.BranchID, BranchName: branch.Name, BranchLocation: branch.Location, FullName: user.FirstName + " " + user.LastName, PhoneNumber: user.PhoneNumber, CompanyID: branch.CompanyID, }) if err != nil { return "", err } // Fetch user details for Chapa payment userInfo, err := s.userStore.GetUserByID(ctx, userID) if err != nil { return "", err } // Build Chapa InitPaymentRequest (matches Chapa API) paymentReq := domain.InitPaymentRequest{ Amount: req.Amount, Currency: req.Currency, Email: userInfo.Email, FirstName: userInfo.FirstName, LastName: userInfo.LastName, TxRef: txID, CallbackURL: s.config.CHAPA_CALLBACK_URL, ReturnURL: s.config.CHAPA_RETURN_URL, } // Call Chapa to initialize payment paymentURL, err := s.chapaClient.InitPayment(ctx, paymentReq) if err != nil { return "", err } // Commit DB transaction if err := tx.Commit(ctx); err != nil { return "", err } return paymentURL, nil } func (s *Service) GetSupportedBanks() ([]domain.ChapaSupportedBank, error) { banks, err := s.chapaClient.FetchBanks() fmt.Printf("\n\nfetched banks: %+v\n\n", banks) if err != nil { return nil, err } // Add formatting logic (same as in original controller) for i := range banks { if banks[i].IsMobilemoney != nil && *(banks[i].IsMobilemoney) == 1 { banks[i].AcctNumberRegex = "/^09[0-9]{8}$/" banks[i].ExampleValue = "0952097177" } else { switch banks[i].AcctLength { case 8: banks[i].ExampleValue = "16967608" case 13: banks[i].ExampleValue = "1000222215735" case 14: banks[i].ExampleValue = "01320089280800" case 16: banks[i].ExampleValue = "1000222215735123" } banks[i].AcctNumberRegex = formatRegex(banks[i].AcctLength) } } return banks, nil } func formatRegex(length int) string { return fmt.Sprintf("/^[0-9]{%d}$/", length) }