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 "" }