Yimaru-BackEnd/internal/services/chapa/client.go
2025-11-03 17:20:35 +03:00

457 lines
14 KiB
Go

package chapa
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Client struct {
baseURL string
secretKey string
httpClient *http.Client
}
func NewClient(baseURL, secretKey string) *Client {
return &Client{
baseURL: baseURL,
secretKey: secretKey,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error) {
payload := map[string]interface{}{
"amount": fmt.Sprintf("%.2f", float64(req.Amount)),
"currency": req.Currency,
"email": req.Email,
"first_name": req.FirstName,
"last_name": req.LastName,
"tx_ref": req.TxRef,
"callback_url": req.CallbackURL,
"return_url": req.ReturnURL,
"phone_number": req.PhoneNumber,
}
fmt.Printf("\n\nChapa Payload: %+v\n\n", payload)
payloadBytes, err := json.Marshal(payload)
if err != nil {
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to marshal payload: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes))
if err != nil {
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to create request: %w", err)
}
fmt.Printf("\n\nBase URL is: %+v\n\n", c.baseURL)
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return domain.ChapaDepositResponse{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body) // <-- Add this
return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) // <-- Log it
}
// if resp.StatusCode != http.StatusOK {
// return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
// }
var response struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
CheckoutURL string `json:"checkout_url"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to decode response: %w", err)
}
return domain.ChapaDepositResponse{
CheckoutURL: response.Data.CheckoutURL,
Reference: req.TxRef,
}, nil
}
func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.ChapaDepositVerification, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/transaction/verify/"+reference, nil)
if err != nil {
return domain.ChapaDepositVerification{}, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return domain.ChapaDepositVerification{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return domain.ChapaDepositVerification{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var verification domain.ChapaDepositVerification
if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
return domain.ChapaDepositVerification{}, fmt.Errorf("failed to decode response: %w", err)
}
var status domain.PaymentStatus
switch verification.Status {
case "success":
status = domain.PaymentStatusCompleted
default:
status = domain.PaymentStatusFailed
}
return domain.ChapaDepositVerification{
Status: status,
Amount: verification.Amount,
Currency: verification.Currency,
}, nil
}
func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
url := fmt.Sprintf("%s/transaction/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
var verification domain.ChapaVerificationResponse
if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
// Normalize payment status for internal use
// switch strings.ToLower(verification.Data.Status) {
// case "success":
// verification.Status = string(domain.PaymentStatusCompleted)
// default:
// verification.Status = string(domain.PaymentStatusFailed)
// }
return &verification, nil
}
func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var response struct {
Status string `json:"status"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
var status domain.PaymentStatus
switch response.Status {
case "success":
status = domain.PaymentStatusCompleted
default:
status = domain.PaymentStatusFailed
}
return &domain.ChapaVerificationResponse{
Status: string(status),
// Amount: response.Amount,
// Currency: response.Currency,
}, nil
}
func (c *Client) GetAllTransactions(ctx context.Context) (domain.ChapaAllTransactionsResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/transactions", nil)
if err != nil {
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
var response domain.ChapaAllTransactionsResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("failed to decode response: %w", err)
}
return response, nil
}
func (c *Client) GetTransactionEvents(ctx context.Context, refId string) ([]domain.ChapaTransactionEvent, error) {
url := fmt.Sprintf("%s/transaction/events/%s", c.baseURL, refId)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
var response struct {
Message string `json:"message"`
Status string `json:"status"`
Data []domain.ChapaTransactionEvent `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return response.Data, nil
}
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var bankResponse domain.BankResponse
if err := json.NewDecoder(resp.Body).Decode(&bankResponse); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
var banks []domain.Bank
for _, bankData := range bankResponse.Data {
bank := domain.Bank{
ID: bankData.ID,
Slug: bankData.Slug,
Swift: bankData.Swift,
Name: bankData.Name,
AcctLength: bankData.AcctLength,
CountryID: bankData.CountryID,
IsMobileMoney: bankData.IsMobileMoney,
IsActive: bankData.IsActive,
IsRTGS: bankData.IsRTGS,
Active: bankData.Active,
Is24Hrs: bankData.Is24Hrs,
CreatedAt: bankData.CreatedAt,
UpdatedAt: bankData.UpdatedAt,
Currency: bankData.Currency,
}
banks = append(banks, bank)
}
return banks, nil
}
func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) {
endpoint := c.baseURL + "/transfers"
fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint)
reqBody, err := json.Marshal(req)
if err != nil {
return false, fmt.Errorf("failed to marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(reqBody))
if err != nil {
return false, fmt.Errorf("failed to create request: %w", err)
}
// Set headers here
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return false, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("chapa api returned status: %d - %s", resp.StatusCode, string(body))
}
var response domain.ChapaWithdrawalResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return false, fmt.Errorf("failed to decode response: %w", err)
}
return response.Status == string(domain.WithdrawalStatusSuccessful), nil
}
func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) {
base, err := url.Parse(c.baseURL)
if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err)
}
endpoint := base.ResolveReference(&url.URL{Path: fmt.Sprintf("/v1/transfers/%s/verify", reference)})
httpReq, err := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
c.setHeaders(httpReq)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("chapa api returned status: %d", resp.StatusCode)
}
var verification domain.ChapaVerificationResponse
if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &verification, nil
}
func (c *Client) CancelTransaction(ctx context.Context, txRef string) (domain.ChapaCancelResponse, error) {
// Construct URL for the cancel transaction endpoint
url := fmt.Sprintf("%s/transaction/cancel/%s", c.baseURL, txRef)
// Create HTTP request with context
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, nil)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to create request: %w", err)
}
// Set authorization header
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
// Execute the HTTP request
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// Handle non-OK responses
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return domain.ChapaCancelResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
// Decode successful response
var response struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
TxRef string `json:"tx_ref"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to decode response: %w", err)
}
// Return mapped domain response
return domain.ChapaCancelResponse{
Message: response.Message,
Status: response.Status,
TxRef: response.Data.TxRef,
Amount: response.Data.Amount,
Currency: response.Data.Currency,
CreatedAt: response.Data.CreatedAt,
UpdatedAt: response.Data.UpdatedAt,
}, nil
}
func (c *Client) setHeaders(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
}