mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-19 11:32:53 +00:00
- 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.
176 lines
5.1 KiB
Go
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
|
|
}
|