feat: 全站多语言支持 (i18n),lang=en 显示英文,其余默认中文
新增 src/lib/locale.ts 作为统一多语言入口,覆盖前台支付链路、 管理后台、API/服务层错误文案,共 35 个文件。URL 参数 lang 全链路透传, 包括 Stripe return_url、页面跳转、layout html lang 属性等。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
752
src/__tests__/payment-flow.test.ts
Normal file
752
src/__tests__/payment-flow.test.ts
Normal file
@@ -0,0 +1,752 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// ============================================================
|
||||
// Mock: EasyPay
|
||||
// ============================================================
|
||||
|
||||
const mockEasyPayCreatePayment = vi.fn();
|
||||
vi.mock('@/lib/easy-pay/client', () => ({
|
||||
createPayment: (...args: unknown[]) => mockEasyPayCreatePayment(...args),
|
||||
queryOrder: vi.fn(),
|
||||
refund: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/easy-pay/sign', () => ({
|
||||
verifySign: vi.fn(),
|
||||
generateSign: vi.fn(),
|
||||
}));
|
||||
|
||||
// ============================================================
|
||||
// Mock: Alipay
|
||||
// ============================================================
|
||||
|
||||
const mockAlipayPageExecute = vi.fn();
|
||||
vi.mock('@/lib/alipay/client', () => ({
|
||||
pageExecute: (...args: unknown[]) => mockAlipayPageExecute(...args),
|
||||
execute: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/alipay/sign', () => ({
|
||||
verifySign: vi.fn(),
|
||||
generateSign: vi.fn(),
|
||||
}));
|
||||
|
||||
// ============================================================
|
||||
// Mock: Wxpay
|
||||
// ============================================================
|
||||
|
||||
const mockWxpayCreatePcOrder = vi.fn();
|
||||
const mockWxpayCreateH5Order = vi.fn();
|
||||
vi.mock('@/lib/wxpay/client', () => ({
|
||||
createPcOrder: (...args: unknown[]) => mockWxpayCreatePcOrder(...args),
|
||||
createH5Order: (...args: unknown[]) => mockWxpayCreateH5Order(...args),
|
||||
queryOrder: vi.fn(),
|
||||
closeOrder: vi.fn(),
|
||||
createRefund: vi.fn(),
|
||||
decipherNotify: vi.fn(),
|
||||
verifyNotifySign: vi.fn(),
|
||||
}));
|
||||
|
||||
// ============================================================
|
||||
// Mock: Config (shared by all providers)
|
||||
// ============================================================
|
||||
|
||||
vi.mock('@/lib/config', () => ({
|
||||
getEnv: () => ({
|
||||
// EasyPay
|
||||
EASY_PAY_PID: 'test-pid',
|
||||
EASY_PAY_PKEY: 'test-pkey',
|
||||
EASY_PAY_API_BASE: 'https://easypay.example.com',
|
||||
EASY_PAY_NOTIFY_URL: 'https://pay.example.com/api/easypay/notify',
|
||||
EASY_PAY_RETURN_URL: 'https://pay.example.com/pay/result',
|
||||
// Alipay
|
||||
ALIPAY_APP_ID: '2021000000000000',
|
||||
ALIPAY_PRIVATE_KEY: 'test-private-key',
|
||||
ALIPAY_PUBLIC_KEY: 'test-public-key',
|
||||
ALIPAY_NOTIFY_URL: 'https://pay.example.com/api/alipay/notify',
|
||||
ALIPAY_RETURN_URL: 'https://pay.example.com/pay/result',
|
||||
// Wxpay
|
||||
WXPAY_APP_ID: 'wx-test-app-id',
|
||||
WXPAY_MCH_ID: 'wx-test-mch-id',
|
||||
WXPAY_PRIVATE_KEY: 'test-private-key',
|
||||
WXPAY_API_V3_KEY: 'test-api-v3-key',
|
||||
WXPAY_PUBLIC_KEY: 'test-public-key',
|
||||
WXPAY_PUBLIC_KEY_ID: 'test-public-key-id',
|
||||
WXPAY_CERT_SERIAL: 'test-cert-serial',
|
||||
WXPAY_NOTIFY_URL: 'https://pay.example.com/api/wxpay/notify',
|
||||
// General
|
||||
NEXT_PUBLIC_APP_URL: 'https://pay.example.com',
|
||||
}),
|
||||
}));
|
||||
|
||||
// ============================================================
|
||||
// Imports (must come after mocks)
|
||||
// ============================================================
|
||||
|
||||
import { EasyPayProvider } from '@/lib/easy-pay/provider';
|
||||
import { AlipayProvider } from '@/lib/alipay/provider';
|
||||
import { WxpayProvider } from '@/lib/wxpay/provider';
|
||||
import { isStripeType } from '@/lib/pay-utils';
|
||||
import { REDIRECT_PAYMENT_TYPES } from '@/lib/constants';
|
||||
import type { CreatePaymentRequest } from '@/lib/payment/types';
|
||||
|
||||
// ============================================================
|
||||
// Helper: simulate shouldAutoRedirect logic from PaymentQRCode
|
||||
// ============================================================
|
||||
|
||||
function shouldAutoRedirect(opts: {
|
||||
expired: boolean;
|
||||
paymentType?: string;
|
||||
payUrl?: string | null;
|
||||
qrCode?: string | null;
|
||||
isMobile: boolean;
|
||||
}): boolean {
|
||||
return (
|
||||
!opts.expired &&
|
||||
!isStripeType(opts.paymentType) &&
|
||||
!!opts.payUrl &&
|
||||
(opts.isMobile || !opts.qrCode)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Tests
|
||||
// ============================================================
|
||||
|
||||
describe('Payment Flow - PC/Mobile, QR/Redirect', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// EasyPay Provider
|
||||
// ----------------------------------------------------------
|
||||
|
||||
describe('EasyPayProvider', () => {
|
||||
let provider: EasyPayProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new EasyPayProvider();
|
||||
});
|
||||
|
||||
it('PC: createPayment returns both payUrl and qrCode', async () => {
|
||||
mockEasyPayCreatePayment.mockResolvedValue({
|
||||
code: 1,
|
||||
trade_no: 'EP-001',
|
||||
payurl: 'https://easypay.example.com/pay/EP-001',
|
||||
qrcode: 'https://qr.alipay.com/fkx12345',
|
||||
});
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-ep-001',
|
||||
amount: 50,
|
||||
paymentType: 'alipay',
|
||||
subject: 'Test Recharge',
|
||||
clientIp: '1.2.3.4',
|
||||
isMobile: false,
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
|
||||
expect(result.tradeNo).toBe('EP-001');
|
||||
expect(result.qrCode).toBe('https://qr.alipay.com/fkx12345');
|
||||
expect(result.payUrl).toBe('https://easypay.example.com/pay/EP-001');
|
||||
|
||||
// PC + has qrCode + has payUrl => shouldAutoRedirect = false (show QR)
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay',
|
||||
payUrl: result.payUrl,
|
||||
qrCode: result.qrCode,
|
||||
isMobile: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('Mobile: createPayment returns payUrl for redirect', async () => {
|
||||
mockEasyPayCreatePayment.mockResolvedValue({
|
||||
code: 1,
|
||||
trade_no: 'EP-002',
|
||||
payurl: 'https://easypay.example.com/pay/EP-002',
|
||||
qrcode: 'https://qr.alipay.com/fkx67890',
|
||||
});
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-ep-002',
|
||||
amount: 100,
|
||||
paymentType: 'wxpay',
|
||||
subject: 'Test Recharge',
|
||||
clientIp: '1.2.3.4',
|
||||
isMobile: true,
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
|
||||
expect(result.tradeNo).toBe('EP-002');
|
||||
expect(result.payUrl).toBeDefined();
|
||||
|
||||
// Mobile + has payUrl => shouldAutoRedirect = true (redirect)
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'wxpay',
|
||||
payUrl: result.payUrl,
|
||||
qrCode: result.qrCode,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('EasyPay does not use isMobile flag itself (delegates to frontend)', async () => {
|
||||
mockEasyPayCreatePayment.mockResolvedValue({
|
||||
code: 1,
|
||||
trade_no: 'EP-003',
|
||||
payurl: 'https://easypay.example.com/pay/EP-003',
|
||||
qrcode: 'weixin://wxpay/bizpayurl?pr=xxx',
|
||||
});
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-ep-003',
|
||||
amount: 10,
|
||||
paymentType: 'alipay',
|
||||
subject: 'Test',
|
||||
clientIp: '1.2.3.4',
|
||||
isMobile: true,
|
||||
};
|
||||
|
||||
await provider.createPayment(request);
|
||||
|
||||
// EasyPay client is called the same way regardless of isMobile
|
||||
expect(mockEasyPayCreatePayment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
outTradeNo: 'order-ep-003',
|
||||
paymentType: 'alipay',
|
||||
}),
|
||||
);
|
||||
// No isMobile parameter forwarded to the underlying client
|
||||
const callArgs = mockEasyPayCreatePayment.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty('isMobile');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Alipay Provider
|
||||
// ----------------------------------------------------------
|
||||
|
||||
describe('AlipayProvider', () => {
|
||||
let provider: AlipayProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new AlipayProvider();
|
||||
});
|
||||
|
||||
it('PC: uses alipay.trade.page.pay, returns payUrl only (no qrCode)', async () => {
|
||||
mockAlipayPageExecute.mockReturnValue(
|
||||
'https://openapi.alipay.com/gateway.do?method=alipay.trade.page.pay&sign=xxx',
|
||||
);
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-ali-001',
|
||||
amount: 100,
|
||||
paymentType: 'alipay_direct',
|
||||
subject: 'Test Recharge',
|
||||
isMobile: false,
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
|
||||
expect(result.tradeNo).toBe('order-ali-001');
|
||||
expect(result.payUrl).toContain('alipay.trade.page.pay');
|
||||
expect(result.qrCode).toBeUndefined();
|
||||
|
||||
// Verify pageExecute was called with PC method
|
||||
expect(mockAlipayPageExecute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
product_code: 'FAST_INSTANT_TRADE_PAY',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
method: 'alipay.trade.page.pay',
|
||||
}),
|
||||
);
|
||||
|
||||
// PC + payUrl only (no qrCode) => shouldAutoRedirect = true (redirect to Alipay cashier page)
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay_direct',
|
||||
payUrl: result.payUrl,
|
||||
qrCode: result.qrCode,
|
||||
isMobile: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('Mobile: uses alipay.trade.wap.pay, returns payUrl', async () => {
|
||||
mockAlipayPageExecute.mockReturnValue(
|
||||
'https://openapi.alipay.com/gateway.do?method=alipay.trade.wap.pay&sign=yyy',
|
||||
);
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-ali-002',
|
||||
amount: 50,
|
||||
paymentType: 'alipay_direct',
|
||||
subject: 'Test Recharge',
|
||||
isMobile: true,
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
|
||||
expect(result.tradeNo).toBe('order-ali-002');
|
||||
expect(result.payUrl).toContain('alipay.trade.wap.pay');
|
||||
expect(result.qrCode).toBeUndefined();
|
||||
|
||||
// Verify pageExecute was called with H5 method
|
||||
expect(mockAlipayPageExecute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
product_code: 'QUICK_WAP_WAY',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
method: 'alipay.trade.wap.pay',
|
||||
}),
|
||||
);
|
||||
|
||||
// Mobile + payUrl => shouldAutoRedirect = true
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay_direct',
|
||||
payUrl: result.payUrl,
|
||||
qrCode: result.qrCode,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('Mobile: falls back to PC page.pay when wap.pay throws', async () => {
|
||||
// First call (wap.pay) throws, second call (page.pay) succeeds
|
||||
mockAlipayPageExecute
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error('WAP pay not available');
|
||||
})
|
||||
.mockReturnValueOnce(
|
||||
'https://openapi.alipay.com/gateway.do?method=alipay.trade.page.pay&sign=fallback',
|
||||
);
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-ali-003',
|
||||
amount: 30,
|
||||
paymentType: 'alipay_direct',
|
||||
subject: 'Test Recharge',
|
||||
isMobile: true,
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
|
||||
expect(result.payUrl).toContain('alipay.trade.page.pay');
|
||||
// pageExecute was called twice: first wap.pay (failed), then page.pay
|
||||
expect(mockAlipayPageExecute).toHaveBeenCalledTimes(2);
|
||||
expect(mockAlipayPageExecute).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ product_code: 'QUICK_WAP_WAY' }),
|
||||
expect.objectContaining({ method: 'alipay.trade.wap.pay' }),
|
||||
);
|
||||
expect(mockAlipayPageExecute).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ product_code: 'FAST_INSTANT_TRADE_PAY' }),
|
||||
expect.objectContaining({ method: 'alipay.trade.page.pay' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('alipay_direct is in REDIRECT_PAYMENT_TYPES', () => {
|
||||
expect(REDIRECT_PAYMENT_TYPES.has('alipay_direct')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Wxpay Provider
|
||||
// ----------------------------------------------------------
|
||||
|
||||
describe('WxpayProvider', () => {
|
||||
let provider: WxpayProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new WxpayProvider();
|
||||
});
|
||||
|
||||
it('PC: uses Native order, returns qrCode (no payUrl)', async () => {
|
||||
mockWxpayCreatePcOrder.mockResolvedValue('weixin://wxpay/bizpayurl?pr=abc123');
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-wx-001',
|
||||
amount: 100,
|
||||
paymentType: 'wxpay_direct',
|
||||
subject: 'Test Recharge',
|
||||
clientIp: '1.2.3.4',
|
||||
isMobile: false,
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
|
||||
expect(result.tradeNo).toBe('order-wx-001');
|
||||
expect(result.qrCode).toBe('weixin://wxpay/bizpayurl?pr=abc123');
|
||||
expect(result.payUrl).toBeUndefined();
|
||||
|
||||
// createPcOrder was called, not createH5Order
|
||||
expect(mockWxpayCreatePcOrder).toHaveBeenCalledTimes(1);
|
||||
expect(mockWxpayCreateH5Order).not.toHaveBeenCalled();
|
||||
|
||||
// PC + qrCode (no payUrl) => shouldAutoRedirect = false (show QR)
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'wxpay_direct',
|
||||
payUrl: result.payUrl,
|
||||
qrCode: result.qrCode,
|
||||
isMobile: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('Mobile: uses H5 order, returns payUrl (no qrCode)', async () => {
|
||||
mockWxpayCreateH5Order.mockResolvedValue(
|
||||
'https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx123',
|
||||
);
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-wx-002',
|
||||
amount: 50,
|
||||
paymentType: 'wxpay_direct',
|
||||
subject: 'Test Recharge',
|
||||
clientIp: '2.3.4.5',
|
||||
isMobile: true,
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
|
||||
expect(result.tradeNo).toBe('order-wx-002');
|
||||
expect(result.payUrl).toContain('tenpay.com');
|
||||
expect(result.qrCode).toBeUndefined();
|
||||
|
||||
// createH5Order was called, not createPcOrder
|
||||
expect(mockWxpayCreateH5Order).toHaveBeenCalledTimes(1);
|
||||
expect(mockWxpayCreatePcOrder).not.toHaveBeenCalled();
|
||||
|
||||
// Mobile + payUrl => shouldAutoRedirect = true
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'wxpay_direct',
|
||||
payUrl: result.payUrl,
|
||||
qrCode: result.qrCode,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('Mobile: falls back to Native qrCode when H5 returns NO_AUTH', async () => {
|
||||
mockWxpayCreateH5Order.mockRejectedValue(new Error('Wxpay API error: [NO_AUTH] not authorized'));
|
||||
mockWxpayCreatePcOrder.mockResolvedValue('weixin://wxpay/bizpayurl?pr=fallback123');
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-wx-003',
|
||||
amount: 30,
|
||||
paymentType: 'wxpay_direct',
|
||||
subject: 'Test Recharge',
|
||||
clientIp: '3.4.5.6',
|
||||
isMobile: true,
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
|
||||
expect(result.tradeNo).toBe('order-wx-003');
|
||||
expect(result.qrCode).toBe('weixin://wxpay/bizpayurl?pr=fallback123');
|
||||
expect(result.payUrl).toBeUndefined();
|
||||
|
||||
// Both were called: H5 failed, then Native succeeded
|
||||
expect(mockWxpayCreateH5Order).toHaveBeenCalledTimes(1);
|
||||
expect(mockWxpayCreatePcOrder).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Mobile + qrCode only (no payUrl) => shouldAutoRedirect = false (show QR)
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'wxpay_direct',
|
||||
payUrl: result.payUrl,
|
||||
qrCode: result.qrCode,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('Mobile: re-throws non-NO_AUTH errors from H5', async () => {
|
||||
mockWxpayCreateH5Order.mockRejectedValue(new Error('Wxpay API error: [SYSTEMERROR] system error'));
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-wx-004',
|
||||
amount: 20,
|
||||
paymentType: 'wxpay_direct',
|
||||
subject: 'Test Recharge',
|
||||
clientIp: '4.5.6.7',
|
||||
isMobile: true,
|
||||
};
|
||||
|
||||
await expect(provider.createPayment(request)).rejects.toThrow('SYSTEMERROR');
|
||||
// Should not fall back to PC order
|
||||
expect(mockWxpayCreatePcOrder).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Mobile without clientIp: falls back to Native qrCode directly', async () => {
|
||||
mockWxpayCreatePcOrder.mockResolvedValue('weixin://wxpay/bizpayurl?pr=noip');
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-wx-005',
|
||||
amount: 10,
|
||||
paymentType: 'wxpay_direct',
|
||||
subject: 'Test Recharge',
|
||||
// No clientIp
|
||||
isMobile: true,
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
|
||||
expect(result.qrCode).toBe('weixin://wxpay/bizpayurl?pr=noip');
|
||||
expect(result.payUrl).toBeUndefined();
|
||||
// H5 was never attempted since clientIp is missing
|
||||
expect(mockWxpayCreateH5Order).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses request.notifyUrl as fallback when WXPAY_NOTIFY_URL is set', async () => {
|
||||
mockWxpayCreatePcOrder.mockResolvedValue('weixin://wxpay/bizpayurl?pr=withnotify');
|
||||
|
||||
const request: CreatePaymentRequest = {
|
||||
orderId: 'order-wx-006',
|
||||
amount: 10,
|
||||
paymentType: 'wxpay_direct',
|
||||
subject: 'Test',
|
||||
isMobile: false,
|
||||
notifyUrl: 'https://pay.example.com/api/wxpay/notify-alt',
|
||||
};
|
||||
|
||||
const result = await provider.createPayment(request);
|
||||
expect(result.qrCode).toBe('weixin://wxpay/bizpayurl?pr=withnotify');
|
||||
// WXPAY_NOTIFY_URL from env takes priority over request.notifyUrl
|
||||
expect(mockWxpayCreatePcOrder).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
notify_url: 'https://pay.example.com/api/wxpay/notify',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// shouldAutoRedirect logic (PaymentQRCode component)
|
||||
// ----------------------------------------------------------
|
||||
|
||||
describe('shouldAutoRedirect (PaymentQRCode logic)', () => {
|
||||
it('PC + qrCode + payUrl => false (show QR code, do not redirect)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay',
|
||||
payUrl: 'https://example.com/pay',
|
||||
qrCode: 'https://qr.alipay.com/xxx',
|
||||
isMobile: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('PC + payUrl only (no qrCode) => true (redirect)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay_direct',
|
||||
payUrl: 'https://openapi.alipay.com/gateway.do?...',
|
||||
qrCode: undefined,
|
||||
isMobile: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('PC + payUrl + empty qrCode string => true (redirect)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay_direct',
|
||||
payUrl: 'https://openapi.alipay.com/gateway.do?...',
|
||||
qrCode: '',
|
||||
isMobile: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('PC + payUrl + null qrCode => true (redirect)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay_direct',
|
||||
payUrl: 'https://openapi.alipay.com/gateway.do?...',
|
||||
qrCode: null,
|
||||
isMobile: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('Mobile + payUrl => true (redirect)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'wxpay_direct',
|
||||
payUrl: 'https://wx.tenpay.com/...',
|
||||
qrCode: undefined,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('Mobile + payUrl + qrCode => true (redirect, mobile always prefers payUrl)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay',
|
||||
payUrl: 'https://easypay.example.com/pay/xxx',
|
||||
qrCode: 'https://qr.alipay.com/xxx',
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('Mobile + qrCode only (no payUrl) => false (show QR code)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'wxpay_direct',
|
||||
payUrl: undefined,
|
||||
qrCode: 'weixin://wxpay/bizpayurl?pr=xxx',
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('Stripe => false (never redirect, uses Payment Element)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'stripe',
|
||||
payUrl: 'https://checkout.stripe.com/xxx',
|
||||
qrCode: undefined,
|
||||
isMobile: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('Stripe on mobile => false (still no redirect)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'stripe',
|
||||
payUrl: 'https://checkout.stripe.com/xxx',
|
||||
qrCode: undefined,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('Expired order => false (never redirect expired orders)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: true,
|
||||
paymentType: 'alipay',
|
||||
payUrl: 'https://example.com/pay',
|
||||
qrCode: undefined,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('No payUrl at all => false (nothing to redirect to)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay',
|
||||
payUrl: undefined,
|
||||
qrCode: undefined,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('Empty payUrl string => false (treated as falsy)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay',
|
||||
payUrl: '',
|
||||
qrCode: undefined,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('Null payUrl => false (treated as falsy)', () => {
|
||||
expect(
|
||||
shouldAutoRedirect({
|
||||
expired: false,
|
||||
paymentType: 'alipay',
|
||||
payUrl: null,
|
||||
qrCode: undefined,
|
||||
isMobile: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Utility function tests
|
||||
// ----------------------------------------------------------
|
||||
|
||||
describe('isStripeType', () => {
|
||||
it('returns true for "stripe"', () => {
|
||||
expect(isStripeType('stripe')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for stripe-prefixed types', () => {
|
||||
expect(isStripeType('stripe_card')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for alipay', () => {
|
||||
expect(isStripeType('alipay')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for wxpay', () => {
|
||||
expect(isStripeType('wxpay')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isStripeType(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for null', () => {
|
||||
expect(isStripeType(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REDIRECT_PAYMENT_TYPES', () => {
|
||||
it('includes alipay_direct', () => {
|
||||
expect(REDIRECT_PAYMENT_TYPES.has('alipay_direct')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not include alipay (easypay version)', () => {
|
||||
expect(REDIRECT_PAYMENT_TYPES.has('alipay')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not include wxpay types', () => {
|
||||
expect(REDIRECT_PAYMENT_TYPES.has('wxpay')).toBe(false);
|
||||
expect(REDIRECT_PAYMENT_TYPES.has('wxpay_direct')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not include stripe', () => {
|
||||
expect(REDIRECT_PAYMENT_TYPES.has('stripe')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user