'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 通道;不得用占位分录伪造。 */ const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); 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'); test('卡密订单号不再硬编码 CARD 前缀,改走统一分配器', () => { assert.doesNotMatch(card, /'CARD'\s*\.\s*\$card_id/); assert.match(card, /allocate_redeem_order_number\(\)/); assert.match(card, /allocate_order_number\(\)/); // 两条兑换路径都必须先取号并在失败时中断 const assetPart = card.slice(card.indexOf('private function redeem_asset_card'), card.indexOf('private function redeem_vip_card')); const vipPart = card.slice(card.indexOf('private function redeem_vip_card'), card.indexOf('private function create_redeem_order')); for (const [name, part] of [['redeem_asset_card', assetPart], ['redeem_vip_card', vipPart]]) { assert.match(part, /\$order_number\s*=\s*\$this->allocate_redeem_order_number\(\);/, name); assert.match(part, /is_wp_error\(\$order_number\)/, name); } }); 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('账本命令保持确定性: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;/); });