feat(revenue): immediate commission credit with independent withdrawable pool

Aligns affiliate commission with the reference theme: commission is credited
to a dedicated COMMISSION asset pool the moment an order is paid, removing the
7-day settlement freeze, the settlement cron, and the settlement_days setting.

- Ledger: add AssetType::COMMISSION (isolated from the recharge balance wallet)
- CommissionLedger: credit-on-record + opportunistic debt repayment; refunds
  claw back the full commission from the pool, carrying any shortfall as debt
  (aksrt_commission_debt) that future commission pays down first
- Withdrawal: make asset-aware via a source discriminator (commission vs
  balance); each pool keeps independent daily limits, pending gate, and
  reject-refund. Balance withdrawal (recharge + creator income) is unchanged
- Fix pay_rebate_self_s: honor self-commission at purchase time
- Revenue card: show pool balance + 累计/已提现 (drop 待结算), debt when present
- Schema 2.0.0 -> 2.1.0: add commission_debt, drop freeze columns
This commit is contained in:
2026-09-28 21:27:44 +08:00
parent cded20ae02
commit 61667aa03c
22 changed files with 569 additions and 268 deletions
@@ -135,7 +135,7 @@ test('core asset paths never use floating-point money helpers', () => {
test('points orders bypass cash commissions and purchase dead paths stay removed', () => {
const commission = fs.readFileSync(path.join(root, 'src', 'Payment', 'CommissionLedger.php'), 'utf8')
const paidStart = commission.indexOf('public function record_paid_order')
const paidEnd = commission.indexOf('public function settle_due', paidStart)
const paidEnd = commission.indexOf('private function credit_commission', paidStart)
const paidMethod = commission.slice(paidStart, paidEnd)
const pointsGuard = paidMethod.indexOf('AssetType::POINTS->value')
const affiliateGuard = paidMethod.indexOf('Settings::affiliate_enabled()')
@@ -61,19 +61,29 @@ test('commission base is the discounted payable amount, not the list price', ()
assert.match(ledger, /AssetAmount::balance\(is_string\(\$order\['amount'\] \?\? null\) \? \$order\['amount'\] : ''\)/)
})
test('settlement stays off the read path and runs on the cron schedule', () => {
test('commission is credited immediately with no settlement freeze', () => {
const ledger = read('src', 'Payment', 'CommissionLedger.php')
const attribution = read('src', 'Payment', 'ReferralAttribution.php')
// 懒结算兜底已移除:读路径不得顺带写库(收益页打开慢的主因)。
assert.doesNotMatch(ledger, /lazy_settle_due|\$lazy_settled/)
// overview() 与 rest_me() 都不再触发结算。
assert.doesNotMatch(ledger, /\$this->lazy_settle_due\(\)/)
assert.doesNotMatch(attribution, /lazy_settle_due/)
assert.doesNotMatch(attribution, /->stats\(/)
// 结算只由 hourly 的 wp-cron 事件驱动。
assert.match(ledger, /add_action\('aksrt_affiliate_settle_due', \[\$this, 'settle_due'\]\)/)
assert.match(ledger, /wp_schedule_event\(time\(\) \+ HOUR_IN_SECONDS, 'hourly', 'aksrt_affiliate_settle_due'\)/)
// 即时入账模型:不再有结算冻结期 / 结算 cron / settle_due 方法。
assert.doesNotMatch(ledger, /aksrt_affiliate_settle_due'?, \[\$this, 'settle_due'\]/)
assert.doesNotMatch(ledger, /wp_schedule_event\([^)]*aksrt_affiliate_settle_due/)
assert.doesNotMatch(ledger, /function settle_due|function settle_one/)
assert.doesNotMatch(ledger, /available_at|settled_at/)
// 遗留结算 cron 需在 init() 主动清理。
assert.match(ledger, /wp_unschedule_event\([^)]*aksrt_affiliate_settle_due/)
// 下单支付即把佣金 credit 到 COMMISSION 资金池。
assert.match(ledger, /AssetType::COMMISSION/)
assert.match(ledger, /private function credit_commission/)
// 佣金明细行状态为 credited(非 pending 冻结)。
assert.match(ledger, /'status'\s*=>\s*'credited'/)
})
test('referral codes are self-serve: no admin ops page, granted at registration', () => {