Files
order_site/apps/backend/src/services/finance/manual-fulfillment-integrity.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

182 lines
7.0 KiB
TypeScript
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.
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
import { resolveManualFulfillmentCost } from './manual-fulfillment-service.js'
test('人工成本:固定成本按配置积分 × 平台换算率', () => {
assert.deepEqual(
resolveManualFulfillmentCost({
creditQuantity: 4,
unitPriceFen: 100,
providedCostFen: undefined,
}),
{ costFen: 400, costMode: 'fixed' },
)
// 固定成本时手填金额不生效(客服零输入)。
assert.equal(
resolveManualFulfillmentCost({ creditQuantity: 2, unitPriceFen: 100, providedCostFen: 999 })
.costFen,
200,
)
})
test('人工成本:成本不固定必须手填正数金额', () => {
assert.deepEqual(
resolveManualFulfillmentCost({
creditQuantity: 0,
unitPriceFen: 1,
providedCostFen: 1234,
}),
{ costFen: 1234, costMode: 'variable' },
)
assert.throws(
() => resolveManualFulfillmentCost({ creditQuantity: 0, unitPriceFen: 1 }),
/成本不固定/,
)
assert.throws(
() => resolveManualFulfillmentCost({ creditQuantity: 0, unitPriceFen: 1, providedCostFen: 0 }),
/成本不固定/,
)
assert.throws(
() =>
resolveManualFulfillmentCost({
creditQuantity: 0,
unitPriceFen: 1,
providedCostFen: -1,
}),
/金额无效/,
)
})
test('人工确认必须单事务:扣款与快照同事务,且使用 with-client 版本', async () => {
const source = await readFile(new URL('./manual-fulfillment-service.ts', import.meta.url), 'utf8')
const start = source.indexOf('export async function confirmManualFulfillment')
const end = source.indexOf('async function resolveFundAccountId')
const body = source.slice(start, end)
assert.ok(body.includes('withTransaction('), '人工确认必须包在事务里')
assert.ok(
body.indexOf('withTransaction(') < body.indexOf('writeFundExpense(client'),
'备用金扣款必须在同一事务内执行',
)
assert.ok(
body.includes('consumePlatformCreditWithClient(client'),
'积分扣减必须复用调用方事务(不能再用自带事务的 consumePlatformCredit)',
)
assert.ok(
!body.includes('consumePlatformCredit('),
'confirm 内不得调用自带事务的 consumePlatformCredit',
)
assert.ok(
body.includes('FOR UPDATE OF oi') && body.includes("subject_type = 'manual_case'"),
'必须先锁订单项并复查快照,避免并发重复扣款',
)
assert.ok(
body.includes("'manual_case', $1") && body.includes('subjectSource'),
'manual_case 的 subject_id 必须是 order_item_id,资金来源只写 metadata',
)
})
test('人工确认前必须自动匹配履约配置,配置保存/定时任务也要回填', async () => {
const [manualSource, configSource, jobSource] = await Promise.all([
readFile(new URL('./manual-fulfillment-service.ts', import.meta.url), 'utf8'),
readFile(
new URL('../cost-center/manual-fulfillment-config-service.ts', import.meta.url),
'utf8',
),
readFile(new URL('../scheduler/cost-finance-sync-job.ts', import.meta.url), 'utf8'),
])
assert.ok(
manualSource.includes('matchManualFulfillmentConfigWithClient(client, orderItemId, now)'),
'确认时必须按 SKU 自动挂渠道',
)
assert.ok(
configSource.includes('rematchManualFulfillments({') &&
configSource.includes('skuKey: config.sku_key') &&
configSource.includes('dateFrom: shiftDate(today, -29)'),
'保存人工 SKU 配置后必须立即回填最近 30 天订单项',
)
assert.ok(
jobSource.includes('rematchManualFulfillments({') &&
jobSource.includes('onlyUnmatched: true') &&
jobSource.includes('dateFrom:'),
'定时同步必须自动回填未匹配的人工订单项,并限制在最近窗口',
)
assert.ok(
manualSource.includes('NULLIF(oi.sku_code') && configSource.includes('NULLIF(oi.sku_code'),
'SKU 匹配必须有 sku_code / sku_name 兜底,不能只认 spec_json',
)
})
test('人工成本缺失必须进入异常队列,且只监控首次配置之后的订单项', async () => {
const source = await readFile(
new URL('../cost-center/cost-snapshot-sync-service.ts', import.meta.url),
'utf8',
)
assert.ok(source.includes("'manual_cost_missing'"), '缺少人工成本缺失异常')
assert.ok(
source.includes('FROM manual_fulfillment_configs') &&
source.includes('s.basis_at >= c.enforce_from'),
'人工异常必须以首次配置时间为起点,避免历史人工单刷屏',
)
assert.ok(
source.includes("cs.subject_type = 'manual_case'") &&
source.includes('cs.order_item_id = s.order_item_id'),
'人工异常回填/收口必须按订单项关联人工快照',
)
})
test('迁移 104 提供订单项级人工成本唯一约束与 subject 语义约束', async () => {
const source = await readFile(
new URL('../../db/migrations/104_finance_controls.sql', import.meta.url),
'utf8',
)
assert.ok(source.includes('uq_cost_snapshots_manual_order_item'), '缺少订单项级人工快照唯一索引')
assert.ok(
source.includes("WHERE subject_type = 'manual_case' AND superseded_at IS NULL"),
'唯一索引必须只约束有效人工快照',
)
assert.ok(
source.includes('subject_id <> order_item_id') &&
source.includes('chk_cost_snapshots_manual_subject'),
'必须把历史 subject_id 归一并加语义约束',
)
})
test('总账/对账/关账/平台退款能力已接入', async () => {
const [migration, ledger, reconciliation, refund, manual] = await Promise.all([
readFile(new URL('../../db/migrations/104_finance_controls.sql', import.meta.url), 'utf8'),
readFile(new URL('./finance-ledger-service.ts', import.meta.url), 'utf8'),
readFile(new URL('./finance-reconciliation-service.ts', import.meta.url), 'utf8'),
readFile(new URL('./order-refund-service.ts', import.meta.url), 'utf8'),
readFile(new URL('./manual-fulfillment-service.ts', import.meta.url), 'utf8'),
])
assert.ok(
migration.includes('finance_journal_entries') && migration.includes('finance_journal_lines'),
)
assert.ok(migration.includes('finance_periods') && migration.includes('finance_reconciliations'))
assert.ok(
ledger.includes('finance_period_closed') && ledger.includes('syncFinanceLedgerFromFacts'),
)
assert.ok(reconciliation.includes('amount_mismatch') && reconciliation.includes('missing_order'))
assert.ok(
refund.includes('approveKuaishouIndustryRefund') &&
refund.includes("source: 'platform'") &&
refund.includes('platform_refund_not_supported'),
'平台退款必须走平台 API 并落 source=platform',
)
const kuaishouRoute = await readFile(
new URL('../../routes/admin/kuaishou-industry.ts', import.meta.url),
'utf8',
)
assert.ok(
kuaishouRoute.includes('/kuaishou-industry/orders/:orderId/refunds/approve-and-record'),
'平台退款路由必须明确收口在快手行业前缀下',
)
assert.ok(
manual.includes('fundAccountId') && manual.includes('creditOwnerAdminUserId'),
'管理员/财务必须可以指定客服账户代录',
)
})