- 新增 internal_sku_mappings / internal_sku_channels,平台字段统一映射到内部中文 SKU - 下单入口按内部 SKU 渠道决策修正默认人工路由,自动发货补全 dash sku - 财务 channel_assign 读取统一渠道表;卡券/人工能力兜底,自动发货/接单要求真实载体 - carrier reconciliation 自动补自动发货任务和接单工单 - pending 落明确原因:无内部 SKU / 无渠道能力 / 无履约载体 - 合并、重判、旧 sku_code 规则清理、部署后 preflight 核对脚本 - 待分类队列按内部 SKU 聚合并展示原因
150 lines
5.0 KiB
TypeScript
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')
|
|
})
|