/** * 兑换券售后退款与订单失效闭环返还专项自动化测试 * * 验证重点: * 1. Partner.php 监听器中 checkIsAllRefund 正确排除 REFUND_STATUS_AGREE (1) 与 COMPLETED (2),确保单品及全单退款顺利触发 * 2. Partner.php orderItemRefundAfter 增加 refundConsume 兜底返还已核销兑换券 * 3. Order.php invalid() 在订单关闭或取消时触发 refundConsume 兜底返还 * 4. 幂等防重与零元消费场景模拟 */ const fs = require('fs'); const path = require('path'); let passCount = 0; let failCount = 0; function assert(condition, message) { if (condition) { console.log(` \x1b[32m[PASS]\x1b[0m ${message}`); passCount++; } else { console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`); failCount++; } } console.log('========================================================'); console.log(' 兑换券退款与失效闭环返还自动化验证'); console.log('========================================================\n'); // 1. Partner.php 监听器审查 const partnerListenerPath = path.resolve(__dirname, '../addons/shopro/listener/Partner.php'); assert(fs.existsSync(partnerListenerPath), 'Partner.php 监听器文件存在'); if (fs.existsSync(partnerListenerPath)) { const content = fs.readFileSync(partnerListenerPath, 'utf-8'); assert(content.includes("(new VoucherService())->refundConsume($order)"), 'orderItemRefundAfter 显式调用 refundConsume 进行已核销额度原路退回'); assert(content.includes('REFUND_STATUS_AGREE'), 'checkIsAllRefund 包含 REFUND_STATUS_AGREE 排除判定'); assert(content.includes('REFUND_STATUS_COMPLETED'), 'checkIsAllRefund 包含 REFUND_STATUS_COMPLETED 排除判定'); assert(content.includes('whereNotIn'), 'checkIsAllRefund 采用 whereNotIn 精准排除已同意和已完成的退款'); } // 2. Order.php 监听器审查 const orderListenerPath = path.resolve(__dirname, '../addons/shopro/listener/Order.php'); assert(fs.existsSync(orderListenerPath), 'Order.php 监听器文件存在'); if (fs.existsSync(orderListenerPath)) { const content = fs.readFileSync(orderListenerPath, 'utf-8'); const invalidIdx = content.indexOf('function invalid('); const invalidBlock = content.slice(invalidIdx, invalidIdx + 500); assert(invalidBlock.includes('refundConsume($order)'), 'Order.php invalid() 包含兑换券原路退回调用'); } // 3. VoucherService.php 幂等机制审查 const voucherServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/VoucherService.php'); assert(fs.existsSync(voucherServicePath), 'VoucherService.php 文件存在'); if (fs.existsSync(voucherServicePath)) { const content = fs.readFileSync(voucherServicePath, 'utf-8'); assert(content.includes('empty($ext[\'voucher_consumed\'])'), 'refundConsume 具备未核销拦截保护'); assert(content.includes('$alreadyRefunded'), 'refundConsume 具备已退款历史统计'); assert(content.includes('$remainVoucher'), 'refundConsume 严格限制单次退款不超过剩余可退额'); assert(content.includes('voucher_consume_refund'), 'refundConsume 记入流水日志'); } // 4. 业务算法模拟 console.log('\n--- 业务算法与边界模拟 ---'); function mockRefundCycle(orderExt, alreadyRefundedSum, requestRefund) { const totalVoucher = Number(orderExt.voucher_fee || 0); const consumed = Boolean(orderExt.voucher_consumed); if (totalVoucher <= 0 || !consumed) { return { refundAmount: 0, reason: '无需退还' }; } const remain = Math.max(0, totalVoucher - alreadyRefundedSum); if (remain <= 0) { return { refundAmount: 0, reason: '已全部退还,拦截重复' }; } const fee = requestRefund !== null ? Math.min(remain, Number(requestRefund || 0)) : remain; return { refundAmount: fee, reason: '成功退还' }; } // 场景 1:已核销 50 个兑换券,首次全额退款 const r1 = mockRefundCycle({ voucher_fee: 50, voucher_consumed: 1 }, 0, null); assert(r1.refundAmount === 50 && r1.reason === '成功退还', '场景 1:已核销订单成功全额退还 50 个兑换券'); // 场景 2:已核销 50 个兑换券,但已退款 50 个,重复触发退款 const r2 = mockRefundCycle({ voucher_fee: 50, voucher_consumed: 1 }, 50, null); assert(r2.refundAmount === 0 && r2.reason === '已全部退还,拦截重复', '场景 2:幂等拦截!避免重复退还兑换券造成双重返还'); // 场景 3:普通商品订单(voucher_fee=0)触发退款 const r3 = mockRefundCycle({ voucher_fee: 0, voucher_consumed: 0 }, 0, null); assert(r3.refundAmount === 0 && r3.reason === '无需退还', '场景 3:普通商品无兑换券核销,安全跳过'); // 场景 4:未支付关闭(voucher_consumed=0)触发退款 const r4 = mockRefundCycle({ voucher_fee: 50, voucher_consumed: 0 }, 0, null); assert(r4.refundAmount === 0 && r4.reason === '无需退还', '场景 4:未付款即关闭的订单不退还兑换券(未发生实际扣减)'); console.log('\n--------------------------------------------------------'); console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`); console.log('========================================================'); if (failCount > 0) { process.exit(1); }