feat(expenses): backfill historical claims into expense cases

This commit is contained in:
caoxiaozhu
2026-07-14 10:09:09 +08:00
parent 11275e4ba6
commit 5ed34c2b8f
11 changed files with 2053 additions and 3 deletions

View File

@@ -26,6 +26,7 @@ const BUSINESS_STATUS_LABELS = {
const EVENT_PRESENTATION = {
claim_draft_created: { label: '报销草稿已创建', icon: 'mdi mdi-file-plus-outline', tone: 'info' },
claim_draft_updated: { label: '报销草稿已更新', icon: 'mdi mdi-file-edit-outline', tone: 'info' },
historical_claim_imported: { label: '历史单据已纳入统一费用事件', icon: 'mdi mdi-history', tone: 'info' },
application_submitted: { label: '费用申请已提交', icon: 'mdi mdi-send-outline', tone: 'info' },
claim_submitted: { label: '报销单已提交', icon: 'mdi mdi-send-outline', tone: 'info' },
approval_stage_completed: { label: '审批节点已完成', icon: 'mdi mdi-account-check-outline', tone: 'success' },
@@ -83,7 +84,30 @@ function buildStateTransitionSummary(payload) {
return transition || (nextStage ? `当前节点:${nextStage}` : '')
}
function buildHistoricalClaimImportSummary(payload) {
const currentStatus = resolveStatusLabel(readField(payload, 'next_status', 'nextStatus'))
const currentStage = compactText(readField(payload, 'next_approval_stage', 'nextApprovalStage'))
const currentStateParts = []
if (currentStatus) {
currentStateParts.push(`纳入时状态:${currentStatus}`)
}
if (currentStage) {
currentStateParts.push(`纳入时节点:${currentStage}`)
}
const currentStateSummary = currentStateParts.length
? `${currentStateParts.join(' · ')}`
: ''
return `系统已根据原单据建立费用事件档案。${currentStateSummary}导入前办理记录以原单据为准,后续动作将在此持续记录。`
}
function buildEventSummary(eventType, payload) {
if (eventType === 'historical_claim_imported') {
return buildHistoricalClaimImportSummary(payload)
}
const reason = compactText(payload?.reason)
if (reason) {
return reason
@@ -159,7 +183,7 @@ function normalizeEvent(event, index) {
icon: presentation.icon,
tone: presentation.tone,
summary: buildEventSummary(eventType, payload),
actorLabel: resolveActorLabel(event),
actorLabel: eventType === 'historical_claim_imported' ? '系统' : resolveActorLabel(event),
occurredAt: compactText(occurredAt),
occurredAtLabel: formatOccurredAt(occurredAt),
sortTimestamp: Number.isNaN(timestamp) ? null : timestamp,

View File

@@ -100,6 +100,64 @@ test('expense case timeline gives returned events an actionable reason', () => {
assert.equal(timeline.events[0].summary, '住宿票据缺少入住人信息')
})
test('expense case timeline explains a historical import without pretending it is a business action', () => {
const timeline = buildExpenseCaseTimelineViewModel({
events: [
{
id: 'historical-import-1',
event_type: 'historical_claim_imported',
actor_id: 'migration-worker',
actor_type: 'user',
occurred_at: '2026-07-14T03:00:00Z',
correlation_id: 'private-correlation',
delivery_status: 'pending',
payload_json: {
reason: '不应覆盖历史导入事件的专用说明',
next_status: 'submitted',
next_approval_stage: '直属领导审批',
migration_batch_id: 'private-batch',
source_table: 'expense_claims'
}
}
]
})
const [event] = timeline.events
assert.equal(event.label, '历史单据已纳入统一费用事件')
assert.equal(event.icon, 'mdi mdi-history')
assert.equal(event.tone, 'info')
assert.equal(event.actorLabel, '系统')
assert.equal(
event.summary,
'系统已根据原单据建立费用事件档案。纳入时状态:已提交 · 纳入时节点:直属领导审批。导入前办理记录以原单据为准,后续动作将在此持续记录。'
)
assert.doesNotMatch(
JSON.stringify(event),
/不应覆盖|private-correlation|pending|private-batch|expense_claims/
)
})
test('expense case timeline keeps the historical import summary honest when state is missing', () => {
const timeline = buildExpenseCaseTimelineViewModel({
events: [
{
id: 'historical-import-without-state',
event_type: 'historical_claim_imported',
actor_id: 'system',
occurred_at: '2026-07-14T03:00:00Z',
payload_json: {}
}
]
})
const [event] = timeline.events
assert.equal(
event.summary,
'系统已根据原单据建立费用事件档案。导入前办理记录以原单据为准,后续动作将在此持续记录。'
)
assert.doesNotMatch(event.summary, /纳入时状态|纳入时节点|undefined|null/)
})
test('expense case timeline describes application archive against the paid reimbursement', () => {
const timeline = buildExpenseCaseTimelineViewModel({
events: [