feat(payment): rebuild order details and asset records
This commit is contained in:
@@ -1,30 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 卡密核销的订单号 / 渠道交易号 / 支付流水号契约。
|
||||
*
|
||||
* 背景(已修复的缺陷):
|
||||
* 卡密充值(balance/credits)与兑换(vip)虽已建订单,但订单号硬编码为
|
||||
* `CARD{卡ID}`、渠道交易号硬编码为 `card_pass_{卡ID}`;且卡密核销产生的资金流水
|
||||
* 没有挂到订单上(order_number 为 NULL),订单详情页的「账本流水号」永远显示「—」。
|
||||
*
|
||||
* 处置:
|
||||
* - 订单号走 OrderRepository::allocate_order_number();
|
||||
* - 渠道交易号走 BusinessNumber::allocate();
|
||||
* - 两者与资金流水号、提现单号同源,均为无前缀的统一流水号,
|
||||
* 后台不再提供任何前缀配置项;
|
||||
* - 余额/积分卡的流水由账本落库后用 attach_order_to_ledger_flow() 补挂订单号,
|
||||
* 并把 event_type 归一为 order_payment;会员卡(0 元、无账本参与)由
|
||||
* record_card_pass_payment() 直接落一条零分录的 order_payment 流水。
|
||||
*
|
||||
* 本契约额外钉住两条容易回退的坑:
|
||||
* 1) **账本命令必须保持确定性** —— LedgerCommand::payload_hash() 覆盖 metadata,
|
||||
* 把随机订单号塞进 credit() 的 metadata 会让重放从「幂等返回」变成
|
||||
* `aksrt_ledger_idempotency_conflict`(实测复现过),因此 metadata 只允许 card_type。
|
||||
* 2) **会员卡的零分录流水是刻意设计** —— prepare_entries() 拒绝 delta=0,
|
||||
* 0 元凭证核销既无现金也无资产分录,只能以「零分录流水行」表达,
|
||||
* record() 为此提供 allow_empty_entries 通道;不得用占位分录伪造。
|
||||
*/
|
||||
/** Card redemption uses the public 22-digit order number as its only business number. */
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
@@ -33,7 +9,7 @@ const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const card = fs.readFileSync(path.join(root, 'src/Payment/ChargeCard.php'), 'utf8');
|
||||
const flows = fs.readFileSync(path.join(root, 'src/Finance/FinancialFlowService.php'), 'utf8');
|
||||
const domain = fs.readFileSync(path.join(root, 'src/Core/DomainModel.php'), 'utf8');
|
||||
|
||||
test('卡密订单号不再硬编码 CARD 前缀,改走统一分配器', () => {
|
||||
assert.doesNotMatch(card, /'CARD'\s*\.\s*\$card_id/);
|
||||
@@ -48,43 +24,18 @@ test('卡密订单号不再硬编码 CARD 前缀,改走统一分配器', () =>
|
||||
}
|
||||
});
|
||||
|
||||
test('渠道交易号不再硬编码 card_pass_ 前缀,改走统一流水号分配器', () => {
|
||||
assert.doesNotMatch(card, /'card_pass_'\s*\.\s*\$card_id/);
|
||||
assert.match(card, /card_pass_transaction_number\(\)/);
|
||||
// 不再读取任何前缀配置,直接取统一流水号
|
||||
assert.doesNotMatch(card, /normalize_prefix|flow_prefix|get_order_settings/);
|
||||
assert.match(card, /private function card_pass_transaction_number\(\): string \{\s*return \\AKSRT\\Finance\\BusinessNumber::allocate\(\);/);
|
||||
// create_redeem_order 只接收调用方传入的交易号,不再自行拼前缀
|
||||
assert.match(card, /'payment_transaction_id'\s*=>\s*\$order_status === Order::STATUS_PAID \? \$transaction_number : ''/);
|
||||
test('card redemption does not create a second flow number', () => {
|
||||
const numberHelper = card.slice(card.indexOf('private function allocate_redeem_order_number'), card.indexOf('private function create_redeem_order'));
|
||||
assert.doesNotMatch(card, /card_pass_transaction_number|FinancialFlowService|record_card_pass_payment|attach_order_to_ledger_flow/);
|
||||
assert.match(numberHelper, /OrderRepository::get_instance\(\)->allocate_order_number\(\)/);
|
||||
assert.match(card, /'payment_transaction_id'\s*=>\s*''/);
|
||||
});
|
||||
|
||||
test('账本命令保持确定性:credit() 的 metadata 不得携带随机订单号', () => {
|
||||
const assetPart = card.slice(card.indexOf('private function redeem_asset_card'), card.indexOf('private function redeem_vip_card'));
|
||||
// metadata 只允许 card_type;order_number 一旦进 metadata,payload_hash 随机化,
|
||||
// 重复请求会撞 uniq_operation_key 后走 replay、hash 不一致即报幂等冲突。
|
||||
assert.match(assetPart, /\['card_type'\s*=>\s*\$type\]/);
|
||||
assert.doesNotMatch(assetPart, /\['card_type'\s*=>\s*\$type,[^\]]*order_number/);
|
||||
// 重放路径必须回取已落库订单号,而不是把本次新分配的号回给前端
|
||||
assert.match(assetPart, /if\s*\(\$result->replayed\)/);
|
||||
assert.match(assetPart, /get_by_request_key\(\$operation_key\)/);
|
||||
});
|
||||
|
||||
test('余额/积分卡:账本提交后补挂订单号,会员卡:直接落零分录支付流水', () => {
|
||||
assert.match(flows, /public function attach_order_to_ledger_flow\(int \$ledger_transaction_id, string \$order_number/);
|
||||
// 只挂到已支付且支付方式为卡密的订单,且不得重复覆盖
|
||||
const attach = flows.slice(flows.indexOf('public function attach_order_to_ledger_flow'));
|
||||
assert.match(attach, /'card_pass'/);
|
||||
assert.match(attach, /order_number IS NULL OR order_number = ''/);
|
||||
assert.match(attach, /event_type = 'order_payment'/);
|
||||
|
||||
assert.match(flows, /public function record_card_pass_payment\(array \$order, bool \$caller_transaction = false\)/);
|
||||
assert.match(flows, /\$voucher_settled = false\)/);
|
||||
// 凭证结算模式豁免两条门槛,且不生成现金分录
|
||||
assert.match(flows, /if \(\$voucher_settled\) \{[\s\S]*?\$metadata\['voucher'\] = 'card_pass';/);
|
||||
assert.match(flows, /private function record\(array \$flow, array \$entries, bool \$caller_transaction, bool \$allow_empty_entries = false\)/);
|
||||
assert.match(flows, /\(\$entries === \[\] && !\$allow_empty_entries\)/);
|
||||
// is_external_recharge 不把卡密当外部收款渠道(核销当下没有现金流入)
|
||||
assert.match(flows, /in_array\(\$method, \['', 'balance', 'points', 'card_pass'\], true\)/);
|
||||
// is_successful_order_payment 认可卡密凭证结算
|
||||
assert.match(flows, /'card_pass'\)\s*\{\s*return true;/);
|
||||
test('card redemption keeps seven order types and canonical labels', () => {
|
||||
assert.match(card, /ORDER_PRODUCT_RECHARGE/);
|
||||
assert.match(card, /ORDER_PRODUCT_VIP/);
|
||||
assert.match(card, /ORDER_PRODUCT_POINTS/);
|
||||
for (const label of ['付费阅读', '付费下载', '付费图片', '付费视频', '购买会员', '余额充值', '购买积分']) {
|
||||
assert.ok(domain.includes(label), `missing order label: ${label}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -190,10 +190,12 @@ test('user asset submissions send idempotency keys', () => {
|
||||
test('profile asset records show only the reason label and the changed amount', () => {
|
||||
for (const file of ['credits-transactions.php', 'credits-wallet-ledger.php']) {
|
||||
const source = fs.readFileSync(path.join(root, 'inc', 'helpers', 'user-center-renderers', file), 'utf8')
|
||||
assert.match(source, /aksrt_asset_reason_label\(\(string\) \$row\['reason'\]\)/)
|
||||
// 记录名称后不再拼接(引用键 / 备注 / 提现单号)。
|
||||
assert.doesNotMatch(source, /\$record_note|uc-transactions__remark|\$row\['ref_key'\]|\$row\['remark'\]|withdrawal_number/)
|
||||
assert.doesNotMatch(source, /uc-transactions__balance|交易 #%d|\$row\['fee_minor'\]|\$row\['ref_id'\]/)
|
||||
assert.match(source, /get_user_meta\([^\n]*'(?:points_record|balance_record)'/)
|
||||
assert.match(source, /\$row\['value'\]/)
|
||||
assert.match(source, /\$row\['desc'\]/)
|
||||
assert.match(source, /\$row\['time'\]/)
|
||||
assert.match(source, /\$row\['order_num'\]/)
|
||||
assert.doesNotMatch(source, /ledger_entry|transaction_id|flow_number|withdrawal_number/)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -321,12 +323,18 @@ test('VIP card redemption is one locked transaction with bounded retry and repla
|
||||
assert.match(method, /vip_card_replay_result/)
|
||||
})
|
||||
|
||||
test('admin ledger keeps cash minor values as decimal strings', () => {
|
||||
test('admin recent asset records read the user-meta histories', () => {
|
||||
const controller = fs.readFileSync(path.join(root, 'src', 'Admin', 'UserController.php'), 'utf8')
|
||||
const page = fs.readFileSync(path.join(root, 'assets', 'admin', 'src', 'pages', 'CmsCredits.vue'), 'utf8')
|
||||
assert.match(controller, /asset_minor_for_response/)
|
||||
assert.match(controller, /decimal_from_minor/)
|
||||
assert.doesNotMatch(page, /\/\s*100(?:\D|$)/)
|
||||
assert.match(controller, /'balance_record'/)
|
||||
assert.match(controller, /'points_record'/)
|
||||
assert.match(controller, /'balance_value'/)
|
||||
assert.match(controller, /'points_value'/)
|
||||
assert.match(page, /用户近期资产记录/)
|
||||
assert.match(page, /api\.getCreditLog\(params\)/)
|
||||
assert.doesNotMatch(page, /统一资产账本|transaction_id|operation_key|toFixed|number_format/)
|
||||
assert.match(page, /Math\.trunc\(raw\)/)
|
||||
assert.match(page, /padStart\(3, '0'\)/)
|
||||
})
|
||||
|
||||
test('asset purchases share the order center and refresh every wallet surface', () => {
|
||||
@@ -346,8 +354,12 @@ test('asset purchases share the order center and refresh every wallet surface',
|
||||
const header = fs.readFileSync(path.join(root, 'inc', 'helpers', 'aksrt-header-userbox.php'), 'utf8')
|
||||
|
||||
assert.match(domain, /ORDER_PRODUCT_POINTS\s*=\s*'points'/)
|
||||
assert.match(domain, /self::ORDER_PRODUCT_POINTS\s*=>\s*__\('积分购买'/)
|
||||
const statuses = ['PENDING', 'PAID', 'REFUNDING', 'REFUNDED', 'CLOSED']
|
||||
const productTypes = ['READING', 'DOWNLOAD', 'IMAGE', 'VIDEO', 'VIP', 'RECHARGE', 'POINTS']
|
||||
for (const type of productTypes) assert.match(domain, new RegExp(`ORDER_PRODUCT_${type}\\s*=`))
|
||||
for (const label of ['付费阅读', '付费下载', '付费图片', '付费视频', '购买会员', '余额充值', '购买积分']) {
|
||||
assert.ok(domain.includes(label), `missing order product label: ${label}`)
|
||||
}
|
||||
const statuses = ['PENDING', 'PAID', 'REFUNDED', 'CLOSED']
|
||||
assert.deepEqual(
|
||||
[...domain.matchAll(/public const ORDER_STATUS_([A-Z_]+)\s*=\s*'([^']+)'/g)].map((match) => [match[1], match[2]]),
|
||||
statuses.map((status) => [status, status.toLowerCase()]),
|
||||
@@ -374,20 +386,21 @@ test('asset purchases share the order center and refresh every wallet surface',
|
||||
)
|
||||
assert.deepEqual(transitions, {
|
||||
PENDING: ['PAID', 'CLOSED'],
|
||||
PAID: ['REFUNDING'],
|
||||
REFUNDING: ['REFUNDED', 'PAID'],
|
||||
PAID: ['REFUNDED'],
|
||||
REFUNDED: [],
|
||||
CLOSED: [],
|
||||
})
|
||||
assert.match(userOrders, /return isset\(DomainModel::order_status_labels\(\)\[\$status\]\) \? \$status : ''/)
|
||||
assert.doesNotMatch(userOrders, /can_open|entitlement_label|resolve_entitlement_url|post_permalink_cache/)
|
||||
assert.match(repository, /SUM\(CASE WHEN status = %s THEN 1 ELSE 0 END\) AS paid/)
|
||||
assert.match(repository, /SUM\(CASE WHEN status = %s THEN 1 ELSE 0 END\) AS paid,/)
|
||||
assert.match(repository, /SUM\(CASE WHEN status = 'refunding' THEN 1 ELSE 0 END\) AS paid_legacy/)
|
||||
assert.match(repository, /'paid' => \(int\) \(\$row\['paid'\] \?\? 0\) \+ \(int\) \(\$row\['paid_legacy'\] \?\? 0\)/)
|
||||
assert.doesNotMatch(repository, /\$completed_statuses|\$completed_placeholders/)
|
||||
assert.match(adminRepository, /\$valid_statuses = array_keys\(\\AKSRT\\Core\\DomainModel::order_status_labels\(\)\)/)
|
||||
assert.doesNotMatch(payOrders, /<select name="new_status">/)
|
||||
assert.doesNotMatch(`${order}\n${repository}\n${userOrders}\n${payOrders}`, /['"]force['"]\s*=>\s*true/)
|
||||
assert.match(paymentRuntime, /\$status_keys = \['pending', 'paid', 'refunding', 'refunded', 'closed'\]/)
|
||||
for (const status of ['pending', 'paid', 'refunding', 'refunded', 'closed']) {
|
||||
assert.match(paymentRuntime, /\$status_keys = \['pending', 'paid', 'refunded', 'closed'\]/)
|
||||
for (const status of ['pending', 'paid', 'refunded', 'closed']) {
|
||||
assert.match(paymentSchema, new RegExp(`payment_order_status_${status}`))
|
||||
}
|
||||
assert.doesNotMatch(paymentSchema, /payment_order_status_(?:completed|cancelled|processing)/)
|
||||
|
||||
@@ -42,25 +42,27 @@ test('order, flow and withdrawal numbers share one prefix-free allocator', () =>
|
||||
assert.match(read('src/Payment/ChargeCard.php'), /BusinessNumber::allocate\(\)/)
|
||||
})
|
||||
|
||||
test('ledger and business state write their flow before commit', () => {
|
||||
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.match(order, /record_order_payment\(\$paid_order, true\)/)
|
||||
assert.match(order, /record_order_refund\(\$refunded_order, true\)/)
|
||||
assert.doesNotMatch(order, /record_order_(?:payment|refund)/)
|
||||
assert.match(withdrawal, /record_withdrawal_payout\([\s\S]*?true[\s\S]*?query\('COMMIT'\)/)
|
||||
})
|
||||
|
||||
test('native and front-end surfaces expose immutable business numbers', () => {
|
||||
assert.match(read('src/Admin/Native/PayMenus.php'), /PayFlows::PAGE_SLUG/)
|
||||
assert.match(read('src/Admin/Native/PayFlows.php'), /流水明细/)
|
||||
// 后台同时提供业务流水号与账本流水号两个口径
|
||||
assert.match(read('src/Admin/Native/PayOrders.php'), /账本流水号/)
|
||||
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'), /提现单号/)
|
||||
// 前台订单详情只对外暴露统一流水号:账本流水号既不下发也不展示
|
||||
assert.match(read('assets/js/order-center.js'), /\["流水号", order\.order_number\]/)
|
||||
// 前台订单详情只展示公开订单号,账本流水号不下发也不展示。
|
||||
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'\]/)
|
||||
})
|
||||
@@ -123,13 +125,13 @@ test('flow retention and archive were deliberately removed', () => {
|
||||
assert.equal(fs.existsSync(path.join(root, 'src/Finance/FinancialFlowRetention.php')), false)
|
||||
})
|
||||
|
||||
test('admin flow search uses an equality lookup for full identifiers', () => {
|
||||
const query = read('src/Finance/FinancialFlowQuery.php')
|
||||
assert.match(query, /private function keyword_hits_identifier\(string \$keyword\): bool/)
|
||||
assert.match(query, /if \(\$this->keyword_hits_identifier\(\$keyword\)\)/)
|
||||
// 精确分支走等值(各列有索引),模糊分支保留 LIKE 兜底,前缀搜索行为不变。
|
||||
assert.match(query, /f\.flow_number = %s OR f\.operation_key = %s/)
|
||||
assert.match(query, /f\.flow_number LIKE %s OR f\.operation_key LIKE %s/)
|
||||
test('order detail workbench includes statistics, orders, after-sales and the new filters', () => {
|
||||
const page = read('src/Admin/Native/PayOrders.php')
|
||||
for (const tab of ['统计', '订单', '售后']) assert.ok(page.includes(`__('${tab}'`))
|
||||
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, /STATUS_PENDING[\s\S]*?STATUS_PROCESSING[\s\S]*?STATUS_APPROVED[\s\S]*?STATUS_REJECTED[\s\S]*?STATUS_CANCELLED[\s\S]*?STATUS_COMPLETED/)
|
||||
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('order creation retries only on order-number unique key collisions', () => {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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 only completed by their own refund application', () => {
|
||||
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]*?gateway_request_id[\s\S]*?gateway_result[\s\S]*?completed_at/)
|
||||
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, /\$gateway->refund\(\$order_number, \(string\) \$row\['refund_amount'\], \$request_id\)/)
|
||||
assert.match(afterSale, /query_refund\(\$request_id, \(string\) \$row\['order_number'\]\)/)
|
||||
assert.match(afterSale, /sanitize_key\(\(string\) \(\$gateway_result\['status'\] \?\? ''\)\) !== 'manual'/)
|
||||
assert.match(afterSale, /\$decision === 'reject'[\s\S]*?compensate_revoke\(\$row, \$order, \$revoke, \$note\)/)
|
||||
assert.match(afterSale, /Order::STATUS_REFUNDED/)
|
||||
assert.match(repository, /can_finalize_order_refund\(\(int\) \(\$extra\['after_sale_id'\] \?\? 0\), \$order_number\)/)
|
||||
assert.match(read('src/Payment/Gateways/PayPalGateway.php'), /'status' => 'manual'/)
|
||||
assert.match(read('src/Payment/Gateways/EpayGateway.php'), /'status'\s*=>\s*'manual'/)
|
||||
})
|
||||
|
||||
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/)
|
||||
})
|
||||
@@ -9,10 +9,11 @@ const read = (relativePath) => fs.readFileSync(path.join(root, relativePath), 'u
|
||||
test('admin order filters expose only the current order and payment categories', () => {
|
||||
const page = read('src/Admin/Native/PayOrders.php')
|
||||
|
||||
assert.match(page, /ORDER_PRODUCT_RESOURCE\s*=>\s*__\('内容付费'/)
|
||||
assert.match(page, /ORDER_PRODUCT_VIP\s*=>\s*__\('VIP'/)
|
||||
assert.match(page, /ORDER_PRODUCT_RECHARGE\s*=>\s*__\('充值'/)
|
||||
assert.match(page, /ORDER_PRODUCT_POINTS\s*=>\s*__\('购买积分'/)
|
||||
for (const [type, label] of [
|
||||
['READING', '付费阅读'], ['DOWNLOAD', '付费下载'], ['IMAGE', '付费图片'], ['VIDEO', '付费视频'],
|
||||
['VIP', '购买会员'], ['RECHARGE', '余额充值'], ['POINTS', '购买积分'],
|
||||
]) assert.match(page, new RegExp(`ORDER_PRODUCT_${type}\\s*=>\\s*__\\('${label}'`))
|
||||
assert.doesNotMatch(page, /ORDER_PRODUCT_RESOURCE\s*=>/)
|
||||
assert.doesNotMatch(page, /shop_product|商城商品/)
|
||||
|
||||
assert.match(page, /''\s*=>\s*__\('支付方式'/)
|
||||
|
||||
Reference in New Issue
Block a user