feat(cost-center): 成本中心与商品映射开发草案 (含文档与订单页调整 WIP)
This commit is contained in:
@@ -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
Reference in New Issue
Block a user