diff --git a/src/utils/__tests__/writeOffItem.test.ts b/src/utils/__tests__/writeOffItem.test.ts index 14756a6..4c3f32c 100644 --- a/src/utils/__tests__/writeOffItem.test.ts +++ b/src/utils/__tests__/writeOffItem.test.ts @@ -3,7 +3,9 @@ import { test } from 'node:test' import { findWriteOffItem, getWriteOffItemAvailability, - getWriteOffItemQuantity + getWriteOffItemQuantity, + getWriteOffStatusText, + resolveWriteOffItemSelection } from '../writeOffItem.ts' const items = [ @@ -51,3 +53,64 @@ test('marks legacy or unsupported or completed item unavailable', () => { assert.equal(getWriteOffItemAvailability({ ...items[0], snapshotOperationSupported: false }).available, false) assert.equal(getWriteOffItemAvailability({ ...items[0], writeOffCount: 2 }).available, false) }) + +test('locks scanned v2 code to the specified write off item id', () => { + const selection = resolveWriteOffItemSelection(items, { + source: 'scan', + orderWriteOffItemId: 'item-2', + packageName: '双人套餐' + }) + + assert.equal(selection.locked, true) + assert.deepEqual(selection.options.map((item) => item.orderWriteOffItemId), ['item-2']) + assert.equal(selection.selectedItemId, 'item-2') +}) + +test('does not auto select when legacy package name matches multiple available items', () => { + const selection = resolveWriteOffItemSelection(items, { + source: 'scan', + packageName: '双人套餐' + }) + + assert.equal(selection.mismatch, false) + assert.equal(selection.requiresManualSelection, true) + assert.equal(selection.selectedItemId, '') + assert.deepEqual(selection.options.map((item) => item.orderWriteOffItemId), ['item-1', 'item-2']) +}) + +test('auto selects the only available item matched by legacy package name', () => { + const selection = resolveWriteOffItemSelection( + [ + { ...items[0], writeOffCount: 2 }, + items[1] + ], + { + source: 'scan', + packageName: '双人套餐' + } + ) + + assert.equal(selection.selectedItemId, 'item-2') + assert.equal(selection.requiresManualSelection, false) +}) + +test('default selection skips unavailable items', () => { + const selection = resolveWriteOffItemSelection([ + { ...items[0], writeOffCount: 2 }, + items[1] + ]) + + assert.equal(selection.selectedItemId, 'item-2') +}) + +test('infers partial write off status from quantity when status is 0', () => { + assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 0, writeOffCount: 0 }), '待核销') + assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 0, writeOffCount: 1 }), '部分核销') + assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 0, writeOffCount: 2 }), '已核销') + assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 1, writeOffCount: 0 }), '已核销') +}) + +test('does not override explicit non-zero write off status with quantity inference', () => { + assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 2, writeOffCount: 0 }), '部分核销') + assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 'PART_WRITE_OFF', writeOffCount: 0 }), '部分核销') +}) diff --git a/src/utils/writeOffItem.ts b/src/utils/writeOffItem.ts index 919ce71..8ed51cd 100644 --- a/src/utils/writeOffItem.ts +++ b/src/utils/writeOffItem.ts @@ -17,6 +17,21 @@ export interface WriteOffItemAvailability { reason: string } +export interface WriteOffItemSelectionInput { + source?: string + orderWriteOffItemId?: string + packageName?: string + fallbackName?: string +} + +export interface WriteOffItemSelection { + options: CommodityPackageConfig[] + selectedItemId: string + locked: boolean + mismatch: boolean + requiresManualSelection: boolean +} + const statusTextMap: Record = { '0': '待核销', '1': '已核销', @@ -86,9 +101,79 @@ export const getFirstAvailableWriteOffItem = (items: CommodityPackageConfig[]) = return items.find((item) => getWriteOffItemAvailability(item).available) } +const matchesWriteOffItemName = (item: CommodityPackageConfig, packageName: string, fallbackName = '') => { + return getWriteOffItemName(item, fallbackName) === packageName || item.packageName === packageName +} + +export const resolveWriteOffItemSelection = ( + items: CommodityPackageConfig[], + input: WriteOffItemSelectionInput = {} +): WriteOffItemSelection => { + const source = String(input.source || '') + const itemId = String(input.orderWriteOffItemId || '').trim() + const packageName = String(input.packageName || '').trim() + const isScan = source === 'scan' + const locked = isScan && Boolean(itemId) + + let options = items + if (locked) { + options = items.filter((item) => getWriteOffItemId(item) === itemId) + } else if (isScan && packageName) { + options = items.filter((item) => matchesWriteOffItemName(item, packageName, input.fallbackName)) + } + + const mismatch = isScan && Boolean(itemId || packageName) && options.length === 0 + const availableOptions = options.filter((item) => getWriteOffItemAvailability(item).available) + + if (locked) { + return { + options, + selectedItemId: getWriteOffItemId(options[0]), + locked, + mismatch, + requiresManualSelection: false + } + } + + if (isScan && packageName && availableOptions.length > 1) { + return { + options, + selectedItemId: '', + locked, + mismatch, + requiresManualSelection: true + } + } + + const matchedById = itemId ? options.find((item) => getWriteOffItemId(item) === itemId) : undefined + const selectedItem = ( + matchedById && getWriteOffItemAvailability(matchedById).available + ? matchedById + : availableOptions[0] || matchedById || options[0] + ) + + return { + options, + selectedItemId: getWriteOffItemId(selectedItem), + locked, + mismatch, + requiresManualSelection: false + } +} + export const getWriteOffStatusText = (item?: CommodityPackageConfig) => { const status = item?.writeOffStatus ?? item?.packageStatus - if (status === undefined || status === null || status === '') return '' const key = String(status) + const writtenOff = toNumber(item?.writeOffCount) + const total = toNumber(item?.count) + if (key === '1') return '已核销' + if ((status === undefined || status === null || status === '' || key === '0') && (writtenOff !== undefined || total !== undefined)) { + const writtenOffCount = Math.max(writtenOff ?? 0, 0) + const totalCount = Math.max(total ?? 0, 0) + if (writtenOffCount <= 0) return '待核销' + if (totalCount > 0 && writtenOffCount >= totalCount) return '已核销' + return '部分核销' + } + if (status === undefined || status === null || status === '') return '' return statusTextMap[key] || key } diff --git a/src/views/verify/VerifyConfirmView.vue b/src/views/verify/VerifyConfirmView.vue index 0f95c24..0575494 100644 --- a/src/views/verify/VerifyConfirmView.vue +++ b/src/views/verify/VerifyConfirmView.vue @@ -9,14 +9,13 @@ import StatusTag from '@/components/StatusTag.vue' import type { CommodityPackageConfig, UserOrderDetail } from '@/types/order' import { canWriteOff } from '@/utils/constants' import { - findWriteOffItem, - getFirstAvailableWriteOffItem, getWriteOffItemAvailability, getWriteOffItemContent, getWriteOffItemId, getWriteOffItemName, getWriteOffItemQuantity, getWriteOffStatusText, + resolveWriteOffItemSelection, toNumber } from '@/utils/writeOffItem' import { @@ -36,8 +35,6 @@ const orderId = computed(() => String(route.query.orderId || '')) const routePackageName = computed(() => String(route.query.packageName || '').trim()) const routeWriteOffItemId = computed(() => String(route.query.orderWriteOffItemId || '').trim()) const isScanSource = computed(() => String(route.query.source || '') === 'scan') -const isScanItemLocked = computed(() => isScanSource.value && Boolean(routeWriteOffItemId.value)) -const hasScanMatchCriteria = computed(() => isScanSource.value && Boolean(routeWriteOffItemId.value || routePackageName.value)) const packageConfigs = computed(() => { if (!detail.value) return [] @@ -51,24 +48,22 @@ const packageConfigs = computed(() => { ] }) -const packageOptions = computed(() => { - if (isScanSource.value && routeWriteOffItemId.value) { - return packageConfigs.value.filter((item) => getWriteOffItemId(item) === routeWriteOffItemId.value) - } - if (isScanSource.value && routePackageName.value) { - return packageConfigs.value.filter( - (item) => - getWriteOffItemName(item, detail.value?.commodityName) === routePackageName.value || - item.packageName === routePackageName.value - ) - } - return packageConfigs.value +const itemSelection = computed(() => { + return resolveWriteOffItemSelection(packageConfigs.value, { + source: String(route.query.source || ''), + orderWriteOffItemId: routeWriteOffItemId.value, + packageName: routePackageName.value, + fallbackName: detail.value?.commodityName + }) }) -const packageMismatch = computed(() => hasScanMatchCriteria.value && packageOptions.value.length === 0) +const packageOptions = computed(() => itemSelection.value.options) +const isScanItemLocked = computed(() => itemSelection.value.locked) +const packageMismatch = computed(() => itemSelection.value.mismatch) +const requiresManualSelection = computed(() => itemSelection.value.requiresManualSelection) const selectedPackage = computed(() => { - return packageOptions.value.find((item) => getWriteOffItemId(item) === selectedItemId.value) || packageOptions.value[0] + return packageOptions.value.find((item) => getWriteOffItemId(item) === selectedItemId.value) }) const quantityInfo = computed(() => getWriteOffItemQuantity(selectedPackage.value)) @@ -119,13 +114,7 @@ const loadDetail = async () => { loading.value = true try { detail.value = await fetchOrderDetail(orderId.value) - const matchedPackage = findWriteOffItem(packageConfigs.value, { - orderWriteOffItemId: routeWriteOffItemId.value, - packageName: routePackageName.value - }) - const firstAvailablePackage = getFirstAvailableWriteOffItem(packageOptions.value) - const fallbackPackage = packageOptions.value[0] || packageConfigs.value[0] - selectedItemId.value = getWriteOffItemId(matchedPackage || firstAvailablePackage || fallbackPackage) + selectedItemId.value = itemSelection.value.selectedItemId } finally { loading.value = false } @@ -282,6 +271,10 @@ onMounted(loadDetail) 该订单当前不可核销,请返回订单详情核对状态。 +
+ + 扫码套餐匹配到多个可核销明细,请选择后再核销。 +
{{ selectedItemAvailability.reason }}