Files
telecom-platform/apps/api-server/internal/payment/gateway_stripe.go
T
nutcas3 cb1d7171e0 feat: Add PaymentController with modular handlers for payment operations, refunds, and webhook processing
- Add PaymentController struct with paymentService/db dependencies and NewPaymentController constructor
- Add SetupRoutes with /payments endpoints (POST /top-up, POST/DELETE/GET /payment-methods, GET /transactions/:id, POST /transactions/:id/refund) and /webhooks/stripe endpoint
- Add ProcessTopUp handler with amount/paymentMethodId validation, subscriberID authentication, and PaymentService.
2026-04-24 08:53:34 +03:00

176 lines
5.1 KiB
Go

package payment
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/config"
)
// StripeGateway implements PaymentGateway for Stripe.
type StripeGateway struct {
apiKey string
webhookSecret string
client *http.Client
config *config.PaymentConfig
}
// NewStripeGateway creates a new Stripe payment gateway.
func NewStripeGateway(cfg *config.PaymentConfig) *StripeGateway {
return &StripeGateway{
apiKey: cfg.StripeAPIKey,
webhookSecret: cfg.StripeWebhookSecret,
client: &http.Client{Timeout: 30 * time.Second},
config: cfg,
}
}
// ProcessPayment processes a payment via Stripe.
func (s *StripeGateway) ProcessPayment(ctx context.Context, req *PaymentRequest) (*PaymentResponse, error) {
intentData := map[string]any{
"amount": int64(req.Amount * 100),
"currency": strings.ToLower(req.Currency),
"description": req.Description,
"customer": req.CustomerID,
"payment_method": req.PaymentMethodID,
"confirmation_method": "manual",
"confirm": true,
"metadata": req.Metadata,
}
if req.WebhookURL != "" {
intentData["transfer_data"] = map[string]string{"destination": req.WebhookURL}
}
intentResp, err := s.makeStripeRequest(ctx, "POST", "/v1/payment_intents", intentData)
if err != nil {
return nil, fmt.Errorf("failed to create payment intent: %w", err)
}
intent, ok := intentResp.(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid payment intent response")
}
status := s.mapStripeStatus(intent["status"].(string))
var failureReason *string
if status == PaymentStatusFailed {
if lastErr, ok := intent["last_payment_error"].(map[string]any); ok {
if message, ok := lastErr["message"].(string); ok {
failureReason = &message
}
}
}
var redirectURL *string
if status == PaymentStatusPending {
if nextAction, ok := intent["next_action"].(map[string]any); ok {
if url, ok := nextAction["redirect_to_url"].(map[string]any); ok {
if redirect, ok := url["url"].(string); ok {
redirectURL = &redirect
}
}
}
}
return &PaymentResponse{
TransactionID: intent["id"].(string),
Status: status,
Amount: req.Amount,
Currency: req.Currency,
ProcessedAt: time.Now(),
FailureReason: failureReason,
Metadata: req.Metadata,
RedirectURL: redirectURL,
}, nil
}
// RefundPayment refunds a payment via Stripe.
func (s *StripeGateway) RefundPayment(ctx context.Context, transactionID string, amount float64) (*RefundResponse, error) {
refundData := map[string]any{
"payment_intent": transactionID,
"amount": int64(amount * 100),
}
refundResp, err := s.makeStripeRequest(ctx, "POST", "/v1/refunds", refundData)
if err != nil {
return nil, fmt.Errorf("failed to create refund: %w", err)
}
refund, ok := refundResp.(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid refund response")
}
status := s.mapStripeStatus(refund["status"].(string))
var failureReason *string
if status == PaymentStatusFailed {
if failureMsg, ok := refund["failure_reason"].(string); ok && failureMsg != "" {
failureReason = &failureMsg
}
}
return &RefundResponse{
RefundID: refund["id"].(string),
TransactionID: transactionID,
Amount: amount,
Currency: "USD",
Status: status,
ProcessedAt: time.Now(),
FailureReason: failureReason,
}, nil
}
// GetPaymentStatus gets payment status from Stripe.
func (s *StripeGateway) GetPaymentStatus(ctx context.Context, transactionID string) (*PaymentStatus, error) {
intentResp, err := s.makeStripeRequest(ctx, "GET", "/v1/payment_intents/"+transactionID, nil)
if err != nil {
return nil, fmt.Errorf("failed to get payment intent: %w", err)
}
intent, ok := intentResp.(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid payment intent response")
}
refundsResp, err := s.makeStripeRequest(ctx, "GET", "/v1/refunds?payment_intent="+transactionID, nil)
if err != nil {
return nil, fmt.Errorf("failed to get refunds: %w", err)
}
refundsData, _ := refundsResp.(map[string]any)
refundsList, _ := refundsData["data"].([]any)
refunds := make([]RefundInfo, len(refundsList))
for i, refundInterface := range refundsList {
refund, _ := refundInterface.(map[string]any)
createdAt, _ := time.Parse(time.RFC3339, refund["created"].(string))
refunds[i] = RefundInfo{
RefundID: refund["id"].(string),
Amount: float64(refund["amount"].(int64)) / 100,
Status: refund["status"].(string),
CreatedAt: createdAt,
}
}
createdAt, _ := time.Parse(time.RFC3339, intent["created"].(string))
var failureReason *string
if lastErr, ok := intent["last_payment_error"].(map[string]any); ok {
if message, ok := lastErr["message"].(string); ok {
failureReason = &message
}
}
return &PaymentStatus{
TransactionID: transactionID,
Status: s.mapStripeStatus(intent["status"].(string)),
Amount: float64(intent["amount"].(int64)) / 100,
Currency: strings.ToUpper(intent["currency"].(string)),
CreatedAt: createdAt,
UpdatedAt: time.Now(),
FailureReason: failureReason,
Refunds: refunds,
}, nil
}