fix(payment): critical fixes from agent audit

- 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
This commit is contained in:
erio
2026-04-07 03:28:30 +08:00
parent 4e494e484a
commit 432ed5e649
6 changed files with 22 additions and 20 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.108.63
0.1.108.64
@@ -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)
@@ -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 {
+3 -2
View File
@@ -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
}
+6 -6
View File
@@ -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 {
+6 -6
View File
@@ -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)