Files
wordpress-theme-aksrt/tests/admin/card-pass-order-numbers.test.cjs
T
kate522 aad83072a8 refactor(pay): 统一流水号,移除订单/流水/提现三类号码前缀
订单号、资金流水号、提现单号此前各自带前缀配置,同一笔业务在不同界面会
呈现多个号码口径,对账与客诉排查都要先做一次前缀换算。现收敛为单一流水号
分配器,不再按业务类型区分号码形态。

- 删除 payment_order_prefix / payment_flow_prefix /
  payment_withdrawal_prefix 三项配置项及其生成、校验链路
- BusinessNumber::allocate() 成为唯一入口,输出 {YmdHis}{6 位随机},
  共 20 位无前缀,满足 VARCHAR(32) 与网关 out_trade_no ≤ 32 的限制
- 订单、资金流水、提现、卡密渠道交易号四处生成点全部改走该分配器
- 后台支付中心表头「订单号 / 提现单号」统一为「流水号」
- 前台用户中心与订单卡片文案同步统一;前台订单详情合并为单行流水号,
  账本流水号不再下发到前台,避免同一单据出现两个对外号码
- 同步更新 financial-flow / pay-card / card-pass-order-numbers 契约测试
  与 BusinessNumberTest 断言

按用户要求不做双路逻辑与数据迁移,直接替换为统一方案,历史行不做转换。

验证:node --test tests/**/*.test.cjs 362/364 通过,新增 37 项契约;失败的
2 项在改动前的干净 HEAD 基线上同样失败(基线 327 项 / 失败 2 项),分别命中
遗留的 fa fa- 标记与 FA 版本号断言,与本次改动无关。phpunit 44 项 / 103
断言全部通过;php -l 全部改动的 PHP 文件通过。
2026-09-18 22:14:27 +08:00

91 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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;/);
});