Files
order_site/apps/backend/src/services/finance/finance-controls.test.ts
T
yml2213 9d5af359fa feat(finance): 完成财务控制、L1 钱包硬化与迁移收敛
- P0/P1:影子总账追加调整、关账/退款/资金/积分幂等、成本版本追溯、对账订单项维度
- 迁移收敛:原 103–114 合并为 103_finance_foundation、104_finance_controls、105_work_order_consignment
- 数据:L1 钱包 BIGINT/CHECK/奖励幂等;重复奖励审计口径修正与人工核对清单
- 报表:销售毛利缺失成本收入提示,旧收支页明确现金流口径
- 文档:同步迁移基线、合并评估、代码复核与上线说明
2026-10-27 16:08:16 +08:00

155 lines
8.3 KiB
TypeScript

import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
const read = (path: URL) => readFile(path, 'utf8')
test('销售事实重算必须冻结已关账期间', async () => {
const source = await read(new URL('./sales-facts-service.ts', import.meta.url))
assert.ok(source.includes('finance_periods'), '重算必须读取会计期间')
assert.ok(source.includes("p.status = 'closed'"), '必须识别已关账期间')
assert.ok(source.includes('skippedClosedPeriods'), '必须返回被跳过的关账期间')
})
test('人工确认/退款不得静默改已关账期间', async () => {
const [manual, refund] = await Promise.all([
read(new URL('./manual-fulfillment-service.ts', import.meta.url)),
read(new URL('./order-refund-service.ts', import.meta.url)),
])
assert.ok(manual.includes('manual_fulfillment_period_closed'))
assert.ok(refund.includes('order_refund_period_closed'))
})
test('总账同步必须追加调整凭证,不能删除重建', async () => {
const source = await read(new URL('./finance-ledger-service.ts', import.meta.url))
assert.ok(!source.includes('DELETE FROM finance_journal_entries'), '已过账凭证不得删除重建')
assert.ok(source.includes('sales_revenue_adjustment'), '收入变化必须走调整凭证')
assert.ok(source.includes('sales_cost_adjustment'), '成本变化必须走调整凭证')
assert.ok(source.includes('finance_sales_sync:'), '按日同步必须加 advisory lock')
})
test('关账必须具备门槛、期间锁和重复关账保护', async () => {
const source = await read(new URL('./finance-ledger-service.ts', import.meta.url))
assert.ok(source.includes('assertPeriodCloseable'), '关账必须校验门槛')
assert.ok(source.includes('finance_period_close_blocked'), '关账失败必须给出明确错误码')
assert.ok(source.includes('finance_period_already_closed'), '重复关账必须拒绝')
assert.ok(source.includes('finance_period:'), '凭证写入与关账必须共用期间锁')
assert.ok(
source.includes('manual_cost_missing') || source.includes('未确认成本'),
'必须拦人工缺成本',
)
})
test('平台退款必须先校验可退金额并落退款意图', async () => {
const source = await read(new URL('./order-refund-service.ts', import.meta.url))
const platformStart = source.indexOf('export async function approveAndRecordKuaishouRefund')
const platformEnd = source.indexOf('type PlatformRefundIntentRow', platformStart)
const body = source.slice(platformStart, platformEnd)
assert.ok(
body.indexOf('getRefundableFen') < body.indexOf('approveKuaishouIndustryRefund'),
'可退金额校验必须发生在平台调用之前',
)
assert.ok(body.includes('beginPlatformRefundIntent'), '必须先落退款意图')
assert.ok(source.includes('finance_refund_intents'), '必须有平台退款状态机')
assert.ok(source.includes('order_refund_external_ref_exists'), '外部退款单号必须幂等')
assert.ok(source.includes('nextPayStatus'), '全额退款撤销必须恢复订单支付状态')
assert.ok(source.includes('订单已退款,成本异常自动关闭'), '撤销必须重新打开成本异常')
})
test('备用金对账确认必须按当前余额重算差额', async () => {
const source = await read(new URL('../admin/admin-fund-service.ts', import.meta.url))
const start = source.indexOf('export async function confirmAdminFundReconciliation')
const end = source.indexOf('export async function getAdminFundSupervisorOptions', start)
const body = source.slice(start, end)
assert.ok(body.includes('actualBalanceFen - currentBalanceFen'), '必须用确认时点余额重算')
assert.ok(body.includes('system_balance_fen = $5'), '必须回写确认时点的系统余额')
assert.ok(body.includes('difference_fen = $6'), '必须回写确认时点的差额')
})
test('迁移 104 提供退款/支出幂等与凭证完整性约束', async () => {
const migration = await read(
new URL('../../db/migrations/104_finance_controls.sql', import.meta.url),
)
assert.ok(migration.includes('uq_order_refunds_source_external_ref'))
assert.ok(migration.includes('uq_manual_expenses_order_item'))
assert.ok(migration.includes('finance_refund_intents'))
assert.ok(migration.includes('trg_finance_journal_balanced'))
assert.ok(migration.includes('trg_finance_journal_immutable'))
assert.ok(migration.includes('trg_fund_entries_balance'))
assert.ok(migration.includes('trg_fund_accounts_balance'))
assert.ok(migration.includes("'finance.post'"))
assert.ok(migration.includes("'finance.close'"))
assert.ok(migration.includes("'finance.reconcile'"))
})
test('财务写路由使用拆分后的权限,人工履约包含 finance', async () => {
const [ledgerRoute, financeRoute, orderRoute] = await Promise.all([
read(new URL('../../routes/admin/finance-ledger.ts', import.meta.url)),
read(new URL('../../routes/admin/finance.ts', import.meta.url)),
read(new URL('../../routes/admin/orders.ts', import.meta.url)),
])
assert.ok(ledgerRoute.includes("requireAdminPermission('finance.post')"))
assert.ok(ledgerRoute.includes("requireAdminPermission('finance.close')"))
assert.ok(ledgerRoute.includes("requireAdminPermission('finance.reconcile')"))
assert.ok(
financeRoute.includes("requireAdminRoles(['admin', 'operator', 'support', 'finance'])"),
'文档承诺财务可代录,路由必须允许 finance',
)
assert.ok(orderRoute.includes("requireAdminPermission('finance.post')"))
})
test('P1:资金/积分写命令支持 requestId 幂等', async () => {
const [fund, credit, migration] = await Promise.all([
read(new URL('../admin/admin-fund-service.ts', import.meta.url)),
read(new URL('../cost-center/platform-credit-service.ts', import.meta.url)),
read(new URL('../../db/migrations/104_finance_controls.sql', import.meta.url)),
])
assert.ok(fund.includes('requestId') && fund.includes('fund_request_duplicate'))
assert.ok(credit.includes('platform_credit_request_duplicate'))
assert.ok(migration.includes('uq_fund_entries_account_request'))
assert.ok(migration.includes('uq_platform_credit_entries_account_request'))
})
test('P1:成本版本追溯改价退休旧版本而不是覆盖金额', async () => {
const [product, affiliate] = await Promise.all([
read(new URL('../cost-center/product-catalog-service.ts', import.meta.url)),
read(new URL('../cost-center/affiliate-product-service.ts', import.meta.url)),
])
assert.ok(product.includes('新增一个从 targetTime 起生效的版本'))
assert.ok(product.includes("status = 'retired'"))
assert.ok(affiliate.includes('新增追溯版本'))
assert.ok(affiliate.includes("status = 'retired'"))
})
test('P1:平台对账支持订单项/平台明细维度', async () => {
const [service, migration] = await Promise.all([
read(new URL('./finance-reconciliation-service.ts', import.meta.url)),
read(new URL('../../db/migrations/104_finance_controls.sql', import.meta.url)),
])
assert.ok(service.includes('orderItemId') && service.includes('platformItemId'))
assert.ok(service.includes('order_item_id'))
assert.ok(migration.includes('platform_item_id'))
assert.ok(migration.includes('order_item_id'))
})
test('L1:钱包金额 BIGINT、非负约束与奖励流水幂等', async () => {
const migration = await read(
new URL('../../db/migrations/104_finance_controls.sql', import.meta.url),
)
assert.ok(migration.includes('ALTER COLUMN total_amount TYPE BIGINT'))
assert.ok(migration.includes('ALTER COLUMN available_amount TYPE BIGINT'))
assert.ok(migration.includes('chk_worker_wallets_nonnegative'))
assert.ok(migration.includes('chk_worker_wallet_ledgers_nonnegative_after'))
assert.ok(migration.includes('worker_wallet_ledger_event_key'))
assert.ok(migration.includes('uq_worker_wallet_ledger_event_key'))
assert.ok(migration.includes('v_worker_wallet_duplicate_rewards'))
assert.ok(migration.includes("NOT (NEW.payload_json ? 'inventoryId')"))
assert.ok(migration.includes('GROUP BY worker_id, related_work_order_id'))
})
test('销售毛利必须同时暴露缺失成本收入,避免只看毛利绝对值', async () => {
const source = await read(new URL('./sales-facts-service.ts', import.meta.url))
assert.ok(source.includes('cost_missing_revenue_fen'))
assert.ok(source.includes('costMissingRevenueFen'))
})