- 完善 user_agent_application 申请差旅报销预审槽位与消息组装 - 增强预算助理报告与风险建议卡片交互 - 重构登录页视觉样式与移动端响应式适配 - 优化个人工作台、文档中心、政策中心、员工管理等页面布局 - 拆分 travelRequestDetailPreReviewModel 为 advice/submit 模型 - 补充报销草稿、风险复核、Item Sync 与模板执行器测试覆盖
794 lines
28 KiB
JavaScript
794 lines
28 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
|
|
import { mapExpenseClaimToRequest } from '../src/composables/useRequests.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 APPROVAL_COMPLETED = '\u5ba1\u6279\u5b8c\u6210'
|
|
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_LEADER_LI_APPROVAL = '\u7b49\u5f85 Leader Li \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 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('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, WAIT_LEADER_LI_APPROVAL, APPROVAL_COMPLETED]
|
|
)
|
|
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 === WAIT_LEADER_LI_APPROVAL)?.rawLabel, DIRECT_MANAGER_APPROVAL)
|
|
assert.equal(request.progressSteps.find((step) => step.label === WAIT_LEADER_LI_APPROVAL)?.current, true)
|
|
})
|
|
|
|
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, APPROVAL_COMPLETED]
|
|
)
|
|
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, APPROVAL_COMPLETED]
|
|
)
|
|
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, '审批完成')
|
|
assert.deepEqual(
|
|
request.progressSteps.map((step) => step.label),
|
|
['创建申请', '直属领导审批', '预算管理者审批', '审批完成']
|
|
)
|
|
assert.equal(request.progressSteps.every((step) => step.done), true)
|
|
assert.equal(request.progressSteps.find((step) => step.label === '直属领导审批')?.time, '李经理通过')
|
|
assert.equal(request.progressSteps.find((step) => step.label === '预算管理者审批')?.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: APPROVAL_COMPLETED,
|
|
risk_flags_json: [
|
|
{
|
|
source: 'manual_approval',
|
|
event_type: 'expense_application_approval',
|
|
operator: '李预算经理',
|
|
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
|
|
next_approval_stage: APPROVAL_COMPLETED,
|
|
budget_approval_merged: true,
|
|
created_at: '2026-05-25T03:00:00.000Z'
|
|
}
|
|
],
|
|
items: []
|
|
})
|
|
|
|
assert.deepEqual(
|
|
request.progressSteps.map((step) => step.label),
|
|
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED]
|
|
)
|
|
assert.equal(request.progressSteps.every((step) => step.done), true)
|
|
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, APPROVAL_COMPLETED]
|
|
)
|
|
assert.equal(request.progressSteps.every((step) => step.done), true)
|
|
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('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: '市场部',
|
|
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: '李经理',
|
|
previous_approval_stage: '直属领导审批',
|
|
next_approval_stage: '财务审批',
|
|
created_at: '2026-05-20T03:30:00.000Z'
|
|
}
|
|
],
|
|
items: []
|
|
})
|
|
|
|
const leaderStep = request.progressSteps.find((step) => step.label === '直属领导审批')
|
|
const financeStep = request.progressSteps.find((step) => step.label === '财务审批')
|
|
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(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: '3 天',
|
|
application_location: '北京',
|
|
application_amount: '3000',
|
|
application_time: '2026-05-20T00:00:00.000Z',
|
|
application_transport_mode: '高铁'
|
|
}
|
|
}
|
|
],
|
|
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, '3 天')
|
|
assert.equal(request.relatedApplication.amountLabel, '¥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'
|
|
},
|
|
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.amountLabel, '¥3,000')
|
|
})
|
|
|
|
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: '市场部',
|
|
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.label === '直属领导审批')
|
|
const submitStep = request.progressSteps.find((step) => step.label === '待提交')
|
|
|
|
assert.equal(submitStep.time, '王五提交')
|
|
assert.match(submitStep.detail, /2026-05-20/)
|
|
assert.equal(leaderStep.current, true)
|
|
assert.equal(leaderStep.time, '停留 3小时15分钟')
|
|
} finally {
|
|
Date.now = originalNow
|
|
}
|
|
})
|