diff --git a/web-admin/scripts/mobile-pl-plan-h5-quota.test.ts b/web-admin/scripts/mobile-pl-plan-h5-quota.test.ts new file mode 100644 index 000000000..d2e34d1d5 --- /dev/null +++ b/web-admin/scripts/mobile-pl-plan-h5-quota.test.ts @@ -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/) +}) diff --git a/web-admin/scripts/quota-cert-waiver.test.ts b/web-admin/scripts/quota-cert-waiver.test.ts index b61e41611..bcf27fe27 100644 --- a/web-admin/scripts/quota-cert-waiver.test.ts +++ b/web-admin/scripts/quota-cert-waiver.test.ts @@ -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 + ), + '证件余量不足' + ) +}) diff --git a/web-admin/src/components/business/pl-plan-quota-dialog/index.vue b/web-admin/src/components/business/pl-plan-quota-dialog/index.vue index 9ebfda80f..e69752e71 100644 --- a/web-admin/src/components/business/pl-plan-quota-dialog/index.vue +++ b/web-admin/src/components/business/pl-plan-quota-dialog/index.vue @@ -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) => { + 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) diff --git a/web-admin/src/utils/pl-quota-cert-waiver.ts b/web-admin/src/utils/pl-quota-cert-waiver.ts index 50dfc2b3c..7fd19255c 100644 --- a/web-admin/src/utils/pl-quota-cert-waiver.ts +++ b/web-admin/src/utils/pl-quota-cert-waiver.ts @@ -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 } diff --git a/web-admin/src/views/m/pl/plan-cert/CertFacePanel.vue b/web-admin/src/views/m/pl/plan-cert/CertFacePanel.vue index 44668d7e2..a20ac500e 100644 --- a/web-admin/src/views/m/pl/plan-cert/CertFacePanel.vue +++ b/web-admin/src/views/m/pl/plan-cert/CertFacePanel.vue @@ -8,7 +8,10 @@