83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import type { ConsignmentMatchCandidate } from '../types/consignment'
|
||
|
||
/** 与寄售仓库「一键查询待匹配订单」同一口径的 0-100 匹配打分。 */
|
||
export function computeConsignmentMatchScore(
|
||
item: Pick<ConsignmentMatchCandidate, 'matchType' | 'matchDetails'>,
|
||
): number {
|
||
const details = (item.matchDetails || []).filter((detail) => detail.status !== 'ignored')
|
||
if (details.length === 0) {
|
||
const type = item.matchType || 'exact'
|
||
if (type === 'exact') return 100
|
||
if (type === 'partial') return 85
|
||
if (type === 'unknown') return 50
|
||
return 0
|
||
}
|
||
const matched = details.reduce((sum, detail) => {
|
||
if (detail.status === 'match') return sum + 1
|
||
if (detail.status === 'partial') return sum + 0.85
|
||
if (detail.status === 'unknown') return sum + 0.5
|
||
return sum
|
||
}, 0)
|
||
return Math.round((matched / details.length) * 100)
|
||
}
|
||
|
||
/** 匹配说明:精确 / 高配 / 缺规格 / 冲突原因。 */
|
||
export function describeConsignmentMatch(
|
||
item: Pick<ConsignmentMatchCandidate, 'matchType' | 'matchReasons'>,
|
||
): string {
|
||
const type = item.matchType || 'exact'
|
||
if (type === 'exact') return '规格完全一致'
|
||
const reasons = (item.matchReasons || []).filter(Boolean)
|
||
if (reasons.length > 0) {
|
||
return `${type === 'conflict' ? '不匹配' : '待核对'}:${reasons.join(';')}`
|
||
}
|
||
if (type === 'partial') return '规格高于要求(非精确)'
|
||
return '需人工核对'
|
||
}
|
||
|
||
/**
|
||
* 选中的库存是否够一次拼满。
|
||
*
|
||
* - 拼单:按剩余份数累加,必须覆盖「总份数 - 已匹配份数」;
|
||
* - 整单:只能选一条,且该条剩余数量必须 >= 订单总数量。
|
||
*/
|
||
export function resolveMatchCoverage(input: {
|
||
sharingEnabled: boolean
|
||
totalQuantity: number
|
||
activeQuantity: number
|
||
selected: Array<Pick<ConsignmentMatchCandidate, 'inventoryId' | 'remainingQuantity'>>
|
||
}): {
|
||
requiredQuantity: number
|
||
selectedQuantity: number
|
||
sufficient: boolean
|
||
gap: number
|
||
} {
|
||
const total = Math.max(1, Math.round(Number(input.totalQuantity || 0)))
|
||
const active = Math.max(0, Math.round(Number(input.activeQuantity || 0)))
|
||
const remaining = input.selected.reduce(
|
||
(sum, item) => sum + Math.max(0, Math.round(Number(item.remainingQuantity || 0))),
|
||
0,
|
||
)
|
||
|
||
if (input.sharingEnabled) {
|
||
const required = Math.max(0, total - active)
|
||
return {
|
||
requiredQuantity: required,
|
||
selectedQuantity: remaining,
|
||
sufficient: required > 0 && remaining >= required,
|
||
gap: Math.max(0, required - remaining),
|
||
}
|
||
}
|
||
|
||
const selectedQuantity = input.selected.reduce(
|
||
(max, item) => Math.max(max, Math.round(Number(item.remainingQuantity || 0))),
|
||
0,
|
||
)
|
||
return {
|
||
requiredQuantity: total,
|
||
selectedQuantity,
|
||
sufficient: input.selected.length === 1 && selectedQuantity >= total,
|
||
gap: Math.max(0, total - selectedQuantity),
|
||
}
|
||
}
|