Files
wordpress-theme-aksrt/tests/widget-system/card-pass-modal-redeem.test.cjs
T
kate522 db0d63b99f fix(pay): 会员卡密单密码默认关闭,卡密盒与其他弹窗一致显示卡号+密码
会员购买/续费弹窗卡密盒此前仅显示卡密输入,与其他两个弹窗(卡号+密码)
不一致。根因是 pay_vip_pass_charge_only_password 的默认值偏离参考主题:

- PaymentRuntime:vip 类型 only_password 由「未设置即视为开启」改为
  !empty(),与其他两类型同构(参考主题 default false)
- ChargeCard:get_public_options 的 vip 回退值 only_password true→false
- SchemaPayment + recharge.js:会员卡密单密码模式开关 default true→false
- 契约测试同步锁定默认关闭与无 isset 回退
- admin SPA 重新构建

已保存过设置且该键被持久化为开启的站点,需在后台「余额充值 → 卡密
兑换 → 会员卡密单密码模式」手动关闭一次。
2026-09-18 17:26:50 +08:00

195 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 卡密兑换渠道契约(三个购买弹窗 · 1:1 对齐参考主题)。
*
* 背景:余额卡片顶部的独立「卡密兑换」通用弹窗已删除,兑换入口收敛到
* 三个购买弹窗的支付方式中(对齐参考主题 zibpay_get_payment_methods +
* zibpay_is_allow_card_pass_pay 过滤器):
* 1) 余额充值弹窗(credits-wallet.php,服务端渲染)
* 2) 购买积分弹窗(credits-balance.php,服务端渲染,Zibll points-pay modal 结构)
* 3) 会员购买/续费弹窗(VipServiceModal JSON 视图模型 + pay-vip.js 渲染)
*
* 后台语义:余额充值 → 卡密兑换 → 三个独立开关
* 启用卡密兑换余额功能 / 启用卡密兑换积分功能 / 启用卡密兑换会员功能
* 任一开关关闭后,对应前台弹窗不出现卡密兑换渠道收款接口。
*
* REST 契约:POST /pay/card-pass/redeem,type 必填(balance/vip/credits),
* 不再有「空类型按卡密自动识别」;核销订单金额恒为 0。
*/
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const root = path.join(__dirname, '..', '..');
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
const wallet = read('inc', 'helpers', 'user-center-renderers', 'credits-wallet.php');
const creditsRenderer = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php');
const creditsJs = read('assets', 'js', 'credits.js');
const payVipJs = read('assets', 'js', 'pay-vip.js');
const chargeCard = read('src', 'Payment', 'ChargeCard.php');
const userOrders = read('src', 'Payment', 'UserOrders.php');
const vipPackages = read('src', 'Content', 'VipServicePackages.php');
const vipPurchase = read('src', 'Content', 'VipServicePurchase.php');
const vipModal = read('src', 'Content', 'VipServiceModal.php');
const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php');
const schemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'recharge.js');
const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js');
test('独立入口与通用弹窗已删除', () => {
assert.equal(
fs.existsSync(path.join(root, 'inc', 'helpers', 'user-center-renderers', 'credits-vip.php')),
false,
'credits-vip.php 死文件应已删除'
);
// 通用卡密弹窗与 initCardPass 不回潮。
assert.doesNotMatch(wallet, /data-aksrt-card-pass-modal|data-aksrt-card-redeem/);
assert.doesNotMatch(creditsJs, /function initCardPass\(|initCardPass\(\)/);
assert.doesNotMatch(creditsRenderer, /data-aksrt-card-pass-modal|data-aksrt-card-redeem/);
// 卡密 logo 就位(三个弹窗共用)。
assert.equal(fs.existsSync(path.join(root, 'assets', 'img', 'pay-card-pass-logo.svg')), true);
});
test('余额充值弹窗:卡密渠道为支付方式之一,卡号/卡密输入按单密码模式渲染', () => {
assert.match(wallet, /'card_pass'\s+=>\s+'pay-card-pass-logo\.svg'/);
// 支付方式列表注入 card_pass(由 get_charge_view / get_public_options['balance'] 驱动)。
assert.match(wallet, /card_pass_redeem\['desc'\]/);
assert.match(wallet, /card_pass_redeem\['only_password'\]/);
// 卡密输入盒契约与参考主题一致。
assert.match(wallet, /data-controller="payment_method" data-condition="==" data-value="card_pass"/);
assert.match(wallet, /name="card_pass\[card\]"/);
assert.match(wallet, /name="card_pass\[password\]"/);
// 仅卡密可用时隐藏金额选择(参考主题语义)。
assert.match(wallet, /\$charge_card_only/);
assert.match(wallet, /\$charge_card_in_methods/);
});
test('购买积分弹窗:Zibll points-pay modal 结构 + 卡密渠道', () => {
// 商品/自定义选择 + 支付方式 + hidden + 立即支付。
assert.match(creditsRenderer, /data-aksrt-points-pay-modal/);
assert.match(creditsRenderer, /data-aksrt-points-pay-form/);
assert.match(creditsRenderer, /data-aksrt-points-pay-open/);
assert.doesNotMatch(creditsRenderer, /data-aksrt-points-exchange-(open|modal|form|amount|preview|close)/);
assert.match(creditsRenderer, /请选择需购买的积分|请输入需购买的积分数额/);
assert.match(creditsRenderer, /name="points_product"/);
assert.match(creditsRenderer, /name="points_custom"/);
assert.match(creditsRenderer, /name="payment_method"/);
assert.match(creditsRenderer, /立即支付/);
// 卡密渠道受「启用卡密兑换积分功能」控制。
assert.match(creditsRenderer, /points_card_pass\['enabled'\]/);
assert.match(creditsRenderer, /data-controller="payment_method" data-condition="==" data-value="card_pass"/);
assert.match(creditsRenderer, /name="card_pass\[password\]"/);
});
test('购买积分前端:商品/支付方式选中模型 + 余额/卡密提交分流', () => {
assert.match(creditsJs, /function initPointsPay\(\)/);
assert.match(creditsJs, /data-aksrt-points-pay-open/);
assert.match(creditsJs, /\[data-for="points_product"\]/);
// 卡密选中:隐藏 .charge-box、显示卡密输入盒(契约与参考主题一致)。
assert.match(creditsJs, /data-controller="payment_method"\]\[data-value="card_pass"/);
assert.match(creditsJs, /value === 'card_pass' \? 'none' : ''/);
// 余额支付 → /pay/balance/points-exchange(product + custom 契约)。
assert.match(creditsJs, /\/pay\/balance\/points-exchange/);
assert.match(creditsJs, /JSON\.stringify\(\{ product: pkg, custom: validCustom \? custom : '0' \}\)/);
// 卡密支付 → /pay/card-pass/redeem type=credits。
assert.match(creditsJs, /redeemCardPass\('credits'/);
// bootAssets 挂载新函数,无死调用。
assert.match(creditsJs, /function bootAssets\(\)[\s\S]*?initPointsPay\(\);/);
});
test('会员弹窗:卡密渠道注入与直兑分流(不创建支付订单)', () => {
// 后端:available_payment_methods 追加 card_pass + 视图模型透传。
assert.match(vipPackages, /'id' => 'card_pass', 'name' => __\('卡密', 'aksrt'\)/);
assert.match(vipPackages, /function vip_card_pass_context\(\)/);
assert.match(vipPackages, /vip_card_pass_context\(\)/);
assert.match(vipModal, /'card_pass' => \$card_pass/);
assert.match(vipModal, /'card_pass' => \$card_pass,/);
// 支付方式组件与其他两个弹窗同源:logo URL 服务端映射 + 余额/货币符号透传。
assert.match(vipModal, /function payment_method_logo_url\(string \$id\): string/);
assert.match(vipModal, /'card_pass'\s+=>\s+'pay-card-pass-logo\.svg'/);
assert.match(vipModal, /'wallet_balance' => \(string\) \(\$catalog\['wallet_balance'\]/);
assert.match(vipModal, /'currency_mark' =>/);
// 后端防御:purchase() 拒绝 card_pass(卡密直兑走 redeem)。
assert.match(vipPurchase, /aksrt_card_pass_redeem_required/);
// 前端:Zibll initiate_pay_input 同构组件——payment-method-radio logo 行、
// 我的余额盒(data-controller == balance)、卡密盒(password_box 类结构)、
// hidden payment_method、initiate-pay 按钮。
assert.match(payVipJs, /payment-method-radio hollow-radio flex-auto pointer/);
assert.match(payVipJs, /item\.setAttribute\("data-for", "payment_method"\)/);
assert.match(payVipJs, /mb10 muted-box padding-h10/);
assert.match(payVipJs, /"data-condition", "=="/);
assert.match(payVipJs, /"data-value", "balance"/);
assert.match(payVipJs, /mb10 padding-h10 padding-w6/);
assert.match(payVipJs, /"data-value", "card_pass"/);
assert.match(payVipJs, /hidden\.name = "payment_method"/);
assert.match(payVipJs, /mt6 but jb-red initiate-pay btn-block radius/);
assert.doesNotMatch(payVipJs, /uc-vip-shop__method|aksrt_vip_pay_method/);
// 显隐:通用 data-controller(==/!=)匹配 + 卡密选中隐藏套餐。
assert.match(payVipJs, /function applyVipMethodVisibility\(/);
assert.match(payVipJs, /querySelectorAll\('\[data-controller="payment_method"\]'\)/);
assert.match(payVipJs, /querySelectorAll\("\.charge-box"\)/);
// 提交分流:卡密直兑 /pay/card-pass/redeem type=vip。
assert.match(payVipJs, /function redeemVipCardPass\(/);
assert.match(payVipJs, /\/pay\/card-pass\/redeem/);
assert.match(payVipJs, /type: "vip"/);
assert.match(payVipJs, /view\.card_pass && view\.card_pass\.enabled/);
});
test('REST 契约:type 必填(balance/vip/credits),无空类型自动识别', () => {
assert.match(chargeCard, /'type' => \['type' => 'string', 'required' => true, 'enum' => \[self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]\]/);
assert.match(chargeCard, /卡密兑换类型无效/);
assert.match(chargeCard, /REDEEMABLE_TYPES = \[self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]/);
// 兼容层:redeem_for_user 仍保留 expected_type 形参(前端恒传明确类型)。
assert.match(chargeCard, /function redeem_for_user\(int \$user_id, string \$password, string \$card = '', string \$expected_type = ''/);
});
test('后端类型闸门:三个独立开关各约束自己的类型', () => {
assert.match(chargeCard, /'balance' => \['enabled' => false, 'desc' => '', 'only_password' => false\]/);
assert.match(chargeCard, /'credits' => \['enabled' => false, 'desc' => '', 'only_password' => false\]/);
assert.match(chargeCard, /'vip'\s+=> \['enabled' => false, 'desc' => '', 'only_password' => false\]/);
// 会员单密码默认关闭(对齐参考主题 default false):未显式开启时卡密盒显示卡号+密码。
assert.doesNotMatch(chargeCard, /'only_password' => true/);
assert.match(read('src', 'Settings', 'PaymentRuntime.php'), /'only_password' => !empty\(\$settings\['pay_vip_pass_charge_only_password'\]\),/);
assert.doesNotMatch(read('src', 'Settings', 'PaymentRuntime.php'), /!isset\(\$settings\['pay_vip_pass_charge_only_password'\]\)/);
assert.match(chargeCard, /卡密兑换余额功能未开启/);
assert.match(chargeCard, /卡密兑换积分功能未开启/);
assert.match(chargeCard, /卡密兑换会员功能未开启/);
assert.match(chargeCard, /积分系统未开启/);
// 收款接口 card_pass 主开关:三开关任一开启即生效。
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
assert.match(paymentRuntime, /\$card_pass_enabled = !empty\(\$settings\['pay_balance_pass_charge_s'\]\)[\s\S]*?\|\| !empty\(\$settings\['pay_vip_pass_charge_s'\]\);/);
});
test('后台 schema:卡密兑换拆分为三个独立开关', () => {
for (const [name, src] of [['SchemaPayment.php', schemaPhp], ['recharge.js', schemaJs]]) {
assert.match(src, /启用卡密兑换余额功能/, name);
assert.match(src, /启用卡密兑换积分功能/, name);
assert.match(src, /启用卡密兑换会员功能/, name);
assert.match(src, /points_pass_exchange_s/, name);
assert.match(src, /points_pass_exchange_desc/, name);
assert.match(src, /points_pass_exchange_only_password/, name);
assert.match(src, /pay_vip_pass_charge_desc/, name);
assert.match(src, /pay_vip_pass_charge_only_password/, name);
// 旧总开关语义不回潮。
assert.doesNotMatch(src, /启用卡密兑换功能'|启用卡密兑换功能"/, name);
}
// recharge.js defaults 补齐三开关字段。
assert.match(schemaJs, /points_pass_exchange_s: false/);
assert.match(schemaJs, /pay_vip_pass_charge_s: false/);
// vip schema 的「会员卡密」组迁移至 recharge 卡密兑换组,原组删除。
assert.doesNotMatch(vipSchemaJs, /title: '会员卡密'/);
assert.doesNotMatch(vipSchemaJs, /pay_vip_pass_charge_only_password/);
const schemaPaymentVip = schemaPhp;
assert.doesNotMatch(schemaPaymentVip, /title' => '会员卡密'/);
});
test('核销订单契约:金额恒为 0,订单名称按 note 生成', () => {
assert.match(chargeCard, /订单金额恒为 0/);
assert.match(userOrders, /if \(\$payment_method === 'card_pass'\)\s*\{\s*\$amount = \$is_points \? '0' : '0\.00';/);
assert.match(userOrders, /if \(!empty\(\$meta\['card_pass'\]\)\)\s*\{[\s\S]*?resolve_card_pass_title\(\$meta\)/);
assert.match(userOrders, /卡密兑换-余额充值/);
assert.match(userOrders, /卡密兑换-积分充值/);
assert.match(userOrders, /购入\[%1\$s\]%2\$s/);
assert.match(userOrders, /永久/);
});