Files
order_site/apps/backend/src/services/cost-center/cost-center.test.ts
T

411 lines
14 KiB
TypeScript

import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
import { costModelAppliesTo, mapProduct } from './cost-center-mappers.js'
import {
normalizeMappingMatchType,
normalizeProviderKey,
resolveMappingNameKey,
} from './cost-center-shared.js'
import { calculateCostTotal, validateCostVersionScope } from './cost-resolution-service.js'
import { createAdminProduct, resolveMappingProductId } from './product-catalog-service.js'
import { productChannelFilterSql, productSourceFilterSql } from './cost-center-mappers.js'
import {
buildTemplateMappingPlan,
buildTemplateProductSku,
inferTemplateUnitName,
} from './template-bootstrap-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,
)
})
test('接单模板改价必须在同一事务中同步成本版本与模板投影', async () => {
const { readFile } = await import('node:fs/promises')
const source = await readFile(new URL('./product-catalog-service.ts', import.meta.url), 'utf8')
assert.ok(source.includes('const costVersionId = await withTransaction'))
assert.ok(source.includes('reward_amount = $1::bigint'))
assert.ok(source.includes('unit_price_fen = $2::bigint'))
assert.ok(source.includes('cost_work_order_future_price_not_supported'))
})
test('成本模型与商品来源必须覆盖第三方卡券渠道', () => {
assert.equal(costModelAppliesTo('third_party_voucher_cost', 'third_party_voucher'), true)
assert.equal(costModelAppliesTo('third_party_voucher_cost', 'manual'), false)
const sourceSql = productSourceFilterSql(1)
assert.equal(sourceSql, 'p.source_kind = $1')
assert.ok(!sourceSql.includes('canonical_sku'))
const channelSql = productChannelFilterSql(1)
assert.ok(channelSql.includes('active_primary_channel'))
assert.ok(channelSql.includes("$1 = 'unrouted'"))
assert.ok(!channelSql.includes('canonical_sku'))
})
test('第三方卡券删除:无历史引用硬删除,有历史引用自动归档并停用规则', async () => {
const { readFile } = await import('node:fs/promises')
const [serviceSource, routeSource] = await Promise.all([
readFile(new URL('./product-catalog-service.ts', import.meta.url), 'utf8'),
readFile(new URL('../../routes/admin/cost-center-catalog.ts', import.meta.url), 'utf8'),
])
const start = serviceSource.indexOf('export async function deleteAdminVoucherProduct')
const end = serviceSource.indexOf('/**\n * 批量归档旧人工归一商品', start)
const body = serviceSource.slice(start, end)
assert.ok(body.includes('withTransaction'), '卡券删除必须在事务内完成')
assert.ok(body.includes("action: 'archived'"), '有历史引用时必须自动归档')
assert.ok(body.includes("status = 'archived'"), '归档必须保留商品和历史成本')
assert.ok(body.includes('UPDATE channel_product_mappings'), '归档后必须停用旧映射,避免继续命中')
assert.ok(body.includes('DELETE FROM cost_versions'), '无历史引用时应清理采购价版本')
assert.ok(body.includes("channel = 'third_party_voucher'"), '必须停用对应卡券渠道规则')
assert.ok(routeSource.includes("'/cost-center/voucher-products/:productId'"))
})
test('人工客服页签只按当前主渠道筛选,不能再按 man 前缀放回人工页', async () => {
const source = await readFile(new URL('./product-catalog-service.ts', import.meta.url), 'utf8')
const sql = productChannelFilterSql(1)
assert.ok(sql.includes('active_primary_channel'))
assert.ok(sql.includes("$1 <> 'unrouted'"))
assert.ok(sql.includes('active_primary_channel = $1'))
assert.ok(!sql.includes('canonical_sku'))
assert.ok(source.includes('FROM v_product_current p'))
assert.ok(source.includes('productChannelFilterSql'))
})
test('成本中心总览异常统计必须走异常表关联,不能全表扫描订单项子查询', async () => {
const { readFile } = await import('node:fs/promises')
const source = await readFile(
new URL('./cost-center-overview-service.ts', import.meta.url),
'utf8',
)
assert.ok(source.includes('JOIN v_order_item_current oi ON oi.id = ce.order_item_id'))
assert.ok(!source.includes('ce.order_item_id IN ('))
assert.ok(source.includes('productChannelFilterSql(1)'))
})
test('运行期停止写入已废弃的 product_channel_capabilities', async () => {
const files = await Promise.all(
[
'./voucher-product-service.ts',
'./product-normalize-service.ts',
'./product-catalog-service.ts',
'../fulfillment/channel-assign-service.ts',
].map((path) => readFile(new URL(path, import.meta.url), 'utf8')),
)
for (const source of files) {
assert.ok(!source.includes('product_channel_capabilities'))
}
})
test('商品来源只读 source_kind,不再按 canonical_sku 前缀回退', () => {
const product = mapProduct({
id: 1,
canonical_sku: 'man:编码与来源故意不一致',
canonical_name: '测试商品',
category: '',
unit_name: '件',
status: 'active',
root_category_id: null,
root_category_name: '',
sub_category_id: null,
sub_category_name: '',
attributes_json: {},
created_at: '',
updated_at: '',
source_kind: 'third_party_voucher',
})
assert.equal(product.source, 'third_party_voucher')
assert.equal(product.sourceKind, 'third_party_voucher')
})
test('阶段 6 不再保留会写旧履约列的运维脚本', async () => {
const packageJson = JSON.parse(
await readFile(new URL('../../../package.json', import.meta.url), 'utf8'),
) as { scripts?: Record<string, string> }
assert.equal(packageJson.scripts?.['finance:sync'], undefined)
assert.equal(packageJson.scripts?.['finance:rebuild-cutover'], undefined)
assert.equal(packageJson.scripts?.['products:merge-duplicates'], undefined)
assert.equal(packageJson.scripts?.['products:reclassify'], undefined)
})
test('人工标准商品禁止绕过人工 SKU 配置手工创建', async () => {
await assert.rejects(
createAdminProduct(
{
canonicalSku: 'man:破片手榴弹-兔兔白日梦',
canonicalName: '破片手榴弹-兔兔白日梦',
},
{} as never,
),
/人工 SKU 配置自动创建/,
)
})
test('第三方卡券商品禁止绕过卡券 SKU 配置手工创建', async () => {
await assert.rejects(
createAdminProduct(
{
canonicalSku: 'tpv:11月改名卡',
canonicalName: '11月改名卡',
},
{} as never,
),
/卡券 SKU 配置自动创建/,
)
})