234 lines
7.9 KiB
JavaScript
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')
|
|
})
|