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

318 lines
8.3 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"
// "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)
}
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
}