mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-10 22:15:13 +00:00
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.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user