Yimaru-BackEnd/internal/services/chapa/service.go

379 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package chapa
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
// "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 dont 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)
}
banks, err := s.GetSupportedBanks()
validBank := false
for _, bank := range banks {
if strconv.FormatInt(bank.Id, 10) == req.BankCode {
validBank = true
break
}
}
if !validBank {
return fmt.Errorf("invalid bank code")
}
// branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
// if err != nil {
// return err
// }
var targetWallet domain.Wallet
targetWallet, err = s.walletStore.GetWalletByID(ctx, req.WalletID)
if err != nil {
return err
}
// 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.IsTransferable || !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)
if req.Amount <= 0 {
return "", fmt.Errorf("amount must be positive")
}
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()
fmt.Printf("\n\nChapa deposit transaction created: %v%v\n\n", branch, user)
// _, 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
}
// fmt.Printf("\n\nCallbackURL is:%v\n\n", s.config.CHAPA_CALLBACK_URL)
// 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: "https://fortunebet.com/api/v1/payments/callback",
ReturnURL: "https://fortunebet.com/api/v1/payment-success",
}
// Call Chapa to initialize payment
var paymentURL string
maxRetries := 3
for range maxRetries {
paymentURL, err = s.chapaClient.InitPayment(ctx, paymentReq)
if err == nil {
break
}
time.Sleep(1 * time.Second) // Backoff
}
// 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)
}