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('orders expose four states, seven product categories and retain 22-digit IDs', () => { const domain = read('src/Core/DomainModel.php') const repository = read('src/Payment/OrderRepositoryCore.php') const order = read('src/Payment/Order.php') for (const status of ['pending', 'paid', 'refunded', 'closed']) { assert.match(domain, new RegExp(`ORDER_STATUS_${status.toUpperCase()}\\s*=\\s*'${status}'`)) } assert.ok(repository.includes("ORDER_NUMBER_PATTERN = '/^\\d{20}$|^\\d{22}$/';")) for (const label of ['付费阅读', '付费下载', '付费图片', '付费视频', '购买会员', '余额充值', '购买积分']) { assert.ok(domain.includes(label), `missing product category ${label}`) } assert.match(repository, /\$row\['status'\] === 'refunding'[\s\S]*?Order::STATUS_PAID/) assert.doesNotMatch(order, /STATUS_REFUNDING\s*=/) }) test('balance and points mutations use user meta, guard debits in SQL and cap history at 50', () => { const assets = read('src/Credits/MetaAssetService.php') const ledger = read('src/Ledger/LedgerService.php') assert.match(assets, /MAX_RECORDS\s*=\s*50/) assert.match(assets, /SELECT GET_LOCK\(%s, 5\)/) assert.match(assets, /FOR UPDATE/) assert.match(assets, /CAST\(meta_value AS SIGNED\) >= %d/) assert.match(assets, /CAST\(meta_value AS DECIMAL\(20,2\)\) >= CAST\(%s AS DECIMAL\(20,2\)\)/) assert.match(assets, /array_slice\(\$history, 0, self::MAX_RECORDS - 1\)/) assert.match(assets, /'balance_record'/) assert.match(assets, /'points_record'/) assert.match(ledger, /post_meta_command\(\$command, \$guard, \$business_mutation\)/) assert.match(ledger, /AssetType::BALANCE, AssetType::POINTS/) }) test('free points keep a bounded daily summary alongside the recent points history', () => { const freePoints = read('src/Credits/PointsFree.php') const assets = read('src/Credits/MetaAssetService.php') assert.match(freePoints, /META_DETAIL\s*=\s*'free_points_detail'/) assert.match(freePoints, /remaining_today\(\$user_id\) < \$points/) assert.match(freePoints, /array_slice\(\$detail, 0, 50, true\)/) assert.match(freePoints, /LedgerService::get_instance\(\)->credit\([\s\S]*?self::bump_detail\(\$user_id, \$points\)/) assert.match(assets, /'points_record'/) }) test('after-sale submissions are order-locked and admins set status directly', () => { const afterSale = read('src/Payment/AfterSaleService.php') const schema = read('src/Core/DBMigratorPart2.php') const repository = read('src/Payment/OrderRepositoryCore.php') // 处理记录落在工单行内:每次状态提交一条「状态 + 备注 + 来源 + 时间」。 assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_order_aftersale[\s\S]*?handling_log LONGTEXT DEFAULT NULL[\s\S]*?refund_amount/) for (const dead of ['admin_note', 'gateway_request_id', 'gateway_refund_id', 'gateway_result', 'reviewed_at', 'completed_at']) { // 只截到本表 CREATE 语句结束,避免误判其它表里同名的 completed_at const block = schema.slice(schema.indexOf('CREATE TABLE {$prefix}aksrt_order_aftersale')) const column = block.slice(0, block.indexOf('ENGINE=InnoDB')) assert.ok(!column.includes(dead), `${dead} column must be removed from aksrt_order_aftersale`) } // create() 的重复申请闸门看「最新一条工单」而不是只看进行中的:已完结同样要挡 // 只挡仍在流转的工单:已完结(completed)不挡,能否再售后由订单是否已退款决定 assert.match(afterSale, /GET_LOCK\(%s, 5\)[\s\S]*?get_user_order\(\$user_id, \$order_number\)[\s\S]*?find_active\(\$order_number\)/) // 待审核 / 处理中:同人同因幂等返回原工单 assert.match(afterSale, /hash_equals\(\(string\) \(\$active\['reason'\] \?\? ''\), \$reason\)/) // 订单已退款走 STATUS_PAID 校验挡掉,工单 completed 不参与这道闸门 assert.ok(!afterSale.includes('该订单的售后申请已完结,不能再次申请'), 'completed-block copy must be removed') assert.match(afterSale, /Order::STATUS_PAID/) assert.match(afterSale, /public function set_status\(int \$id, string \$status, string \$note = ''\)/) assert.match(afterSale, /in_array\(\$status, \[self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_COMPLETED, self::STATUS_REJECTED, self::STATUS_CANCELLED\], true\)/) assert.ok(!afterSale.includes('STATUS_APPROVED'), 'approved status removed') assert.ok(!afterSale.includes('function review('), 'decision-based review removed') assert.ok(!afterSale.includes('approve('), 'refund pipeline removed') assert.match(repository, /can_finalize_order_refund\(\(int\) \(\$extra\['after_sale_id'\] \?\? 0\), \$order_number\)/) }) test('withdrawal holds and returns use the selected user-meta or commission pool', () => { const withdrawal = read('src/Payment/Withdrawal.php') const assets = read('src/Credits/MetaAssetService.php') assert.match(withdrawal, /source_balance_decimal\(\$user_id, \$source\)/) assert.match(withdrawal, /private function source_balance_decimal\(int \$user_id, string \$source\)/) assert.match(withdrawal, /\$this->source_asset\(\$source\)/) assert.match(withdrawal, /\$this->source_balance_decimal\(\$user_id, \$refund_source\)/) assert.match(assets, /AssetType::BALANCE->value, AssetType::POINTS->value/) assert.match(assets, /function write_balance\([\s\S]*?\$wpdb->usermeta/) })