package arifpay import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "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) (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), } 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) (*domain.CancelCheckoutSessionResponse, 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, nil } func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRequest, userId int64, isDepost bool) error { // 1. Get transfer by SessionID transfer, err := s.transferStore.GetTransferByReference(ctx, req.Transaction.TransactionID) if err != nil { return err } wallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) if err != nil { return err } if transfer.Verified { return errors.New("transfer already verified") } // 2. Update transfer status newStatus := 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" && isDepost) || (newStatus == "FAILED" && !isDepost) { _, err = s.walletSvc.AddToWallet(ctx, wallets[0].ID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ Value: req.Transaction.TransactionID, Valid: true, }, BankNumber: domain.ValidString{ Value: "", Valid: false, }, }, "") if err != nil { return err } } return nil } func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { // Step 1: Create Session referenceNum := uuid.NewString() sessionReq := domain.CheckoutSessionClientRequest{ Amount: req.Amount, CustomerEmail: req.CustomerEmail, CustomerPhone: req.CustomerPhone, } sessionResp, err := s.CreateCheckoutSession(sessionReq, false) if err != nil { return fmt.Errorf("failed to create session: %w", err) } // Step 2: Execute Transfer transferURL := fmt.Sprintf("%s/api/Telebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL) reqBody := map[string]any{ "Sessionid": sessionResp["sessionId"], "Phonenumber": req.PhoneNumber, } payload, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("failed to marshal transfer request: %w", err) } transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) if err != nil { 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 { return fmt.Errorf("failed to execute transfer request: %w", err) } defer transferResp.Body.Close() if transferResp.StatusCode >= 300 { 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", sessionResp["sessionId"]), Status: string(domain.PaymentStatusPending), PaymentMethod: domain.TRANSFER_ARIFPAY, } if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { return fmt.Errorf("failed to store transfer: %w", err) } // Step 4: Deduct from wallet userWallets, err := s.walletSvc.GetWalletsByUser(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, userWallets[0].ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, "", ) if err != nil { return fmt.Errorf("failed to deduct from wallet: %w", err) } return nil } func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { // Step 1: Create Session referenceNum := uuid.NewString() sessionReq := domain.CheckoutSessionClientRequest{ Amount: req.Amount, CustomerEmail: req.CustomerEmail, CustomerPhone: req.CustomerPhone, } sessionResp, err := s.CreateCheckoutSession(sessionReq, false) if err != nil { return fmt.Errorf("cbebirr: failed to create session: %w", err) } // Step 2: Execute Transfer transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL) reqBody := map[string]any{ "Sessionid": sessionResp["sessionId"], "Phonenumber": req.PhoneNumber, } payload, err := json.Marshal(reqBody) if err != nil { 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 { 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 { return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err) } defer transferResp.Body.Close() if transferResp.StatusCode >= 300 { body, _ := io.ReadAll(transferResp.Body) return fmt.Errorf("cbebirr: 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", sessionResp["sessionId"]), Status: string(domain.PaymentStatusPending), PaymentMethod: domain.TRANSFER_ARIFPAY, } if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { return fmt.Errorf("cbebirr: failed to store transfer: %w", err) } // Step 4: Deduct from user wallet userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) if err != nil { return fmt.Errorf("cbebirr: failed to get user wallets: %w", err) } if len(userWallets) == 0 { return fmt.Errorf("cbebirr: no wallet found for user %d", userId) } _, err = s.walletSvc.DeductFromWallet( ctx, userWallets[0].ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, "", ) if err != nil { return fmt.Errorf("cbebirr: failed to deduct from wallet: %w", err) } return nil } func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { // Step 1: Create Session referenceNum := uuid.NewString() sessionReq := domain.CheckoutSessionClientRequest{ Amount: req.Amount, CustomerEmail: req.CustomerEmail, CustomerPhone: req.CustomerPhone, } sessionResp, err := s.CreateCheckoutSession(sessionReq, false) if err != nil { return fmt.Errorf("Mpesa: failed to create session: %w", err) } // Step 2: Execute Transfer transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL) reqBody := map[string]any{ "Sessionid": sessionResp["sessionId"], "Phonenumber": req.PhoneNumber, } payload, err := json.Marshal(reqBody) if err != nil { 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 { 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 { return fmt.Errorf("Mpesa: failed to execute transfer request: %w", err) } defer transferResp.Body.Close() if transferResp.StatusCode >= 300 { body, _ := io.ReadAll(transferResp.Body) return fmt.Errorf("Mpesa: 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", sessionResp["sessionId"]), Status: string(domain.PaymentStatusPending), PaymentMethod: domain.TRANSFER_ARIFPAY, } if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { return fmt.Errorf("Mpesa: failed to store transfer: %w", err) } // Step 4: Deduct from user wallet userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) if err != nil { return fmt.Errorf("Mpesa: failed to get user wallets: %w", err) } if len(userWallets) == 0 { return fmt.Errorf("Mpesa: no wallet found for user %d", userId) } _, err = s.walletSvc.DeductFromWallet( ctx, userWallets[0].ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, "", ) if err != nil { return fmt.Errorf("Mpesa: failed to deduct from wallet: %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"}, } }