const test = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const root = path.resolve(__dirname, '../..') const read = file => fs.readFileSync(path.join(root, file), 'utf8') test('financial flow and withdrawal numbers use the new-only schema', () => { const schema = read('src/Core/DBMigratorPart2.php') assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_financial_flow \([\s\S]*?UNIQUE KEY uniq_flow_number \(flow_number\)/) assert.match(schema, /UNIQUE KEY uniq_flow_operation \(operation_key\)/) assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_financial_flow_entry \([\s\S]*?UNIQUE KEY uniq_flow_line \(flow_id, line_no\)/) assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_withdrawal \([\s\S]*?withdrawal_number VARCHAR\(32\) NOT NULL[\s\S]*?UNIQUE KEY uniq_withdrawal_number \(withdrawal_number\)/) }) test('order, flow and withdrawal numbers share one prefix-free allocator', () => { const jsSchema = read('assets/admin/src/schema/sections/payment/gateway.js') const phpSchema = read('src/Admin/Schema/SchemaPayment.php') const provider = read('src/Admin/SchemaProvider.php') const runtime = read('src/Settings/PaymentRuntime.php') const number = read('src/Finance/BusinessNumber.php') // 三个前缀配置项及其校验/归一化链路必须彻底消失。 for (const source of [jsSchema, phpSchema]) { assert.doesNotMatch(source, /payment_order_prefix|payment_flow_prefix|payment_withdrawal_prefix/) assert.doesNotMatch(source, /business_prefix/) } assert.doesNotMatch(provider, /business_prefix|normalize_prefix/) assert.doesNotMatch(runtime, /payment_order_prefix|payment_flow_prefix|payment_withdrawal_prefix/) assert.doesNotMatch(runtime, /['"](prefix|flow_prefix|withdrawal_prefix)['"]\s*=>/) assert.doesNotMatch(number, /normalize_prefix|MAX_PREFIX_LENGTH|candidate\(/) // 唯一入口:无前缀的统一流水号分配器。 assert.match(number, /public static function allocate\(\?\\?DateTimeInterface/) assert.match(number, /YmdHis/) // 订单号、资金流水号、提现单号、卡密渠道交易号全部收敛到该分配器。 assert.match(read('src/Payment/OrderRepositoryCore.php'), /BusinessNumber::allocate\(\)/) assert.match(read('src/Finance/FinancialFlowService.php'), /BusinessNumber::allocate\(\)/) assert.match(read('src/Payment/Withdrawal.php'), /BusinessNumber::allocate\(\)/) assert.match(read('src/Payment/ChargeCard.php'), /BusinessNumber::allocate\(\)/) }) test('commission ledger and withdrawals retain their flow while order flows are removed', () => { const ledger = read('src/Ledger/LedgerService.php') const order = read('src/Payment/OrderRepositoryCore.php') const withdrawal = read('src/Payment/Withdrawal.php') assert.ok(ledger.indexOf('record_ledger(') < ledger.indexOf("query('COMMIT')")) assert.doesNotMatch(order, /record_order_(?:payment|refund)/) assert.match(withdrawal, /record_withdrawal_payout\([\s\S]*?true[\s\S]*?query\('COMMIT'\)/) }) test('orders have no independent flow page or visible flow number', () => { const menu = read('src/Admin/Native/PayMenus.php') const payOrders = read('src/Admin/Native/PayOrders.php') assert.doesNotMatch(menu, /PayFlows|流水明细/) assert.equal(fs.existsSync(path.join(root, 'src/Admin/Native/PayFlows.php')), false) assert.equal(fs.existsSync(path.join(root, 'src/Finance/FinancialFlowQuery.php')), false) assert.match(payOrders, /订单明细/) assert.doesNotMatch(payOrders, /账本流水号|资金流水号/) assert.match(read('src/Admin/Native/PayWithdrawals.php'), /esc_html__\('订单号'/) assert.doesNotMatch(read('src/Admin/Native/PayWithdrawals.php'), /esc_html__\('流水号'|提现单号/) // 前台订单详情只展示公开订单号,账本流水号不下发也不展示。 assert.match(read('assets/js/order-center.js'), /\["订单号", order\.order_number\]/) assert.doesNotMatch(read('assets/js/order-center.js'), /flow_number|账本流水号/) assert.doesNotMatch(read('src/Payment/UserOrders.php'), /\$view\['flow_number'\]/) }) test('ledger history carries no financial flow coupling', () => { const query = read('src/Ledger/LedgerQuery.php') const records = [ read('inc/helpers/user-center-renderers/credits-transactions.php'), read('inc/helpers/user-center-renderers/credits-wallet-ledger.php'), ].join('\n') // 前台余额/积分记录只展示中文变动原因与变动额:提现单号既不查询也不下发, // 账本历史不再为了它 LEFT JOIN financial_flow。 assert.doesNotMatch(query, /financial_flow|withdrawal_number/) assert.doesNotMatch(records, /\$row\['ref_key'\]|\$row\['remark'\]|withdrawal_number|uc-transactions__remark/) }) test('user withdrawal history uses one paginated ten-item endpoint', () => { const controller = read('src/Payment/CommissionLedger.php') const renderer = read('inc/helpers/user-center-renderers/credits-wallet.php') const script = read('assets/js/credits-withdraw-history.js') const styles = read('assets/css/routes/user-center/credits.css') assert.match(controller, /\$withdrawal_args\['per_page'\]\['default'\]\s*=\s*10/) assert.match(controller, /\$withdrawal_args\['per_page'\]\['maximum'\]\s*=\s*10/) assert.match(controller, /'total'\s*=>\s*\$total/) assert.match(controller, /'max_pages'\s*=>\s*\$max_pages/) assert.match(controller, /'user_id'\s*=>\s*\$user_id/) assert.doesNotMatch(renderer, /get_user_withdrawals\(/) assert.doesNotMatch(renderer, /withdraw_history/) assert.match(renderer, /data-aksrt-withdraw-history-list/) assert.match(script, /\/earnings\/withdrawals\?page=/) assert.match(script, /&per_page=10/) assert.match(script, /data-aksrt-withdraw-history-page/) assert.match(script, /实际到账:/) assert.match(script, /元)/) assert.match(script, /appendText\(row, text\(item && item\.created_at\)/) assert.doesNotMatch(script, /item\.note/) assert.doesNotMatch(styles, /uc-withdraw-history__note/) assert.match(styles, /\.uc-withdraw-history-modal__dialog\s*\{[\s\S]*?max-width: min\(860px/) assert.match(styles, /\.uc-withdraw-history__item\s*\{[\s\S]*?display: flex/) assert.match(styles, /\.uc-withdraw-history-modal__dialog\s*\{[\s\S]*?overflow: hidden/) }) test('flow table is indexed for the admin listing and date range filters', () => { const schema = read('src/Core/DBMigratorPart2.php') // 后台默认视图是 WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n,外加同条件的 // COUNT(*),界面上的日期区间筛选也只有 created_at 可用;created_at 必须是索引前导列, // 否则每次翻页都是全表扫描 + filesort。 assert.match(schema, /KEY idx_flow_created \(created_at\),/) // 复合索引前导列是 event_type(筛选时可空),替代不了上面那条。 assert.match(schema, /KEY idx_flow_event_time \(event_type, created_at\),/) }) test('flow retention and archive were deliberately removed', () => { // 归档方案(归档表 + 每日清理 cron)已移除:一条 created_at 索引覆盖了它要解决的问题, // 而它会在财务表上执行 DELETE,并需要额外的归档浏览入口才闭环。 assert.doesNotMatch(read('src/Core/DBMigratorPart2.php'), /financial_flow_archive/) assert.doesNotMatch(read('src/Core/DBMigratorPart3.php'), /financial_flow_archive/) assert.doesNotMatch(read('functions.php'), /FinancialFlowRetention/) assert.equal(fs.existsSync(path.join(root, 'src/Finance/FinancialFlowRetention.php')), false) }) test('order detail page drops tabs; ticket center replaces the after-sale submenu', () => { const page = read('src/Admin/Native/PayOrders.php') const menus = read('src/Admin/Native/PayMenus.php') const repository = read('src/Payment/OrderRepositoryCore.php') // 订单明细页不再有 Tab 分页:统计 Tab 与页面整体删除 assert.ok(!page.includes('render_workbench_tabs'), 'workbench tabs must be removed') assert.ok(!page.includes("$_GET['tab']"), 'tab query param must not be read') assert.ok(!page.includes('render_statistics_page'), 'statistics page must be removed') assert.ok(!page.includes('stat_card'), 'stat cards must be removed') assert.ok(!repository.includes('get_admin_order_stats'), 'dead stats query must be removed') // 售后整体移除:工单改走 users.php 的「工单管理」子菜单,支付中心不再承载售后 assert.ok(!menus.includes('aksrt_after_sales'), 'after-sales submenu slug must be gone') assert.ok(!page.includes('render_after_sales_admin_page'), 'after-sales admin page entry must be gone') assert.ok(!page.includes('PAGE_AFTER_SALES_SLUG'), 'after-sales slug constant must be gone') assert.ok(!page.includes('after_sale'), 'no after-sale handler may remain on the orders page') const usersMenus = read('src/Admin/Native/UsersMenus.php') assert.ok(usersMenus.includes("'aksrt_tickets'"), 'ticket submenu slug registered under users.php') assert.ok(usersMenus.includes('render_tickets_page'), 'ticket admin page entry registered') const ticketTrait = read('src/Admin/Native/UsersMenusTicketsCenter.php') assert.ok(ticketTrait.includes('use UsersMenusTicketsList;'), 'ticket list renderer trait composed') // 部门 / 状态 / 预设回复已迁至 SPA「工单中心」,原生页只留列表。 assert.ok(!ticketTrait.includes('use UsersMenusTicketsConfig;'), 'config tab must not be composed any more') const domain = read('src/Core/DomainModel.php') for (const status of ['pending', 'paid', 'refunded', 'closed']) assert.ok(domain.includes(`ORDER_STATUS_${status.toUpperCase()}`)) assert.match(page, /ORDER_PRODUCT_READING[\s\S]*?付费阅读[\s\S]*?ORDER_PRODUCT_DOWNLOAD[\s\S]*?付费下载[\s\S]*?ORDER_PRODUCT_IMAGE[\s\S]*?付费图片[\s\S]*?ORDER_PRODUCT_VIDEO[\s\S]*?付费视频[\s\S]*?ORDER_PRODUCT_VIP[\s\S]*?购买会员[\s\S]*?ORDER_PRODUCT_RECHARGE[\s\S]*?余额充值[\s\S]*?ORDER_PRODUCT_POINTS[\s\S]*?购买积分/) }) test('paid orders expose a refund button next to the detail link', () => { const page = read('src/Admin/Native/PayOrders.php') // 「退款」紧跟在「详情」右侧,仅已支付订单可见——已退款是已支付的唯一后继状态 assert.match(page, /详情', 'aksrt'\) \. '<\/a>';\s*\n\s*\/\/ 已支付是已退款的唯一前置状态[\s\S]*?aksrt_pay_refund_order/) assert.ok(page.includes("if ($status_key === \\AKSRT\\Core\\DomainModel::ORDER_STATUS_PAID) {"), 'refund button only renders for paid orders') assert.ok(page.includes('aksrt_pay_refund_order_'), 'per-order nonce covers the refund') assert.ok(page.includes("add_action('admin_post_aksrt_pay_refund_order', [$this, 'handle_refund_order'])"), 'refund action registered') assert.ok(page.includes('handle_refund_order'), 'refund handler exists') // 退款闸门只剩「订单为已支付」:工单系统与订单退款已解耦,不再要求先有进行中的工单 assert.ok(!page.includes('AfterSaleService'), 'no after-sale lookup in the refund path') assert.ok(!page.includes('after_sale_id'), 'refund is no longer bound to an after-sale application') assert.ok(page.includes("'allow_refund' => true"), 'refund opts into the guarded transition') const repository = read('src/Payment/OrderRepositoryCore.php') assert.ok(!repository.includes('can_finalize_order_refund'), 'OrderRepository no longer gates refunds on an after-sale application') // 各种失败都要给出可操作的提示,而不是静默失败 for (const code of ['order_refunded', 'order_refund_failed', 'order_refund_missing', 'order_refund_not_paid']) { assert.ok(page.includes(`'${code}'`), `flash code ${code} is mapped`) } assert.ok(!page.includes('order_refund_no_ticket'), 'the no-ticket refund failure is gone') }) test('order creation retries only on order-number unique key collisions', () => { const repository = read('src/Payment/OrderRepositoryCore.php') assert.match(repository, /private const ORDER_NUMBER_MAX_ATTEMPTS = \d+;/) assert.match(repository, /for \(\$attempt = 1; \$attempt <= \$max_attempts; \$attempt\+\+\)/) // 只对订单号唯一键冲突重试;request_key 等冲突属于调用方幂等语义,必须原样失败。 assert.match(repository, /stripos\(\(string\) \$wpdb->last_error, 'uniq_order_number'\)/) // 调用方显式传入的合规订单号不得换号重试。 assert.match(repository, /\$max_attempts = \$caller_supplied \? 1 : self::ORDER_NUMBER_MAX_ATTEMPTS;/) }) test('caller-supplied order numbers are constrained to allocator shapes', () => { const repository = read('src/Payment/OrderRepositoryCore.php') // 22 位是当前规则,20 位兼容扩位前的历史号码(导入旧数据 / 旧单重放)。 // 用精确字符串断言:正则字面量里转义 \d 与花括号极易写错,会静默失配。 assert.ok(repository.includes("ORDER_NUMBER_PATTERN = '/^\\d{20}$|^\\d{22}$/';")) // 非标量(数组 / 对象)不得进入 (string) 转换,否则会写入 "Array"。 assert.match(repository, /is_scalar\(\$data\['order_number'\]\)/) // 不合规时必须回落到分配器,而不是原样写库。 assert.match(repository, /\$order_number = \$caller_supplied \? \$declared_number : \$this->allocate_order_number\(\);/) // 弃用不受控号码时在 WP_DEBUG 下留痕。 assert.match(repository, /error_log\(sprintf\('\[AKSRT Order\] 弃用不合规的自带订单号:%s'/) }) test('order cleanup no longer guards on financial records', () => { const repository = read('src/Payment/OrderRepositoryCore.php') const payOrders = read('src/Admin/Native/PayOrders.php') assert.match(repository, /public function clear_closed_orders\(int \$days = 14\): int/) assert.match(repository, /public function delete_order\(string \$order_number\): bool/) for (const source of [repository, payOrders]) { assert.doesNotMatch(source, /has_financial_records|count_closed_orders_with_records|financial_records_exists_sql/) assert.doesNotMatch(source, /orders_clear_blocked|order_delete_blocked/) } })