Files
order_site/apps/backend/src/services/fulfillment/channel-assign-service.test.ts
T
yml2213 5fe33b0b46 feat(finance): 统一内部 SKU 驱动的履约与财务分流
- 新增 internal_sku_mappings / internal_sku_channels,平台字段统一映射到内部中文 SKU
- 下单入口按内部 SKU 渠道决策修正默认人工路由,自动发货补全 dash sku
- 财务 channel_assign 读取统一渠道表;卡券/人工能力兜底,自动发货/接单要求真实载体
- carrier reconciliation 自动补自动发货任务和接单工单
- pending 落明确原因:无内部 SKU / 无渠道能力 / 无履约载体
- 合并、重判、旧 sku_code 规则清理、部署后 preflight 核对脚本
- 待分类队列按内部 SKU 聚合并展示原因
2026-11-02 14:17:08 +08:00

150 lines
5.0 KiB
TypeScript

import assert from 'node:assert/strict'
import test from 'node:test'
import {
resolveChannelDecision,
type ChannelCandidateItem,
type ChannelRule,
} from './channel-assign-service.js'
function buildItem(patch: Partial<ChannelCandidateItem> = {}): ChannelCandidateItem {
return {
id: 1,
provider: '91kaquan',
shop_id: 'shop-a',
sku_code: 'suit_xxx',
sku_name: '套装-测试',
manual_sku_key: 'suit_xxx',
internal_sku: '套装-测试',
normalized_sku_name: '套装-测试',
has_work_order: false,
has_affiliate_task: false,
has_manual_task: false,
capability_channel: null,
basis_date: '2026-10-31',
...patch,
}
}
function buildRule(patch: Partial<ChannelRule> = {}): ChannelRule {
return {
id: 1,
provider: '',
shop_id: '',
match_key_type: 'sku_code',
match_key: 'suit_xxx',
channel: 'affiliate_dash',
priority: 60,
enabled: true,
...patch,
}
}
test('渠道判定:精确 SKU 规则优先于载体证据', () => {
const decision = resolveChannelDecision(buildItem({ has_work_order: true }), [
buildRule({ channel: 'third_party_voucher', priority: 10 }),
])
assert.equal(decision.channel, 'third_party_voucher')
assert.equal(decision.source, 'auto_rule')
assert.equal(decision.conflict, false)
})
test('渠道判定:无规则时按载体证据判定', () => {
const workOrder = resolveChannelDecision(buildItem({ has_work_order: true }), [])
assert.equal(workOrder.channel, 'work_order')
assert.equal(workOrder.source, 'carrier_evidence')
const affiliate = resolveChannelDecision(buildItem({ has_affiliate_task: true }), [])
assert.equal(affiliate.channel, 'affiliate_dash')
assert.equal(affiliate.source, 'carrier_evidence')
})
test('渠道判定:既无规则也无载体时保持 pending,不兜底人工', () => {
const decision = resolveChannelDecision(buildItem(), [])
assert.equal(decision.channel, null)
assert.equal(decision.conflict, false)
})
test('渠道判定:同优先级多渠道判为冲突,不静默选择', () => {
const decision = resolveChannelDecision(buildItem(), [
buildRule({ id: 1, channel: 'affiliate_dash', priority: 10 }),
buildRule({ id: 2, channel: 'manual', priority: 10 }),
])
assert.equal(decision.channel, null)
assert.equal(decision.conflict, true)
assert.deepEqual(decision.conflictChannels.sort(), ['affiliate_dash', 'manual'])
})
test('渠道判定:不同优先级时取优先级更小的', () => {
const decision = resolveChannelDecision(buildItem(), [
buildRule({ id: 1, channel: 'affiliate_dash', priority: 60 }),
buildRule({ id: 2, channel: 'manual', priority: 70 }),
])
assert.equal(decision.channel, 'affiliate_dash')
})
test('渠道判定:provider / shop 限定生效,空串表示不限', () => {
const scoped = buildRule({ provider: 'pdd', shop_id: 'shop-b' })
assert.equal(resolveChannelDecision(buildItem(), [scoped]).channel, null)
const wide = buildRule({ provider: '', shop_id: '' })
assert.equal(resolveChannelDecision(buildItem(), [wide]).channel, 'affiliate_dash')
})
test('渠道判定:内部 SKU 与 SKU 名称两种键都能命中', () => {
const byInternal = buildRule({
match_key_type: 'internal_sku',
match_key: 'nick-001',
channel: 'manual',
})
assert.equal(
resolveChannelDecision(buildItem({ manual_sku_key: 'nick-001', internal_sku: 'nick-001' }), [
byInternal,
]).channel,
'manual',
)
const byName = buildRule({
match_key_type: 'sku_name',
match_key: '套装-测试',
channel: 'third_party_voucher',
})
assert.equal(
resolveChannelDecision(buildItem({ normalized_sku_name: '套装-测试' }), [byName]).channel,
'third_party_voucher',
)
})
test('渠道判定:停用的规则不参与', () => {
const decision = resolveChannelDecision(buildItem(), [buildRule({ enabled: false })])
assert.equal(decision.channel, null)
})
test('渠道判定:卡券/人工能力可兜底,自动发货/接单必须靠载体', () => {
const voucher = resolveChannelDecision(
buildItem({ capability_channel: 'third_party_voucher' }),
[],
)
assert.equal(voucher.channel, 'third_party_voucher')
assert.equal(voucher.source, 'auto_mapping')
const manual = resolveChannelDecision(buildItem({ capability_channel: 'manual' }), [])
assert.equal(manual.channel, 'manual')
assert.equal(manual.source, 'auto_mapping')
const affiliate = resolveChannelDecision(buildItem({ capability_channel: 'affiliate_dash' }), [])
assert.equal(affiliate.channel, null, '自动发货能力没有任务时不能只改财务口径')
const workOrder = resolveChannelDecision(buildItem({ capability_channel: 'work_order' }), [])
assert.equal(workOrder.channel, null, '接单能力没有工单时不能只改财务口径')
})
test('渠道判定:人工任务作为人工载体证据', () => {
const decision = resolveChannelDecision(buildItem({ has_manual_task: true }), [])
assert.equal(decision.channel, 'manual')
assert.equal(decision.source, 'carrier_evidence')
})