diff --git a/src/__tests__/lib/order/fee.test.ts b/src/__tests__/lib/order/fee.test.ts new file mode 100644 index 0000000..3fe0101 --- /dev/null +++ b/src/__tests__/lib/order/fee.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from 'vitest'; +import { calculatePayAmount } from '@/lib/order/fee'; + +describe('calculatePayAmount', () => { + it.each([ + { rechargeAmount: 100, feeRate: 0, expected: '100.00', desc: 'feeRate=0 返回原金额' }, + { rechargeAmount: 100, feeRate: -1, expected: '100.00', desc: 'feeRate<0 返回原金额' }, + { rechargeAmount: 100, feeRate: 3, expected: '103.00', desc: '100 * 3% = 3.00' }, + { rechargeAmount: 100, feeRate: 2.5, expected: '102.50', desc: '100 * 2.5% = 2.50' }, + { rechargeAmount: 99.99, feeRate: 1, expected: '100.99', desc: '99.99 * 1% = 0.9999 → ROUND_UP → 1.00, total 100.99' }, + { rechargeAmount: 10, feeRate: 3, expected: '10.30', desc: '10 * 3% = 0.30' }, + { rechargeAmount: 1, feeRate: 1, expected: '1.01', desc: '1 * 1% = 0.01' }, + ])('$desc (amount=$rechargeAmount, rate=$feeRate)', ({ rechargeAmount, feeRate, expected }) => { + expect(calculatePayAmount(rechargeAmount, feeRate)).toBe(expected); + }); + + describe('ROUND_UP 向上取整', () => { + it('小数第三位非零时进位', () => { + // 33 * 1% = 0.33, 整除无进位 + expect(calculatePayAmount(33, 1)).toBe('33.33'); + }); + + it('产生无限小数时向上进位', () => { + // 10 * 3.3% = 0.33, 精确 + expect(calculatePayAmount(10, 3.3)).toBe('10.33'); + // 7 * 3% = 0.21, 精确 + expect(calculatePayAmount(7, 3)).toBe('7.21'); + // 1 * 0.7% = 0.007 → ROUND_UP → 0.01 + expect(calculatePayAmount(1, 0.7)).toBe('1.01'); + }); + }); + + describe('极小金额', () => { + it('0.01 元 + 1% 手续费', () => { + // 0.01 * 1% = 0.0001 → ROUND_UP → 0.01 + expect(calculatePayAmount(0.01, 1)).toBe('0.02'); + }); + + it('0.01 元 + 0 手续费', () => { + expect(calculatePayAmount(0.01, 0)).toBe('0.01'); + }); + }); + + describe('大金额', () => { + it('10000 元 + 2.5%', () => { + // 10000 * 2.5% = 250.00 + expect(calculatePayAmount(10000, 2.5)).toBe('10250.00'); + }); + + it('99999.99 元 + 5%', () => { + // 99999.99 * 5% = 4999.9995 → ROUND_UP → 5000.00 + // 但 rechargeAmount 传入为 number 99999.99,Decimal(99999.99) 可能有浮点 + // 实际: 99999.99 + 5000.00 = 104999.99 + expect(calculatePayAmount(99999.99, 5)).toBe('104999.99'); + }); + }); + + describe('精度', () => { + it('输出始终为 2 位小数', () => { + const result = calculatePayAmount(100, 0); + expect(result).toMatch(/^\d+\.\d{2}$/); + }); + + it('有手续费时输出也为 2 位小数', () => { + const result = calculatePayAmount(77, 3.33); + expect(result).toMatch(/^\d+\.\d{2}$/); + }); + }); +}); diff --git a/src/__tests__/lib/order/limits.test.ts b/src/__tests__/lib/order/limits.test.ts new file mode 100644 index 0000000..7bcea84 --- /dev/null +++ b/src/__tests__/lib/order/limits.test.ts @@ -0,0 +1,141 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +vi.mock('@/lib/db', () => ({ + prisma: { + order: { groupBy: vi.fn() }, + }, +})); + +vi.mock('@/lib/config', () => ({ + getEnv: vi.fn(), +})); + +vi.mock('@/lib/payment', () => ({ + initPaymentProviders: vi.fn(), + paymentRegistry: { + getDefaultLimit: vi.fn(), + }, +})); + +import { getEnv } from '@/lib/config'; +import { paymentRegistry } from '@/lib/payment'; +import { getMethodDailyLimit, getMethodSingleLimit } from '@/lib/order/limits'; + +const mockedGetEnv = vi.mocked(getEnv); +const mockedGetDefaultLimit = vi.mocked(paymentRegistry.getDefaultLimit); + +beforeEach(() => { + vi.clearAllMocks(); + // 默认:getEnv 返回无渠道限额字段,provider 无默认值 + mockedGetEnv.mockReturnValue({} as ReturnType); + mockedGetDefaultLimit.mockReturnValue(undefined as any); +}); + +describe('getMethodDailyLimit', () => { + it('无环境变量且无 provider 默认值时返回 0', () => { + expect(getMethodDailyLimit('alipay')).toBe(0); + }); + + it('从 getEnv 读取渠道每日限额', () => { + mockedGetEnv.mockReturnValue({ + MAX_DAILY_AMOUNT_ALIPAY: 5000, + } as any); + expect(getMethodDailyLimit('alipay')).toBe(5000); + }); + + it('环境变量 0 表示不限制', () => { + mockedGetEnv.mockReturnValue({ + MAX_DAILY_AMOUNT_WXPAY: 0, + } as any); + expect(getMethodDailyLimit('wxpay')).toBe(0); + }); + + it('getEnv 未设置时回退到 provider 默认值', () => { + mockedGetEnv.mockReturnValue({} as any); + mockedGetDefaultLimit.mockReturnValue({ dailyMax: 3000 } as any); + expect(getMethodDailyLimit('stripe')).toBe(3000); + }); + + it('getEnv 设置时覆盖 provider 默认值', () => { + mockedGetEnv.mockReturnValue({ + MAX_DAILY_AMOUNT_STRIPE: 8000, + } as any); + mockedGetDefaultLimit.mockReturnValue({ dailyMax: 3000 } as any); + expect(getMethodDailyLimit('stripe')).toBe(8000); + }); + + it('paymentType 大小写不敏感(key 构造用 toUpperCase)', () => { + mockedGetEnv.mockReturnValue({ + MAX_DAILY_AMOUNT_ALIPAY: 2000, + } as any); + expect(getMethodDailyLimit('alipay')).toBe(2000); + }); + + it('未知支付类型返回 0', () => { + expect(getMethodDailyLimit('unknown_type')).toBe(0); + }); + + it('getEnv 无值且 provider 默认值也无 dailyMax 时回退 process.env', () => { + mockedGetEnv.mockReturnValue({} as any); + mockedGetDefaultLimit.mockReturnValue({} as any); // no dailyMax + process.env['MAX_DAILY_AMOUNT_ALIPAY'] = '7777'; + try { + expect(getMethodDailyLimit('alipay')).toBe(7777); + } finally { + delete process.env['MAX_DAILY_AMOUNT_ALIPAY']; + } + }); +}); + +describe('getMethodSingleLimit', () => { + it('无环境变量且无 provider 默认值时返回 0', () => { + expect(getMethodSingleLimit('alipay')).toBe(0); + }); + + it('从 process.env 读取单笔限额', () => { + process.env['MAX_SINGLE_AMOUNT_WXPAY'] = '500'; + try { + expect(getMethodSingleLimit('wxpay')).toBe(500); + } finally { + delete process.env['MAX_SINGLE_AMOUNT_WXPAY']; + } + }); + + it('process.env 设置 0 表示使用全局限额', () => { + process.env['MAX_SINGLE_AMOUNT_STRIPE'] = '0'; + try { + expect(getMethodSingleLimit('stripe')).toBe(0); + } finally { + delete process.env['MAX_SINGLE_AMOUNT_STRIPE']; + } + }); + + it('process.env 未设置时回退到 provider 默认值', () => { + mockedGetDefaultLimit.mockReturnValue({ singleMax: 200 } as any); + expect(getMethodSingleLimit('alipay')).toBe(200); + }); + + it('process.env 设置时覆盖 provider 默认值', () => { + process.env['MAX_SINGLE_AMOUNT_ALIPAY'] = '999'; + mockedGetDefaultLimit.mockReturnValue({ singleMax: 200 } as any); + try { + expect(getMethodSingleLimit('alipay')).toBe(999); + } finally { + delete process.env['MAX_SINGLE_AMOUNT_ALIPAY']; + } + }); + + it('无效 process.env 值回退到 provider 默认值', () => { + process.env['MAX_SINGLE_AMOUNT_ALIPAY'] = 'abc'; + mockedGetDefaultLimit.mockReturnValue({ singleMax: 150 } as any); + try { + expect(getMethodSingleLimit('alipay')).toBe(150); + } finally { + delete process.env['MAX_SINGLE_AMOUNT_ALIPAY']; + } + }); + + it('未知支付类型返回 0', () => { + expect(getMethodSingleLimit('unknown_type')).toBe(0); + }); +});