379 lines
11 KiB
Go
379 lines
11 KiB
Go
package chapa
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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.Service
|
|
userStore user.UserStore
|
|
cfg *config.Config
|
|
chapaClient *Client
|
|
}
|
|
|
|
func NewService(
|
|
transferStore wallet.TransferStore,
|
|
walletStore wallet.Service,
|
|
userStore user.UserStore,
|
|
cfg *config.Config,
|
|
chapaClient *Client,
|
|
|
|
) *Service {
|
|
return &Service{
|
|
transferStore: transferStore,
|
|
walletStore: walletStore,
|
|
userStore: userStore,
|
|
cfg: cfg,
|
|
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()
|
|
reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, 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{
|
|
Message: fmt.Sprintf("Depositing %v into wallet using chapa. Reference Number %v", amount.Float32(), reference),
|
|
Amount: amount,
|
|
Type: domain.DEPOSIT,
|
|
PaymentMethod: domain.TRANSFER_CHAPA,
|
|
ReferenceNumber: reference,
|
|
// ReceiverWalletID: 1,
|
|
SenderWalletID: domain.ValidInt64{
|
|
Value: senderWallet.ID,
|
|
Valid: true,
|
|
},
|
|
Verified: false,
|
|
}
|
|
|
|
payload := domain.ChapaDepositRequest{
|
|
Amount: amount,
|
|
Currency: "ETB",
|
|
Email: user.Email,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
TxRef: reference,
|
|
CallbackURL: s.cfg.CHAPA_CALLBACK_URL,
|
|
ReturnURL: s.cfg.CHAPA_RETURN_URL,
|
|
}
|
|
|
|
// Initialize payment with Chapa
|
|
response, err := s.chapaClient.InitializePayment(ctx, payload)
|
|
|
|
fmt.Printf("\n\nChapa payload is: %+v\n\n", payload)
|
|
|
|
if err != nil {
|
|
// Update payment status to failed
|
|
// _ = s.transferStore.(payment.ID, domain.PaymentStatusFailed)
|
|
return "", fmt.Errorf("failed to initialize payment: %w", err)
|
|
}
|
|
|
|
tempTransfer, err := s.transferStore.CreateTransfer(ctx, transfer)
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to save payment: %w", err)
|
|
}
|
|
|
|
fmt.Printf("\n\nTemp transfer is: %v\n\n", tempTransfer)
|
|
|
|
return response.CheckoutURL, nil
|
|
}
|
|
|
|
func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) {
|
|
// Parse and validate amount
|
|
amount, err := strconv.ParseInt(req.Amount, 10, 64)
|
|
if err != nil || amount <= 0 {
|
|
return nil, domain.ErrInvalidWithdrawalAmount
|
|
}
|
|
|
|
// Get user details
|
|
// user, err := s.userStore.GetUserByID(ctx, userID)
|
|
// if err != nil {
|
|
// return nil, fmt.Errorf("failed to get user: %w", err)
|
|
// }
|
|
|
|
// Get user's wallet
|
|
wallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user wallets: %w", err)
|
|
}
|
|
|
|
var withdrawWallet domain.Wallet
|
|
for _, wallet := range wallets {
|
|
if wallet.IsWithdraw {
|
|
withdrawWallet = wallet
|
|
break
|
|
}
|
|
}
|
|
|
|
if withdrawWallet.ID == 0 {
|
|
return nil, errors.New("withdrawal wallet not found")
|
|
}
|
|
// Check balance
|
|
if withdrawWallet.Balance < domain.Currency(amount) {
|
|
return nil, domain.ErrInsufficientBalance
|
|
}
|
|
|
|
// Generate unique reference
|
|
reference := uuid.New().String()
|
|
|
|
createTransfer := domain.CreateTransfer{
|
|
Message: fmt.Sprintf("Withdrawing %d into wallet using chapa. Reference Number %s", amount, reference),
|
|
Amount: domain.Currency(amount),
|
|
Type: domain.WITHDRAW,
|
|
SenderWalletID: domain.ValidInt64{
|
|
Value: withdrawWallet.ID,
|
|
Valid: true,
|
|
},
|
|
Status: string(domain.PaymentStatusPending),
|
|
Verified: false,
|
|
ReferenceNumber: reference,
|
|
PaymentMethod: domain.TRANSFER_CHAPA,
|
|
}
|
|
|
|
transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create transfer record: %w", err)
|
|
}
|
|
// Initiate transfer with Chapa
|
|
transferReq := domain.ChapaWithdrawalRequest{
|
|
AccountName: req.AccountName,
|
|
AccountNumber: req.AccountNumber,
|
|
Amount: fmt.Sprintf("%d", amount),
|
|
Currency: req.Currency,
|
|
Reference: reference,
|
|
// BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
|
|
BankCode: req.BankCode,
|
|
}
|
|
|
|
success, err := s.chapaClient.InitiateTransfer(ctx, transferReq)
|
|
if err != nil {
|
|
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
|
|
return nil, fmt.Errorf("failed to initiate transfer: %w", err)
|
|
}
|
|
|
|
if !success {
|
|
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
|
|
return nil, errors.New("chapa rejected the transfer request")
|
|
}
|
|
|
|
// Update withdrawal status to processing
|
|
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil {
|
|
return nil, fmt.Errorf("failed to update withdrawal status: %w", err)
|
|
}
|
|
// Deduct from wallet (or wait for webhook confirmation depending on your flow)
|
|
newBalance := withdrawWallet.Balance - domain.Currency(amount)
|
|
if err := s.walletStore.UpdateBalance(ctx, withdrawWallet.ID, newBalance); err != nil {
|
|
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
|
|
}
|
|
|
|
return &transfer, 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
|
|
}
|
|
|
|
func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
|
|
// Lookup transfer by reference
|
|
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err)
|
|
}
|
|
|
|
if transfer.Verified {
|
|
return &domain.ChapaVerificationResponse{
|
|
Status: string(domain.PaymentStatusCompleted),
|
|
Amount: float64(transfer.Amount) / 100,
|
|
Currency: "ETB",
|
|
}, nil
|
|
}
|
|
|
|
// Validate sender wallet
|
|
if !transfer.SenderWalletID.Valid {
|
|
return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
|
|
}
|
|
|
|
var verification *domain.ChapaVerificationResponse
|
|
|
|
// Decide verification method based on type
|
|
switch strings.ToLower(string(transfer.Type)) {
|
|
case "deposit":
|
|
// Use Chapa Payment Verification
|
|
verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err)
|
|
}
|
|
|
|
if verification.Status == string(domain.PaymentStatusSuccessful) {
|
|
// Mark verified
|
|
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
|
|
return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err)
|
|
}
|
|
|
|
// Credit wallet
|
|
_, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value,
|
|
transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{},
|
|
fmt.Sprintf("Added %v to wallet using Chapa", transfer.Amount.Float32()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to credit wallet: %w", err)
|
|
}
|
|
}
|
|
|
|
case "withdraw":
|
|
// Use Chapa Transfer Verification
|
|
verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err)
|
|
}
|
|
|
|
if verification.Status == string(domain.PaymentStatusSuccessful) {
|
|
// Mark verified (withdraw doesn't affect balance)
|
|
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
|
|
return nil, fmt.Errorf("failed to mark withdrawal transfer as verified: %w", err)
|
|
}
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type)
|
|
}
|
|
|
|
return verification, nil
|
|
}
|
|
|
|
func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error {
|
|
// Find payment by reference
|
|
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference)
|
|
if err != nil {
|
|
return ErrPaymentNotFound
|
|
}
|
|
|
|
if payment.Verified {
|
|
return nil
|
|
}
|
|
|
|
// Verify payment with Chapa
|
|
// verification, err := s.chapaClient.VerifyPayment(ctx, transfer.Reference)
|
|
// if err != nil {
|
|
// return fmt.Errorf("failed to verify payment: %w", err)
|
|
// }
|
|
|
|
// Update payment status
|
|
// verified := false
|
|
// if transfer.Status == string(domain.PaymentStatusCompleted) {
|
|
// verified = true
|
|
// }
|
|
|
|
// If payment is completed, credit user's wallet
|
|
if transfer.Status == string(domain.PaymentStatusSuccessful) {
|
|
|
|
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
|
|
return fmt.Errorf("failed to update payment status: %w", err)
|
|
}
|
|
|
|
if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{
|
|
ReferenceNumber: domain.ValidString{
|
|
Value: transfer.Reference,
|
|
},
|
|
}, fmt.Sprintf("Added %v to wallet using Chapa")); err != nil {
|
|
return fmt.Errorf("failed to credit user wallet: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error {
|
|
// Find payment by reference
|
|
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference)
|
|
if err != nil {
|
|
return ErrPaymentNotFound
|
|
}
|
|
|
|
if transfer.Verified {
|
|
return nil
|
|
}
|
|
|
|
// Verify payment with Chapa
|
|
// verification, err := s.chapaClient.VerifyPayment(ctx, payment.Reference)
|
|
// if err != nil {
|
|
// return fmt.Errorf("failed to verify payment: %w", err)
|
|
// }
|
|
|
|
// Update payment status
|
|
// verified := false
|
|
// if transfer.Status == string(domain.PaymentStatusCompleted) {
|
|
// verified = true
|
|
// }
|
|
|
|
if payment.Status == string(domain.PaymentStatusSuccessful) {
|
|
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
|
|
return fmt.Errorf("failed to update payment status: %w", err)
|
|
} // If payment is completed, credit user's walle
|
|
} else {
|
|
_, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{},
|
|
domain.TRANSFER_DIRECT, domain.PaymentDetails{},
|
|
fmt.Sprintf("Added %v to wallet by system because chapa withdraw is unsuccessful"))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to credit user wallet: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|