feat(cost-center): 成本中心与商品映射开发草案 (含文档与订单页调整 WIP)

This commit is contained in:
yml2213
2026-10-20 10:01:52 +08:00
parent 7542d547bf
commit 111bc76357
32 changed files with 7088 additions and 156 deletions
@@ -0,0 +1,273 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
buildTemplateMappingPlan,
buildTemplateProductSku,
calculateCostTotal,
inferTemplateUnitName,
normalizeMappingMatchType,
normalizeProviderKey,
resolveMappingNameKey,
resolveMappingProductId,
validateCostVersionScope,
} from './admin-cost-center-service.js'
test('calculateCostTotal supports per_item, per_order and percent_of_revenue', () => {
assert.equal(
calculateCostTotal({
pricingMode: 'per_item',
amountFen: 250,
rateBps: 0,
quantity: 5,
revenueFen: 10000,
}),
1250,
)
assert.equal(
calculateCostTotal({
pricingMode: 'per_order',
amountFen: 500,
rateBps: 0,
quantity: 5,
revenueFen: 10000,
}),
500,
)
assert.equal(
calculateCostTotal({
pricingMode: 'percent_of_revenue',
amountFen: 0,
rateBps: 500,
quantity: 1,
revenueFen: 10000,
}),
500,
)
})
test('calculateCostTotal clamps invalid rate and negative amounts', () => {
assert.equal(
calculateCostTotal({
pricingMode: 'percent_of_revenue',
amountFen: -1,
rateBps: 20000,
quantity: 1,
revenueFen: 10000,
}),
10000,
)
assert.equal(
calculateCostTotal({
pricingMode: 'per_item',
amountFen: -100,
rateBps: 0,
quantity: 3,
revenueFen: 0,
}),
0,
)
})
test('validateCostVersionScope requires the fields that identify the scope', () => {
assert.doesNotThrow(() =>
validateCostVersionScope('global', {
provider: '',
shopId: '',
canonicalProductId: null,
workProductRuleId: null,
}),
)
assert.throws(() =>
validateCostVersionScope('shop', {
provider: 'kuaishou-91',
shopId: '',
canonicalProductId: null,
workProductRuleId: null,
}),
)
assert.throws(() =>
validateCostVersionScope('product', {
provider: '',
shopId: '',
canonicalProductId: null,
workProductRuleId: null,
}),
)
assert.throws(() =>
validateCostVersionScope('rule', {
provider: '',
shopId: '',
canonicalProductId: null,
workProductRuleId: null,
}),
)
})
test('normalizeProviderKey keeps mappings aligned with orders.provider', () => {
assert.equal(normalizeProviderKey('91kaquan'), '91kaquan')
assert.equal(normalizeProviderKey('91'), '91kaquan')
assert.equal(normalizeProviderKey('open_91'), '91kaquan')
assert.equal(normalizeProviderKey('kuaishou-91'), '91kaquan')
assert.equal(normalizeProviderKey('douyin-mashida'), 'douyin_mashida')
assert.equal(normalizeProviderKey('pdd'), 'pdd')
})
test('template bootstrap keeps rule keys stable and infers reasonable units', () => {
assert.equal(buildTemplateProductSku('bb'), 'tpl:bb')
assert.equal(buildTemplateProductSku('rule-123'), 'tpl:rule-123')
assert.equal(inferTemplateUnitName('指挥官密钥5个'), '个')
assert.equal(inferTemplateUnitName('苏尔南钻石100颗'), '颗')
assert.equal(inferTemplateUnitName('10把指定红枪'), '把')
assert.equal(inferTemplateUnitName('10套橙色七级套'), '套')
assert.equal(inferTemplateUnitName('碎梦浮光珍宝礼盒'), '件')
})
test('template mapping plan uses exact SKU code when it is unambiguous', () => {
const plan = buildTemplateMappingPlan([
{
provider: 'pdd',
shop_id: 'feimeng',
sku_code: 'A-1',
sku_name: '指挥官密钥1个',
product_rule_id: 9,
rule_key: 'bb',
item_count: 8,
},
{
provider: 'pdd',
shop_id: 'feimeng',
sku_code: 'A-2',
sku_name: '指挥官密钥5个',
product_rule_id: 9,
rule_key: 'bb',
item_count: 3,
},
])
assert.equal(plan.conflicts.length, 0)
assert.equal(plan.codeConflictKeyCount, 0)
assert.equal(plan.candidates.filter((row) => row.matchType === 'sku_code').length, 2)
assert.equal(plan.evidenceItemCount, 11)
assert.equal(plan.unresolvedItemCount, 0)
})
test('template mapping plan disambiguates reused 91 SKU codes by name and series', () => {
const plan = buildTemplateMappingPlan([
{
provider: '91kaquan',
shop_id: '141242642',
sku_code: '25873410102642',
sku_name: '指挥官密钥1个',
product_rule_id: 9,
rule_key: 'bb',
item_count: 10,
},
{
provider: '91kaquan',
shop_id: '141242642',
sku_code: '25873410102642',
sku_name: '指挥官密钥5个',
product_rule_id: 9,
rule_key: 'bb',
item_count: 5,
},
{
provider: '91kaquan',
shop_id: '141242642',
sku_code: '25873410102642',
sku_name: '1个精致荣耀专属礼盒',
product_rule_id: 16,
rule_key: 'dd',
item_count: 8,
},
])
assert.equal(plan.codeConflictKeyCount, 1)
assert.equal(plan.conflicts.length, 0)
assert.equal(plan.unresolvedItemCount, 0)
assert.ok(
plan.candidates.some(
(row) =>
row.matchType === 'sku_code_series' &&
row.skuNameKey === '指挥官密钥' &&
row.ruleKey === 'bb',
),
)
assert.ok(
plan.candidates.some(
(row) =>
row.matchType === 'sku_code_series' &&
row.skuNameKey === '精致荣耀专属礼盒' &&
row.ruleKey === 'dd',
),
)
assert.ok(
plan.candidates.some(
(row) =>
row.matchType === 'sku_code_name' &&
row.skuNameKey === '指挥官密钥5个' &&
row.ruleKey === 'bb',
),
)
})
test('template mapping plan reports same code and name mapping to multiple rules', () => {
const plan = buildTemplateMappingPlan([
{
provider: '91kaquan',
shop_id: '3676797936',
sku_code: '185470033055936',
sku_name: '指挥官密钥1个',
product_rule_id: 9,
rule_key: 'bb',
item_count: 20,
},
{
provider: '91kaquan',
shop_id: '3676797936',
sku_code: '185470033055936',
sku_name: '指挥官密钥1个',
product_rule_id: 20,
rule_key: 'ab',
item_count: 5,
},
])
assert.equal(plan.codeConflictKeyCount, 1)
assert.equal(plan.unresolvedItemCount, 25)
assert.equal(plan.conflicts.length, 1)
assert.deepEqual(plan.conflicts[0]?.ruleKeys, ['ab', 'bb'])
assert.equal(
plan.candidates.some((row) => row.matchType === 'sku_code_name'),
false,
)
})
test('mapping match keys normalize name and quantity series', () => {
assert.equal(normalizeMappingMatchType('sku_code_series'), 'sku_code_series')
assert.equal(resolveMappingNameKey('sku_code_name', ' 指挥官密钥5个 '), '指挥官密钥5个')
assert.equal(resolveMappingNameKey('sku_code_series', '指挥官秘钥5个'), '指挥官密钥')
assert.equal(resolveMappingNameKey('sku_name_series', '指挥官密钥10个'), '指挥官密钥')
})
test('mapping lookup follows specificity and concrete-shop priority', () => {
const key = (...parts: string[]) => parts.join('\u0000')
const byKey = new Map<string, number>([
[key('91kaquan', 'shop-a', 'C-1', 'sku_code', ''), 10],
[key('91kaquan', '', 'C-1', 'sku_code', ''), 20],
[key('91kaquan', '', 'C-1', 'sku_code_name', '指挥官密钥1个'), 30],
[key('91kaquan', '', 'C-1', 'sku_code_series', '指挥官密钥'), 40],
[key('91kaquan', '', '', 'sku_name_series', '指挥官密钥'), 50],
])
const item = {
id: 1,
provider: '91kaquan',
shop_id: 'shop-a',
sku_code: 'C-1',
normalized_sku_name: '指挥官密钥1个',
normalized_sku_series: '指挥官密钥',
}
assert.equal(resolveMappingProductId({ byKey }, item), 10)
assert.equal(
resolveMappingProductId({ byKey }, { ...item, shop_id: 'shop-b', sku_code: 'C-2' }),
50,
)
})
File diff suppressed because it is too large Load Diff