2025-11-05 15:06:07 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
2025-11-05 15:30:53 +00:00
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"attune-heart-therapy/internal/services"
|
|
|
|
|
|
2025-11-05 15:06:07 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type PaymentHandler struct {
|
2025-11-05 15:30:53 +00:00
|
|
|
paymentService services.PaymentService
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 15:30:53 +00:00
|
|
|
func NewPaymentHandler(paymentService services.PaymentService) *PaymentHandler {
|
|
|
|
|
return &PaymentHandler{
|
|
|
|
|
paymentService: paymentService,
|
|
|
|
|
}
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 15:30:53 +00:00
|
|
|
// CreatePaymentIntent handles POST /api/payments/intent for payment intent creation
|
2025-11-05 15:06:07 +00:00
|
|
|
func (h *PaymentHandler) CreatePaymentIntent(c *gin.Context) {
|
2025-11-05 15:30:53 +00:00
|
|
|
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,
|
|
|
|
|
})
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 15:30:53 +00:00
|
|
|
// ConfirmPayment handles POST /api/payments/confirm for payment confirmation
|
2025-11-05 15:06:07 +00:00
|
|
|
func (h *PaymentHandler) ConfirmPayment(c *gin.Context) {
|
2025-11-05 15:30:53 +00:00
|
|
|
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,
|
|
|
|
|
})
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 15:30:53 +00:00
|
|
|
// HandleWebhook handles POST /api/payments/webhook for Stripe webhooks
|
2025-11-05 15:06:07 +00:00
|
|
|
func (h *PaymentHandler) HandleWebhook(c *gin.Context) {
|
2025-11-05 15:30:53 +00:00
|
|
|
// 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",
|
|
|
|
|
})
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|