2026-06-01 17:07:14 +08:00
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
|
|
|
|
|
|
import { buildDetailAlerts } from '../src/utils/detailAlerts.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
canViewRiskForContext,
|
|
|
|
|
|
filterRiskCardsForVisibility,
|
|
|
|
|
|
resolveRiskActionability,
|
|
|
|
|
|
resolveRiskDomain,
|
|
|
|
|
|
resolveRiskVisibilityScope
|
|
|
|
|
|
} from '../src/utils/riskVisibility.js'
|
|
|
|
|
|
|
|
|
|
|
|
const submitter = {
|
|
|
|
|
|
id: 'EMP-001',
|
|
|
|
|
|
employeeId: 'EMP-001',
|
|
|
|
|
|
name: '张三',
|
|
|
|
|
|
roleCodes: []
|
|
|
|
|
|
}
|
|
|
|
|
|
const budgetManager = {
|
|
|
|
|
|
id: 'EMP-P8',
|
|
|
|
|
|
name: '预算管理员',
|
|
|
|
|
|
grade: 'P8',
|
|
|
|
|
|
departmentName: '研发部',
|
|
|
|
|
|
roleCodes: ['budget_monitor']
|
|
|
|
|
|
}
|
|
|
|
|
|
const financeUser = {
|
|
|
|
|
|
id: 'FIN-001',
|
|
|
|
|
|
name: '财务',
|
|
|
|
|
|
roleCodes: ['finance']
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const applicationRequest = {
|
|
|
|
|
|
id: 'AP-202606010001',
|
|
|
|
|
|
claimId: 'application-claim-id',
|
|
|
|
|
|
documentTypeCode: 'application',
|
|
|
|
|
|
approvalKey: 'in_progress',
|
|
|
|
|
|
node: '预算管理者审批',
|
|
|
|
|
|
employeeId: 'EMP-001',
|
|
|
|
|
|
departmentName: '研发部',
|
|
|
|
|
|
typeCode: 'travel_application',
|
|
|
|
|
|
typeLabel: '差旅费用申请',
|
|
|
|
|
|
reason: '北京出差申请',
|
|
|
|
|
|
location: '北京',
|
|
|
|
|
|
occurredDisplay: '2026-06-01 至 2026-06-03',
|
|
|
|
|
|
amountValue: 10000,
|
|
|
|
|
|
riskFlags: [
|
|
|
|
|
|
{
|
|
|
|
|
|
source: 'budget_control',
|
|
|
|
|
|
severity: 'high',
|
|
|
|
|
|
label: '预算可用余额不足',
|
|
|
|
|
|
message: '当前部门预算余额不足。',
|
|
|
|
|
|
business_stage: 'expense_application',
|
|
|
|
|
|
risk_domain: 'budget',
|
|
|
|
|
|
visibility_scope: 'budget_manager',
|
|
|
|
|
|
actionability: 'budget_governance'
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
expenseItems: []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
test('application submitter cannot see budget governance alerts in detail topbar', () => {
|
|
|
|
|
|
const alerts = buildDetailAlerts(applicationRequest, { currentUser: submitter })
|
|
|
|
|
|
assert.deepEqual(alerts.map((item) => item.label), ['SLA 催单次数 0'])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('budget manager can see application budget governance alerts', () => {
|
|
|
|
|
|
const alerts = buildDetailAlerts(applicationRequest, { currentUser: budgetManager })
|
|
|
|
|
|
assert.equal(alerts[0].label, '高风险 1 项')
|
|
|
|
|
|
assert.equal(alerts[0].tone, 'danger')
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('reimbursement submitter sees only fixable claim risks', () => {
|
|
|
|
|
|
const request = {
|
|
|
|
|
|
id: 'RE-202606010001',
|
|
|
|
|
|
claimId: 'reimbursement-claim-id',
|
|
|
|
|
|
documentTypeCode: 'claim',
|
|
|
|
|
|
approvalKey: 'draft',
|
|
|
|
|
|
node: '待提交',
|
|
|
|
|
|
employeeId: 'EMP-001',
|
|
|
|
|
|
typeCode: 'travel',
|
|
|
|
|
|
expenseItems: []
|
|
|
|
|
|
}
|
|
|
|
|
|
const cards = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'ticket-date',
|
|
|
|
|
|
businessStage: 'reimbursement',
|
|
|
|
|
|
tone: 'high',
|
|
|
|
|
|
risk: '票据日期早于申请行程。',
|
|
|
|
|
|
risk_domain: 'trip',
|
|
|
|
|
|
visibility_scope: 'submitter',
|
|
|
|
|
|
actionability: 'fixable_by_submitter'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'budget-detail',
|
|
|
|
|
|
businessStage: 'reimbursement',
|
|
|
|
|
|
tone: 'medium',
|
|
|
|
|
|
risk: '预算占用率超过 90%。',
|
|
|
|
|
|
risk_domain: 'budget',
|
|
|
|
|
|
visibility_scope: 'budget_manager',
|
|
|
|
|
|
actionability: 'budget_governance'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const visibleCards = filterRiskCardsForVisibility(cards, { request, currentUser: submitter })
|
|
|
|
|
|
assert.deepEqual(visibleCards.map((card) => card.id), ['ticket-date'])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-03 22:15:45 +08:00
|
|
|
|
test('reimbursement detail still shows submitter-fixable attachment risks when viewer identity is incomplete', () => {
|
|
|
|
|
|
const request = {
|
|
|
|
|
|
id: 'RE-20260603083825-876B85XW',
|
|
|
|
|
|
claimId: '2ad80b25-b153-407e-91be-ed2651045fb1',
|
|
|
|
|
|
documentTypeCode: 'claim',
|
|
|
|
|
|
approvalKey: 'draft',
|
|
|
|
|
|
node: 'pending-submit',
|
|
|
|
|
|
employeeId: 'EMP-CLAIM-OWNER',
|
|
|
|
|
|
typeCode: 'travel',
|
|
|
|
|
|
expenseItems: []
|
|
|
|
|
|
}
|
|
|
|
|
|
const currentUserWithoutEmployeeMatch = {
|
|
|
|
|
|
id: 'FRONTEND-AUTH-SNAPSHOT',
|
|
|
|
|
|
employeeId: '',
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
roleCodes: []
|
|
|
|
|
|
}
|
|
|
|
|
|
const cards = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'hotel-limit-risk',
|
|
|
|
|
|
source: 'attachment_analysis',
|
|
|
|
|
|
businessStage: 'reimbursement',
|
|
|
|
|
|
tone: 'high',
|
|
|
|
|
|
risk: 'hotel limit exceeded',
|
|
|
|
|
|
risk_domain: 'invoice',
|
|
|
|
|
|
visibility_scope: 'submitter',
|
|
|
|
|
|
actionability: 'fixable_by_submitter'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const visibleCards = filterRiskCardsForVisibility(cards, {
|
|
|
|
|
|
request,
|
|
|
|
|
|
currentUser: currentUserWithoutEmployeeMatch
|
|
|
|
|
|
})
|
|
|
|
|
|
assert.deepEqual(visibleCards.map((card) => card.id), ['hotel-limit-risk'])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-01 17:07:14 +08:00
|
|
|
|
test('finance can see reimbursement compliance risks but not budget governance detail', () => {
|
|
|
|
|
|
const request = {
|
|
|
|
|
|
id: 'RE-202606010002',
|
|
|
|
|
|
documentTypeCode: 'claim',
|
|
|
|
|
|
approvalKey: 'in_progress',
|
|
|
|
|
|
node: '财务审批',
|
|
|
|
|
|
employeeId: 'EMP-001',
|
|
|
|
|
|
typeCode: 'travel',
|
|
|
|
|
|
expenseItems: []
|
|
|
|
|
|
}
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
canViewRiskForContext(
|
|
|
|
|
|
{
|
|
|
|
|
|
businessStage: 'reimbursement',
|
|
|
|
|
|
risk: '发票抬头与报销主体不一致。',
|
|
|
|
|
|
risk_domain: 'invoice',
|
|
|
|
|
|
actionability: 'fixable_by_submitter'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ request, currentUser: financeUser }
|
|
|
|
|
|
),
|
|
|
|
|
|
true
|
|
|
|
|
|
)
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
canViewRiskForContext(
|
|
|
|
|
|
{
|
|
|
|
|
|
businessStage: 'reimbursement',
|
|
|
|
|
|
risk: '预算余额不足。',
|
|
|
|
|
|
risk_domain: 'budget',
|
|
|
|
|
|
actionability: 'budget_governance'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ request, currentUser: financeUser }
|
|
|
|
|
|
),
|
|
|
|
|
|
false
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('legacy risk text falls back to semantic visibility defaults', () => {
|
|
|
|
|
|
const legacyFlag = {
|
|
|
|
|
|
source: 'submission_review',
|
|
|
|
|
|
severity: 'high',
|
|
|
|
|
|
message: '住宿发票城市与行程城市不一致。',
|
|
|
|
|
|
business_stage: 'reimbursement'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(resolveRiskDomain(legacyFlag), 'trip')
|
|
|
|
|
|
assert.equal(resolveRiskActionability(legacyFlag, { businessStage: 'reimbursement' }), 'fixable_by_submitter')
|
|
|
|
|
|
assert.equal(resolveRiskVisibilityScope(legacyFlag, { businessStage: 'reimbursement' }), 'submitter')
|
|
|
|
|
|
})
|
2026-06-20 10:17:37 +08:00
|
|
|
|
|
|
|
|
|
|
test('application submitter can see fixable policy/trip risks in detail', () => {
|
|
|
|
|
|
// 申请单申请人在详情页可见可自行整改的风险(信息完整性、差旅、金额),
|
|
|
|
|
|
// 以便申请时知晓风险及原因并补充修正。
|
|
|
|
|
|
const cards = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'application-fields-missing',
|
|
|
|
|
|
businessStage: 'expense_application',
|
|
|
|
|
|
tone: 'low',
|
|
|
|
|
|
risk: '差旅申请基础信息不完整,请补充地点、事由、起止时间和预计金额。',
|
|
|
|
|
|
risk_domain: 'policy',
|
|
|
|
|
|
visibility_scope: 'submitter',
|
|
|
|
|
|
actionability: 'fixable_by_submitter'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'budget-detail',
|
|
|
|
|
|
businessStage: 'expense_application',
|
|
|
|
|
|
tone: 'high',
|
|
|
|
|
|
risk: '预算可用余额不足。',
|
|
|
|
|
|
risk_domain: 'budget',
|
|
|
|
|
|
visibility_scope: 'budget_manager',
|
|
|
|
|
|
actionability: 'budget_governance'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'profile-detail',
|
|
|
|
|
|
businessStage: 'expense_application',
|
|
|
|
|
|
tone: 'medium',
|
|
|
|
|
|
risk: '历史差旅画像异常。',
|
|
|
|
|
|
risk_domain: 'profile',
|
|
|
|
|
|
visibility_scope: 'leader',
|
|
|
|
|
|
actionability: 'review_decision'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const visibleCards = filterRiskCardsForVisibility(cards, { request: applicationRequest, currentUser: submitter })
|
|
|
|
|
|
// 申请人只可见 fixable_by_submitter 的信息完整性类风险,
|
|
|
|
|
|
// budget 走预算审批人、profile 走领导,申请人均不可见。
|
|
|
|
|
|
assert.deepEqual(visibleCards.map((card) => card.id), ['application-fields-missing'])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application leader can see review_decision risks that submitter cannot', () => {
|
|
|
|
|
|
// 审批人可见 review_decision 类风险(画像、审批流程等),
|
|
|
|
|
|
// 满足诉求2:提交后领导能看到风险点。
|
|
|
|
|
|
const cards = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'profile-detail',
|
|
|
|
|
|
businessStage: 'expense_application',
|
|
|
|
|
|
tone: 'medium',
|
|
|
|
|
|
risk: '历史差旅画像异常。',
|
|
|
|
|
|
risk_domain: 'profile',
|
|
|
|
|
|
visibility_scope: 'leader',
|
|
|
|
|
|
actionability: 'review_decision'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'application-fields-missing',
|
|
|
|
|
|
businessStage: 'expense_application',
|
|
|
|
|
|
tone: 'low',
|
|
|
|
|
|
risk: '差旅申请基础信息不完整。',
|
|
|
|
|
|
risk_domain: 'policy',
|
|
|
|
|
|
visibility_scope: 'submitter',
|
|
|
|
|
|
actionability: 'fixable_by_submitter'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const visibleCards = filterRiskCardsForVisibility(cards, {
|
|
|
|
|
|
request: applicationRequest,
|
|
|
|
|
|
currentUser: { id: 'EMP-P7', name: '直属领导' },
|
|
|
|
|
|
canViewApprovalRiskAdvice: true
|
|
|
|
|
|
})
|
|
|
|
|
|
assert.deepEqual(visibleCards.map((card) => card.id), ['profile-detail', 'application-fields-missing'])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('application fixable risks derive submitter semantics without hardcoded leader fallback', () => {
|
|
|
|
|
|
// 验证申请单阶段 policy/trip/amount 域不再被硬编码为 leader/review_decision,
|
|
|
|
|
|
// 而是沿用与报销单一致的 fixable_by_submitter 语义。
|
|
|
|
|
|
const policyFlag = {
|
|
|
|
|
|
source: 'submission_review',
|
|
|
|
|
|
severity: 'low',
|
|
|
|
|
|
message: '差旅申请基础信息不完整。',
|
|
|
|
|
|
business_stage: 'expense_application'
|
|
|
|
|
|
}
|
|
|
|
|
|
assert.equal(resolveRiskActionability(policyFlag, { businessStage: 'expense_application' }), 'fixable_by_submitter')
|
|
|
|
|
|
assert.equal(resolveRiskVisibilityScope(policyFlag, { businessStage: 'expense_application' }), 'submitter')
|
|
|
|
|
|
})
|