Files
wordpress-theme-aksrt/tests/widget-system/order-after-sale-assets-contract.test.cjs
T
kate522 26ce9a5904 feat(after-sale): 售后处理记录改为每次提交留痕并新增时间线
工单行内新增 handling_log 列,用户申请、用户撤销与后台每次改状态都落一条
「状态 + 备注 + 来源 + 时间」,按时间正序存放、上限 50 条(对齐
MetaAssetService::MAX_RECORDS)。set_status 因此变成读-改-写,补上
GET_LOCK/RELEASE_LOCK 包裹,并在锁内重读工单快照——锁外取快照仍会被并发
追加覆盖。

三处界面由「只显示最后一条」改为回放整条处理链路:

- 后台订单明细「售后记录」按工单渲染头部(状态/金额/申请时间)+ 原因 +
  完整处理记录
- 售后面板手风琴「处理备注」单独占整行并置于「申请说明」上方,删掉已无
  意义的「审核时间」
- 前台售后详细弹窗与订单详情弹窗的售后记录改为时间线:左列绳结状态条
  (节点 + 状态 + 时间),右列处理备注;样式只在 modal-runtime.css 定义
  一份,两个弹窗共用

清理随之失效的死代码:工单的 admin_note / reviewed_at / completed_at,以及
全仓库无写入方的 gateway_request_id / gateway_refund_id / gateway_result;
连带删掉 gatewayResultText()、uninstall 里从未存在过的 aksrt_after_sale
表项,并把两处后台页面重复的状态标签数组收成 after_sale_status_labels()、
重复的 .aksrt-as-log 样式收成 after_sale_log_css()。

顺带修复 88669f69 重构退款流水线时误删 refund_amount()、却仍在 create()
里调用的问题——此前任何用户提交售后都会直接 fatal。

验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;新增
after-sale-log-harness.php 驱动真实 AfterSaleService,覆盖首条日志、空备注
也留痕、撤销记录、50 条上限与畸形行规范化;两个后台页面经 dev harness
渲染校验「处理备注」宽行位于「申请说明」之前;未做浏览器视觉验收。
2026-10-09 13:59:50 +08:00

86 lines
4.9 KiB
JavaScript

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`)
}
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, /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/)
})