payment.service.test.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. /**
  2. * Payment Service 单元测试
  3. * 测试订单创建、支付回调、会员激活
  4. */
  5. import { describe, it, expect, beforeEach, vi } from 'vitest';
  6. import { generateOrderNo } from '@/modules/payment/payment.service';
  7. // 直接 Mock prisma 模型
  8. const mockOrderFindFirst = vi.fn();
  9. const mockOrderFindUnique = vi.fn();
  10. const mockOrderUpdate = vi.fn();
  11. const mockOrderCreate = vi.fn();
  12. const mockUserUpdate = vi.fn();
  13. const mockUserFindUnique = vi.fn();
  14. const mockPlanFindUnique = vi.fn();
  15. const mockSubscriptionFindFirst = vi.fn();
  16. const mockSubscriptionCreate = vi.fn();
  17. const mockSubscriptionUpdate = vi.fn();
  18. const mockTokenBalanceFindUnique = vi.fn();
  19. const mockTokenBalanceUpdate = vi.fn();
  20. const mockTokenBalanceCreate = vi.fn();
  21. vi.mock('@/models', () => ({
  22. prisma: {
  23. order: {
  24. findFirst: (...args: any[]) => mockOrderFindFirst(...args),
  25. findUnique: (...args: any[]) => mockOrderFindUnique(...args),
  26. update: (...args: any[]) => mockOrderUpdate(...args),
  27. create: (...args: any[]) => mockOrderCreate(...args),
  28. },
  29. user: {
  30. update: (...args: any[]) => mockUserUpdate(...args),
  31. findUnique: (...args: any[]) => mockUserFindUnique(...args),
  32. },
  33. subscriptionPlan: {
  34. findUnique: (...args: any[]) => mockPlanFindUnique(...args),
  35. },
  36. subscription: {
  37. findFirst: (...args: any[]) => mockSubscriptionFindFirst(...args),
  38. create: (...args: any[]) => mockSubscriptionCreate(...args),
  39. update: (...args: any[]) => mockSubscriptionUpdate(...args),
  40. },
  41. tokenBalance: {
  42. findUnique: (...args: any[]) => mockTokenBalanceFindUnique(...args),
  43. update: (...args: any[]) => mockTokenBalanceUpdate(...args),
  44. create: (...args: any[]) => mockTokenBalanceCreate(...args),
  45. },
  46. },
  47. }));
  48. describe('generateOrderNo', () => {
  49. it('生成格式正确的订单号', () => {
  50. const orderNo = generateOrderNo();
  51. expect(orderNo).toMatch(/^PAY\d{8}[A-Z0-9]{6}$/);
  52. });
  53. it('每次调用生成不同的订单号', () => {
  54. const orderNo1 = generateOrderNo();
  55. const orderNo2 = generateOrderNo();
  56. expect(orderNo1).not.toBe(orderNo2);
  57. });
  58. it('订单号包含当前日期', () => {
  59. const orderNo = generateOrderNo();
  60. const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
  61. expect(orderNo).toContain(today);
  62. });
  63. });
  64. describe('createPaymentOrder', () => {
  65. beforeEach(() => {
  66. vi.clearAllMocks();
  67. });
  68. it('套餐不存在时抛出错误', async () => {
  69. const { createPaymentOrder } = await import('@/modules/payment/payment.service');
  70. mockPlanFindUnique.mockResolvedValue(null);
  71. await expect(
  72. createPaymentOrder(1, 999, 'mock')
  73. ).rejects.toThrow('套餐不存在');
  74. });
  75. it('成功创建 mock 订单', async () => {
  76. const { createPaymentOrder } = await import('@/modules/payment/payment.service');
  77. mockPlanFindUnique.mockResolvedValue({ id: 1, name: '专业版', priceMonthly: 59, priceYearly: 590 });
  78. mockOrderCreate.mockResolvedValue({ id: 1, status: 'pending' });
  79. const result = await createPaymentOrder(1, 1, 'mock');
  80. expect(result.planName).toBe('专业版');
  81. expect(result.amount).toBe(59);
  82. expect(result.orderNo).toBeDefined();
  83. });
  84. it('月付使用 monthly 价格', async () => {
  85. const { createPaymentOrder } = await import('@/modules/payment/payment.service');
  86. mockPlanFindUnique.mockResolvedValue({ id: 1, name: '专业版', priceMonthly: 59, priceYearly: 590 });
  87. mockOrderCreate.mockResolvedValue({ id: 1, status: 'pending' });
  88. const result = await createPaymentOrder(1, 1, 'mock', 'monthly');
  89. expect(result.amount).toBe(59);
  90. });
  91. it('年付使用 yearly 价格', async () => {
  92. const { createPaymentOrder } = await import('@/modules/payment/payment.service');
  93. mockPlanFindUnique.mockResolvedValue({ id: 1, name: '专业版', priceMonthly: 59, priceYearly: 590 });
  94. mockOrderCreate.mockResolvedValue({ id: 1, status: 'pending' });
  95. const result = await createPaymentOrder(1, 1, 'mock', 'yearly');
  96. expect(result.amount).toBe(590);
  97. });
  98. });
  99. describe('handlePaymentCallback', () => {
  100. beforeEach(() => {
  101. vi.clearAllMocks();
  102. });
  103. it('订单不存在时抛出错误', async () => {
  104. const { handlePaymentCallback } = await import('@/modules/payment/payment.service');
  105. mockOrderFindFirst.mockResolvedValue(null);
  106. await expect(
  107. handlePaymentCallback('NONEXISTENT', 'PAY123', 'success')
  108. ).rejects.toThrow('订单不存在');
  109. });
  110. it('订单已支付时返回幂等成功', async () => {
  111. const { handlePaymentCallback } = await import('@/modules/payment/payment.service');
  112. mockOrderFindFirst.mockResolvedValue({
  113. id: 1,
  114. orderNo: 'PAY20260519000001',
  115. status: 'paid',
  116. userId: 1,
  117. });
  118. const result = await handlePaymentCallback('PAY20260519000001', 'PAY123', 'success');
  119. expect(result.success).toBe(true);
  120. expect(result.message).toContain('已支付');
  121. });
  122. it('支付失败时更新订单状态', async () => {
  123. const { handlePaymentCallback } = await import('@/modules/payment/payment.service');
  124. mockOrderFindFirst
  125. .mockResolvedValueOnce({
  126. id: 1,
  127. orderNo: 'PAY20260519000001',
  128. status: 'pending',
  129. userId: 1,
  130. })
  131. .mockResolvedValueOnce({
  132. id: 1,
  133. orderNo: 'PAY20260519000001',
  134. status: 'failed',
  135. userId: 1,
  136. });
  137. mockOrderUpdate.mockResolvedValue({ id: 1, status: 'failed' });
  138. const result = await handlePaymentCallback('PAY20260519000001', 'PAY123', 'failed');
  139. expect(result.success).toBe(false);
  140. expect(result.message).toContain('失败');
  141. });
  142. it('支付成功时调用 activateSubscription', async () => {
  143. const { handlePaymentCallback } = await import('@/modules/payment/payment.service');
  144. mockOrderFindFirst
  145. .mockResolvedValueOnce({
  146. id: 1,
  147. orderNo: 'PAY20260519000001',
  148. status: 'pending',
  149. userId: 1,
  150. planId: 2,
  151. productType: 'monthly',
  152. })
  153. .mockResolvedValueOnce({
  154. id: 1,
  155. orderNo: 'PAY20260519000001',
  156. status: 'paid',
  157. userId: 1,
  158. planId: 2,
  159. });
  160. mockOrderUpdate.mockResolvedValue({});
  161. mockPlanFindUnique.mockResolvedValue({ id: 2, level: 2, monthlyTokens: 15000 });
  162. mockSubscriptionFindFirst.mockResolvedValue(null);
  163. mockUserUpdate.mockResolvedValue({});
  164. mockTokenBalanceFindUnique.mockResolvedValue(null);
  165. mockTokenBalanceCreate.mockResolvedValue({});
  166. const result = await handlePaymentCallback('PAY20260519000001', 'PAY123', 'success');
  167. expect(result.success).toBe(true);
  168. expect(result.message).toContain('成功');
  169. });
  170. });