379 lines
9.9 KiB
Go
379 lines
9.9 KiB
Go
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 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)
|
||
}
|
||
|
||
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)
|
||
}
|