698 lines
21 KiB
Go
698 lines
21 KiB
Go
package arifpay
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type ArifpayService struct {
|
|
cfg *config.Config
|
|
transferStore wallet.TransferStore
|
|
walletSvc *wallet.Service
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewArifpayService(cfg *config.Config, transferStore wallet.TransferStore, walletSvc *wallet.Service, httpClient *http.Client) *ArifpayService {
|
|
return &ArifpayService{
|
|
cfg: cfg,
|
|
transferStore: transferStore,
|
|
walletSvc: walletSvc,
|
|
httpClient: httpClient,
|
|
}
|
|
}
|
|
|
|
func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool, userId int64) (map[string]any, error) {
|
|
// Generate unique nonce
|
|
nonce := uuid.NewString()
|
|
|
|
var NotifyURL string
|
|
|
|
if isDeposit {
|
|
NotifyURL = s.cfg.ARIFPAY.C2BNotifyUrl
|
|
} else {
|
|
NotifyURL = s.cfg.ARIFPAY.B2CNotifyUrl
|
|
}
|
|
|
|
// Construct full checkout request
|
|
checkoutReq := domain.CheckoutSessionRequest{
|
|
CancelURL: s.cfg.ARIFPAY.CancelUrl,
|
|
Phone: req.CustomerPhone, // must be in format 2519...
|
|
Email: req.CustomerEmail,
|
|
Nonce: nonce,
|
|
SuccessURL: s.cfg.ARIFPAY.SuccessUrl,
|
|
ErrorURL: s.cfg.ARIFPAY.ErrorUrl,
|
|
NotifyURL: NotifyURL,
|
|
PaymentMethods: s.cfg.ARIFPAY.PaymentMethods,
|
|
ExpireDate: s.cfg.ARIFPAY.ExpireDate,
|
|
Items: []struct {
|
|
Name string `json:"name"`
|
|
Quantity int `json:"quantity"`
|
|
Price float64 `json:"price"`
|
|
Description string `json:"description"`
|
|
}{
|
|
{
|
|
Name: s.cfg.ARIFPAY.ItemName,
|
|
Quantity: s.cfg.ARIFPAY.Quantity,
|
|
Price: req.Amount,
|
|
Description: s.cfg.ARIFPAY.Description,
|
|
},
|
|
},
|
|
Beneficiaries: []struct {
|
|
AccountNumber string `json:"accountNumber"`
|
|
Bank string `json:"bank"`
|
|
Amount float64 `json:"amount"`
|
|
}{
|
|
{
|
|
AccountNumber: s.cfg.ARIFPAY.BeneficiaryAccountNumber,
|
|
Bank: s.cfg.ARIFPAY.Bank,
|
|
Amount: req.Amount,
|
|
},
|
|
},
|
|
Lang: s.cfg.ARIFPAY.Lang,
|
|
}
|
|
|
|
// Marshal to JSON
|
|
payload, err := json.Marshal(checkoutReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal checkout request: %w", err)
|
|
}
|
|
|
|
// Send request to Arifpay API
|
|
url := fmt.Sprintf("%s/api/checkout/session", s.cfg.ARIFPAY.BaseURL)
|
|
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
httpReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
|
|
|
resp, err := s.httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read response
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("failed to create checkout session: %s", string(body))
|
|
}
|
|
|
|
// Optionally unmarshal response to struct
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return nil, fmt.Errorf("invalid response from Arifpay: %w", err)
|
|
}
|
|
|
|
data := result["data"].(map[string]interface{})
|
|
// paymentURL := data["paymentUrl"].(string)
|
|
|
|
// Store transfer in DB
|
|
transfer := domain.CreateTransfer{
|
|
Amount: domain.Currency(req.Amount),
|
|
Verified: false,
|
|
Type: domain.DEPOSIT,
|
|
ReferenceNumber: nonce,
|
|
SessionID: fmt.Sprintf("%v", data["sessionId"]),
|
|
Status: string(domain.PaymentStatusPending),
|
|
CashierID: domain.ValidInt64{
|
|
Value: userId,
|
|
Valid: true,
|
|
},
|
|
}
|
|
|
|
if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (any, error) {
|
|
// Build the cancel URL
|
|
url := fmt.Sprintf("%s/api/sandbox/checkout/session/%s", s.cfg.ARIFPAY.BaseURL, sessionID)
|
|
|
|
// Create the request
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Add headers
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
|
|
|
// Execute request
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute cancel request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read response body
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read cancel response: %w", err)
|
|
}
|
|
|
|
// Handle non-200 status codes
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("cancel request failed: status=%d, body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
// Decode into response struct
|
|
var cancelResp domain.CancelCheckoutSessionResponse
|
|
if err := json.Unmarshal(body, &cancelResp); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal cancel response: %w", err)
|
|
}
|
|
|
|
return cancelResp.Data, nil
|
|
}
|
|
|
|
func (s *ArifpayService) ProcessWebhook(ctx context.Context, req domain.WebhookRequest, isDeposit bool) error {
|
|
// 1. Get transfer by SessionID
|
|
transfer, err := s.transferStore.GetTransferByReference(ctx, req.Nonce)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userId := transfer.DepositorID.Value
|
|
|
|
wallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if transfer.Verified {
|
|
return errors.New("transfer already verified")
|
|
}
|
|
|
|
// 2. Update transfer status
|
|
newStatus := strings.ToLower(req.Transaction.TransactionStatus)
|
|
// if req.Transaction.TransactionStatus != "" {
|
|
// newStatus = req.Transaction.TransactionStatus
|
|
// }
|
|
|
|
err = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, newStatus)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 3. If SUCCESS -> update customer wallet balance
|
|
if (newStatus == "success" && isDeposit) || (newStatus == "failed" && !isDeposit) {
|
|
_, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{
|
|
ReferenceNumber: domain.ValidString{
|
|
Value: req.Nonce,
|
|
Valid: true,
|
|
},
|
|
BankNumber: domain.ValidString{
|
|
Value: "",
|
|
Valid: false,
|
|
},
|
|
}, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error {
|
|
// Step 1: Create Session
|
|
|
|
userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user wallets: %w", err)
|
|
}
|
|
// if len(userWallets) == 0 {
|
|
// return fmt.Errorf("no wallet found for user %d", userId)
|
|
// }
|
|
|
|
_, err = s.walletSvc.DeductFromWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deduct from wallet: %w", err)
|
|
}
|
|
|
|
referenceNum := uuid.NewString()
|
|
|
|
sessionReq := domain.CheckoutSessionClientRequest{
|
|
Amount: req.Amount,
|
|
CustomerEmail: req.CustomerEmail,
|
|
CustomerPhone: "251" + req.CustomerPhone[:9],
|
|
}
|
|
|
|
sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId)
|
|
if err != nil {
|
|
_, err = s.walletSvc.AddToWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
domain.PaymentDetails{},
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deduct from wallet: %w", err)
|
|
}
|
|
return fmt.Errorf("failed to create session: %w", err)
|
|
}
|
|
|
|
sessionRespData := sessionResp["data"].(map[string]any)
|
|
|
|
// Step 2: Execute Transfer
|
|
transferURL := fmt.Sprintf("%s/api/Telebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
|
|
reqBody := map[string]any{
|
|
"Sessionid": sessionRespData["sessionId"],
|
|
"Phonenumber": "251" + req.CustomerPhone[:9],
|
|
}
|
|
|
|
payload, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
_, err = s.walletSvc.AddToWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
domain.PaymentDetails{},
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deduct from wallet: %w", err)
|
|
}
|
|
return fmt.Errorf("failed to marshal transfer request: %w", err)
|
|
}
|
|
|
|
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
|
|
if err != nil {
|
|
_, err = s.walletSvc.AddToWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
domain.PaymentDetails{},
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deduct from wallet: %w", err)
|
|
}
|
|
return fmt.Errorf("failed to build transfer request: %w", err)
|
|
}
|
|
transferReq.Header.Set("Content-Type", "application/json")
|
|
transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
|
|
|
transferResp, err := s.httpClient.Do(transferReq)
|
|
if err != nil {
|
|
_, err = s.walletSvc.AddToWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
domain.PaymentDetails{},
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deduct from wallet: %w", err)
|
|
}
|
|
return fmt.Errorf("failed to execute transfer request: %w", err)
|
|
}
|
|
defer transferResp.Body.Close()
|
|
|
|
if transferResp.StatusCode != http.StatusOK {
|
|
_, err = s.walletSvc.AddToWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
domain.PaymentDetails{},
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deduct from wallet: %w", err)
|
|
}
|
|
body, _ := io.ReadAll(transferResp.Body)
|
|
return fmt.Errorf("transfer failed with status %d: %s", transferResp.StatusCode, string(body))
|
|
}
|
|
|
|
// Step 3: Store transfer in DB
|
|
transfer := domain.CreateTransfer{
|
|
Amount: domain.Currency(req.Amount),
|
|
Verified: false,
|
|
Type: domain.WITHDRAW, // B2C = payout
|
|
ReferenceNumber: referenceNum,
|
|
SessionID: fmt.Sprintf("%v", sessionRespData["sessionId"]),
|
|
Status: string(domain.PaymentStatusPending),
|
|
PaymentMethod: domain.TRANSFER_ARIFPAY,
|
|
CashierID: domain.ValidInt64{
|
|
Value: userId,
|
|
Valid: true,
|
|
},
|
|
}
|
|
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
|
|
return fmt.Errorf("failed to store transfer: %w", err)
|
|
}
|
|
|
|
// Step 4: Deduct from wallet
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error {
|
|
// Step 1: Deduct from user wallet first
|
|
userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
|
|
if err != nil {
|
|
return fmt.Errorf("cbebirr: failed to get user wallet: %w", err)
|
|
}
|
|
|
|
_, err = s.walletSvc.DeductFromWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("cbebirr: failed to deduct from wallet: %w", err)
|
|
}
|
|
|
|
referenceNum := uuid.NewString()
|
|
|
|
// Step 2: Create Session
|
|
sessionReq := domain.CheckoutSessionClientRequest{
|
|
Amount: req.Amount,
|
|
CustomerEmail: req.CustomerEmail,
|
|
CustomerPhone: "251" + req.CustomerPhone[:9],
|
|
}
|
|
|
|
sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId)
|
|
if err != nil {
|
|
// refund wallet if session creation fails
|
|
_, refundErr := s.walletSvc.AddToWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
domain.PaymentDetails{},
|
|
"",
|
|
)
|
|
if refundErr != nil {
|
|
return fmt.Errorf("cbebirr: refund failed after session creation error: %v", refundErr)
|
|
}
|
|
return fmt.Errorf("cbebirr: failed to create session: %w", err)
|
|
}
|
|
|
|
// Step 3: Execute Transfer
|
|
transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
|
|
reqBody := map[string]any{
|
|
"Sessionid": sessionResp["sessionId"],
|
|
"Phonenumber": "251" + req.CustomerPhone[:9],
|
|
}
|
|
|
|
payload, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
|
|
return fmt.Errorf("cbebirr: failed to marshal transfer request: %w", err)
|
|
}
|
|
|
|
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
|
|
if err != nil {
|
|
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
|
|
return fmt.Errorf("cbebirr: failed to build transfer request: %w", err)
|
|
}
|
|
transferReq.Header.Set("Content-Type", "application/json")
|
|
transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
|
|
|
transferResp, err := s.httpClient.Do(transferReq)
|
|
if err != nil {
|
|
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
|
|
return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err)
|
|
}
|
|
defer transferResp.Body.Close()
|
|
|
|
if transferResp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(transferResp.Body)
|
|
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
|
|
return fmt.Errorf("cbebirr: transfer failed with status %d: %s", transferResp.StatusCode, string(body))
|
|
}
|
|
|
|
// Step 4: Store transfer in DB
|
|
transfer := domain.CreateTransfer{
|
|
Amount: domain.Currency(req.Amount),
|
|
Verified: false,
|
|
Type: domain.WITHDRAW, // B2C = payout
|
|
ReferenceNumber: referenceNum,
|
|
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]),
|
|
Status: string(domain.PaymentStatusPending),
|
|
PaymentMethod: domain.TRANSFER_ARIFPAY,
|
|
CashierID: domain.ValidInt64{
|
|
Value: userId,
|
|
Valid: true,
|
|
},
|
|
}
|
|
|
|
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
|
|
return fmt.Errorf("cbebirr: failed to store transfer: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error {
|
|
// Step 1: Deduct from user wallet first
|
|
userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
|
|
if err != nil {
|
|
return fmt.Errorf("mpesa: failed to get user wallet: %w", err)
|
|
}
|
|
|
|
_, err = s.walletSvc.DeductFromWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("mpesa: failed to deduct from wallet: %w", err)
|
|
}
|
|
|
|
referenceNum := uuid.NewString()
|
|
|
|
// Step 2: Create Session
|
|
sessionReq := domain.CheckoutSessionClientRequest{
|
|
Amount: req.Amount,
|
|
CustomerEmail: req.CustomerEmail,
|
|
CustomerPhone: "251" + req.CustomerPhone[:9],
|
|
}
|
|
|
|
sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId)
|
|
if err != nil {
|
|
// Refund wallet if session creation fails
|
|
_, refundErr := s.walletSvc.AddToWallet(
|
|
ctx,
|
|
userWallet.RegularID,
|
|
domain.Currency(req.Amount),
|
|
domain.ValidInt64{},
|
|
domain.TRANSFER_ARIFPAY,
|
|
domain.PaymentDetails{},
|
|
"",
|
|
)
|
|
if refundErr != nil {
|
|
return fmt.Errorf("mpesa: refund failed after session creation error: %v", refundErr)
|
|
}
|
|
return fmt.Errorf("mpesa: failed to create session: %w", err)
|
|
}
|
|
|
|
// Step 3: Execute Transfer
|
|
transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
|
|
reqBody := map[string]any{
|
|
"Sessionid": sessionResp["sessionId"],
|
|
"Phonenumber": "251" + req.CustomerPhone[:9],
|
|
}
|
|
|
|
payload, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
|
|
return fmt.Errorf("mpesa: failed to marshal transfer request: %w", err)
|
|
}
|
|
|
|
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
|
|
if err != nil {
|
|
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
|
|
return fmt.Errorf("mpesa: failed to build transfer request: %w", err)
|
|
}
|
|
transferReq.Header.Set("Content-Type", "application/json")
|
|
transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
|
|
|
transferResp, err := s.httpClient.Do(transferReq)
|
|
if err != nil {
|
|
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
|
|
return fmt.Errorf("mpesa: failed to execute transfer request: %w", err)
|
|
}
|
|
defer transferResp.Body.Close()
|
|
|
|
if transferResp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(transferResp.Body)
|
|
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
|
|
return fmt.Errorf("mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body))
|
|
}
|
|
|
|
// Step 4: Store transfer in DB
|
|
transfer := domain.CreateTransfer{
|
|
Amount: domain.Currency(req.Amount),
|
|
Verified: false,
|
|
Type: domain.WITHDRAW, // B2C = payout
|
|
ReferenceNumber: referenceNum,
|
|
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]),
|
|
Status: string(domain.PaymentStatusPending),
|
|
PaymentMethod: domain.TRANSFER_ARIFPAY,
|
|
CashierID: domain.ValidInt64{
|
|
Value: userId,
|
|
Valid: true,
|
|
},
|
|
}
|
|
|
|
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
|
|
return fmt.Errorf("mpesa: failed to store transfer: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ArifpayService) VerifyTransactionByTransactionID(ctx context.Context, req domain.ArifpayVerifyByTransactionIDRequest) (*domain.WebhookRequest, error) {
|
|
endpoint := fmt.Sprintf("%s/api/checkout/getSessionByTransactionId", s.cfg.ARIFPAY.BaseURL)
|
|
|
|
// Marshal request payload
|
|
bodyBytes, err := json.Marshal(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
// Build HTTP request
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(bodyBytes))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
httpReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
|
|
|
// Execute request
|
|
resp, err := s.httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to call verify transaction API: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read response body
|
|
respBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
// Handle non-200 responses
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(respBytes))
|
|
}
|
|
|
|
// Decode into domain response
|
|
var result domain.WebhookRequest
|
|
if err := json.Unmarshal(respBytes, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *ArifpayService) VerifyTransactionBySessionID(ctx context.Context, sessionID string) (*domain.WebhookRequest, error) {
|
|
endpoint := fmt.Sprintf("%s/api/ms/transaction/status/%s", s.cfg.ARIFPAY.BaseURL, sessionID)
|
|
|
|
// Create HTTP GET request
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Set required headers
|
|
httpReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
|
|
|
// Execute request
|
|
resp, err := s.httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to call verify transaction API: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read response body
|
|
respBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
// Handle non-200 responses
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(respBytes))
|
|
}
|
|
|
|
// Decode into domain response
|
|
var result domain.WebhookRequest
|
|
if err := json.Unmarshal(respBytes, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *ArifpayService) GetPaymentMethodsMapping() []domain.ARIFPAYPaymentMethod {
|
|
return []domain.ARIFPAYPaymentMethod{
|
|
{ID: 1, Name: "ACCOUNT"},
|
|
{ID: 2, Name: "NONYMOUS_ACCOUNT"},
|
|
{ID: 3, Name: "ANONYMOUS_CARD"},
|
|
{ID: 4, Name: "TELEBIRR"},
|
|
{ID: 5, Name: "AWASH"},
|
|
{ID: 6, Name: "AWASH_WALLET"},
|
|
{ID: 7, Name: "PSS"},
|
|
{ID: 8, Name: "CBE"},
|
|
{ID: 9, Name: "AMOLE"},
|
|
{ID: 10, Name: "BOA"},
|
|
{ID: 11, Name: "KACHA"},
|
|
{ID: 12, Name: "ETHSWITCH"},
|
|
{ID: 13, Name: "TELEBIRR_USSD"},
|
|
{ID: 14, Name: "HELLOCASH"},
|
|
{ID: 15, Name: "MPESSA"},
|
|
}
|
|
}
|