/** * 提现收款渠道契约。 * * 渠道由后台「提现策略 → 收款渠道」的拖拽结构定义,可自定义名称与提示, * 不再固定为支付宝/微信两个勾选项。前台下拉、后端校验与后台审核页都读同一份配置。 */ 'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); const root = path.resolve(__dirname, '..', '..'); const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); test('收款渠道在两侧 schema 都是可拖拽的 repeater', () => { const php = read('src/Admin/Schema/SchemaPayment.php'); const js = read('assets/admin/src/schema/sections/payment/withdrawal.js'); for (const source of [php, js]) { assert.match(source, /affiliate_payout_channels/); assert.match(source, /(?:type:\s*'repeater'|'type'\s*=>\s*'repeater')/); // 结构:key / label / account_hint / note / enabled for (const field of ['key', 'label', 'account_hint', 'note', 'enabled']) { assert.match(source, new RegExp(`key:\\s*'${field}'|'key'\\s*=>\\s*'${field}'`), field); } } // 不再是勾选列表。 assert.doesNotMatch(php, /affiliate_payout_channels[^\n]*'type'\s*=>\s*'checkbox'/); assert.doesNotMatch(js, /affiliate_payout_channels[^\n]*type:\s*'checkbox'/); }); test('运行时按自定义渠道解析,未配置时回落内置两项', () => { const runtime = read('src/Settings/PaymentRuntime.php'); assert.match(runtime, /public static function payout_channels\(\$raw\): array/); assert.match(runtime, /public static function enabled_payout_channels\(\): array/); assert.match(runtime, /public static function payout_channel\(string \$key\): \?array/); // 不再把渠道白名单硬编码成 alipay/wechat。 assert.doesNotMatch(runtime, /array_intersect\(\['alipay', 'wechat'\]/); // 默认渠道由 schema 层唯一定义(纯数据,不依赖运行时),运行时引用它。 assert.match(read('src/Admin/Schema/SchemaPayment.php'), /public static function default_payout_channels\(\): array/); assert.match(runtime, /SchemaPayment::default_payout_channels\(\)/); }); test('前台弹窗与后端校验都按渠道 key 工作', () => { const modal = read('inc/helpers/user-center-renderers/withdraw-modal.php'); const ledger = read('src/Payment/CommissionLedger.php'); const client = read('assets/js/credits.js'); // 弹窗按配置渲染,携带自定义名称与提示。 assert.doesNotMatch(modal, /array_intersect\(\['alipay', 'wechat'\]/); assert.match(modal, /data-hint="<\?php echo esc_attr/); assert.match(modal, /data-note="<\?php echo esc_attr/); assert.match(modal, /data-jio-withdraw-note/); // 切换渠道时同步提示文案,且必须挂在提现弹窗的初始化里 // (同一文件还有充值弹窗,挂错块会让整段逻辑变成死代码)。 assert.equal((client.match(/function syncChannelHint\(\)/g) || []).length, 1); const initAt = client.indexOf('function initWithdraw()'); const syncAt = client.indexOf('function syncChannelHint()'); const nextFnAt = client.indexOf(String.fromCharCode(10) + String.fromCharCode(9) + 'function ', syncAt + 1); assert.ok(initAt >= 0 && syncAt > initAt, 'syncChannelHint must live inside initWithdraw()'); assert.ok(nextFnAt === -1 || syncAt < nextFnAt, 'syncChannelHint must not leak past initWithdraw()'); assert.match(client, /methodField\.addEventListener\('change', syncChannelHint\)/); // 后端只校验渠道存在且启用,不再限定内置两项。 assert.match(ledger, /\$channel_keys = array_map/); assert.doesNotMatch(ledger, /in_array\(\$method,\s*\[?'alipay'/); }); test('后台提现审核读取同一份渠道配置', () => { const page = read('src/Admin/Native/PayWithdrawals.php'); const list = read('src/Admin/Native/PayWithdrawalsList.php'); // 渠道名来自设置,而不是硬编码映射。 assert.match(page, /private function method_labels\(\): array/); assert.match(page, /get_withdrawal_settings\(\)/); assert.doesNotMatch(page, /'alipay' => __\('支付宝'/); assert.match(page, /private function method_notes\(\): array/); // 审核弹窗展示渠道说明。 assert.match(list, /'method_note' => __\('渠道说明'/); });