mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-02 09:52:20 +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"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
|
||||
)
|
||||
|
||||
// handlePaymentSucceeded handles successful payment events.
|
||||
func (wh *WebhookHandler) handlePaymentSucceeded(ctx context.Context, event WebhookEvent) error {
|
||||
paymentIntent, ok := event.Data["object"].(map[string]any)
|
||||
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)
|
||||
|
||||
transaction, err := wh.db.GetTransactionByGatewayID(ctx, transactionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get transaction: %w", err)
|
||||
}
|
||||
|
||||
transaction.Status = "COMPLETED"
|
||||
transaction.UpdatedAt = time.Now()
|
||||
if err := wh.db.UpdateTransaction(ctx, transaction); err != nil {
|
||||
return fmt.Errorf("failed to update transaction: %w", err)
|
||||
}
|
||||
|
||||
if transaction.Type == "TOP_UP" {
|
||||
if err := wh.db.UpdateSubscriberBalance(ctx, transaction.SubscriberID, amount); err != nil {
|
||||
return fmt.Errorf("failed to update subscriber balance: %w", err)
|
||||
}
|
||||
if err := wh.sendTopUpNotification(ctx, transaction.SubscriberID, amount, currency); 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]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid payment intent object")
|
||||
}
|
||||
transactionID, ok := paymentIntent["id"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing transaction ID")
|
||||
}
|
||||
|
||||
transaction, err := wh.db.GetTransactionByGatewayID(ctx, transactionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get transaction: %w", err)
|
||||
}
|
||||
|
||||
transaction.Status = "FAILED"
|
||||
transaction.UpdatedAt = time.Now()
|
||||
if lastErr, ok := paymentIntent["last_payment_error"].(map[string]any); ok {
|
||||
if message, ok := lastErr["message"].(string); ok {
|
||||
transaction.Description += " - Failed: " + message
|
||||
}
|
||||
}
|
||||
if err := wh.db.UpdateTransaction(ctx, transaction); err != nil {
|
||||
return fmt.Errorf("failed to update transaction: %w", err)
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
if err := wh.db.CreateAlert(ctx, alert); 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]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid payment intent object")
|
||||
}
|
||||
transactionID, ok := paymentIntent["id"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("missing transaction ID")
|
||||
}
|
||||
|
||||
transaction, err := wh.db.GetTransactionByGatewayID(ctx, transactionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get transaction: %w", err)
|
||||
}
|
||||
transaction.Status = "CANCELLED"
|
||||
transaction.UpdatedAt = time.Now()
|
||||
if err := wh.db.UpdateTransaction(ctx, transaction); 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]any)
|
||||
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)
|
||||
|
||||
transaction, err := wh.db.GetTransactionByChargeID(ctx, chargeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find transaction for charge %s: %w", chargeID, err)
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
if err := wh.db.CreateAlert(ctx, alert); 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 {
|
||||
subscriber, err := wh.db.GetSubscriber(ctx, subscriberID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get subscriber: %w", err)
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
if err := wh.db.CreateNotification(ctx, notification); err != nil {
|
||||
return fmt.Errorf("failed to create notification: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Top-up notification sent to subscriber %d: %.2f %s", subscriberID, amount, currency)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user