Files
X-Financial/web/tests/requestProgressSteps.test.mjs

1253 lines
46 KiB
JavaScript
Raw Permalink Normal View History

import assert from 'node:assert/strict'
import test from 'node:test'
import { mapExpenseClaimToRequest } from '../src/composables/useRequests.js'
import {
buildApplicationDetailFactItems,
buildRelatedApplicationFactItems
} from '../src/utils/expenseApplicationDetail.js'
const CREATE_APPLICATION = '\u521b\u5efa\u7533\u8bf7'
const DIRECT_MANAGER_APPROVAL = '\u76f4\u5c5e\u9886\u5bfc\u5ba1\u6279'
const BUDGET_MANAGER_APPROVAL = '\u9884\u7b97\u7ba1\u7406\u8005\u5ba1\u6279'
const FINANCE_APPROVAL = '\u8d22\u52a1\u5ba1\u6279'
const APPROVAL_COMPLETED = '\u5ba1\u6279\u5b8c\u6210'
const APPLICATION_LINK_STATUS = '\u5173\u8054\u5355\u636e\u72b6\u6001'
const APPLICATION_ARCHIVE = '\u7533\u8bf7\u5f52\u6863'
const RETURNED = '\u9000\u56de'
const WAIT_SUBMIT = '\u5f85\u63d0\u4ea4'
const LINKED_APPLICATION = '\u5173\u8054\u5355\u636e'
const PAID = '\u5df2\u4ed8\u6b3e'
const ARCHIVED = '\u5df2\u5f52\u6863'
const WAIT_APPROVAL = '\u7b49\u5f85\u6279\u590d'
const WAIT_BUDGET_ZHAO_APPROVAL = '\u7b49\u5f85 \u8d75\u9884\u7b97 \u6279\u590d'
const WAIT_BUDGET_P8_EXECUTIVE_APPROVAL = '\u7b49\u5f85 P8 Executive \u6279\u590d'
const WAIT_FINANCE_FIONA_APPROVAL = '\u7b49\u5f85 Fiona Finance \u6279\u590d'
const LEADER_RETURNED_STATUS = '\u9886\u5bfc\u5df2\u9000\u56de\uff0c\u5f85\u91cd\u65b0\u63d0\u4ea4'
test('claim mapper exposes employee identifier for reviewer risk profile lookup', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-profile-1',
claim_no: 'EXP-PROFILE-1',
employee_id: 'emp-profile-1',
employee_name: 'Alice',
department_name: 'Finance',
expense_type: 'travel',
reason: 'Trip',
location: 'Shanghai',
amount: 1200,
invoice_count: 1,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T02:00:00.000Z',
status: 'submitted',
approval_stage: DIRECT_MANAGER_APPROVAL,
risk_flags_json: [],
items: []
})
assert.equal(request.employeeId, 'emp-profile-1')
assert.equal(request.employee_id, 'emp-profile-1')
assert.equal(request.profileEmployeeId, 'emp-profile-1')
})
test('claim mapper falls back to employee name for legacy profile lookup', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-profile-legacy',
claim_no: 'EXP-PROFILE-LEGACY',
employee_name: 'Legacy Alice',
department_name: 'Finance',
expense_type: 'travel',
reason: 'Trip',
location: 'Shanghai',
amount: 1200,
invoice_count: 1,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T02:00:00.000Z',
status: 'submitted',
approval_stage: DIRECT_MANAGER_APPROVAL,
risk_flags_json: [],
items: []
})
assert.equal(request.employeeId, '')
assert.equal(request.employee_id, '')
assert.equal(request.profileEmployeeId, 'Legacy Alice')
})
test('claim mapper keeps low reimbursement risk as low risk instead of medium', () => {
const riskMessage = '票据商品或服务描述较笼统,建议审批人核对真实用途和明细清单。'
const request = mapExpenseClaimToRequest({
id: 'claim-low-risk-1',
claim_no: 'RE-LOW-RISK-1',
employee_name: 'Alice',
department_name: 'Finance',
expense_type: 'travel',
reason: 'Trip',
location: 'Shanghai',
amount: 354,
invoice_count: 1,
occurred_at: '2026-02-20T00:00:00.000Z',
created_at: '2026-06-03T04:22:16.000Z',
updated_at: '2026-06-03T04:25:48.000Z',
status: 'draft',
approval_stage: WAIT_SUBMIT,
risk_flags_json: [
{
source: 'submission_review',
hit_source: 'rule_center',
severity: 'low',
action: 'warning',
label: '差旅票据服务内容笼统低风险',
message: riskMessage,
risk_domain: 'invoice',
visibility_scope: 'submitter',
actionability: 'fixable_by_submitter',
business_stage: 'reimbursement'
}
],
items: [
{
id: 'item-low-risk-train',
item_date: '2026-02-20',
item_type: 'train_ticket',
item_reason: '武汉-上海',
item_location: '',
item_amount: 354,
invoice_id: 'claim-low-risk-1/item-low-risk-train/train.pdf'
}
]
})
assert.equal(request.riskTone, 'low')
assert.equal(request.riskLabel, '低风险')
assert.equal(request.riskSummary, riskMessage)
assert.equal(request.expenseItems[0].riskTone, 'low')
assert.equal(request.expenseItems[0].riskLabel, '低风险')
assert.equal(request.expenseItems[0].riskText, riskMessage)
})
test('application claims are mapped as application documents', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-1',
claim_no: 'AP-20260525103045-ABCDEFGH',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: '支撑国网服务器上线部署',
location: '上海',
amount: 12000,
invoice_count: 0,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T02:00:00.000Z',
status: 'submitted',
approval_stage: '直属领导审批',
risk_flags_json: [],
items: []
})
assert.equal(request.documentTypeCode, 'application')
assert.equal(request.documentTypeLabel, '申请单')
assert.equal(request.typeLabel, '差旅费用申请')
assert.equal(request.secondaryStatusLabel, '申请材料')
assert.equal(request.secondaryStatusValue, '已进入审批流程')
assert.equal(request.expenseTableSummary, '预计金额已随申请提交')
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, '等待 Leader Li 批复', APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
assert.equal(request.progressSteps.some((step) => step.label === '财务审批'), false)
assert.equal(request.progressSteps.some((step) => step.label === DIRECT_MANAGER_APPROVAL), false)
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
assert.equal(request.progressSteps.find((step) => step.label === '等待 Leader Li 批复')?.rawLabel, DIRECT_MANAGER_APPROVAL)
assert.equal(request.progressSteps.find((step) => step.label === '等待 Leader Li 批复')?.current, true)
})
test('travel application detail splits trip time into departure and return rows', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-trip-time',
claim_no: 'AP-20260602103045-TRIPTIME',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: '支撑国网仿生产环境部署',
location: '上海',
amount: 3000,
invoice_count: 0,
occurred_at: '2026-02-20T00:00:00.000Z',
submitted_at: '2026-02-20T02:00:00.000Z',
created_at: '2026-02-20T01:30:00.000Z',
updated_at: '2026-02-20T02:00:00.000Z',
status: 'submitted',
approval_stage: '直属领导审批',
risk_flags_json: [
{
source: 'application_detail',
application_detail: {
application_type: '差旅费用申请',
time: '2026-02-20 至 2026-02-23',
location: '上海',
reason: '支撑国网仿生产环境部署',
days: '4 天',
transport_mode: '火车',
amount: '3000'
}
}
],
items: []
})
const factItems = buildApplicationDetailFactItems(request)
assert.deepEqual(
factItems
.filter((item) => ['trip_start_time', 'trip_return_time'].includes(item.key))
.map((item) => [item.label, item.value]),
[
['出发时间', '2026-02-20'],
['返回时间', '2026-02-23']
]
)
assert.equal(factItems.some((item) => item.label === '发生时间'), false)
assert.equal(factItems.some((item) => item.label === '行程时间'), false)
})
test('application claims wait for department P8 budget monitor after leader approval', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-budget',
claim_no: 'AP-20260525103145-BUDGET',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: '支撑国网服务器上线部署',
location: '上海',
amount: 12000,
invoice_count: 0,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T03:00:00.000Z',
status: 'submitted',
approval_stage: BUDGET_MANAGER_APPROVAL,
risk_flags_json: [
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: 'Leader Li',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: BUDGET_MANAGER_APPROVAL,
next_approver_name: '赵预算',
next_approver_grade: 'P8',
created_at: '2026-05-25T03:00:00.000Z'
}
],
items: []
})
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_ZHAO_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_ZHAO_APPROVAL)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过')
})
test('application budget wait label uses claim-level budget approver snapshot', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-budget-snapshot',
claim_no: 'AP-20260525103145-BUDGET-SNAPSHOT',
employee_name: 'Applicant Zhang',
department_name: 'Engineering',
manager_name: 'Leader Li',
budget_approver_name: 'P8 Executive',
budget_approver_grade: 'P8',
budget_approver_role_code: 'executive',
expense_type: 'travel_application',
reason: 'Production deployment support',
location: 'Beijing',
amount: 12000,
invoice_count: 0,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T03:00:00.000Z',
status: 'submitted',
approval_stage: BUDGET_MANAGER_APPROVAL,
risk_flags_json: [
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: 'Leader Li',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: BUDGET_MANAGER_APPROVAL,
created_at: '2026-05-25T03:00:00.000Z'
}
],
items: []
})
assert.equal(request.budgetApproverName, 'P8 Executive')
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_P8_EXECUTIVE_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_P8_EXECUTIVE_APPROVAL)?.current, true)
})
test('returned application claims include leader return node and supplement status', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-returned',
claim_no: 'APP-20260525-RETURNED',
employee_name: 'Applicant Zhang',
department_name: 'Delivery',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: 'Project onsite support',
location: 'Shanghai',
amount: 12000,
invoice_count: 0,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: null,
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T04:00:00.000Z',
status: 'returned',
approval_stage: WAIT_SUBMIT,
risk_flags_json: [
{
source: 'manual_return',
event_type: 'expense_application_return',
operator: 'Leader Li',
opinion: 'Need clearer budget explanation.',
return_stage_key: 'direct_manager',
next_status: 'returned',
next_approval_stage: WAIT_SUBMIT,
return_count: 2,
created_at: '2026-05-25T04:00:00.000Z'
}
],
items: []
})
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, RETURNED, WAIT_SUBMIT]
)
assert.equal(request.progressSteps.find((step) => step.label === RETURNED)?.time, 'Leader Li\u9000\u56de')
assert.match(request.progressSteps.find((step) => step.label === RETURNED)?.detail, /2026-05-25/)
assert.equal(request.progressSteps.find((step) => step.label === WAIT_SUBMIT)?.current, true)
assert.equal(request.secondaryStatusValue, LEADER_RETURNED_STATUS)
assert.equal(request.secondaryStatusTone, 'warning')
assert.equal(request.progressSteps.some((step) => step.label === APPROVAL_COMPLETED), false)
})
test('approved application claims complete after budget approval', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-approved',
claim_no: 'AP-20260525113045-HGFEDCBA',
employee_name: '张三',
department_name: '交付部',
manager_name: '李经理',
expense_type: 'travel_application',
reason: '支撑国网服务器上线部署',
location: '上海',
amount: 12000,
invoice_count: 0,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T03:00:00.000Z',
status: 'approved',
approval_stage: '审批完成',
risk_flags_json: [
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: '李经理',
previous_approval_stage: '直属领导审批',
next_approval_stage: '预算管理者审批',
next_approver_name: '赵预算',
next_approver_grade: 'P8',
created_at: '2026-05-25T03:00:00.000Z'
},
{
source: 'budget_approval',
event_type: 'expense_application_budget_approval',
operator: '赵预算',
previous_approval_stage: '预算管理者审批',
next_approval_stage: '审批完成',
created_at: '2026-05-25T03:00:00.000Z'
}
],
items: []
})
assert.equal(request.documentTypeCode, 'application')
assert.equal(request.workflowNode, APPLICATION_LINK_STATUS)
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, BUDGET_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.time, '未关联')
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, '李经理通过')
assert.equal(request.progressSteps.find((step) => step.label === BUDGET_MANAGER_APPROVAL)?.time, '赵预算通过')
})
test('application claims hide budget step when leader approval also covers budget approval', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-merged-budget',
claim_no: 'APP-20260525-MERGED',
employee_name: '张三',
department_name: '交付部',
manager_name: '李预算经理',
expense_type: 'travel_application',
reason: '支撑国网服务器上线部署',
location: '上海',
amount: 12000,
invoice_count: 0,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T03:00:00.000Z',
status: 'approved',
approval_stage: APPLICATION_LINK_STATUS,
risk_flags_json: [
{
source: 'approval_routing',
event_type: 'expense_application_route_decision',
requires_budget_review: true,
route: 'budget_manager',
budget_result: {
metrics: {
after_usage_rate: '99.27',
claim_amount_ratio: '1.27',
over_budget_amount: '0.00'
}
},
created_at: '2026-05-25T03:00:00.000Z'
},
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: '李预算经理',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: APPLICATION_LINK_STATUS,
budget_approval_merged: true,
route_decision: {
requires_budget_review: true,
route: 'budget_manager',
budget_result: {
metrics: {
after_usage_rate: '99.27',
claim_amount_ratio: '1.27',
over_budget_amount: '0.00'
}
}
},
created_at: '2026-05-25T03:00:00.000Z'
}
],
items: []
})
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, '李预算经理通过')
})
test('approved application claims hide budget step when dynamic route skipped budget review', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-skipped-budget',
claim_no: 'APP-20260525-SKIPPED',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: 'Project onsite support',
location: 'Shanghai',
amount: 500,
invoice_count: 0,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T03:00:00.000Z',
status: 'approved',
approval_stage: APPROVAL_COMPLETED,
risk_flags_json: [
{
source: 'approval_routing',
event_type: 'expense_application_route_decision',
requires_budget_review: false,
route: 'approval_done',
created_at: '2026-05-25T03:00:00.000Z'
},
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: 'Leader Li',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: APPROVAL_COMPLETED,
route_decision: {
requires_budget_review: false,
route: 'approval_done'
},
created_at: '2026-05-25T03:00:00.000Z'
}
],
items: []
})
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过')
})
test('approved application claims hide stale budget route below 90 percent threshold', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-stale-budget-route',
claim_no: 'APP-20260525-STALE-BUDGET',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: 'Project onsite support',
location: 'Shanghai',
amount: 8500,
invoice_count: 0,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T03:00:00.000Z',
status: 'approved',
approval_stage: APPROVAL_COMPLETED,
risk_flags_json: [
{
source: 'approval_routing',
event_type: 'expense_application_route_decision',
requires_budget_review: true,
route: 'budget_manager',
budget_result: {
metrics: {
after_usage_rate: '85.00',
claim_amount_ratio: '85.00',
over_budget_amount: '0.00'
}
},
created_at: '2026-05-25T03:00:00.000Z'
},
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: 'Leader Li',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: BUDGET_MANAGER_APPROVAL,
route_decision: {
requires_budget_review: true,
route: 'budget_manager',
budget_result: {
metrics: {
after_usage_rate: '85.00',
claim_amount_ratio: '85.00',
over_budget_amount: '0.00'
}
}
},
created_at: '2026-05-25T03:00:00.000Z'
}
],
items: []
})
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
})
test('approved application claims show linked reimbursement status before archive', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-linked-draft',
claim_no: 'AP-202606050001-LINKED',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: 'Project onsite support',
location: 'Shanghai',
amount: 500,
invoice_count: 0,
occurred_at: '2026-06-05T00:00:00.000Z',
submitted_at: '2026-06-05T02:00:00.000Z',
created_at: '2026-06-05T01:30:00.000Z',
updated_at: '2026-06-05T03:00:00.000Z',
status: 'approved',
approval_stage: APPLICATION_LINK_STATUS,
risk_flags_json: [
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: 'Leader Li',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: APPLICATION_LINK_STATUS,
generated_draft_claim_no: 'RE-202606050001-LINKED',
created_at: '2026-06-05T03:00:00.000Z'
}
],
items: []
})
const linkStep = request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)
assert.equal(request.workflowNode, APPLICATION_LINK_STATUS)
assert.equal(linkStep?.current, true)
assert.equal(linkStep?.time, '关联中 RE-202606050001-LINKED')
assert.equal(request.secondaryStatusValue, '关联中 RE-202606050001-LINKED')
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
})
test('application claims are archived only after linked reimbursement is paid', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-archived',
claim_no: 'AP-202606050001-ARCHIVED',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: 'Project onsite support',
location: 'Shanghai',
amount: 500,
invoice_count: 0,
occurred_at: '2026-06-05T00:00:00.000Z',
submitted_at: '2026-06-05T02:00:00.000Z',
created_at: '2026-06-05T01:30:00.000Z',
updated_at: '2026-06-07T03:00:00.000Z',
status: 'approved',
approval_stage: APPLICATION_ARCHIVE,
risk_flags_json: [
{
source: 'application_archive_sync',
event_type: 'expense_application_archived_by_reimbursement',
reimbursement_claim_no: 'RE-202606050001-ARCHIVED',
created_at: '2026-06-07T03:00:00.000Z'
}
],
items: []
})
assert.equal(request.workflowNode, APPLICATION_ARCHIVE)
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.every((step) => step.done), true)
assert.equal(request.secondaryStatusValue, '已归档')
})
test('progress steps show approval operator time and current stay duration', () => {
const originalNow = Date.now
Date.now = () => new Date('2026-05-20T05:00:00.000Z').getTime()
try {
const request = mapExpenseClaimToRequest({
id: 'claim-1',
claim_no: 'EXP-202605-001',
employee_name: '张三',
department_name: '市场部',
finance_owner_name: 'Wang Finance Group',
finance_approver_name: 'Fiona Finance',
expense_type: 'transport',
reason: '交通报销',
location: '上海',
amount: 88,
invoice_count: 1,
occurred_at: '2026-05-20T01:00:00.000Z',
submitted_at: '2026-05-20T02:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T03:30:00.000Z',
status: 'submitted',
approval_stage: FINANCE_APPROVAL,
risk_flags_json: [
{
source: 'manual_approval',
operator: '李经理',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: FINANCE_APPROVAL,
created_at: '2026-05-20T03:30:00.000Z'
}
],
items: []
})
const leaderStep = request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)
const financeStep = request.progressSteps.find((step) => step.rawLabel === FINANCE_APPROVAL)
const firstStep = request.progressSteps[0]
assert.equal(request.riskSummary, '无')
assert.equal(firstStep.label, LINKED_APPLICATION)
assert.equal(leaderStep.time, '李经理通过')
assert.match(leaderStep.detail, /2026-05-20/)
assert.match(leaderStep.title, /李经理审批通过/)
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
assert.equal(request.financeOwnerName, 'Wang Finance Group')
assert.equal(request.financeApproverName, 'Fiona Finance')
assert.equal(financeStep.label, WAIT_FINANCE_FIONA_APPROVAL)
assert.equal(financeStep.rawLabel, FINANCE_APPROVAL)
assert.equal(financeStep.current, true)
assert.equal(financeStep.time, '停留 1小时30分钟')
} finally {
Date.now = originalNow
}
})
test('progress steps do not expose approver email when manager name is available', () => {
const originalNow = Date.now
Date.now = () => new Date('2026-05-20T05:00:00.000Z').getTime()
try {
const request = mapExpenseClaimToRequest({
id: 'claim-email-operator',
claim_no: 'EXP-202605-003',
employee_name: '张三',
department_name: '市场部',
manager_name: '李经理',
expense_type: 'transport',
reason: '交通报销',
location: '上海',
amount: 88,
invoice_count: 1,
occurred_at: '2026-05-20T01:00:00.000Z',
submitted_at: '2026-05-20T02:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T03:30:00.000Z',
status: 'submitted',
approval_stage: '财务审批',
risk_flags_json: [
{
source: 'manual_approval',
operator: 'manager@example.com',
operator_username: 'manager@example.com',
previous_approval_stage: '直属领导审批',
next_approval_stage: '财务审批',
created_at: '2026-05-20T03:30:00.000Z'
}
],
items: []
})
const leaderStep = request.progressSteps.find((step) => step.label === '直属领导审批')
assert.equal(leaderStep.time, '李经理通过')
assert.ok(!leaderStep.title.includes('manager@example.com'))
} finally {
Date.now = originalNow
}
})
test('travel expense items describe departure return and lodging time below the date', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-travel-time-labels',
claim_no: 'EXP-202605-TRAVEL',
employee_name: '张三',
department_name: '市场部',
expense_type: 'travel',
reason: '北京客户现场出差',
location: '北京',
amount: 1108,
invoice_count: 3,
occurred_at: '2026-05-13T01:00:00.000Z',
created_at: '2026-05-13T01:30:00.000Z',
updated_at: '2026-05-13T03:30:00.000Z',
status: 'draft',
approval_stage: '待提交',
risk_flags_json: [],
items: [
{
id: 'outbound-train',
item_type: 'train_ticket',
item_reason: '广州南-北京南',
item_location: '北京',
item_date: '2026-05-13',
item_amount: 354,
invoice_id: 'outbound.png'
},
{
id: 'hotel',
item_type: 'hotel_ticket',
item_reason: '北京全季酒店',
item_location: '北京',
item_date: '2026-05-14',
item_amount: 100,
invoice_id: 'hotel.png'
},
{
id: 'return-train',
item_type: 'train_ticket',
item_reason: '北京南-广州南',
item_location: '广州',
item_date: '2026-05-15',
item_amount: 354,
invoice_id: 'return.png'
},
{
id: 'allowance',
item_type: 'travel_allowance',
item_reason: '系统自动计算出差补贴',
item_location: '北京',
item_date: '2026-05-15',
item_amount: 300,
invoice_id: '',
is_system_generated: true
}
]
})
assert.equal(request.expenseItems.find((item) => item.id === 'outbound-train')?.dayLabel, '出发时间')
assert.equal(request.expenseItems.find((item) => item.id === 'return-train')?.dayLabel, '返回时间')
assert.equal(request.expenseItems.find((item) => item.id === 'hotel')?.dayLabel, '住宿时间')
assert.equal(request.expenseItems.find((item) => item.id === 'outbound-train')?.detail, '起始地-目的地')
assert.equal(request.expenseItems.find((item) => item.id === 'return-train')?.detail, '起始地-目的地')
assert.equal(request.expenseItems.find((item) => item.id === 'hotel')?.detail, '目的地酒店')
assert.equal(request.expenseItems.at(-1)?.id, 'allowance')
assert.equal(request.expenseItems.at(-1)?.dayLabel, '系统自动计算')
})
test('ticket description helper does not show the destination city as detail text', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-ticket-detail-helper',
claim_no: 'EXP-202605-ROUTE',
employee_name: '张三',
department_name: '市场部',
expense_type: 'travel',
reason: '上海项目出差',
location: '上海',
amount: 520,
invoice_count: 2,
occurred_at: '2026-05-13T01:00:00.000Z',
created_at: '2026-05-13T01:30:00.000Z',
updated_at: '2026-05-13T03:30:00.000Z',
status: 'draft',
approval_stage: '待提交',
risk_flags_json: [],
items: [
{
id: 'flight',
item_type: 'flight_ticket',
item_reason: '广州白云-上海虹桥',
item_location: '上海',
item_date: '2026-05-13',
item_amount: 320,
invoice_id: 'flight.png'
},
{
id: 'ship',
item_type: 'ship_ticket',
item_reason: '上海港-舟山港',
item_location: '舟山',
item_date: '2026-05-14',
item_amount: 200,
invoice_id: 'ship.png'
}
]
})
assert.equal(request.expenseItems.find((item) => item.id === 'flight')?.detail, '起始地-目的地')
assert.equal(request.expenseItems.find((item) => item.id === 'ship')?.detail, '起始地-目的地')
assert.equal(request.expenseItems.find((item) => item.id === 'ship')?.name, '轮船票')
})
test('finance approval moves reimbursement to pending payment step', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-finance-completed',
claim_no: 'EXP-202605-004',
employee_name: '张三',
department_name: '市场部',
expense_type: 'transport',
reason: '交通报销',
location: '上海',
amount: 88,
invoice_count: 1,
occurred_at: '2026-05-20T01:00:00.000Z',
submitted_at: '2026-05-20T02:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T04:00:00.000Z',
status: 'pending_payment',
approval_stage: '待付款',
risk_flags_json: [
{
source: 'manual_approval',
operator: '李经理',
previous_approval_stage: '直属领导审批',
next_approval_stage: '财务审批',
created_at: '2026-05-20T03:00:00.000Z'
},
{
source: 'finance_approval',
operator: '财务复核',
previous_approval_stage: '财务审批',
next_approval_stage: '待付款',
created_at: '2026-05-20T04:00:00.000Z'
}
],
items: []
})
const financeStep = request.progressSteps.find((step) => step.label === '财务审批')
const paymentStep = request.progressSteps.find((step) => step.label === '待付款')
assert.equal(request.riskSummary, '无')
assert.equal(request.workflowNode, '待付款')
assert.equal(request.approvalKey, 'pending_payment')
assert.equal(financeStep.time, '财务复核通过')
assert.match(financeStep.detail, /2026-05-20/)
assert.equal(paymentStep.current, true)
assert.equal(paymentStep.done, false)
})
test('paid reimbursement marks payment progress step as complete', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-finance-paid',
claim_no: 'EXP-202605-005',
employee_name: '张三',
department_name: '市场部',
expense_type: 'transport',
reason: '交通报销',
location: '上海',
amount: 88,
invoice_count: 1,
occurred_at: '2026-05-20T01:00:00.000Z',
submitted_at: '2026-05-20T02:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T05:00:00.000Z',
status: 'paid',
approval_stage: '已付款',
risk_flags_json: [
{
source: 'finance_approval',
operator: '财务复核',
previous_approval_stage: '财务审批',
next_approval_stage: '待付款',
created_at: '2026-05-20T04:00:00.000Z'
},
{
source: 'payment',
event_type: 'expense_claim_payment_completed',
operator: '财务付款',
previous_approval_stage: '待付款',
next_approval_stage: '已付款',
created_at: '2026-05-20T05:00:00.000Z'
},
{
source: 'application_handoff',
event_type: 'expense_application_to_reimbursement_draft',
application_claim_id: 'application-1',
application_claim_no: 'APP-20260520-001',
application_detail: {
application_type: '差旅费用申请',
application_content: '差旅费用申请 / 北京',
application_reason: '支撑国网仿生产环境部署',
application_days: '4 天',
application_location: '北京',
application_amount: '3000',
application_time: '2026-05-20 至 2026-05-23',
application_transport_mode: '高铁',
application_lodging_daily_cap: '600元/天',
application_subsidy_daily_cap: '120元/天',
application_transport_policy: '按真实票据复核',
application_policy_estimate: '交通按真实票据 + 住宿 2,400元 + 补贴 480元',
application_rule_name: '差旅标准规则',
application_rule_version: '2026.05'
}
}
],
items: []
})
const paymentStep = request.progressSteps.find((step) => step.label === '待付款')
const paidStep = request.progressSteps.find((step) => step.label === PAID)
const archivedStep = request.progressSteps.find((step) => step.label === ARCHIVED)
const linkedStep = request.progressSteps.find((step) => step.label === LINKED_APPLICATION)
assert.equal(request.workflowNode, '已付款')
assert.equal(request.approvalStatus, '已付款')
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[LINKED_APPLICATION, '待提交', '直属领导审批', '财务审批', '待付款', PAID, ARCHIVED]
)
assert.equal(paymentStep.time, '待付款')
assert.equal(paidStep.time, '已付款')
assert.equal(paidStep.done, true)
assert.equal(archivedStep.time, ARCHIVED)
assert.equal(archivedStep.done, true)
assert.equal(linkedStep.time, '已关联 APP-20260520-001')
assert.equal(request.relatedApplication.claimNo, 'APP-20260520-001')
assert.equal(request.relatedApplication.reason, '支撑国网仿生产环境部署')
assert.equal(request.relatedApplication.days, '4 天')
assert.equal(request.relatedApplication.time, '2026-05-20 至 2026-05-23')
assert.equal(request.relatedApplication.tripStartDate, '2026-05-20')
assert.equal(request.relatedApplication.tripEndDate, '2026-05-23')
assert.equal(request.relatedApplication.transportMode, '高铁')
assert.equal(request.relatedApplication.lodgingDailyCap, '600元/天')
assert.equal(request.relatedApplication.subsidyDailyCap, '120元/天')
assert.equal(request.relatedApplication.transportPolicy, '按真实票据复核')
assert.equal(request.relatedApplication.policyEstimate, '交通按真实票据 + 住宿 2,400元 + 补贴 480元')
assert.equal(request.relatedApplication.ruleLabel, '差旅标准规则 / 2026.05')
assert.equal(request.relatedApplication.amountLabel, '¥3,000')
assert.deepEqual(
buildRelatedApplicationFactItems(request).map((item) => [item.label, item.value]),
[
['关联单据单号', 'APP-20260520-001'],
['申请内容', '差旅费用申请 / 北京'],
['出发时间', '2026-05-20'],
['返回时间', '2026-05-23'],
['申请天数', '4 天'],
['申请事由', '支撑国网仿生产环境部署'],
['申请地点', '北京'],
['出行方式', '高铁'],
['住宿上限/天', '600元/天'],
['补贴标准/天', '120元/天'],
['交通费用口径', '按真实票据复核'],
['规则测算参考', '交通按真实票据 + 住宿 2,400元 + 补贴 480元'],
['规则依据', '差旅标准规则 / 2026.05'],
['预计金额', '¥3,000']
]
)
})
test('reimbursement detail resolves linked application from guided entry context', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-linked-context',
claim_no: 'EXP-20260520-009',
employee_name: '张三',
department_name: '交付部',
expense_type: 'travel',
reason: '支撑国网仿生产环境部署',
location: '北京',
amount: 654,
invoice_count: 1,
occurred_at: '2026-05-20T01:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T02:00:00.000Z',
status: 'draft',
approval_stage: '待提交',
risk_flags_json: [
{
source: 'application_link',
event_type: 'expense_reimbursement_application_linked',
review_form_values: {
application_claim_id: 'application-guided-1',
application_claim_no: 'AP-202605-001',
application_reason: '支撑国网仿生产环境部署',
application_location: '北京',
application_amount: '3000',
application_amount_label: '¥3,000',
application_business_time: '2026-05-20 至 2026-05-23',
application_days: '4 天',
application_transport_mode: '高铁',
application_lodging_daily_cap: '600元/天',
application_subsidy_daily_cap: '120元/天',
application_transport_policy: '按真实票据复核',
application_policy_estimate: '交通按真实票据 + 住宿 2,400元 + 补贴 480元'
},
expense_scene_selection: {
application_claim_no: 'AP-202605-001'
}
}
],
items: []
})
assert.equal(request.relatedApplication.claimNo, 'AP-202605-001')
assert.equal(request.relatedApplication.reason, '支撑国网仿生产环境部署')
assert.equal(request.relatedApplication.location, '北京')
assert.equal(request.relatedApplication.time, '2026-05-20 至 2026-05-23')
assert.equal(request.relatedApplication.tripStartDate, '2026-05-20')
assert.equal(request.relatedApplication.tripEndDate, '2026-05-23')
assert.equal(request.relatedApplication.days, '4 天')
assert.equal(request.relatedApplication.transportMode, '高铁')
assert.equal(request.relatedApplication.lodgingDailyCap, '600元/天')
assert.equal(request.relatedApplication.subsidyDailyCap, '120元/天')
assert.equal(request.relatedApplication.policyEstimate, '交通按真实票据 + 住宿 2,400元 + 补贴 480元')
assert.equal(request.relatedApplication.amountLabel, '¥3,000')
assert.deepEqual(request.expenseItems, [])
})
test('reimbursement detail hides stale application placeholder and allowance rows before receipts', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-linked-stale-placeholder',
claim_no: 'EXP-20260520-010',
employee_name: '张三',
department_name: '交付部',
expense_type: 'travel',
reason: '支撑国网仿生产环境部署',
location: '上海',
amount: 3480,
invoice_count: 0,
occurred_at: '2026-02-20T00:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T02:00:00.000Z',
status: 'draft',
approval_stage: '待提交',
risk_flags_json: [
{
source: 'application_link',
event_type: 'expense_reimbursement_application_linked',
review_form_values: {
application_claim_id: 'application-guided-stale',
application_claim_no: 'AP-202605-010',
application_reason: '支撑国网仿生产环境部署',
application_location: '上海',
application_amount: '3000',
application_amount_label: '¥3,000',
application_business_time: '2026-02-20 至 2026-02-23',
application_days: '4 天',
application_transport_mode: '火车'
}
}
],
items: [
{
id: 'placeholder-travel',
item_type: 'travel',
item_reason: '支撑国网仿生产环境部署',
item_location: '上海',
item_amount: 3000,
item_date: '2026-02-20',
invoice_id: ''
},
{
id: 'stale-allowance',
item_type: 'travel_allowance',
item_reason: '系统自动计算出差补贴上海市1天120.00元/天',
item_location: '直辖市/特区',
item_amount: 120,
item_date: '2026-02-20',
invoice_id: ''
}
]
})
assert.equal(request.relatedApplication.claimNo, 'AP-202605-010')
assert.equal(request.relatedApplication.days, '4 天')
assert.equal(request.amount, 0)
assert.deepEqual(request.expenseItems, [])
assert.equal(request.expenseTableSummary, '暂无费用明细')
})
test('reimbursement detail hides stale allowance when linked application days differ', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-linked-stale-allowance',
claim_no: 'EXP-20260520-011',
employee_name: '张三',
department_name: '交付部',
expense_type: 'travel',
reason: '支撑国网仿生产环境部署',
location: '上海',
amount: 474,
invoice_count: 1,
occurred_at: '2026-02-20T00:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T02:00:00.000Z',
status: 'draft',
approval_stage: '待提交',
risk_flags_json: [
{
source: 'application_link',
event_type: 'expense_reimbursement_application_linked',
application_claim_no: 'AP-202605-011',
application_detail: {
application_reason: '支撑国网仿生产环境部署',
application_location: '上海',
application_time: '2026-02-20 至 2026-02-23',
application_days: '4 天',
application_transport_mode: '火车'
}
}
],
items: [
{
id: 'outbound-train',
item_type: 'train_ticket',
item_reason: '武汉-上海',
item_location: '上海',
item_amount: 354,
item_date: '2026-02-20',
invoice_id: 'ticket-1.png'
},
{
id: 'stale-allowance',
item_type: 'travel_allowance',
item_reason: '系统自动计算出差补贴上海市1天120.00元/天',
item_location: '直辖市/特区',
item_amount: 120,
item_date: '2026-02-20',
invoice_id: ''
}
]
})
assert.equal(request.relatedApplication.days, '4 天')
assert.deepEqual(request.expenseItems.map((item) => item.id), ['outbound-train'])
assert.equal(request.amount, 354)
assert.equal(request.expenseTableSummary, '共 1 条费用明细,已关联 1 张票据')
})
test('current direct manager step shows how long the claim has stayed there', () => {
const originalNow = Date.now
Date.now = () => new Date('2026-05-20T05:15:00.000Z').getTime()
try {
const request = mapExpenseClaimToRequest({
id: 'claim-2',
claim_no: 'EXP-202605-002',
employee_name: '王五',
department_name: '市场部',
manager_name: '李经理',
expense_type: 'office',
reason: '办公用品',
location: '上海',
amount: 128,
invoice_count: 1,
occurred_at: '2026-05-20T01:00:00.000Z',
submitted_at: '2026-05-20T02:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T02:00:00.000Z',
status: 'submitted',
approval_stage: '直属领导审批',
risk_flags_json: [],
items: []
})
const leaderStep = request.progressSteps.find((step) => step.rawLabel === '直属领导审批')
const submitStep = request.progressSteps.find((step) => step.label === '待提交')
assert.equal(submitStep.time, '王五提交')
assert.match(submitStep.detail, /2026-05-20/)
assert.equal(leaderStep.label, '等待批复')
assert.doesNotMatch(leaderStep.label, /李经理/)
assert.equal(leaderStep.rawLabel, '直属领导审批')
assert.equal(leaderStep.current, true)
assert.equal(leaderStep.time, '停留 3小时15分钟')
} finally {
Date.now = originalNow
}
})