diff --git a/.gitignore b/.gitignore index 13569637..41c9e9b2 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ !/tests/widget-system/single-cover-height-contract.test.cjs !/tests/widget-system/frontend-lifecycle-contract.test.cjs !/tests/widget-system/card-pass-universal-redeem.test.cjs +!/tests/widget-system/card-pass-modal-redeem.test.cjs !/tests/widget-system/balance-charge-modal.test.cjs !/tests/admin/ /tests/admin/* diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css index 60ac09cd..6211f49b 100644 --- a/assets/css/routes/user-center/credits.css +++ b/assets/css/routes/user-center/credits.css @@ -765,27 +765,6 @@ body.uc-modal-open { font-weight: 600; color: #b45309; } -.uc-vip-shop__methods { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px 12px; - margin-top: 8px; - padding-top: 12px; - border-top: 1px solid var(--color-line-soft, rgba(0,0,0,0.06)); -} -.uc-vip-shop__methods-label { - font-size: 12px; - font-weight: 600; - color: var(--color-ink-3, #6b7280); -} -.uc-vip-shop__method { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 13px; - cursor: pointer; -} .uc-vip-shop__tip { margin: 10px 0 0; font-size: 12px; diff --git a/assets/js/pay-vip.js b/assets/js/pay-vip.js index dc3f2f97..4245ad6d 100644 --- a/assets/js/pay-vip.js +++ b/assets/js/pay-vip.js @@ -102,43 +102,64 @@ append(parent, wrap); } - function renderMethods(parent, methods, cardPass) { + function renderMethods(parent, methods, cardPass, wallet) { methods = Array.isArray(methods) ? methods : []; if (!methods.length) { append(parent, node("div", "muted-2-color text-center padding-10", "暂无可用支付方式")); return; } - if (methods.length === 1) { - var one = node("input"); - one.type = "hidden"; - one.name = "aksrt_vip_pay_method"; - one.value = text(methods[0].id || "balance"); - one.setAttribute("data-aksrt-vip-method-fixed", ""); - append(parent, one); - } else { - var wrap = node("div", "uc-vip-shop__methods mb10"); - wrap.setAttribute("data-aksrt-vip-methods", ""); - append(wrap, node("span", "uc-vip-shop__methods-label", "支付方式")); + wallet = wallet || {}; + var defaultMethod = text(methods[0].id || "balance"); + + // 支付方式选择行(Zibll zibpay_get_initiate_pay_input 同构: + // .payment-method-radio.hollow-radio + logo 图 + data-for 选中模型; + // 与余额充值/购买积分弹窗完全一致的组件)。 + if (methods.length > 1) { + append(parent, node("div", "muted-2-color em09 mb6", "请选择支付方式")); + var row = node("div", "flex mb10"); methods.forEach(function (method, index) { - var label = node("label", "uc-vip-shop__method"); - var input = node("input"); - input.type = "radio"; - input.name = "aksrt_vip_pay_method"; - input.value = text(method.id || "balance"); - input.checked = index === 0; - append(label, input); - append(label, node("span", "", method.name || method.id || "")); - append(wrap, label); + var id = text(method.id || "balance"); + var item = node("div", "flex jc hh payment-method-radio hollow-radio flex-auto pointer" + (index === 0 ? " active" : "")); + item.setAttribute("data-for", "payment_method"); + item.setAttribute("data-value", id); + if (method.logo) { + var img = node("img"); + img.src = text(method.logo); + img.alt = id + "-logo"; + img.decoding = "async"; + append(item, img); + } + append(item, node("div", "", method.name || id || "")); + append(row, item); }); - append(parent, wrap); + append(parent, row); } - // 卡密渠道输入盒(契约与参考主题一致:data-controller="payment_method")。 + + // 我的余额盒(Zibll user_balance_box 同构:选中余额渠道时显示)。 + if (methods.some(function (m) { return text(m.id) === "balance"; })) { + var balanceBox = node("div", "mb10 muted-box padding-h10"); + balanceBox.setAttribute("data-controller", "payment_method"); + balanceBox.setAttribute("data-condition", "=="); + balanceBox.setAttribute("data-value", "balance"); + if (defaultMethod !== "balance") balanceBox.style.display = "none"; + var balanceRow = node("div", "flex jsb ac"); + append(balanceRow, node("span", "muted-2-color", "我的余额")); + var balanceValue = node("div", "c-blue-2"); + append(balanceValue, node("span", "px12 mr3", wallet.mark || "¥")); + append(balanceValue, node("span", "em14", text(wallet.balance || "0.00"))); + append(balanceRow, balanceValue); + append(balanceBox, balanceRow); + append(parent, balanceBox); + } + + // 卡密盒(Zibll password_box 同构:mb10 padding-h10 padding-w6 + + // data-controller="payment_method" 契约 + form-control 输入)。 if (cardPass) { - var box = node("div", "uc-vip-shop__card-pass mb10"); + var box = node("div", "mb10 padding-h10 padding-w6"); box.setAttribute("data-controller", "payment_method"); box.setAttribute("data-condition", "=="); box.setAttribute("data-value", "card_pass"); - box.style.display = "none"; + if (defaultMethod !== "card_pass") box.style.display = "none"; if (cardPass.desc) append(box, node("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc)); append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码")); if (!cardPass.only_password) { @@ -157,15 +178,26 @@ append(box, pwInput); append(parent, box); } + + var hidden = node("input"); + hidden.type = "hidden"; + hidden.name = "payment_method"; + hidden.value = defaultMethod; + append(parent, hidden); } - // 卡密渠道显隐:选中 card_pass 时隐藏套餐(.charge-box)、显示卡密输入盒。 + // 渠道显隐:通用 data-controller 契约(==/!= 匹配当前 payment_method); + // 选中 card_pass 时隐藏套餐(.charge-box),与参考主题 pay.js 行为一致。 function applyVipMethodVisibility(root) { var pane = root.closest ? root.closest(".aksrt-vip-pane") : null; pane = pane || root; var value = selectedMethod(pane); - var cardBox = pane.querySelector('[data-controller="payment_method"][data-value="card_pass"]'); - if (cardBox) cardBox.style.display = value === "card_pass" ? "" : "none"; + pane.querySelectorAll('[data-controller="payment_method"]').forEach(function (el) { + var condition = el.getAttribute("data-condition") || "=="; + var target = el.getAttribute("data-value") || ""; + var match = condition === "!=" ? value !== target : value === target; + el.style.display = match ? "" : "none"; + }); pane.querySelectorAll(".charge-box").forEach(function (el) { el.style.display = value === "card_pass" ? "none" : ""; }); @@ -286,6 +318,7 @@ var groups = Array.isArray(catalog.level_packages) ? catalog.level_packages : []; var methods = Array.isArray(view.payment_methods) ? view.payment_methods : []; var cardPass = view.card_pass && view.card_pass.enabled ? view.card_pass : null; + var wallet = { balance: text(catalog.wallet_balance || "0.00"), mark: text(view.currency_mark || "¥") }; var allowed = view.allowed_actions || {}; var reasons = view.action_reasons || {}; var modal = node("div", "box-body payvip-modal"); @@ -365,12 +398,12 @@ var products = node("div", "pay-vip-products"); renderProducts(products, group.items); - renderMethods(products, methods); + renderMethods(products, methods, cardPass, wallet); var actionName = actionKey(action); var actionAllowed = allowed[actionName] === true; var reason = actionReason(reasons[actionName]); var canPurchase = actionAllowed && methods.length > 0; - var submit = node("button", "but jb-red btn-block padding-lg radius aksrt-vip-submit", canPurchase ? (group.items[0].cta_label || actionLabel(action)) : (reason || "暂无可用支付方式")); + var submit = node("button", "mt6 but jb-red initiate-pay btn-block radius aksrt-vip-submit", canPurchase ? (group.items[0].cta_label || actionLabel(action)) : (reason || "暂无可用支付方式")); submit.type = "button"; submit.disabled = !canPurchase; submit.setAttribute("data-vip-submit", ""); @@ -401,12 +434,10 @@ } function selectedMethod(root) { - var fixed = root.querySelector("[data-aksrt-vip-method-fixed]"); - if (fixed && fixed.value) return fixed.value; - var checked = root.querySelector('input[name="aksrt_vip_pay_method"]:checked'); - if (checked) return checked.value; - var first = root.querySelector('input[name="aksrt_vip_pay_method"]'); - return first ? first.value : "balance"; + var hidden = root.querySelector('input[name="payment_method"]'); + if (hidden && hidden.value) return hidden.value; + var first = root.querySelector('[data-for="payment_method"]'); + return first ? first.getAttribute("data-value") : "balance"; } function selectedPackage(root) { @@ -538,12 +569,18 @@ function bindModalUi(root) { if (!root || root.dataset.aksrtVipBound === "1") return; root.dataset.aksrtVipBound = "1"; - root.addEventListener("change", function (event) { - var input = event.target.closest ? event.target.closest('input[name="aksrt_vip_pay_method"]') : null; - if (!input) return; - applyVipMethodVisibility(input); - }); root.addEventListener("click", function (event) { + var methodItem = event.target.closest ? event.target.closest('[data-for="payment_method"]') : null; + if (methodItem) { + var pane = methodItem.closest(".aksrt-vip-pane") || root; + pane.querySelectorAll('[data-for="payment_method"]').forEach(function (el) { + el.classList.toggle("active", el === methodItem); + }); + var hidden = pane.querySelector('input[name="payment_method"]'); + if (hidden) hidden.value = methodItem.getAttribute("data-value") || ""; + applyVipMethodVisibility(pane); + return; + } var tab = event.target.closest("[data-vip-tab]"); if (tab) { event.preventDefault(); diff --git a/src/Content/VipServiceModal.php b/src/Content/VipServiceModal.php index a5f6a77f..8e655901 100644 --- a/src/Content/VipServiceModal.php +++ b/src/Content/VipServiceModal.php @@ -64,6 +64,15 @@ trait VipServiceModal { } $points_exchange = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : []; $methods = is_array($catalog['payment_methods'] ?? null) ? $catalog['payment_methods'] : []; + // 支付方式 logo(与充值/积分弹窗的 payment-method-radio 组件同源)。 + $methods = array_map(function ($m) { + $id = sanitize_key((string) ($m['id'] ?? '')); + return [ + 'id' => $id, + 'name' => (string) ($m['name'] ?? $id), + 'logo' => $this->payment_method_logo_url($id), + ]; + }, $methods); $card_pass = is_array($catalog['card_pass'] ?? null) ? array_merge(['enabled' => false, 'desc' => '', 'only_password' => false], $catalog['card_pass']) : ['enabled' => false, 'desc' => '', 'only_password' => false]; @@ -111,6 +120,7 @@ trait VipServiceModal { 'currency_name' => (string) ($catalog['currency_name'] ?? ''), 'level_packages' => $groups, 'points_exchange' => $points_exchange, + 'wallet_balance' => (string) ($catalog['wallet_balance'] ?? '0.00'), 'vip_url' => (string) ($catalog['vip_url'] ?? ''), ], 'member' => [ @@ -128,6 +138,7 @@ trait VipServiceModal { 'levels' => $level_view, 'payment_methods' => $methods, 'card_pass' => $card_pass, + 'currency_mark' => (string) (\AKSRT\Settings\Settings::get_payment_gateway_settings()['currency']['mark'] ?? '¥'), 'allowed_actions' => [ 'purchase' => $package_ids !== [] && $methods !== [], 'renew' => $has_renew && $methods !== [], @@ -146,6 +157,24 @@ trait VipServiceModal { ]; } + /** + * 支付方式 logo 映射(assets/img/pay-*-logo.svg,与充值/积分弹窗一致)。 + */ + private function payment_method_logo_url(string $id): string { + $logos = [ + 'balance' => 'pay-balance-logo.svg', + 'alipay' => 'pay-alipay-logo.svg', + 'epay_alipay' => 'pay-alipay-logo.svg', + 'wechatpay' => 'pay-wechat-logo.svg', + 'epay_wxpay' => 'pay-wechat-logo.svg', + 'paypal' => 'pay-paypal-logo.svg', + 'stripe' => 'pay-stripe-logo.svg', + 'card_pass' => 'pay-card-pass-logo.svg', + ]; + $file = $logos[$id] ?? ''; + return $file === '' ? '' : esc_url_raw(get_template_directory_uri() . '/assets/img/' . $file); + } + /** * Mini hover card for header (legacy aksrtpay_get_vip_card). */ diff --git a/tests/widget-system/card-pass-modal-redeem.test.cjs b/tests/widget-system/card-pass-modal-redeem.test.cjs new file mode 100644 index 00000000..7cfcd7e9 --- /dev/null +++ b/tests/widget-system/card-pass-modal-redeem.test.cjs @@ -0,0 +1,190 @@ +/** + * 卡密兑换渠道契约(三个购买弹窗 · 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' => true\]/); + 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, /永久/); +});