文件
Henglianshutong/tests/verify_voucher_refund_closed_loop_spec.js
chen b598d56483 feat(voucher): 兑换券业务严格隔离、前台全面去¥化、不足强拦截与售后闭环返还加固
1. 普通商品购买流程中严格禁止兑换券抵扣,仅限积分商城支持同等兑换
2. 积分商城卡片、详情、规格及结算页全面去除「¥」符号,强化「个兑换券 / 券」纯资产心智
3. 兑换券与积分逻辑完全对齐,兑换券不足时禁止掏现金补差,前端置灰禁用+后端抛出「兑换券不足」异常
4. 修复售后退款状态机识别偏差,并在订单失效/取消中增加已核销兑换券原路返还兜底
5. 新增及更新 6 套专项自动化测试(累计 200 项断言全部通过)
2026-09-21 11:49:28 +08:00

103 行
5.2 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 兑换券售后退款与订单失效闭环返还专项自动化测试
*
* 验证重点:
* 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);
}