159 lines
5.2 KiB
TypeScript
159 lines
5.2 KiB
TypeScript
import assert from 'node:assert/strict'
|
||
import { readFileSync } from 'node:fs'
|
||
import { fileURLToPath } from 'node:url'
|
||
import test from 'node:test'
|
||
|
||
import {
|
||
computeConsignmentMatchScore,
|
||
describeConsignmentMatch,
|
||
resolveMatchCoverage,
|
||
} from '../src/utils/consignment-match-coverage.ts'
|
||
|
||
test('匹配打分:与仓库列表同口径(精确 100 / 高配 85 / 未知 50 / 冲突 0)', () => {
|
||
assert.equal(computeConsignmentMatchScore({ matchType: 'exact', matchDetails: [] }), 100)
|
||
assert.equal(computeConsignmentMatchScore({ matchType: 'partial', matchDetails: [] }), 85)
|
||
assert.equal(computeConsignmentMatchScore({ matchType: 'unknown', matchDetails: [] }), 50)
|
||
assert.equal(computeConsignmentMatchScore({ matchType: 'conflict', matchDetails: [] }), 0)
|
||
assert.equal(
|
||
computeConsignmentMatchScore({
|
||
matchType: 'partial',
|
||
matchDetails: [
|
||
{
|
||
key: 'grade',
|
||
label: '等级',
|
||
orderValue: 'A',
|
||
inventoryValue: 'S',
|
||
status: 'match',
|
||
},
|
||
{
|
||
key: 'series',
|
||
label: '系列',
|
||
orderValue: 'A',
|
||
inventoryValue: 'S',
|
||
status: 'partial',
|
||
},
|
||
],
|
||
}),
|
||
93,
|
||
)
|
||
})
|
||
|
||
test('匹配说明:精确/非精确/冲突都必须有可读文案', () => {
|
||
assert.equal(describeConsignmentMatch({ matchType: 'exact' }), '规格完全一致')
|
||
assert.equal(
|
||
describeConsignmentMatch({ matchType: 'partial', matchReasons: [] }),
|
||
'规格高于要求(非精确)',
|
||
)
|
||
assert.equal(
|
||
describeConsignmentMatch({
|
||
matchType: 'conflict',
|
||
matchReasons: ['系统不一致', '区服不一致'],
|
||
}),
|
||
'不匹配:系统不一致;区服不一致',
|
||
)
|
||
})
|
||
|
||
test('拼单覆盖:1+1 不够 3 份,2+1 才允许提交', () => {
|
||
const short = resolveMatchCoverage({
|
||
sharingEnabled: true,
|
||
totalQuantity: 3,
|
||
activeQuantity: 0,
|
||
selected: [
|
||
{ inventoryId: 1, remainingQuantity: 1 },
|
||
{ inventoryId: 2, remainingQuantity: 1 },
|
||
],
|
||
})
|
||
assert.equal(short.requiredQuantity, 3)
|
||
assert.equal(short.selectedQuantity, 2)
|
||
assert.equal(short.sufficient, false)
|
||
assert.equal(short.gap, 1)
|
||
|
||
const enough = resolveMatchCoverage({
|
||
sharingEnabled: true,
|
||
totalQuantity: 3,
|
||
activeQuantity: 0,
|
||
selected: [
|
||
{ inventoryId: 1, remainingQuantity: 2 },
|
||
{ inventoryId: 2, remainingQuantity: 1 },
|
||
],
|
||
})
|
||
assert.equal(enough.selectedQuantity, 3)
|
||
assert.equal(enough.sufficient, true)
|
||
assert.equal(enough.gap, 0)
|
||
})
|
||
|
||
test('拼单覆盖:已匹配 1 份时只需再选 2 份', () => {
|
||
const result = resolveMatchCoverage({
|
||
sharingEnabled: true,
|
||
totalQuantity: 3,
|
||
activeQuantity: 1,
|
||
selected: [{ inventoryId: 1, remainingQuantity: 2 }],
|
||
})
|
||
assert.equal(result.requiredQuantity, 2)
|
||
assert.equal(result.sufficient, true)
|
||
})
|
||
|
||
test('整单覆盖:必须一条库存满足全部数量', () => {
|
||
const short = resolveMatchCoverage({
|
||
sharingEnabled: false,
|
||
totalQuantity: 3,
|
||
activeQuantity: 0,
|
||
selected: [{ inventoryId: 1, remainingQuantity: 2 }],
|
||
})
|
||
assert.equal(short.requiredQuantity, 3)
|
||
assert.equal(short.selectedQuantity, 2)
|
||
assert.equal(short.sufficient, false)
|
||
assert.equal(short.gap, 1)
|
||
|
||
const enough = resolveMatchCoverage({
|
||
sharingEnabled: false,
|
||
totalQuantity: 3,
|
||
activeQuantity: 0,
|
||
selected: [{ inventoryId: 1, remainingQuantity: 3 }],
|
||
})
|
||
assert.equal(enough.sufficient, true)
|
||
})
|
||
|
||
test('工单页:internal_matching 显示「手动匹配寄卖」入口', () => {
|
||
const source = readFileSync(
|
||
fileURLToPath(new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url)),
|
||
'utf8',
|
||
)
|
||
assert.ok(source.includes('手动匹配寄卖'), '内部匹配中的按钮文案必须是手动匹配寄卖')
|
||
assert.ok(
|
||
source.includes("String(row.status || '') === 'internal_matching'"),
|
||
'按钮文案必须只在 internal_matching 下切换',
|
||
)
|
||
})
|
||
|
||
test('工单页:手动匹配寄卖成功后必须按真实查询键刷新列表与库存', () => {
|
||
const source = readFileSync(
|
||
fileURLToPath(new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url)),
|
||
'utf8',
|
||
)
|
||
const modalIndex = source.indexOf('<ConsignmentWorkOrderMatchModal')
|
||
assert.ok(modalIndex > 0, '工单页必须挂载手动匹配寄卖弹窗')
|
||
const matchedIndex = source.indexOf('onMatched={() => {', modalIndex)
|
||
const matchedBlock = source.slice(matchedIndex, source.indexOf('}}', matchedIndex) + 2)
|
||
assert.ok(matchedBlock.includes('refreshAll()'), '匹配成功必须调用 refreshAll() 重取列表')
|
||
assert.ok(
|
||
!matchedBlock.includes("'admin-work-orders'") &&
|
||
!matchedBlock.includes("'admin-work-orders-stats'"),
|
||
'不得 invalidate 不存在的 admin-work-orders / admin-work-orders-stats 查询键',
|
||
)
|
||
|
||
const refreshIndex = source.indexOf('async function refreshAll()')
|
||
const refreshBlock = source.slice(
|
||
refreshIndex,
|
||
source.indexOf('async function runAction', refreshIndex),
|
||
)
|
||
for (const key of [
|
||
'admin-worker-platform-orders',
|
||
'admin-worker-platform-summary',
|
||
'admin-consignment-inventories',
|
||
'admin-consignment-stats',
|
||
]) {
|
||
assert.ok(refreshBlock.includes(key), `refreshAll() 必须刷新 ${key}`)
|
||
}
|
||
})
|