From 3d0f0188fb7ae02536c9bd3bb7b6c2200fa1bccf Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Fri, 17 Apr 2026 15:50:03 +0300 Subject: [PATCH] feat: Add Stripe payment gateway with customer management, invoicing, and webhook validation - Add StripeGateway with secret key, webhook secret, and HTTP client with 30s timeout - Add ProcessPayment to create payment intents with amount conversion to cents and metadata - Add CreateCustomer to create Stripe customers with subscriber details and IMSI metadata - Add ProcessRefund to process full or partial refunds via payment intent ID - Add GetPaymentStatus to retrieve payment intent status and details --- .../internal/payment/gateways/stripe.go | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 apps/api-server/internal/payment/gateways/stripe.go diff --git a/apps/api-server/internal/payment/gateways/stripe.go b/apps/api-server/internal/payment/gateways/stripe.go new file mode 100644 index 0000000..c071116 --- /dev/null +++ b/apps/api-server/internal/payment/gateways/stripe.go @@ -0,0 +1,254 @@ +package gateways + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/stripe/stripe-go/v82" + "github.com/stripe/stripe-go/v82/customer" + "github.com/stripe/stripe-go/v82/invoice" + "github.com/stripe/stripe-go/v82/invoiceitem" + "github.com/stripe/stripe-go/v82/paymentintent" + "github.com/stripe/stripe-go/v82/refund" + "github.com/stripe/stripe-go/v82/webhook" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" +) + +// StripeGateway implements payment processing via Stripe +type StripeGateway struct { + secretKey string + webhookSecret string + client *http.Client +} + +// NewStripeGateway creates a new Stripe payment gateway +func NewStripeGateway(secretKey, webhookSecret string) *StripeGateway { + stripe.Key = secretKey + + return &StripeGateway{ + secretKey: secretKey, + webhookSecret: webhookSecret, + client: &http.Client{Timeout: 30 * time.Second}, + } +} + +// ProcessPayment processes a payment via Stripe +func (g *StripeGateway) ProcessPayment(ctx context.Context, req *PaymentRequest) (*PaymentResponse, error) { + // Create payment intent + params := &stripe.PaymentIntentParams{ + Amount: stripe.Int64(int64(req.Amount * 100)), // Convert to cents + Currency: stripe.String(string(req.Currency)), + Metadata: map[string]string{ + "subscriber_id": fmt.Sprintf("%d", req.SubscriberID), + "invoice_id": fmt.Sprintf("%d", req.InvoiceID), + }, + } + + if req.CustomerID != "" { + params.Customer = stripe.String(req.CustomerID) + } + + pi, err := paymentintent.New(params) + if err != nil { + return nil, fmt.Errorf("failed to create payment intent: %w", err) + } + + return &PaymentResponse{ + TransactionID: pi.ID, + Status: string(pi.Status), + ClientSecret: pi.ClientSecret, + Amount: req.Amount, + Currency: req.Currency, + CreatedAt: time.Unix(pi.Created, 0), + }, nil +} + +// CreateCustomer creates a Stripe customer +func (g *StripeGateway) CreateCustomer(ctx context.Context, subscriber *models.Subscriber) (*CustomerResponse, error) { + params := &stripe.CustomerParams{ + Email: stripe.String(subscriber.Email), + Name: stripe.String(fmt.Sprintf("%s %s", subscriber.FirstName, subscriber.LastName)), + Metadata: map[string]string{ + "subscriber_id": fmt.Sprintf("%d", subscriber.ID), + "imsi": string(subscriber.IMSI), + }, + } + + cust, err := customer.New(params) + if err != nil { + return nil, fmt.Errorf("failed to create customer: %w", err) + } + + return &CustomerResponse{ + CustomerID: cust.ID, + Email: cust.Email, + Name: cust.Name, + CreatedAt: time.Unix(cust.Created, 0), + }, nil +} + +// ProcessRefund processes a refund via Stripe +func (g *StripeGateway) ProcessRefund(ctx context.Context, transactionID string, amount float64) (*RefundResponse, error) { + params := &stripe.RefundParams{ + PaymentIntent: stripe.String(transactionID), + } + + if amount > 0 { + params.Amount = stripe.Int64(int64(amount * 100)) // Convert to cents + } + + refund, err := refund.New(params) + if err != nil { + return nil, fmt.Errorf("failed to process refund: %w", err) + } + + return &RefundResponse{ + RefundID: refund.ID, + Amount: float64(refund.Amount) / 100.0, + Status: string(refund.Status), + CreatedAt: time.Unix(refund.Created, 0), + }, nil +} + +// GetPaymentStatus retrieves payment status from Stripe +func (g *StripeGateway) GetPaymentStatus(ctx context.Context, transactionID string) (*PaymentStatusResponse, error) { + pi, err := paymentintent.Get(transactionID, nil) + if err != nil { + return nil, fmt.Errorf("failed to get payment intent: %w", err) + } + + return &PaymentStatusResponse{ + TransactionID: pi.ID, + Status: string(pi.Status), + Amount: float64(pi.Amount) / 100.0, + Currency: string(pi.Currency), + CreatedAt: time.Unix(pi.Created, 0), + }, nil +} + +// ValidateWebhook validates and processes Stripe webhooks +func (g *StripeGateway) ValidateWebhook(ctx context.Context, signatureHeader string, body []byte) (*WebhookEvent, error) { + event, err := webhook.ConstructEvent(body, signatureHeader, g.webhookSecret) + if err != nil { + return nil, fmt.Errorf("webhook validation failed: %w", err) + } + + return &WebhookEvent{ + Type: string(event.Type), + EventID: event.ID, + Created: time.Unix(event.Created, 0), + Data: event.Data.Raw, + }, nil +} + +// CreateInvoice creates a Stripe invoice +func (g *StripeGateway) CreateInvoice(ctx context.Context, customerID string, items []InvoiceItem) (*InvoiceResponse, error) { + // Create invoice items first + for _, item := range items { + itemParams := &stripe.InvoiceItemParams{ + Customer: stripe.String(customerID), + Amount: stripe.Int64(int64(item.Amount * 100)), + Currency: stripe.String(string(item.Currency)), + Description: stripe.String(item.Description), + Quantity: stripe.Int64(int64(item.Quantity)), + } + + _, err := invoiceitem.New(itemParams) + if err != nil { + return nil, fmt.Errorf("failed to create invoice item: %w", err) + } + } + + // Create the invoice + params := &stripe.InvoiceParams{ + Customer: stripe.String(customerID), + AutoAdvance: stripe.Bool(true), + } + + inv, err := invoice.New(params) + if err != nil { + return nil, fmt.Errorf("failed to create invoice: %w", err) + } + + return &InvoiceResponse{ + InvoiceID: inv.ID, + Status: string(inv.Status), + Amount: float64(inv.Total) / 100.0, + Currency: string(inv.Currency), + CreatedAt: time.Unix(inv.Created, 0), + }, nil +} + +// PaymentRequest represents a payment request +type PaymentRequest struct { + SubscriberID uint `json:"subscriber_id"` + InvoiceID uint `json:"invoice_id"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + CustomerID string `json:"customer_id,omitempty"` + Description string `json:"description,omitempty"` +} + +// PaymentResponse represents a payment response +type PaymentResponse struct { + TransactionID string `json:"transaction_id"` + Status string `json:"status"` + ClientSecret string `json:"client_secret"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + CreatedAt time.Time `json:"created_at"` +} + +// CustomerResponse represents a customer creation response +type CustomerResponse struct { + CustomerID string `json:"customer_id"` + Email string `json:"email"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` +} + +// RefundResponse represents a refund response +type RefundResponse struct { + RefundID string `json:"refund_id"` + Amount float64 `json:"amount"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` +} + +// PaymentStatusResponse represents payment status response +type PaymentStatusResponse struct { + TransactionID string `json:"transaction_id"` + Status string `json:"status"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + CreatedAt time.Time `json:"created_at"` +} + +// WebhookEvent represents a webhook event +type WebhookEvent struct { + Type string `json:"type"` + EventID string `json:"event_id"` + Created time.Time `json:"created"` + Data json.RawMessage `json:"data"` +} + +// InvoiceItem represents an invoice line item +type InvoiceItem struct { + Description string `json:"description"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + Quantity int `json:"quantity"` +} + +// InvoiceResponse represents an invoice response +type InvoiceResponse struct { + InvoiceID string `json:"invoice_id"` + Status string `json:"status"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + CreatedAt time.Time `json:"created_at"` +}