|
|
@@ -0,0 +1,228 @@
|
|
|
+import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
+import { DataSource } from 'typeorm';
|
|
|
+import { PaymentMtService } from '../../src/services/payment.mt.service.js';
|
|
|
+import { PaymentMtEntity } from '../../src/entities/payment.mt.entity.js';
|
|
|
+import { PaymentStatus } from '../../src/entities/payment.types.js';
|
|
|
+
|
|
|
+// Mock 微信支付SDK
|
|
|
+vi.mock('wechatpay-node-v3', () => {
|
|
|
+ return {
|
|
|
+ default: vi.fn().mockImplementation(() => ({
|
|
|
+ refunds: vi.fn().mockResolvedValue({
|
|
|
+ id: 'mock_refund_id_123',
|
|
|
+ out_refund_no: 'REFUND_ORDER_123_1234567890',
|
|
|
+ status: 'SUCCESS'
|
|
|
+ }),
|
|
|
+ verifySign: vi.fn().mockResolvedValue(true),
|
|
|
+ decipher_gcm: vi.fn().mockReturnValue(JSON.stringify({
|
|
|
+ out_refund_no: 'REFUND_ORDER_123_1234567890',
|
|
|
+ refund_status: 'SUCCESS'
|
|
|
+ }))
|
|
|
+ }))
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+// Mock 系统配置服务
|
|
|
+vi.mock('@d8d/core-module-mt/system-config-module-mt', () => ({
|
|
|
+ SystemConfigServiceMt: vi.fn().mockImplementation(() => ({
|
|
|
+ getConfigsByKeys: vi.fn().mockResolvedValue({
|
|
|
+ 'wx.payment.merchant.id': 'mock_merchant_id',
|
|
|
+ 'wx.mini.app.id': 'mock_app_id',
|
|
|
+ 'wx.payment.v3.key': 'mock_v3_key',
|
|
|
+ 'wx.payment.notify.url': 'mock_notify_url',
|
|
|
+ 'wx.payment.cert.serial.no': 'mock_cert_serial_no',
|
|
|
+ 'wx.payment.public.key': 'mock_public_key',
|
|
|
+ 'wx.payment.private.key': 'mock_private_key'
|
|
|
+ })
|
|
|
+ }))
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock 订单服务
|
|
|
+vi.mock('@d8d/orders-module-mt', () => ({
|
|
|
+ OrderMtService: vi.fn().mockImplementation(() => ({})),
|
|
|
+ OrderMt: vi.fn()
|
|
|
+}));
|
|
|
+
|
|
|
+describe('PaymentRefund Integration Tests', () => {
|
|
|
+ let dataSource: DataSource;
|
|
|
+ let paymentService: PaymentMtService;
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ // 创建内存数据库连接
|
|
|
+ dataSource = new DataSource({
|
|
|
+ type: 'sqlite',
|
|
|
+ database: ':memory:',
|
|
|
+ entities: [PaymentMtEntity],
|
|
|
+ synchronize: true,
|
|
|
+ logging: false
|
|
|
+ });
|
|
|
+
|
|
|
+ await dataSource.initialize();
|
|
|
+ paymentService = new PaymentMtService(dataSource);
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('refund method', () => {
|
|
|
+ it('应该成功处理已支付订单的退款', async () => {
|
|
|
+ // 准备测试数据
|
|
|
+ const payment = new PaymentMtEntity();
|
|
|
+ payment.tenantId = 1;
|
|
|
+ payment.externalOrderId = 1001;
|
|
|
+ payment.userId = 1;
|
|
|
+ payment.totalAmount = 1000; // 10元
|
|
|
+ payment.description = '测试订单';
|
|
|
+ payment.paymentStatus = PaymentStatus.PAID;
|
|
|
+ payment.outTradeNo = 'PAYMENT_1001_1234567890';
|
|
|
+ payment.openid = 'mock_openid';
|
|
|
+ payment.wechatTransactionId = 'mock_transaction_id';
|
|
|
+
|
|
|
+ await dataSource.getRepository(PaymentMtEntity).save(payment);
|
|
|
+
|
|
|
+ // 执行退款
|
|
|
+ const refundResult = await paymentService.refund(
|
|
|
+ 1,
|
|
|
+ 'PAYMENT_1001_1234567890',
|
|
|
+ 1000,
|
|
|
+ '测试退款'
|
|
|
+ );
|
|
|
+
|
|
|
+ // 验证退款结果
|
|
|
+ expect(refundResult).toMatchObject({
|
|
|
+ refundId: expect.stringContaining('REFUND_PAYMENT_1001_1234567890'),
|
|
|
+ outRefundNo: expect.stringContaining('REFUND_PAYMENT_1001_1234567890'),
|
|
|
+ refundStatus: 'SUCCESS',
|
|
|
+ refundAmount: 1000,
|
|
|
+ refundTime: expect.any(String)
|
|
|
+ });
|
|
|
+
|
|
|
+ // 验证支付记录已更新
|
|
|
+ const updatedPayment = await dataSource.getRepository(PaymentMtEntity).findOne({
|
|
|
+ where: { outTradeNo: 'PAYMENT_1001_1234567890', tenantId: 1 }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(updatedPayment).toBeDefined();
|
|
|
+ expect(updatedPayment?.refundStatus).toBe(PaymentStatus.REFUNDED);
|
|
|
+ expect(updatedPayment?.refundAmount).toBe(1000);
|
|
|
+ expect(updatedPayment?.refundTime).toBeInstanceOf(Date);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该对不存在的支付记录抛出错误', async () => {
|
|
|
+ await expect(
|
|
|
+ paymentService.refund(1, 'NON_EXISTENT_ORDER', 1000)
|
|
|
+ ).rejects.toThrow('支付记录不存在');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该对未支付订单抛出错误', async () => {
|
|
|
+ const payment = new PaymentMtEntity();
|
|
|
+ payment.tenantId = 1;
|
|
|
+ payment.externalOrderId = 1002;
|
|
|
+ payment.userId = 1;
|
|
|
+ payment.totalAmount = 1000;
|
|
|
+ payment.description = '测试订单';
|
|
|
+ payment.paymentStatus = PaymentStatus.PENDING;
|
|
|
+ payment.outTradeNo = 'PAYMENT_1002_1234567890';
|
|
|
+ payment.openid = 'mock_openid';
|
|
|
+
|
|
|
+ await dataSource.getRepository(PaymentMtEntity).save(payment);
|
|
|
+
|
|
|
+ await expect(
|
|
|
+ paymentService.refund(1, 'PAYMENT_1002_1234567890', 1000)
|
|
|
+ ).rejects.toThrow('订单支付状态不正确');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该对无效退款金额抛出错误', async () => {
|
|
|
+ const payment = new PaymentMtEntity();
|
|
|
+ payment.tenantId = 1;
|
|
|
+ payment.externalOrderId = 1003;
|
|
|
+ payment.userId = 1;
|
|
|
+ payment.totalAmount = 1000;
|
|
|
+ payment.description = '测试订单';
|
|
|
+ payment.paymentStatus = PaymentStatus.PAID;
|
|
|
+ payment.outTradeNo = 'PAYMENT_1003_1234567890';
|
|
|
+ payment.openid = 'mock_openid';
|
|
|
+ payment.wechatTransactionId = 'mock_transaction_id';
|
|
|
+
|
|
|
+ await dataSource.getRepository(PaymentMtEntity).save(payment);
|
|
|
+
|
|
|
+ // 测试退款金额为0
|
|
|
+ await expect(
|
|
|
+ paymentService.refund(1, 'PAYMENT_1003_1234567890', 0)
|
|
|
+ ).rejects.toThrow('退款金额无效');
|
|
|
+
|
|
|
+ // 测试退款金额超过支付金额
|
|
|
+ await expect(
|
|
|
+ paymentService.refund(1, 'PAYMENT_1003_1234567890', 2000)
|
|
|
+ ).rejects.toThrow('退款金额无效');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('handleRefundCallback method', () => {
|
|
|
+ it('应该成功处理退款回调', async () => {
|
|
|
+ const mockCallbackData = {
|
|
|
+ resource: {
|
|
|
+ ciphertext: 'mock_ciphertext',
|
|
|
+ associated_data: '',
|
|
|
+ nonce: 'mock_nonce'
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const mockHeaders = {
|
|
|
+ 'wechatpay-timestamp': 'mock_timestamp',
|
|
|
+ 'wechatpay-nonce': 'mock_nonce',
|
|
|
+ 'wechatpay-signature': 'mock_signature',
|
|
|
+ 'wechatpay-serial': 'mock_serial'
|
|
|
+ };
|
|
|
+
|
|
|
+ const mockRawBody = 'mock_raw_body';
|
|
|
+
|
|
|
+ // 执行退款回调处理
|
|
|
+ await expect(
|
|
|
+ paymentService.handleRefundCallback(mockCallbackData, mockHeaders, mockRawBody)
|
|
|
+ ).resolves.not.toThrow();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('multi-tenant refund data isolation', () => {
|
|
|
+ it('应该只退款特定租户的支付记录', async () => {
|
|
|
+ // 创建租户1的支付记录
|
|
|
+ const payment1 = new PaymentMtEntity();
|
|
|
+ payment1.tenantId = 1;
|
|
|
+ payment1.externalOrderId = 1004;
|
|
|
+ payment1.userId = 1;
|
|
|
+ payment1.totalAmount = 1000;
|
|
|
+ payment1.description = '租户1订单';
|
|
|
+ payment1.paymentStatus = PaymentStatus.PAID;
|
|
|
+ payment1.outTradeNo = 'PAYMENT_1004_1234567890';
|
|
|
+ payment1.openid = 'mock_openid';
|
|
|
+ payment1.wechatTransactionId = 'mock_transaction_id';
|
|
|
+
|
|
|
+ // 创建租户2的支付记录
|
|
|
+ const payment2 = new PaymentMtEntity();
|
|
|
+ payment2.tenantId = 2;
|
|
|
+ payment2.externalOrderId = 1004;
|
|
|
+ payment2.userId = 1;
|
|
|
+ payment2.totalAmount = 1000;
|
|
|
+ payment2.description = '租户2订单';
|
|
|
+ payment2.paymentStatus = PaymentStatus.PAID;
|
|
|
+ payment2.outTradeNo = 'PAYMENT_1004_1234567890';
|
|
|
+ payment2.openid = 'mock_openid';
|
|
|
+ payment2.wechatTransactionId = 'mock_transaction_id';
|
|
|
+
|
|
|
+ await dataSource.getRepository(PaymentMtEntity).save([payment1, payment2]);
|
|
|
+
|
|
|
+ // 为租户1执行退款
|
|
|
+ await paymentService.refund(1, 'PAYMENT_1004_1234567890', 1000);
|
|
|
+
|
|
|
+ // 验证租户1的支付记录已更新
|
|
|
+ const tenant1Payment = await dataSource.getRepository(PaymentMtEntity).findOne({
|
|
|
+ where: { outTradeNo: 'PAYMENT_1004_1234567890', tenantId: 1 }
|
|
|
+ });
|
|
|
+ expect(tenant1Payment?.refundStatus).toBe(PaymentStatus.REFUNDED);
|
|
|
+
|
|
|
+ // 验证租户2的支付记录未受影响
|
|
|
+ const tenant2Payment = await dataSource.getRepository(PaymentMtEntity).findOne({
|
|
|
+ where: { outTradeNo: 'PAYMENT_1004_1234567890', tenantId: 2 }
|
|
|
+ });
|
|
|
+ expect(tenant2Payment?.refundStatus).toBeUndefined();
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|