318 lines
9.4 KiB
Go
318 lines
9.4 KiB
Go
package chapa
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"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,
|
|
chapaClient *Client,
|
|
|
|
) *Service {
|
|
return &Service{
|
|
transferStore: transferStore,
|
|
walletStore: walletStore,
|
|
userStore: userStore,
|
|
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()
|
|
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{
|
|
Amount: amount,
|
|
Type: domain.DEPOSIT,
|
|
PaymentMethod: domain.TRANSFER_CHAPA,
|
|
ReferenceNumber: reference,
|
|
// ReceiverWalletID: 1,
|
|
SenderWalletID: senderWallet.ID,
|
|
Verified: false,
|
|
}
|
|
|
|
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
|
|
return "", fmt.Errorf("failed to save payment: %w", err)
|
|
}
|
|
|
|
// Initialize payment with Chapa
|
|
response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{
|
|
Amount: amount,
|
|
Currency: "ETB",
|
|
Email: user.Email,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
TxRef: reference,
|
|
CallbackURL: "https://fortunebet.com/api/v1/payments/callback",
|
|
ReturnURL: "https://fortunebet.com/api/v1/payment-success",
|
|
})
|
|
|
|
if err != nil {
|
|
// Update payment status to failed
|
|
// _ = s.transferStore.(payment.ID, domain.PaymentStatusFailed)
|
|
return "", fmt.Errorf("failed to initialize payment: %w", err)
|
|
}
|
|
|
|
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{
|
|
Amount: domain.Currency(amount),
|
|
Type: domain.WITHDRAW,
|
|
ReceiverWalletID: 1,
|
|
SenderWalletID: withdrawWallet.ID,
|
|
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 || !success {
|
|
// Update withdrawal status to failed
|
|
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
|
|
return nil, fmt.Errorf("failed to initiate transfer: %w", err)
|
|
}
|
|
|
|
// 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) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
|
|
// First check if we already have a verified record
|
|
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
|
|
if err == nil && transfer.Verified {
|
|
return &domain.ChapaVerificationResponse{
|
|
Status: string(domain.PaymentStatusCompleted),
|
|
Amount: float64(transfer.Amount) / 100, // Convert from cents/kobo
|
|
Currency: "ETB",
|
|
}, nil
|
|
}
|
|
|
|
// If not verified or not found, verify with Chapa
|
|
verification, err := s.chapaClient.VerifyPayment(ctx, txRef)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to verify payment: %w", err)
|
|
}
|
|
|
|
// Update our records if payment is successful
|
|
if verification.Status == domain.PaymentStatusCompleted {
|
|
err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update verification status: %w", err)
|
|
}
|
|
|
|
// Credit user's wallet
|
|
err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID, transfer.Amount)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
|
|
}
|
|
}
|
|
|
|
return &domain.ChapaVerificationResponse{
|
|
Status: string(verification.Status),
|
|
Amount: float64(verification.Amount),
|
|
Currency: verification.Currency,
|
|
}, 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 err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
|
|
return fmt.Errorf("failed to update payment status: %w", err)
|
|
}
|
|
|
|
// If payment is completed, credit user's wallet
|
|
if transfer.Status == string(domain.PaymentStatusCompleted) {
|
|
if err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID, payment.Amount); 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 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 wallet
|
|
if payment.Status == string(domain.PaymentStatusFailed) {
|
|
if err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID, transfer.Amount); err != nil {
|
|
return fmt.Errorf("failed to credit user wallet: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|