Files
MyClub/internal/services/eshop/revolut_service.go
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

283 lines
8.6 KiB
Go

package eshop
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"fotbal-club/internal/config"
"fotbal-club/internal/models"
"fotbal-club/pkg/httpclient"
)
// RevolutService handles Revolut payment integration
type RevolutService struct {
cfg *config.Config
client *http.Client
apiBase string
}
// Revolut API structures
type RevolutOrderRequest struct {
Amount int64 `json:"amount"` // Amount in minor currency units (cents)
Currency string `json:"currency"` // 3-letter currency code (EUR, CZK, etc.)
Description string `json:"description"` // Order description
MerchantOrderID string `json:"merchant_order_id,omitempty"` // Your internal order ID
Customer *RevolutCustomer `json:"customer,omitempty"`
}
type RevolutCustomer struct {
Email string `json:"email,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Phone string `json:"phone,omitempty"`
}
type RevolutOrderResponse struct {
ID string `json:"id"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
Status string `json:"status"` // PENDING, COMPLETED, FAILED
CheckoutURL string `json:"checkout_url,omitempty"`
MerchantOrderID string `json:"merchant_order_id,omitempty"`
CreatedAt string `json:"created_at"`
}
type RevolutWebhookPayload struct {
Type string `json:"type"` // ORDER_COMPLETED, ORDER_CANCELLED, etc.
OrderID string `json:"order_id"`
Order RevolutOrderResponse `json:"order"`
Timestamp string `json:"timestamp"`
}
func NewRevolutService(cfg *config.Config) *RevolutService {
base := "https://sandbox-merchant.revolut.com/api"
if strings.ToLower(strings.TrimSpace(cfg.RevolutEnvironment)) == "production" {
base = "https://merchant.revolut.com/api"
}
return &RevolutService{
cfg: cfg,
client: httpclient.DefaultClient(),
apiBase: base,
}
}
// CreatePayment initializes a Revolut payment for the given order
func (s *RevolutService) CreatePayment(order *models.EshopOrder) (*PaymentResult, error) {
if !s.cfg.RevolutEnabled {
return nil, fmt.Errorf("Revolut payment provider is disabled")
}
apiKey := strings.TrimSpace(s.cfg.RevolutAPIKey)
if apiKey == "" {
return nil, fmt.Errorf("Revolut API key not configured")
}
// Parse billing address to get customer information
var billingAddress map[string]interface{}
if order.BillingAddressJSON != "" {
if err := json.Unmarshal([]byte(order.BillingAddressJSON), &billingAddress); err != nil {
return nil, fmt.Errorf("failed to parse billing address: %w", err)
}
}
// Create order request
orderReq := RevolutOrderRequest{
Amount: order.TotalAmountCents,
Currency: order.Currency,
Description: fmt.Sprintf("Objednávka %s", order.OrderNumber),
MerchantOrderID: order.OrderNumber,
Customer: &RevolutCustomer{
Email: order.Email,
FirstName: getStringFromMap(billingAddress, "first_name"),
LastName: getStringFromMap(billingAddress, "last_name"),
Phone: getStringFromMap(billingAddress, "phone"),
},
}
// Convert to JSON
jsonData, err := json.Marshal(orderReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal Revolut order request: %w", err)
}
// Create HTTP request
url := fmt.Sprintf("%s/1.0/orders", s.apiBase)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create Revolut order request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
// Execute request
resp, err := s.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute Revolut order request: %w", err)
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read Revolut order response: %w", err)
}
// Check status code
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("Revolut API error: status %d, response: %s", resp.StatusCode, string(body))
}
// Parse response
var orderResp RevolutOrderResponse
if err := json.Unmarshal(body, &orderResp); err != nil {
return nil, fmt.Errorf("failed to parse Revolut order response: %w", err)
}
// Store raw payload for debugging
rawPayload := string(body)
// Return payment result
return &PaymentResult{
RedirectURL: orderResp.CheckoutURL,
ProviderPaymentID: orderResp.ID,
RawPayloadJSON: rawPayload,
}, nil
}
// VerifyWebhook verifies and parses incoming Revolut webhooks
func (s *RevolutService) VerifyWebhook(payload []byte, signature string) (bool, error) {
webhookSecret := strings.TrimSpace(s.cfg.RevolutWebhookSecret)
if webhookSecret == "" {
return false, fmt.Errorf("Revolut webhook secret not configured")
}
// TODO: Implement webhook signature verification
// Revolut uses HMAC-SHA256 for webhook signatures
// For now, we'll accept all webhooks in sandbox mode
if strings.ToLower(strings.TrimSpace(s.cfg.RevolutEnvironment)) == "sandbox" {
return true, nil
}
// In production, verify the signature here
// This would involve computing HMAC-SHA256 of the payload with the webhook secret
// and comparing it with the provided signature
return true, nil
}
// ParseWebhook parses a Revolut webhook payload
func (s *RevolutService) ParseWebhook(payload []byte) (*RevolutWebhookPayload, error) {
var webhook RevolutWebhookPayload
if err := json.Unmarshal(payload, &webhook); err != nil {
return nil, fmt.Errorf("failed to parse Revolut webhook: %w", err)
}
return &webhook, nil
}
// GetOrderStatus retrieves the current status of a Revolut order
func (s *RevolutService) GetOrderStatus(revolutOrderID string) (*RevolutOrderResponse, error) {
if !s.cfg.RevolutEnabled {
return nil, fmt.Errorf("Revolut payment provider is disabled")
}
apiKey := strings.TrimSpace(s.cfg.RevolutAPIKey)
if apiKey == "" {
return nil, fmt.Errorf("Revolut API key not configured")
}
url := fmt.Sprintf("%s/1.0/orders/%s", s.apiBase, revolutOrderID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create Revolut status request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := s.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute Revolut status request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read Revolut status response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Revolut API error: status %d, response: %s", resp.StatusCode, string(body))
}
var orderResp RevolutOrderResponse
if err := json.Unmarshal(body, &orderResp); err != nil {
return nil, fmt.Errorf("failed to parse Revolut status response: %w", err)
}
return &orderResp, nil
}
// RefundOrder processes a refund for a Revolut order
func (s *RevolutService) RefundOrder(revolutOrderID string, amount int64, reason string) error {
if !s.cfg.RevolutEnabled {
return fmt.Errorf("Revolut payment provider is disabled")
}
apiKey := strings.TrimSpace(s.cfg.RevolutAPIKey)
if apiKey == "" {
return fmt.Errorf("Revolut API key not configured")
}
// Refund request structure
refundReq := map[string]interface{}{
"amount": amount,
"reason": reason,
}
jsonData, err := json.Marshal(refundReq)
if err != nil {
return fmt.Errorf("failed to marshal Revolut refund request: %w", err)
}
url := fmt.Sprintf("%s/1.0/orders/%s/refund", s.apiBase, revolutOrderID)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create Revolut refund request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := s.client.Do(req)
if err != nil {
return fmt.Errorf("failed to execute Revolut refund request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("Revolut refund API error: status %d, response: %s", resp.StatusCode, string(body))
}
return nil
}
// getStringFromMap safely extracts a string value from a map, returning empty string if not found or not a string
func getStringFromMap(m map[string]interface{}, key string) string {
if m == nil {
return ""
}
if val, ok := m[key]; ok {
if str, ok := val.(string); ok {
return str
}
}
return ""
}