mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-21 20:24:27 +00:00
feat: Add payment webhook handler with Stripe integration and transaction processing
- Add WebhookHandler with Stripe webhook validation and event processing - Add webhook event handlers: payment succeeded, failed, canceled, and dispute created - Update transaction status and subscriber balance on successful payments - Create alerts for failed payments and disputes with appropriate severity levels - Add sendTopUpNotification to create notifications for successful top-ups - Add PaymentController
This commit is contained in:
@@ -0,0 +1,594 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/database"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
|
||||
)
|
||||
|
||||
// WebhookEvent represents a payment webhook event
|
||||
type WebhookEvent struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Livemode bool `json:"livemode"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// WebhookHandler handles payment webhooks
|
||||
type WebhookHandler struct {
|
||||
paymentService *PaymentService
|
||||
db *database.Database
|
||||
}
|
||||
|
||||
// NewWebhookHandler creates a new webhook handler
|
||||
func NewWebhookHandler(paymentService *PaymentService, db *database.Database) *WebhookHandler {
|
||||
return &WebhookHandler{
|
||||
paymentService: paymentService,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleStripeWebhook handles Stripe webhooks
|
||||
func (wh *WebhookHandler) HandleStripeWebhook(c *gin.Context) {
|
||||
body, err := c.GetRawData()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"})
|
||||
return
|
||||
}
|
||||
|
||||
signature := c.GetHeader("Stripe-Signature")
|
||||
if signature == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing signature"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate webhook
|
||||
valid, err := wh.paymentService.ValidateWebhook(c.Request.Context(), signature, body)
|
||||
if err != nil {
|
||||
log.Printf("Webhook validation error: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid webhook"})
|
||||
return
|
||||
}
|
||||
|
||||
if !valid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse event
|
||||
var event WebhookEvent
|
||||
if err := json.Unmarshal(body, &event); err != nil {
|
||||
log.Printf("Failed to parse webhook event: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid event format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle different event types
|
||||
switch event.Type {
|
||||
case "payment_intent.succeeded":
|
||||
err = wh.handlePaymentSucceeded(c.Request.Context(), event)
|
||||
case "payment_intent.payment_failed":
|
||||
err = wh.handlePaymentFailed(c.Request.Context(), event)
|
||||
case "payment_intent.canceled":
|
||||
err = wh.handlePaymentCanceled(c.Request.Context(), event)
|
||||
case "charge.dispute.created":
|
||||
err = wh.handleDisputeCreated(c.Request.Context(), event)
|
||||
default:
|
||||
log.Printf("Unhandled webhook event type: %s", event.Type)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error handling webhook event %s: %v", event.Type, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process webhook"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "received"})
|
||||
}
|
||||
|
||||
// handlePaymentSucceeded handles successful payment events
|
||||
func (wh *WebhookHandler) handlePaymentSucceeded(ctx context.Context, event WebhookEvent) error {
|
||||
paymentIntent, ok := event.Data["object"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid payment intent object")
|
||||
}
|
||||
|
||||
transactionID, ok := paymentIntent["id"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing transaction ID")
|
||||
}
|
||||
|
||||
amount := float64(paymentIntent["amount"].(int64)) / 100
|
||||
currency := paymentIntent["currency"].(string)
|
||||
|
||||
// Get transaction from database
|
||||
transaction, err := wh.db.GetTransactionByGatewayID(ctx, transactionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get transaction: %w", err)
|
||||
}
|
||||
|
||||
// Update transaction status
|
||||
transaction.Status = "COMPLETED"
|
||||
transaction.UpdatedAt = time.Now()
|
||||
|
||||
err = wh.db.UpdateTransaction(ctx, transaction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update transaction: %w", err)
|
||||
}
|
||||
|
||||
// If it's a top-up, update subscriber balance
|
||||
if transaction.Type == "TOP_UP" {
|
||||
err = wh.db.UpdateSubscriberBalance(ctx, transaction.SubscriberID, amount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update subscriber balance: %w", err)
|
||||
}
|
||||
|
||||
// Send notification to subscriber
|
||||
err = wh.sendTopUpNotification(ctx, transaction.SubscriberID, amount, currency)
|
||||
if err != nil {
|
||||
log.Printf("Failed to send top-up notification: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Payment succeeded: %s, amount: %.2f %s", transactionID, amount, currency)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handlePaymentFailed handles failed payment events
|
||||
func (wh *WebhookHandler) handlePaymentFailed(ctx context.Context, event WebhookEvent) error {
|
||||
paymentIntent, ok := event.Data["object"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid payment intent object")
|
||||
}
|
||||
|
||||
transactionID, ok := paymentIntent["id"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing transaction ID")
|
||||
}
|
||||
|
||||
// Get transaction from database
|
||||
transaction, err := wh.db.GetTransactionByGatewayID(ctx, transactionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get transaction: %w", err)
|
||||
}
|
||||
|
||||
// Update transaction status
|
||||
transaction.Status = "FAILED"
|
||||
transaction.UpdatedAt = time.Now()
|
||||
|
||||
// Add failure reason if available
|
||||
if lastPaymentError, ok := paymentIntent["last_payment_error"].(map[string]interface{}); ok {
|
||||
if message, ok := lastPaymentError["message"].(string); ok {
|
||||
transaction.Description += " - Failed: " + message
|
||||
}
|
||||
}
|
||||
|
||||
err = wh.db.UpdateTransaction(ctx, transaction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update transaction: %w", err)
|
||||
}
|
||||
|
||||
// Create alert for failed payment
|
||||
subID := int(transaction.SubscriberID)
|
||||
alert := &models.Alert{
|
||||
SubscriberID: &subID,
|
||||
Type: models.AlertTypePaymentFailed,
|
||||
Severity: models.AlertSeverityMedium,
|
||||
Message: fmt.Sprintf("Payment of %.2f %s failed", transaction.Amount, transaction.Currency),
|
||||
Resolved: false,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
err = wh.db.CreateAlert(ctx, alert)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create payment failure alert: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Payment failed: %s", transactionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handlePaymentCanceled handles canceled payment events
|
||||
func (wh *WebhookHandler) handlePaymentCanceled(ctx context.Context, event WebhookEvent) error {
|
||||
paymentIntent, ok := event.Data["object"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid payment intent object")
|
||||
}
|
||||
|
||||
transactionID, ok := paymentIntent["id"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing transaction ID")
|
||||
}
|
||||
|
||||
// Get transaction from database
|
||||
transaction, err := wh.db.GetTransactionByGatewayID(ctx, transactionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get transaction: %w", err)
|
||||
}
|
||||
|
||||
// Update transaction status
|
||||
transaction.Status = "CANCELLED"
|
||||
transaction.UpdatedAt = time.Now()
|
||||
|
||||
err = wh.db.UpdateTransaction(ctx, transaction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update transaction: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Payment canceled: %s", transactionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDisputeCreated handles dispute events
|
||||
func (wh *WebhookHandler) handleDisputeCreated(ctx context.Context, event WebhookEvent) error {
|
||||
dispute, ok := event.Data["object"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid dispute object")
|
||||
}
|
||||
|
||||
chargeID, ok := dispute["charge"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing charge ID")
|
||||
}
|
||||
|
||||
amount := float64(dispute["amount"].(int64)) / 100
|
||||
reason, _ := dispute["reason"].(string)
|
||||
|
||||
// Find transaction by charge ID
|
||||
transaction, err := wh.db.GetTransactionByChargeID(ctx, chargeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find transaction for charge %s: %w", chargeID, err)
|
||||
}
|
||||
|
||||
// Create high-priority alert for dispute
|
||||
dispSubID := int(transaction.SubscriberID)
|
||||
alert := &models.Alert{
|
||||
SubscriberID: &dispSubID,
|
||||
Type: models.AlertTypePaymentFailed,
|
||||
Severity: models.AlertSeverityHigh,
|
||||
Message: fmt.Sprintf("Payment dispute created for %.2f %s. Reason: %s", amount, transaction.Currency, reason),
|
||||
Resolved: false,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
err = wh.db.CreateAlert(ctx, alert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create dispute alert: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Dispute created for charge %s, amount: %.2f", chargeID, amount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendTopUpNotification sends a notification for successful top-up
|
||||
func (wh *WebhookHandler) sendTopUpNotification(ctx context.Context, subscriberID uint, amount float64, currency string) error {
|
||||
// Get subscriber details
|
||||
subscriber, err := wh.db.GetSubscriber(ctx, subscriberID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get subscriber: %w", err)
|
||||
}
|
||||
|
||||
// Create notification
|
||||
notification := &models.Notification{
|
||||
SubscriberID: subscriberID,
|
||||
Type: "PAYMENT_SUCCESS",
|
||||
Title: "Top-up Successful",
|
||||
Message: fmt.Sprintf("Your account has been topped up with %.2f %s. New balance: %.2f %s",
|
||||
amount, currency, subscriber.Balance, currency),
|
||||
Read: false,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = wh.db.CreateNotification(ctx, notification)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create notification: %w", err)
|
||||
}
|
||||
|
||||
// Send email notification (implementation would depend on email service)
|
||||
// This is a placeholder for email sending logic
|
||||
log.Printf("Top-up notification sent to subscriber %d: %.2f %s", subscriberID, amount, currency)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PaymentController handles payment-related HTTP endpoints
|
||||
type PaymentController struct {
|
||||
paymentService *PaymentService
|
||||
db *database.Database
|
||||
}
|
||||
|
||||
// NewPaymentController creates a new payment controller
|
||||
func NewPaymentController(paymentService *PaymentService, db *database.Database) *PaymentController {
|
||||
return &PaymentController{
|
||||
paymentService: paymentService,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupRoutes sets up payment routes
|
||||
func (pc *PaymentController) SetupRoutes(router *gin.RouterGroup) {
|
||||
payments := router.Group("/payments")
|
||||
{
|
||||
payments.POST("/top-up", pc.ProcessTopUp)
|
||||
payments.POST("/payment-methods", pc.CreatePaymentMethod)
|
||||
payments.DELETE("/payment-methods/:id", pc.DeletePaymentMethod)
|
||||
payments.GET("/payment-methods", pc.ListPaymentMethods)
|
||||
payments.GET("/transactions", pc.ListTransactions)
|
||||
payments.GET("/transactions/:id", pc.GetTransaction)
|
||||
payments.POST("/transactions/:id/refund", pc.RefundTransaction)
|
||||
}
|
||||
|
||||
webhook := router.Group("/webhooks")
|
||||
{
|
||||
webhook.POST("/stripe", pc.HandleStripeWebhook)
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessTopUp processes a top-up payment
|
||||
func (pc *PaymentController) ProcessTopUp(c *gin.Context) {
|
||||
var req struct {
|
||||
Amount float64 `json:"amount" binding:"required,min=0.01"`
|
||||
PaymentMethodID string `json:"paymentMethodId" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get subscriber ID from context (would be set by auth middleware)
|
||||
subscriberID := c.GetInt("subscriberID")
|
||||
if subscriberID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
transaction, err := pc.paymentService.ProcessTopUp(c.Request.Context(), subscriberID, req.Amount, req.PaymentMethodID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"transaction": transaction,
|
||||
"message": "Top-up processed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// CreatePaymentMethod creates a new payment method
|
||||
func (pc *PaymentController) CreatePaymentMethod(c *gin.Context) {
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Token string `json:"token" binding:"required"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
subscriberID := c.GetInt("subscriberID")
|
||||
if subscriberID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
createReq := &CreatePaymentMethodRequest{
|
||||
Type: PaymentMethodType(req.Type),
|
||||
Token: req.Token,
|
||||
IsDefault: req.IsDefault,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
paymentMethod, err := pc.paymentService.CreatePaymentMethod(c.Request.Context(), subscriberID, createReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"paymentMethod": paymentMethod,
|
||||
"message": "Payment method created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// DeletePaymentMethod deletes a payment method
|
||||
func (pc *PaymentController) DeletePaymentMethod(c *gin.Context) {
|
||||
paymentMethodID := c.Param("id")
|
||||
if paymentMethodID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Payment method ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
subscriberID := c.GetInt("subscriberID")
|
||||
if subscriberID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify payment method belongs to subscriber
|
||||
pm, err := pc.db.GetPaymentMethod(c.Request.Context(), paymentMethodID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Payment method not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if pm.SubscriberID != uint(subscriberID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Payment method does not belong to subscriber"})
|
||||
return
|
||||
}
|
||||
|
||||
err = pc.paymentService.gateway.DeletePaymentMethod(c.Request.Context(), pm.GatewayID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
err = pc.db.DeletePaymentMethod(c.Request.Context(), paymentMethodID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Payment method deleted successfully"})
|
||||
}
|
||||
|
||||
// ListPaymentMethods lists payment methods for a subscriber
|
||||
func (pc *PaymentController) ListPaymentMethods(c *gin.Context) {
|
||||
subscriberID := c.GetInt("subscriberID")
|
||||
if subscriberID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
paymentMethods, err := pc.db.ListPaymentMethods(c.Request.Context(), uint(subscriberID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"paymentMethods": paymentMethods})
|
||||
}
|
||||
|
||||
// ListTransactions lists transactions for a subscriber
|
||||
func (pc *PaymentController) ListTransactions(c *gin.Context) {
|
||||
subscriberID := c.GetInt("subscriberID")
|
||||
if subscriberID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
transactions, err := pc.db.ListTransactions(c.Request.Context(), uint(subscriberID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"transactions": transactions})
|
||||
}
|
||||
|
||||
// GetTransaction gets a specific transaction
|
||||
func (pc *PaymentController) GetTransaction(c *gin.Context) {
|
||||
transactionID := c.Param("id")
|
||||
if transactionID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Transaction ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
subscriberID := c.GetInt("subscriberID")
|
||||
if subscriberID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
transaction, err := pc.db.GetTransaction(c.Request.Context(), transactionID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Transaction not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if transaction.SubscriberID != uint(subscriberID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Transaction does not belong to subscriber"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"transaction": transaction})
|
||||
}
|
||||
|
||||
// RefundTransaction refunds a transaction
|
||||
func (pc *PaymentController) RefundTransaction(c *gin.Context) {
|
||||
transactionID := c.Param("id")
|
||||
if transactionID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Transaction ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Amount float64 `json:"amount" binding:"required,min=0.01"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
subscriberID := c.GetInt("subscriberID")
|
||||
if subscriberID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get transaction
|
||||
transaction, err := pc.db.GetTransaction(c.Request.Context(), transactionID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Transaction not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if transaction.SubscriberID != uint(subscriberID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Transaction does not belong to subscriber"})
|
||||
return
|
||||
}
|
||||
|
||||
if transaction.Status != "COMPLETED" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot refund non-completed transaction"})
|
||||
return
|
||||
}
|
||||
|
||||
// Process refund
|
||||
refundResp, err := pc.paymentService.gateway.RefundPayment(c.Request.Context(), transaction.TransactionID, req.Amount)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create refund record
|
||||
refund := &models.Transaction{
|
||||
SubscriberID: transaction.SubscriberID,
|
||||
TransactionID: refundResp.RefundID,
|
||||
Type: "REFUND",
|
||||
Amount: req.Amount,
|
||||
Currency: refundResp.Currency,
|
||||
Status: string(refundResp.Status),
|
||||
Description: fmt.Sprintf("Refund for transaction %s", transactionID),
|
||||
ParentID: &transaction.ID,
|
||||
CreatedAt: refundResp.ProcessedAt,
|
||||
UpdatedAt: refundResp.ProcessedAt,
|
||||
}
|
||||
|
||||
err = pc.db.CreateTransaction(c.Request.Context(), refund)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save refund"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update subscriber balance
|
||||
err = pc.db.UpdateSubscriberBalance(c.Request.Context(), transaction.SubscriberID, -req.Amount)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update balance"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"refund": refund,
|
||||
"message": "Refund processed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// HandleStripeWebhook handles Stripe webhooks (delegates to webhook handler)
|
||||
func (pc *PaymentController) HandleStripeWebhook(c *gin.Context) {
|
||||
webhookHandler := NewWebhookHandler(pc.paymentService, pc.db)
|
||||
webhookHandler.HandleStripeWebhook(c)
|
||||
}
|
||||
Reference in New Issue
Block a user