mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
283 lines
8.6 KiB
Go
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 ""
|
|
}
|