Files
X-Financial/web/tests/approval-tasks-service.test.mjs

234 lines
7.9 KiB
JavaScript

import assert from 'node:assert/strict'
import test from 'node:test'
import {
addSignApprovalTask,
approveApprovalTask,
batchApproveApprovalTasks,
countersignApprovalTask,
delegateApprovalTask,
escalateApprovalTask,
fetchApprovalTaskCandidates,
fetchApprovalTaskDetail,
fetchApprovalTasks,
returnApprovalTask,
revokeApprovalTaskDelegation,
transferApprovalTask
} from '../src/services/approvalTasks.js'
function jsonResponse(payload) {
return {
ok: true,
status: 200,
async json() {
return payload
}
}
}
function pendingTask(overrides = {}) {
return {
id: 'task-001',
claim_id: 'claim-001',
status: 'pending',
version: 3,
can_act: true,
available_actions: ['approve', 'delegate'],
batch_eligible: true,
claim_status_snapshot: 'submitted',
claim_stage_snapshot: '直属领导审批',
priority_reasons_json: [{ code: 'sla_near_due', label: '即将超时', weight: 16 }],
...overrides
}
}
test('审批任务列表发送服务端分页筛选参数并规范化任务', async () => {
let capturedUrl = ''
global.fetch = async (url) => {
capturedUrl = String(url)
return jsonResponse({
items: [{ task: pendingTask(), claim: { id: 'claim-001', claim_no: 'BX-001' } }],
total: 41,
page: 2,
page_size: 25,
total_pages: 2,
generated_at: '2026-07-16T10:00:00Z'
})
}
const result = await fetchApprovalTasks({
page: 2,
pageSize: 25,
status: 'pending',
riskLevel: 'high',
slaState: 'overdue',
nodeKey: 'direct_manager',
keyword: '张 三',
batchEligible: true,
sort: 'priority_desc'
})
const url = new URL(capturedUrl, 'http://local.test')
assert.equal(url.pathname, '/api/v1/approval-tasks')
assert.equal(url.searchParams.get('page'), '2')
assert.equal(url.searchParams.get('page_size'), '25')
assert.equal(url.searchParams.get('status'), 'pending')
assert.equal(url.searchParams.get('risk_level'), 'high')
assert.equal(url.searchParams.get('sla_state'), 'overdue')
assert.equal(url.searchParams.get('node_key'), 'direct_manager')
assert.equal(url.searchParams.get('keyword'), '张 三')
assert.equal(url.searchParams.get('batch_eligible'), 'true')
assert.equal(url.searchParams.get('sort'), 'priority_desc')
assert.equal(result.total, 41)
assert.equal(result.pageSize, 25)
assert.equal(result.items[0].task.claimId, 'claim-001')
assert.equal(result.items[0].task.priorityReasons[0].code, 'sla_near_due')
assert.equal(result.items[0].claim.claim_no, 'BX-001')
})
test('审批任务详情和候选人使用任务作用域接口', async () => {
const calls = []
global.fetch = async (url) => {
calls.push(String(url))
if (String(url).includes('/candidates?')) {
return jsonResponse({
items: [{
employee_id: 'employee-002',
employee_no: 'E002',
name: '李审批',
email: 'li@example.com',
qualified: false,
reason: '不属于当前部门'
}],
total: 1
})
}
return jsonResponse({ task: pendingTask(), claim: { id: 'claim-001' } })
}
const detail = await fetchApprovalTaskDetail('task/001')
const candidates = await fetchApprovalTaskCandidates('task/001', {
action: 'delegate',
keyword: '李 审批',
limit: 30
})
assert.equal(calls[0], '/api/v1/approval-tasks/task%2F001')
const candidateUrl = new URL(calls[1], 'http://local.test')
assert.equal(candidateUrl.pathname, '/api/v1/approval-tasks/task%2F001/candidates')
assert.equal(candidateUrl.searchParams.get('action'), 'delegate')
assert.equal(candidateUrl.searchParams.get('keyword'), '李 审批')
assert.equal(candidateUrl.searchParams.get('limit'), '30')
assert.equal(detail.task.id, 'task-001')
assert.equal(candidates.items[0].qualified, false)
assert.equal(candidates.items[0].reason, '不属于当前部门')
})
test('所有审批动作携带请求幂等键和任务版本', async () => {
const calls = []
global.fetch = async (url, options) => {
calls.push({ url: String(url), body: JSON.parse(options.body) })
return jsonResponse({ task: pendingTask({ version: 4 }), related_tasks: [], replayed: false })
}
const base = {
requestId: 'request-action-001',
expectedTaskVersion: 3,
reason: '业务审批处理'
}
await delegateApprovalTask('task-001', {
...base,
targetEmployeeId: 'employee-002',
expiresAt: '2026-07-18T10:00:00Z'
})
await revokeApprovalTaskDelegation('task-001', base)
await transferApprovalTask('task-001', { ...base, targetEmployeeId: 'employee-003' })
await addSignApprovalTask('task-001', {
...base,
participantEmployeeIds: ['employee-004', 'employee-005', 'employee-004']
})
await countersignApprovalTask('task-001', {
...base,
participantEmployeeIds: ['employee-006', 'employee-007']
})
await escalateApprovalTask('task-001', base)
await approveApprovalTask('task-001', { ...base, opinion: '同意通过' })
await returnApprovalTask('task-001', {
...base,
reasonCodes: ['missing_invoice', 'missing_invoice', 'amount_mismatch']
})
assert.deepEqual(calls.map((call) => call.url), [
'/api/v1/approval-tasks/task-001/delegate',
'/api/v1/approval-tasks/task-001/delegation/revoke',
'/api/v1/approval-tasks/task-001/transfer',
'/api/v1/approval-tasks/task-001/add-sign',
'/api/v1/approval-tasks/task-001/countersign',
'/api/v1/approval-tasks/task-001/escalate',
'/api/v1/approval-tasks/task-001/approve',
'/api/v1/approval-tasks/task-001/return'
])
calls.forEach((call) => {
assert.equal(call.body.request_id, 'request-action-001')
assert.equal(call.body.expected_task_version, 3)
assert.equal(call.body.reason, '业务审批处理')
})
assert.equal(calls[0].body.target_employee_id, 'employee-002')
assert.equal(calls[0].body.expires_at, '2026-07-18T10:00:00Z')
assert.deepEqual(calls[3].body.participant_employee_ids, ['employee-004', 'employee-005'])
assert.equal(calls[6].body.opinion, '同意通过')
assert.deepEqual(calls[7].body.reason_codes, ['missing_invoice', 'amount_mismatch'])
})
test('批量审批发送逐项快照并规范化部分成功结果', async () => {
let capturedUrl = ''
let capturedBody = null
global.fetch = async (url, options) => {
capturedUrl = String(url)
capturedBody = JSON.parse(options.body)
return jsonResponse({
batch_request_id: 'batch-request-001',
status: 'partial',
succeeded_count: 1,
blocked_count: 1,
conflict_count: 1,
items: [
{ task_id: 'task-001', claim_id: 'claim-001', status: 'succeeded', code: 'OK', message: '已通过' },
{ task_id: 'task-002', claim_id: 'claim-002', status: 'blocked', code: 'OPEN_RISK', message: '存在开放高风险' },
{ task_id: 'task-003', claim_id: 'claim-003', status: 'conflict', code: 'STALE_TASK', message: '任务已更新' }
]
})
}
const result = await batchApproveApprovalTasks({
batchRequestId: 'batch-request-001',
items: [
pendingTask({ id: 'task-001' }),
pendingTask({ id: 'task-002', version: 5 }),
{
taskId: 'task-003',
expectedTaskVersion: 2,
expectedStatus: 'submitted',
expectedApprovalStage: '财务审批',
opinion: '票据已核对'
}
]
})
assert.equal(capturedUrl, '/api/v1/approval-tasks/batch-approve')
assert.equal(capturedBody.batch_request_id, 'batch-request-001')
assert.deepEqual(capturedBody.items[0], {
task_id: 'task-001',
expected_task_version: 3,
expected_status: 'submitted',
expected_approval_stage: '直属领导审批'
})
assert.equal(capturedBody.items[1].expected_task_version, 5)
assert.equal(capturedBody.items[2].opinion, '票据已核对')
assert.equal(result.status, 'partial')
assert.equal(result.succeededCount, 1)
assert.equal(result.blockedCount, 1)
assert.equal(result.conflictCount, 1)
assert.equal(result.items[1].code, 'OPEN_RISK')
})