payment-refund.integration.test.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import { describe, it, expect, beforeEach, vi } from 'vitest';
  2. import { DataSource } from 'typeorm';
  3. import {
  4. IntegrationTestDatabase,
  5. setupIntegrationDatabaseHooksWithEntities
  6. } from '@d8d/shared-test-util';
  7. import { PaymentMtService } from '../../src/services/payment.mt.service.js';
  8. import { PaymentMtEntity } from '../../src/entities/payment.mt.entity.js';
  9. import { PaymentStatus } from '../../src/entities/payment.types.js';
  10. // Mock 微信支付SDK
  11. vi.mock('wechatpay-node-v3', () => {
  12. return {
  13. default: vi.fn().mockImplementation(() => ({
  14. refunds: vi.fn().mockResolvedValue({
  15. id: 'mock_refund_id_123',
  16. out_refund_no: 'REFUND_ORDER_123_1234567890',
  17. status: 'SUCCESS'
  18. }),
  19. verifySign: vi.fn().mockResolvedValue(true),
  20. decipher_gcm: vi.fn().mockReturnValue(JSON.stringify({
  21. out_refund_no: 'REFUND_ORDER_123_1234567890',
  22. refund_status: 'SUCCESS'
  23. }))
  24. }))
  25. };
  26. });
  27. // Mock 系统配置服务
  28. vi.mock('@d8d/core-module-mt/system-config-module-mt', () => ({
  29. SystemConfigServiceMt: vi.fn().mockImplementation(() => ({
  30. getConfigsByKeys: vi.fn().mockResolvedValue({
  31. 'wx.payment.merchant.id': 'mock_merchant_id',
  32. 'wx.mini.app.id': 'mock_app_id',
  33. 'wx.payment.v3.key': 'mock_v3_key',
  34. 'wx.payment.notify.url': 'mock_notify_url',
  35. 'wx.payment.cert.serial.no': 'mock_cert_serial_no',
  36. 'wx.payment.public.key': 'mock_public_key',
  37. 'wx.payment.private.key': 'mock_private_key'
  38. })
  39. }))
  40. }));
  41. // 导入订单实体
  42. import { OrderMt, OrderGoodsMt, OrderRefundMt } from '@d8d/orders-module-mt';
  43. import { GoodsMt, GoodsCategoryMt } from '@d8d/goods-module-mt';
  44. import { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
  45. import { FileMt } from '@d8d/file-module-mt';
  46. import { MerchantMt } from '@d8d/merchant-module-mt';
  47. import { SupplierMt } from '@d8d/supplier-module-mt';
  48. import { DeliveryAddressMt } from '@d8d/delivery-address-module-mt';
  49. import { AreaEntityMt } from '@d8d/geo-areas-mt';
  50. import { SystemConfigMt } from '@d8d/core-module-mt/system-config-module-mt/entities';
  51. // 设置集成测试钩子
  52. setupIntegrationDatabaseHooksWithEntities([
  53. PaymentMtEntity, OrderMt, OrderGoodsMt, OrderRefundMt, GoodsMt, GoodsCategoryMt,
  54. UserEntityMt, RoleMt, FileMt, MerchantMt, SupplierMt, DeliveryAddressMt, AreaEntityMt, SystemConfigMt
  55. ])
  56. describe('PaymentRefund Integration Tests', () => {
  57. let dataSource: DataSource;
  58. let paymentService: PaymentMtService;
  59. beforeEach(async () => {
  60. // 获取集成测试数据库连接
  61. dataSource = await IntegrationTestDatabase.getDataSource();
  62. paymentService = new PaymentMtService(dataSource);
  63. });
  64. describe('refund method', () => {
  65. it('应该成功处理已支付订单的退款', async () => {
  66. // 准备测试数据
  67. const payment = new PaymentMtEntity();
  68. payment.tenantId = 1;
  69. payment.externalOrderId = 1001;
  70. payment.userId = 1;
  71. payment.totalAmount = 1000; // 10元
  72. payment.description = '测试订单';
  73. payment.paymentStatus = PaymentStatus.PAID;
  74. payment.outTradeNo = 'PAYMENT_1001_1234567890';
  75. payment.openid = 'mock_openid';
  76. payment.wechatTransactionId = 'mock_transaction_id';
  77. await dataSource.getRepository(PaymentMtEntity).save(payment);
  78. // 执行退款
  79. const refundResult = await paymentService.refund(
  80. 1,
  81. 'PAYMENT_1001_1234567890',
  82. 1000,
  83. '测试退款'
  84. );
  85. // 验证退款结果
  86. expect(refundResult).toMatchObject({
  87. refundId: expect.stringContaining('REFUND_PAYMENT_1001_1234567890'),
  88. outRefundNo: expect.stringContaining('REFUND_PAYMENT_1001_1234567890'),
  89. refundStatus: 'SUCCESS',
  90. refundAmount: 1000,
  91. refundTime: expect.any(String)
  92. });
  93. // 验证支付记录已更新
  94. const updatedPayment = await dataSource.getRepository(PaymentMtEntity).findOne({
  95. where: { outTradeNo: 'PAYMENT_1001_1234567890', tenantId: 1 }
  96. });
  97. expect(updatedPayment).toBeDefined();
  98. expect(updatedPayment?.refundStatus).toBe(PaymentStatus.REFUNDED);
  99. expect(updatedPayment?.refundAmount).toBe(1000);
  100. expect(updatedPayment?.refundTime).toBeInstanceOf(Date);
  101. });
  102. it('应该对不存在的支付记录抛出错误', async () => {
  103. await expect(
  104. paymentService.refund(1, 'NON_EXISTENT_ORDER', 1000)
  105. ).rejects.toThrow('支付记录不存在');
  106. });
  107. it('应该对未支付订单抛出错误', async () => {
  108. const payment = new PaymentMtEntity();
  109. payment.tenantId = 1;
  110. payment.externalOrderId = 1002;
  111. payment.userId = 1;
  112. payment.totalAmount = 1000;
  113. payment.description = '测试订单';
  114. payment.paymentStatus = PaymentStatus.PENDING;
  115. payment.outTradeNo = 'PAYMENT_1002_1234567890';
  116. payment.openid = 'mock_openid';
  117. await dataSource.getRepository(PaymentMtEntity).save(payment);
  118. await expect(
  119. paymentService.refund(1, 'PAYMENT_1002_1234567890', 1000)
  120. ).rejects.toThrow('订单支付状态不正确');
  121. });
  122. it('应该对无效退款金额抛出错误', async () => {
  123. const payment = new PaymentMtEntity();
  124. payment.tenantId = 1;
  125. payment.externalOrderId = 1003;
  126. payment.userId = 1;
  127. payment.totalAmount = 1000;
  128. payment.description = '测试订单';
  129. payment.paymentStatus = PaymentStatus.PAID;
  130. payment.outTradeNo = 'PAYMENT_1003_1234567890';
  131. payment.openid = 'mock_openid';
  132. payment.wechatTransactionId = 'mock_transaction_id';
  133. await dataSource.getRepository(PaymentMtEntity).save(payment);
  134. // 测试退款金额为0
  135. await expect(
  136. paymentService.refund(1, 'PAYMENT_1003_1234567890', 0)
  137. ).rejects.toThrow('退款金额无效');
  138. // 测试退款金额超过支付金额
  139. await expect(
  140. paymentService.refund(1, 'PAYMENT_1003_1234567890', 2000)
  141. ).rejects.toThrow('退款金额无效');
  142. });
  143. });
  144. describe('handleRefundCallback method', () => {
  145. it('应该成功处理退款回调', async () => {
  146. const mockCallbackData = {
  147. resource: {
  148. ciphertext: 'mock_ciphertext',
  149. associated_data: '',
  150. nonce: 'mock_nonce'
  151. }
  152. };
  153. const mockHeaders = {
  154. 'wechatpay-timestamp': 'mock_timestamp',
  155. 'wechatpay-nonce': 'mock_nonce',
  156. 'wechatpay-signature': 'mock_signature',
  157. 'wechatpay-serial': 'mock_serial'
  158. };
  159. const mockRawBody = 'mock_raw_body';
  160. // 执行退款回调处理
  161. await expect(
  162. paymentService.handleRefundCallback(mockCallbackData, mockHeaders, mockRawBody)
  163. ).resolves.not.toThrow();
  164. });
  165. });
  166. describe('multi-tenant refund data isolation', () => {
  167. it('应该只退款特定租户的支付记录', async () => {
  168. // 创建租户1的支付记录
  169. const payment1 = new PaymentMtEntity();
  170. payment1.tenantId = 1;
  171. payment1.externalOrderId = 1004;
  172. payment1.userId = 1;
  173. payment1.totalAmount = 1000;
  174. payment1.description = '租户1订单';
  175. payment1.paymentStatus = PaymentStatus.PAID;
  176. payment1.outTradeNo = 'PAYMENT_1004_1234567890';
  177. payment1.openid = 'mock_openid';
  178. payment1.wechatTransactionId = 'mock_transaction_id';
  179. // 创建租户2的支付记录
  180. const payment2 = new PaymentMtEntity();
  181. payment2.tenantId = 2;
  182. payment2.externalOrderId = 1004;
  183. payment2.userId = 1;
  184. payment2.totalAmount = 1000;
  185. payment2.description = '租户2订单';
  186. payment2.paymentStatus = PaymentStatus.PAID;
  187. payment2.outTradeNo = 'PAYMENT_1004_1234567890';
  188. payment2.openid = 'mock_openid';
  189. payment2.wechatTransactionId = 'mock_transaction_id';
  190. await dataSource.getRepository(PaymentMtEntity).save([payment1, payment2]);
  191. // 为租户1执行退款
  192. await paymentService.refund(1, 'PAYMENT_1004_1234567890', 1000);
  193. // 验证租户1的支付记录已更新
  194. const tenant1Payment = await dataSource.getRepository(PaymentMtEntity).findOne({
  195. where: { outTradeNo: 'PAYMENT_1004_1234567890', tenantId: 1 }
  196. });
  197. expect(tenant1Payment?.refundStatus).toBe(PaymentStatus.REFUNDED);
  198. // 验证租户2的支付记录未受影响
  199. const tenant2Payment = await dataSource.getRepository(PaymentMtEntity).findOne({
  200. where: { outTradeNo: 'PAYMENT_1004_1234567890', tenantId: 2 }
  201. });
  202. expect(tenant2Payment?.refundStatus).toBeNull();
  203. });
  204. });
  205. });