veza/veza-backend-api/internal/services/hyperswitch/client.go
2026-03-05 23:03:43 +01:00

189 lines
5.8 KiB
Go

package hyperswitch
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// Client is the Hyperswitch API client for payment operations.
type Client struct {
baseURL string
apiKey string
httpClient *http.Client
}
// NewClient creates a new Hyperswitch client.
func NewClient(baseURL, apiKey string) *Client {
return &Client{
baseURL: baseURL,
apiKey: apiKey,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// CreatePaymentRequest is the request body for POST /payments.
type CreatePaymentRequest struct {
Amount int64 `json:"amount"` // Amount in minor units (e.g. centimes for EUR)
Currency string `json:"currency"` // e.g. "EUR"
ReturnURL string `json:"return_url,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// PaymentResponse is the response from POST /payments.
type PaymentResponse struct {
PaymentID string `json:"payment_id"`
ClientSecret string `json:"client_secret"`
Status string `json:"status"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
}
// PaymentStatus is the response from GET /payments/{payment_id}.
type PaymentStatus struct {
PaymentID string `json:"payment_id"`
Status string `json:"status"`
}
// CreatePayment creates a payment in Hyperswitch and returns client_secret for frontend.
func (c *Client) CreatePayment(ctx context.Context, amount int64, currency, orderID, returnURL string, metadata map[string]string) (*PaymentResponse, error) {
if metadata == nil {
metadata = make(map[string]string)
}
if orderID != "" {
metadata["order_id"] = orderID
}
reqBody := CreatePaymentRequest{
Amount: amount,
Currency: currency,
ReturnURL: returnURL,
Metadata: metadata,
}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshal create payment request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/payments", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("api-key", c.apiKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("hyperswitch create payment failed: status %d", resp.StatusCode)
}
var out PaymentResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &out, nil
}
// CreatePaymentSimple creates a payment and returns paymentID and clientSecret.
// Convenience wrapper for PaymentProvider interface.
func (c *Client) CreatePaymentSimple(ctx context.Context, amount int64, currency, orderID, returnURL string, metadata map[string]string) (paymentID, clientSecret string, err error) {
resp, err := c.CreatePayment(ctx, amount, currency, orderID, returnURL, metadata)
if err != nil {
return "", "", err
}
return resp.PaymentID, resp.ClientSecret, nil
}
// GetPaymentStatus retrieves payment status string from Hyperswitch.
func (c *Client) GetPaymentStatus(ctx context.Context, paymentID string) (string, error) {
status, err := c.GetPayment(ctx, paymentID)
if err != nil {
return "", err
}
return status.Status, nil
}
// GetPayment retrieves payment status from Hyperswitch.
func (c *Client) GetPayment(ctx context.Context, paymentID string) (*PaymentStatus, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/payments/"+paymentID, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("api-key", c.apiKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("hyperswitch get payment failed: status %d", resp.StatusCode)
}
var out PaymentStatus
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &out, nil
}
// CreateRefundRequest is the request body for POST /refunds (v0.403 R2)
type CreateRefundRequest struct {
PaymentID string `json:"payment_id"`
Amount *int64 `json:"amount,omitempty"` // nil = full refund
Reason string `json:"reason,omitempty"`
RefundType string `json:"refund_type,omitempty"` // "instant" or "scheduled"
}
// RefundResponse is the response from POST /refunds
type RefundResponse struct {
RefundID string `json:"refund_id"`
PaymentID string `json:"payment_id"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
Status string `json:"status"`
}
// CreateRefund creates a refund against a payment (v0.403 R2)
func (c *Client) CreateRefund(ctx context.Context, paymentID string, amount *int64, reason string) (*RefundResponse, error) {
reqBody := CreateRefundRequest{
PaymentID: paymentID,
Amount: amount,
Reason: reason,
RefundType: "instant",
}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshal refund request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/refunds", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("api-key", c.apiKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("hyperswitch create refund failed: status %d", resp.StatusCode)
}
var out RefundResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &out, nil
}