fix(pl): 新规则安排不再被本地合同绑证缺口误拦。

H5 运输计划验收发现 customer_category 在服务端自动选证已通过时,前端仍用合同绑证空缺拦截安排;同步用证面板从占用回填证号并补专项单测。
This commit is contained in:
wuzilong committed 2026-11-15 08:22:46 +08:00
1 parent c5ae2d957c
commit d5f3a79ea5
5 files changed
+157 -5

No files matched your search

@@ -0,0 +1,86 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { toxicArrangeBlockMessage } from '../src/utils/pl-quota-cert-waiver'
const detail = readFileSync(new URL('../src/views/m/pl/plan/detail.vue', import.meta.url), 'utf8')
const stopCard = readFileSync(
new URL('../src/views/m/pl/plan/PlanStopReadCard.vue', import.meta.url),
'utf8'
)
const loadSection = readFileSync(
new URL('../src/views/m/pl/plan/MobilePlanLoadSection.vue', import.meta.url),
'utf8'
)
const deliverySection = readFileSync(
new URL('../src/views/m/pl/plan/MobilePlanDeliverySection.vue', import.meta.url),
'utf8'
)
const certFace = readFileSync(
new URL('../src/views/m/pl/plan-cert/CertFacePanel.vue', import.meta.url),
'utf8'
)
const arrangeSubmit = readFileSync(
new URL('../src/composables/usePlPlanArrangeSubmit.ts', import.meta.url),
'utf8'
)
const formDialog = readFileSync(
new URL('../src/components/business/pl-plan-form-dialog/index.vue', import.meta.url),
'utf8'
)
const detailActions = readFileSync(
new URL('../src/utils/mp-pl-plan-detail-actions.ts', import.meta.url),
'utf8'
)
test('H5 计划详情提供查看用证入口并挂载 CertFacePanel', () => {
assert.match(detail, /查看用证/)
assert.match(detail, /CertFacePanel/)
assert.match(detail, /quotaOpen/)
})
test('H5 装送段可编辑同市,只读卡标明同市只需购买证', () => {
assert.match(loadSection, /sameCity/)
assert.match(deliverySection, /sameCity/)
assert.match(stopCard, /同市只需购买证/)
})
test('H5 用证面板新规则只读提示,且占用明细可回填证号', () => {
assert.match(certFace, /客户\+品类自动选证/)
assert.match(certFace, /安排不提供手工选证/)
assert.match(certFace, /certNoSnapshot/)
assert.match(certFace, /sourceStopProductId/)
})
test('H5/共用安排提交不附带手工 quotaConfiguration', () => {
assert.doesNotMatch(arrangeSubmit, /quotaConfiguration/)
assert.match(formDialog, /toxicArrangeBlockMessage/)
assert.match(formDialog, /buildArrangePayloadFromPlanForm/)
})
test('新规则安排以服务端齐备/余量检查为准,不被本地合同绑证缺口误拦', () => {
const unbound = '合同HT-1尚未绑定购买证和运输证,请先到合同中绑定后再安排运输'
assert.equal(
toxicArrangeBlockMessage(
{ certQuotaRuleVersion: 'customer_category', arrangeBlockMessage: '' },
unbound
),
''
)
assert.equal(
toxicArrangeBlockMessage(
{
certQuotaRuleVersion: 'customer_category',
arrangeBlockMessage: '新规则下证书齐备只放行安排'
},
unbound
),
'新规则下证书齐备只放行安排'
)
})
test('H5 计划详情无运输归档操作(归档在 Web;齐备完善在 plan-cert)', () => {
assert.doesNotMatch(detailActions, /archive/)
assert.doesNotMatch(detailActions, /归档/)
assert.match(detailActions, /arrange/)
})
@@ -17,3 +17,20 @@ test('unmarked plans still block on the server message or the local certificate
assert.equal(toxicArrangeBlockMessage({ arrangeBlockMessage: '' }, unbound), unbound)
assert.equal(toxicArrangeBlockMessage(null, unbound), unbound)
})
test('customer_category plans trust the server arrange check and ignore local contract-binding gaps', () => {
assert.equal(
toxicArrangeBlockMessage(
{ certQuotaRuleVersion: 'customer_category', arrangeBlockMessage: '' },
unbound
),
''
)
assert.equal(
toxicArrangeBlockMessage(
{ certQuotaRuleVersion: 'customer_category', arrangeBlockMessage: '证件余量不足' },
unbound
),
'证件余量不足'
)
})
@@ -757,13 +757,33 @@
return !label || label === '—' ? '' : label
}
function boundCerts(row: QuotaSourceOption) {
return (
const fromStop =
plan.value?.stopProducts?.find(
(item) =>
item.sourceStopType === row.sourceStopType &&
item.sourceStopProductId === row.sourceStopProductId
)?.certificates || []
)
if (fromStop.length) return fromStop
const sourceId = `${row.sourceStopType}:${row.sourceStopProductId}`
const fromUsage: { certType: CertType; certId: string; certNo: string; quantityUnit: string }[] =
[]
for (const usage of plan.value?.usages || []) {
const kind = String(usage.certType || '') as CertType
if (kind !== 'purchase' && kind !== 'transport') continue
const items = Array.isArray(usage.items) ? usage.items : []
const hit = items.some((item: Record<string, unknown>) => {
const typed = `${String(item.sourceStopType || '')}:${String(item.sourceStopProductId || '')}`
return String(item.sourceId || '') === sourceId || typed === sourceId
})
if (!hit) continue
fromUsage.push({
certType: kind,
certId: String(usage.certId || ''),
certNo: String(usage.certNoSnapshot || ''),
quantityUnit: String(usage.quantityUnit || '')
})
}
return fromUsage
}
function certsOf(row: QuotaSourceOption, kind: CertType) {
return boundCerts(row).filter((cert) => cert.certType === kind)
+7 -1
View File
@@ -1,10 +1,16 @@
/** 已标记证书齐备时,只保留服务端仍要拦住的来源问题,不再用本地两证缺口补一条拦截。 */
export function toxicArrangeBlockMessage(
quota: { quotaCertWaived?: boolean; arrangeBlockMessage?: string } | null | undefined,
quota: {
quotaCertWaived?: boolean
arrangeBlockMessage?: string
certQuotaRuleVersion?: string | null
} | null | undefined,
unboundMessage: string
) {
const serverMessage = (quota?.arrangeBlockMessage || '').trim()
if (quota?.quotaCertWaived) return serverMessage
// 新规则由服务端自动选证;本地「合同绑证」缺口不能再当安排拦截条件
if (quota?.certQuotaRuleVersion === 'customer_category') return serverMessage
return serverMessage || unboundMessage
}
@@ -8,7 +8,10 @@
<template v-else-if="plan">
<section class="mp-wv-section mp-cert-quota">
<p class="mp-wv-card__meta">当前车号 {{ plan.licensePlate || '待安排' }}</p>
<p v-if="plan.canSaveConfiguration === false" class="plan-empty-hint">
<p v-if="plan.certQuotaRuleVersion === 'customer_category'" class="plan-empty-hint">
新规则计划由系统按客户+品类自动选证,本页仅查看占用;安排不提供手工选证。
</p>
<p v-else-if="plan.canSaveConfiguration === false" class="plan-empty-hint">
已安排计划请从调整运输提交用证变更;已归档计划仅查看占用。
</p>
<h3>全部装送段用证</h3>
@@ -274,12 +277,32 @@
for (const [key, ids] of Object.entries(next.pairs)) pairs[key] = ids
}
function bound(row: QuotaSourceOption, kind: CertType) {
return (
const fromStop = (
plan.value?.stopProducts?.find(
(item) =>
item.sourceStopType === row.sourceStopType && item.sourceStopProductId === row.sourceStopProductId
)?.certificates || []
).filter((cert) => cert.certType === kind)
if (fromStop.length) return fromStop
// 新规则无合同绑证时,安排后从占用明细回填证号
const sourceId = `${row.sourceStopType}:${row.sourceStopProductId}`
const fromUsage: { certType: CertType; certId: string; certNo: string; quantityUnit: string }[] = []
for (const usage of plan.value?.usages || []) {
if (String(usage.certType || '') !== kind) continue
const items = Array.isArray(usage.items) ? usage.items : []
const hit = items.some((item: Record<string, unknown>) => {
const typed = `${String(item.sourceStopType || '')}:${String(item.sourceStopProductId || '')}`
return String(item.sourceId || '') === sourceId || typed === sourceId
})
if (!hit) continue
fromUsage.push({
certType: kind,
certId: String(usage.certId || ''),
certNo: String(usage.certNoSnapshot || ''),
quantityUnit: String(usage.quantityUnit || '')
})
}
return fromUsage
}
function display(cert: QuotaCert, key: string) {
if (key === 'status') return certStatusLabels[cert.status] || cert.status || '—'