feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user