feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造

- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-01 17:07:14 +08:00
parent 7989f3a159
commit 92444e7eae
285 changed files with 25075 additions and 2986 deletions

View File

@@ -14,8 +14,62 @@ 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',
@@ -46,11 +100,12 @@ test('application claims are mapped as application documents', () => {
assert.equal(request.expenseTableSummary, '预计金额已随申请提交')
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, BUDGET_MANAGER_APPROVAL, APPROVAL_COMPLETED]
[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)
})
@@ -96,6 +151,48 @@ test('application claims wait for department P8 budget monitor after leader appr
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',
@@ -235,6 +332,57 @@ test('application claims hide budget step when leader approval also covers budge
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()
@@ -567,6 +715,48 @@ test('paid reimbursement marks payment progress step as complete', () => {
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()