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.ChapaPaymentVerificationResponse, 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.ChapaPaymentVerificationResponse 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.ChapaTransferVerificationResponse, 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.ChapaTransferVerificationResponse{ 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.BankData, 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.BankData for _, bankData := range bankResponse.Data { bank := domain.BankData{ 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) InitializeTransfer(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.ChapaTransferVerificationResponse, 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.ChapaTransferVerificationResponse 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") }