282 lines
9.3 KiB
JavaScript
282 lines
9.3 KiB
JavaScript
import test from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
|
||
import {
|
||
buildAiAttachmentAssociationActions,
|
||
buildAiAttachmentAssociationMessage,
|
||
buildAiAttachmentAssociationResultMessage,
|
||
resolveAiAttachmentAssociationMatch
|
||
} from '../src/utils/aiAttachmentAssociationModel.js'
|
||
import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
|
||
|
||
function findAction(actions, actionType) {
|
||
return actions.find((action) => action.action_type === actionType)
|
||
}
|
||
|
||
test('根据票据日期和行程自动匹配最可能关联的报销单', () => {
|
||
const claims = [
|
||
{
|
||
id: 'claim-wuhan-shanghai',
|
||
claim_no: 'BX-20260220-001',
|
||
expense_type: 'travel',
|
||
status: 'draft',
|
||
reason: '辅助国网仿生产服务器部署,武汉往返上海',
|
||
location: '上海',
|
||
occurred_at: '2026-02-20'
|
||
},
|
||
{
|
||
id: 'claim-nanjing',
|
||
claim_no: 'BX-20260301-002',
|
||
expense_type: 'travel',
|
||
status: 'draft',
|
||
reason: '南京客户拜访',
|
||
location: '南京',
|
||
occurred_at: '2026-03-01'
|
||
}
|
||
]
|
||
const ocrDocuments = [
|
||
{
|
||
filename: '2月20 武汉-上海.pdf',
|
||
summary: '武汉至上海高铁票',
|
||
document_fields: [
|
||
{ key: 'date', label: '日期', value: '2026-02-20' },
|
||
{ key: 'route', label: '行程', value: '武汉-上海' }
|
||
]
|
||
},
|
||
{
|
||
filename: '2月23 上海-武汉.pdf',
|
||
summary: '上海至武汉高铁票',
|
||
document_fields: [
|
||
{ key: 'date', label: '日期', value: '2026-02-23' },
|
||
{ key: 'route', label: '行程', value: '上海-武汉' }
|
||
]
|
||
}
|
||
]
|
||
|
||
const match = resolveAiAttachmentAssociationMatch(claims, ocrDocuments)
|
||
|
||
assert.equal(match.highConfidence, true)
|
||
assert.equal(match.best.record.claimId, 'claim-wuhan-shanghai')
|
||
assert.match(match.best.reasons.join('、'), /日期/)
|
||
assert.match(match.best.reasons.join('、'), /地点|行程/)
|
||
|
||
const message = buildAiAttachmentAssociationMessage({
|
||
match,
|
||
fileNames: ocrDocuments.map((item) => item.filename),
|
||
ocrDocuments
|
||
})
|
||
assert.match(message, /BX-20260220-001/)
|
||
assert.match(message, /确认是否自动归集/)
|
||
assert.match(message, /ai-trusted-html:start/)
|
||
assert.ok(message.indexOf('票据识别结果') < message.indexOf('可能关联单据'))
|
||
|
||
const html = renderAiConversationHtml(message)
|
||
assert.match(html, /ai-ocr-recognition-card/)
|
||
assert.match(html, /ai-attachment-association-card/)
|
||
assert.match(html, /可能关联单据/)
|
||
assert.doesNotMatch(html, /26429165800002785705/)
|
||
|
||
const actions = buildAiAttachmentAssociationActions(match, 'assoc-1', { includeOcrDetails: true })
|
||
assert.equal(actions[0].action_type, 'show_ai_attachment_ocr_details')
|
||
assert.equal(findAction(actions, 'confirm_ai_attachment_association').payload.association_id, 'assoc-1')
|
||
assert.ok(findAction(actions, 'open_application_detail'))
|
||
})
|
||
|
||
test('自动归集消息展示票面 OCR 关键字段', () => {
|
||
const match = resolveAiAttachmentAssociationMatch([
|
||
{
|
||
id: 'claim-wuhan-shanghai',
|
||
claim_no: 'BX-20260220-001',
|
||
expense_type: 'travel',
|
||
status: 'draft',
|
||
reason: '辅助国网仿生产服务器部署,武汉往返上海',
|
||
location: '上海',
|
||
occurred_at: '2026-02-20'
|
||
}
|
||
], [
|
||
{
|
||
filename: '2月20 武汉-上海.pdf',
|
||
summary: '电子发票(铁路电子客票) 武汉-上海 票价 354元',
|
||
document_fields: [
|
||
{ key: 'amount', label: '金额', value: '354元' },
|
||
{ key: 'departed_at', label: '列车出发时间', value: '2026-02-20 07:55' },
|
||
{ key: 'route', label: '行程', value: '武汉-上海' }
|
||
]
|
||
}
|
||
])
|
||
|
||
const message = buildAiAttachmentAssociationMessage({
|
||
match,
|
||
fileNames: ['2月20 武汉-上海.pdf'],
|
||
ocrDocuments: [
|
||
{
|
||
filename: '2月20 武汉-上海.pdf',
|
||
summary: '电子发票(铁路电子客票) 武汉-上海 票价 354元',
|
||
document_fields: [
|
||
{ key: 'amount', label: '金额', value: '354元' },
|
||
{ key: 'departed_at', label: '列车出发时间', value: '2026-02-20 07:55' },
|
||
{ key: 'route', label: '行程', value: '武汉-上海' }
|
||
]
|
||
}
|
||
]
|
||
})
|
||
const html = renderAiConversationHtml(message)
|
||
|
||
assert.ok(message.indexOf('票据识别结果') < message.indexOf('可能关联单据'))
|
||
assert.match(message, /票面识别/)
|
||
assert.match(html, /ai-ocr-recognition-card/)
|
||
assert.match(html, /金额:354元/)
|
||
assert.match(html, /列车出发时间:2026-02-20 07:55/)
|
||
assert.match(html, /行程:武汉-上海/)
|
||
})
|
||
|
||
test('自动归集卡片不把票据数字残片当成单据事项', () => {
|
||
const dirtyReason = ':26429165800002785705; :2026; 05'
|
||
const ocrDocuments = [
|
||
{
|
||
filename: '2月20 武汉-上海.pdf',
|
||
summary: '电子发票(铁路电子客票) 武汉-上海 票价 354元',
|
||
document_fields: [
|
||
{ key: 'invoice_number', label: '', value: '26429165800002785705' },
|
||
{ key: 'year', label: '', value: '2026' },
|
||
{ key: 'amount', label: '金额', value: '354元' },
|
||
{ key: 'route', label: '行程', value: '武汉-上海' }
|
||
]
|
||
}
|
||
]
|
||
const match = resolveAiAttachmentAssociationMatch([
|
||
{
|
||
id: 'claim-dirty-reason',
|
||
claim_no: 'R74CB7C2R',
|
||
expense_type: 'travel',
|
||
status: 'draft',
|
||
reason: dirtyReason,
|
||
location: '上海',
|
||
occurred_at: '2026-02-20'
|
||
}
|
||
], ocrDocuments)
|
||
|
||
const message = buildAiAttachmentAssociationMessage({
|
||
match,
|
||
fileNames: ['2月20 武汉-上海.pdf'],
|
||
ocrDocuments
|
||
})
|
||
const html = renderAiConversationHtml(message)
|
||
|
||
assert.match(html, /R74CB7C2R/)
|
||
assert.match(html, /ai-ocr-recognition-card/)
|
||
assert.match(html, /票面识别/)
|
||
assert.match(html, /金额:354元/)
|
||
assert.match(html, /行程:武汉-上海/)
|
||
assert.doesNotMatch(html, /单据事项/)
|
||
assert.doesNotMatch(html, /26429165800002785705/)
|
||
assert.doesNotMatch(html, /:2026/)
|
||
})
|
||
|
||
test('没有可关联草稿时给出清晰提示', () => {
|
||
const match = resolveAiAttachmentAssociationMatch([
|
||
{
|
||
id: 'claim-submitted',
|
||
claim_no: 'BX-20260220-001',
|
||
status: 'submitted',
|
||
reason: '已提交报销单',
|
||
location: '上海',
|
||
occurred_at: '2026-02-20'
|
||
}
|
||
], [
|
||
{
|
||
filename: '2月20 武汉-上海.pdf',
|
||
summary: '武汉至上海高铁票',
|
||
document_fields: [
|
||
{ key: 'date', label: '日期', value: '2026-02-20' },
|
||
{ key: 'route', label: '行程', value: '武汉-上海' }
|
||
]
|
||
}
|
||
])
|
||
|
||
assert.equal(match.highConfidence, false)
|
||
assert.equal(match.rankedRecords.length, 0)
|
||
|
||
const message = buildAiAttachmentAssociationMessage({ match, fileNames: ['2月20 武汉-上海.pdf'] })
|
||
assert.match(message, /没有查到可关联的报销草稿/)
|
||
})
|
||
|
||
test('自动归集结果用卡片告知上传结果', () => {
|
||
const message = buildAiAttachmentAssociationResultMessage({
|
||
claimNo: 'BX-20260220-001',
|
||
uploadedCount: 2,
|
||
skippedCount: 0,
|
||
fileNames: ['2月20 武汉-上海.pdf', '2月23 上海-武汉.pdf']
|
||
})
|
||
const html = renderAiConversationHtml(message)
|
||
|
||
assert.match(message, /已完成自动归集/)
|
||
assert.match(html, /ai-attachment-association-card/)
|
||
assert.match(html, /2 份/)
|
||
assert.match(html, /BX-20260220-001/)
|
||
})
|
||
|
||
test('低置信候选也提供快捷确认关联动作', () => {
|
||
const claims = [
|
||
{
|
||
id: 'claim-shanghai',
|
||
claim_no: 'R74CB7C2R',
|
||
expense_type: 'travel',
|
||
status: 'draft',
|
||
reason: '出差报销',
|
||
location: '上海',
|
||
occurred_at: '2026-05-18'
|
||
}
|
||
]
|
||
const ocrDocuments = [
|
||
{
|
||
filename: '2月20 武汉-上海.pdf',
|
||
summary: 'G458 Wuhan Shanghaihongqiao',
|
||
document_fields: [
|
||
{ key: 'route', label: '行程', value: 'Wuhan Shanghaihongqiao' }
|
||
]
|
||
}
|
||
]
|
||
|
||
const match = resolveAiAttachmentAssociationMatch(claims, ocrDocuments)
|
||
assert.equal(match.highConfidence, false)
|
||
assert.equal(match.recommended.record.claimId, 'claim-shanghai')
|
||
|
||
const message = buildAiAttachmentAssociationMessage({
|
||
match,
|
||
fileNames: ['2月20 武汉-上海.pdf'],
|
||
ocrDocuments
|
||
})
|
||
const html = renderAiConversationHtml(message)
|
||
assert.match(html, /候选单据待核对/)
|
||
assert.match(html, /ai-attachment-association-card/)
|
||
|
||
const actions = buildAiAttachmentAssociationActions(match, 'assoc-low-confidence', { includeOcrDetails: true })
|
||
assert.equal(actions[0].action_type, 'show_ai_attachment_ocr_details')
|
||
assert.equal(findAction(actions, 'confirm_ai_attachment_association').payload.claim_no, 'R74CB7C2R')
|
||
})
|
||
|
||
test('旧版纯文本关联消息也渲染为卡片', () => {
|
||
const legacyMessage = [
|
||
'### 我已先识别票据,并匹配到最可能的报销单',
|
||
'',
|
||
'本次附件:1 份(2月20 武汉-上海.pdf)',
|
||
'',
|
||
'识别摘要:2月20 武汉-上海.pdf:G458 Wuhan Shanghaihongqiao',
|
||
'',
|
||
'推荐关联:R74CB7C2R',
|
||
'',
|
||
'单据事项:上海差旅报销',
|
||
'',
|
||
'匹配依据:地点或行程包含 上海;当前单据仍是可归集草稿',
|
||
'',
|
||
'你可以直接点下方“查看匹配单据”核对详情,不需要再手动查找。'
|
||
].join('\n')
|
||
|
||
const html = renderAiConversationHtml(legacyMessage)
|
||
assert.match(html, /ai-attachment-association-card/)
|
||
assert.match(html, /R74CB7C2R/)
|
||
assert.doesNotMatch(html, /\*\*R74CB7C2R\*\*/)
|
||
assert.doesNotMatch(html, /ai-html-title/)
|
||
})
|