package handler import ( "io" "log/slog" "net/http" "strings" "github.com/Wei-Shaw/sub2api/internal/payment" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" ) // PaymentWebhookHandler handles payment provider webhook callbacks. type PaymentWebhookHandler struct { paymentService *service.PaymentService registry *payment.Registry } // maxWebhookBodySize is the maximum allowed webhook request body size (1 MB). const maxWebhookBodySize = 1 << 20 // NewPaymentWebhookHandler creates a new PaymentWebhookHandler. func NewPaymentWebhookHandler(paymentService *service.PaymentService, registry *payment.Registry) *PaymentWebhookHandler { return &PaymentWebhookHandler{ paymentService: paymentService, registry: registry, } } // EasyPayNotify handles EasyPay payment notifications. // POST /api/v1/payment/webhook/easypay func (h *PaymentWebhookHandler) EasyPayNotify(c *gin.Context) { h.handleNotify(c, "easypay") } // AlipayNotify handles Alipay payment notifications. // POST /api/v1/payment/webhook/alipay func (h *PaymentWebhookHandler) AlipayNotify(c *gin.Context) { h.handleNotify(c, "alipay") } // WxpayNotify handles WeChat Pay payment notifications. // POST /api/v1/payment/webhook/wxpay func (h *PaymentWebhookHandler) WxpayNotify(c *gin.Context) { h.handleNotify(c, "wxpay") } // StripeWebhook handles Stripe webhook events. // POST /api/v1/payment/webhook/stripe func (h *PaymentWebhookHandler) StripeWebhook(c *gin.Context) { h.handleNotify(c, "stripe") } // handleNotify is the shared logic for all provider webhook handlers. func (h *PaymentWebhookHandler) handleNotify(c *gin.Context, providerKey string) { var rawBody string if c.Request.Method == http.MethodGet { // GET callbacks (e.g. EasyPay) pass params as URL query string rawBody = c.Request.URL.RawQuery } else { body, err := io.ReadAll(io.LimitReader(c.Request.Body, maxWebhookBodySize)) if err != nil { slog.Error("[Payment Webhook] failed to read body", "provider", providerKey, "error", err) c.String(http.StatusBadRequest, "failed to read body") return } rawBody = string(body) } provider, err := h.registry.GetProviderByKey(providerKey) if err != nil { slog.Warn("[Payment Webhook] provider not registered", "provider", providerKey, "error", err) c.String(http.StatusOK, successResponse(providerKey)) return } headers := make(map[string]string) for k := range c.Request.Header { headers[strings.ToLower(k)] = c.GetHeader(k) } notification, err := provider.VerifyNotification(c.Request.Context(), rawBody, headers) if err != nil { slog.Error("[Payment Webhook] verify failed", "provider", providerKey, "error", err) c.String(http.StatusBadRequest, "verify failed") return } // nil notification means irrelevant event (e.g. Stripe non-payment event); return success. if notification == nil { c.String(http.StatusOK, successResponse(providerKey)) return } if err := h.paymentService.HandlePaymentNotification(c.Request.Context(), notification, providerKey); err != nil { slog.Error("[Payment Webhook] handle notification failed", "provider", providerKey, "error", err) c.String(http.StatusInternalServerError, "handle failed") return } c.String(http.StatusOK, successResponse(providerKey)) } // successResponse returns the provider-specific success response string. func successResponse(providerKey string) string { switch providerKey { case "stripe": return "" default: return "success" } }