backend-service/internal/handlers/payment.go

172 lines
4.3 KiB
Go
Raw Permalink Normal View History

package handlers
import (
"io"
"net/http"
"strings"
"attune-heart-therapy/internal/services"
"github.com/gin-gonic/gin"
)
type PaymentHandler struct {
paymentService services.PaymentService
}
func NewPaymentHandler(paymentService services.PaymentService) *PaymentHandler {
return &PaymentHandler{
paymentService: paymentService,
}
}
// CreatePaymentIntent handles POST /api/payments/intent for payment intent creation
func (h *PaymentHandler) CreatePaymentIntent(c *gin.Context) {
var req services.CreatePaymentIntentRequest
// Bind JSON request to struct
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request format",
"details": err.Error(),
})
return
}
// Set default currency if not provided
if req.Currency == "" {
req.Currency = "usd"
}
// Create payment intent
paymentIntent, err := h.paymentService.CreatePaymentIntent(req.Amount, req.Currency)
if err != nil {
if strings.Contains(err.Error(), "amount must be greater than 0") {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid amount",
"details": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to create payment intent",
"details": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "Payment intent created successfully",
"client_secret": paymentIntent.ClientSecret,
"payment_intent": paymentIntent.ID,
"amount": paymentIntent.Amount,
"currency": paymentIntent.Currency,
"status": paymentIntent.Status,
})
}
// ConfirmPayment handles POST /api/payments/confirm for payment confirmation
func (h *PaymentHandler) ConfirmPayment(c *gin.Context) {
var req services.ConfirmPaymentRequest
// Bind JSON request to struct
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request format",
"details": err.Error(),
})
return
}
// Confirm payment
paymentIntent, err := h.paymentService.ConfirmPayment(req.PaymentIntentID)
if err != nil {
if strings.Contains(err.Error(), "payment intent ID is required") {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Payment intent ID is required",
"details": err.Error(),
})
return
}
// Check if it's a Stripe error (payment failed, card declined, etc.)
if strings.Contains(err.Error(), "Your card was declined") ||
strings.Contains(err.Error(), "insufficient_funds") ||
strings.Contains(err.Error(), "card_declined") {
c.JSON(http.StatusPaymentRequired, gin.H{
"error": "Payment failed",
"details": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to confirm payment",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Payment confirmed successfully",
"payment_intent": paymentIntent.ID,
"status": paymentIntent.Status,
"amount": paymentIntent.Amount,
"currency": paymentIntent.Currency,
})
}
// HandleWebhook handles POST /api/payments/webhook for Stripe webhooks
func (h *PaymentHandler) HandleWebhook(c *gin.Context) {
// Get the raw body
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Failed to read request body",
"details": err.Error(),
})
return
}
// Get Stripe signature from header
signature := c.GetHeader("Stripe-Signature")
if signature == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Missing Stripe signature header",
})
return
}
// Process webhook
err = h.paymentService.HandleWebhook(body, signature)
if err != nil {
if strings.Contains(err.Error(), "webhook signature") {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid webhook signature",
"details": err.Error(),
})
return
}
if strings.Contains(err.Error(), "webhook payload is empty") {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Empty webhook payload",
"details": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to process webhook",
"details": err.Error(),
})
return
}
// Return 200 to acknowledge receipt of the webhook
c.JSON(http.StatusOK, gin.H{
"message": "Webhook processed successfully",
})
}