From 432ed5e649c8668fce4e42c81dadbe7370e6d271 Mon Sep 17 00:00:00 2001 From: erio Date: Tue, 7 Apr 2026 03:28:30 +0800 Subject: [PATCH] fix(payment): critical fixes from agent audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix CreateOrderResult field names (snake_case → camelCase to match backend) - Fix MethodLimits JSON tags to consistent snake_case - Fix Stripe webhook header case sensitivity (lowercase keys for map lookup) - Fix MaxAmount=0 backend validation (0 = no limit, not reject all) - Fix structured error for INVALID_AMOUNT per CLAUDE.md spec --- backend/cmd/server/VERSION | 2 +- backend/internal/handler/payment_webhook_handler.go | 3 ++- backend/internal/service/payment_config_service.go | 8 ++++---- backend/internal/service/payment_service.go | 5 +++-- frontend/src/types/payment.ts | 12 ++++++------ frontend/src/views/user/PaymentView.vue | 12 ++++++------ 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index bee90cb581..50b3d1c333 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.108.63 +0.1.108.64 diff --git a/backend/internal/handler/payment_webhook_handler.go b/backend/internal/handler/payment_webhook_handler.go index 60c2d16493..34aebf9d9b 100644 --- a/backend/internal/handler/payment_webhook_handler.go +++ b/backend/internal/handler/payment_webhook_handler.go @@ -4,6 +4,7 @@ import ( "io" "log/slog" "net/http" + "strings" "github.com/Wei-Shaw/sub2api/internal/payment" "github.com/Wei-Shaw/sub2api/internal/service" @@ -70,7 +71,7 @@ func (h *PaymentWebhookHandler) handleNotify(c *gin.Context, providerKey string) headers := make(map[string]string) for k := range c.Request.Header { - headers[k] = c.GetHeader(k) + headers[strings.ToLower(k)] = c.GetHeader(k) } notification, err := provider.VerifyNotification(c.Request.Context(), string(body), headers) diff --git a/backend/internal/service/payment_config_service.go b/backend/internal/service/payment_config_service.go index dc29204c5f..ed1eca8093 100644 --- a/backend/internal/service/payment_config_service.go +++ b/backend/internal/service/payment_config_service.go @@ -78,11 +78,11 @@ type UpdatePaymentConfigRequest struct { // MethodLimits holds per-payment-type limits. type MethodLimits struct { - PaymentType string `json:"paymentType"` - FeeRate float64 `json:"feeRate"` + PaymentType string `json:"payment_type"` + FeeRate float64 `json:"fee_rate"` DailyLimit float64 `json:"daily_limit"` - SingleMin float64 `json:"singleMin"` - SingleMax float64 `json:"singleMax"` + SingleMin float64 `json:"single_min"` + SingleMax float64 `json:"single_max"` } type CreateProviderInstanceRequest struct { diff --git a/backend/internal/service/payment_service.go b/backend/internal/service/payment_service.go index d9447922c2..e0fb897aa3 100644 --- a/backend/internal/service/payment_service.go +++ b/backend/internal/service/payment_service.go @@ -210,8 +210,9 @@ func (s *PaymentService) validateOrderInput(ctx context.Context, req CreateOrder if req.OrderType == "subscription" { return s.validateSubOrder(ctx, req) } - if req.Amount < cfg.MinAmount || req.Amount > cfg.MaxAmount { - return nil, infraerrors.BadRequest("INVALID_AMOUNT", fmt.Sprintf("amount must be between %.2f and %.2f", cfg.MinAmount, cfg.MaxAmount)) + if (cfg.MinAmount > 0 && req.Amount < cfg.MinAmount) || (cfg.MaxAmount > 0 && req.Amount > cfg.MaxAmount) { + return nil, infraerrors.BadRequest("INVALID_AMOUNT", "amount out of range"). + WithMetadata(map[string]string{"min": fmt.Sprintf("%.2f", cfg.MinAmount), "max": fmt.Sprintf("%.2f", cfg.MaxAmount)}) } return nil, nil } diff --git a/frontend/src/types/payment.ts b/frontend/src/types/payment.ts index e0156da838..a7fa9eaae1 100644 --- a/frontend/src/types/payment.ts +++ b/frontend/src/types/payment.ts @@ -120,12 +120,12 @@ export interface CreateOrderRequest { } export interface CreateOrderResult { - order_id: number - pay_url?: string - qr_code?: string - client_secret?: string - pay_amount: number - expires_at: string + orderId: number + payUrl?: string + qrCode?: string + clientSecret?: string + payAmount: number + expiresAt: string } export interface DashboardStats { diff --git a/frontend/src/views/user/PaymentView.vue b/frontend/src/views/user/PaymentView.vue index 2ae9da6cac..73aa82e055 100644 --- a/frontend/src/views/user/PaymentView.vue +++ b/frontend/src/views/user/PaymentView.vue @@ -230,12 +230,12 @@ async function createOrder(orderAmount: number, orderType: string, planId?: numb orderType: orderType, planId: planId, }) - if (result.client_secret) { - router.push({ path: '/payment/stripe', query: { order_id: String(result.order_id), client_secret: result.client_secret } }) - } else if (result.qr_code) { - router.push({ path: '/payment/qrcode', query: { order_id: String(result.order_id), qr: result.qr_code || '', pay_url: result.pay_url || '' } }) - } else if (result.pay_url) { - window.location.href = result.pay_url + if (result.clientSecret) { + router.push({ path: '/payment/stripe', query: { order_id: String(result.orderId), client_secret: result.clientSecret } }) + } else if (result.qrCode) { + router.push({ path: '/payment/qrcode', query: { order_id: String(result.orderId), qr: result.qrCode || '', pay_url: result.payUrl || '' } }) + } else if (result.payUrl) { + window.location.href = result.payUrl } else { errorMessage.value = t('payment.result.failed') appStore.showError(errorMessage.value)