From 1f2d0499ed4933d41f6ecfe4dc1ec01e9fa4dcbd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:49:40 +0000 Subject: [PATCH] fix: honor ENABLED_PAYMENT_TYPES in user config api Co-authored-by: gopkg-dev <58848833+gopkg-dev@users.noreply.github.com> --- src/__tests__/app/api/user-route.test.ts | 105 +++++++++++++++++++++++ src/app/api/user/route.ts | 22 ++++- 2 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/app/api/user-route.test.ts diff --git a/src/__tests__/app/api/user-route.test.ts b/src/__tests__/app/api/user-route.test.ts new file mode 100644 index 0000000..cc78262 --- /dev/null +++ b/src/__tests__/app/api/user-route.test.ts @@ -0,0 +1,105 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { NextRequest } from 'next/server'; + +const mockGetCurrentUserByToken = vi.fn(); +const mockGetUser = vi.fn(); +const mockGetSystemConfig = vi.fn(); +const mockQueryMethodLimits = vi.fn(); +const mockGetSupportedTypes = vi.fn(); + +vi.mock('@/lib/sub2api/client', () => ({ + getCurrentUserByToken: (...args: unknown[]) => mockGetCurrentUserByToken(...args), + getUser: (...args: unknown[]) => mockGetUser(...args), +})); + +vi.mock('@/lib/config', () => ({ + getEnv: () => ({ + MIN_RECHARGE_AMOUNT: 1, + MAX_RECHARGE_AMOUNT: 1000, + MAX_DAILY_RECHARGE_AMOUNT: 10000, + PAY_HELP_IMAGE_URL: undefined, + PAY_HELP_TEXT: undefined, + STRIPE_PUBLISHABLE_KEY: 'pk_test', + }), +})); + +vi.mock('@/lib/order/limits', () => ({ + queryMethodLimits: (...args: unknown[]) => mockQueryMethodLimits(...args), +})); + +vi.mock('@/lib/payment', () => ({ + initPaymentProviders: vi.fn(), + paymentRegistry: { + getSupportedTypes: (...args: unknown[]) => mockGetSupportedTypes(...args), + }, +})); + +vi.mock('@/lib/pay-utils', () => ({ + getPaymentDisplayInfo: (type: string) => ({ + channel: type === 'alipay_direct' ? 'alipay' : type, + provider: type, + }), +})); + +vi.mock('@/lib/locale', () => ({ + resolveLocale: () => 'zh', +})); + +vi.mock('@/lib/system-config', () => ({ + getSystemConfig: (...args: unknown[]) => mockGetSystemConfig(...args), +})); + +import { GET } from '@/app/api/user/route'; + +function createRequest() { + return new NextRequest('https://pay.example.com/api/user?user_id=1&token=test-token'); +} + +describe('GET /api/user', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockGetCurrentUserByToken.mockResolvedValue({ id: 1 }); + mockGetUser.mockResolvedValue({ id: 1, status: 'active' }); + mockGetSupportedTypes.mockReturnValue(['alipay', 'wxpay', 'stripe']); + mockQueryMethodLimits.mockResolvedValue({ + alipay: { maxDailyAmount: 1000 }, + wxpay: { maxDailyAmount: 1000 }, + stripe: { maxDailyAmount: 1000 }, + }); + mockGetSystemConfig.mockImplementation(async (key: string) => { + if (key === 'ENABLED_PAYMENT_TYPES') return undefined; + if (key === 'BALANCE_PAYMENT_DISABLED') return 'false'; + return undefined; + }); + }); + + it('filters enabled payment types by ENABLED_PAYMENT_TYPES config', async () => { + mockGetSystemConfig.mockImplementation(async (key: string) => { + if (key === 'ENABLED_PAYMENT_TYPES') return 'alipay,wxpay'; + if (key === 'BALANCE_PAYMENT_DISABLED') return 'false'; + return undefined; + }); + + const response = await GET(createRequest()); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.config.enabledPaymentTypes).toEqual(['alipay', 'wxpay']); + expect(mockQueryMethodLimits).toHaveBeenCalledWith(['alipay', 'wxpay']); + }); + + it('falls back to supported payment types when ENABLED_PAYMENT_TYPES is empty', async () => { + mockGetSystemConfig.mockImplementation(async (key: string) => { + if (key === 'ENABLED_PAYMENT_TYPES') return ' '; + if (key === 'BALANCE_PAYMENT_DISABLED') return 'false'; + return undefined; + }); + + const response = await GET(createRequest()); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.config.enabledPaymentTypes).toEqual(['alipay', 'wxpay', 'stripe']); + expect(mockQueryMethodLimits).toHaveBeenCalledWith(['alipay', 'wxpay', 'stripe']); + }); +}); diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 86db1ea..c35481a 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -7,6 +7,20 @@ import { getPaymentDisplayInfo } from '@/lib/pay-utils'; import { resolveLocale } from '@/lib/locale'; import { getSystemConfig } from '@/lib/system-config'; +function resolveEnabledPaymentTypes(supportedTypes: string[], configuredTypes: string | undefined): string[] { + if (configuredTypes === undefined) return supportedTypes; + + const configuredTypeSet = new Set( + configuredTypes + .split(',') + .map((type) => type.trim()) + .filter(Boolean), + ); + if (configuredTypeSet.size === 0) return supportedTypes; + + return supportedTypes.filter((type) => configuredTypeSet.has(type)); +} + export async function GET(request: NextRequest) { const locale = resolveLocale(request.nextUrl.searchParams.get('lang')); const userId = Number(request.nextUrl.searchParams.get('user_id')); @@ -40,12 +54,14 @@ export async function GET(request: NextRequest) { const env = getEnv(); initPaymentProviders(); - const enabledTypes = paymentRegistry.getSupportedTypes(); - const [user, methodLimits, balanceDisabledVal] = await Promise.all([ + const supportedTypes = paymentRegistry.getSupportedTypes(); + const [user, configuredEnabledTypes, balanceDisabledVal] = await Promise.all([ getUser(userId), - queryMethodLimits(enabledTypes), + getSystemConfig('ENABLED_PAYMENT_TYPES'), getSystemConfig('BALANCE_PAYMENT_DISABLED'), ]); + const enabledTypes = resolveEnabledPaymentTypes(supportedTypes, configuredEnabledTypes); + const methodLimits = await queryMethodLimits(enabledTypes); const balanceDisabled = balanceDisabledVal === 'true'; // 攢集 sublabel 覆盖