上一个提交把「工单 completed」当成了不能再申请售后的判据,方向搞反了: 能否再售后应该看订单有没有被标「已退款」,而不是看工单状态。订单已退款时 status_group 变成 refunded、enabled 直接为 false,那道闸门本来就够用;用工单 状态再判一次,反而把「工单已完结但订单没退款」的用户堵死,售后详细弹窗底部 也只剩下一个「我知道了」死胡同。 - reapplyable_statuses 补上 STATUS_COMPLETED:rejected / cancelled / completed 一律放行再次申请,售后详细弹窗底部按钮对已完成订单渲染「再次申请售后」 - can_apply 还原为只看有没有进行中的工单(!open),不再二次校验工单状态 - create() 的重复申请闸门还原为 find_active():只挡仍在流转的工单, completed 不挡;订单已退款由上面的 STATUS_PAID 校验先挡掉 - 点「再次申请售后」后清 has_application 落回申请表单,提交走 create() 新建工单,链路本来就通,无需改动 harness 场景名改为 settled_ticket_still_allows_reapply,断言 completed / rejected / cancelled 三种已终结状态都能新建工单;待审核工单同因幂等、换因 挡住的场景保留。 验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;after-sale-log-harness 7/7。未做浏览器视觉验收。
92 lines
5.5 KiB
JavaScript
92 lines
5.5 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`)
|
|
}
|
|
// 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/)
|
|
})
|