216 lines
7.9 KiB
JavaScript
216 lines
7.9 KiB
JavaScript
|
|
import assert from 'node:assert/strict'
|
||
|
|
import { readFileSync } from 'node:fs'
|
||
|
|
import test from 'node:test'
|
||
|
|
import { fileURLToPath } from 'node:url'
|
||
|
|
|
||
|
|
import {
|
||
|
|
executeRiskDispositionAction,
|
||
|
|
normalizeRiskDisposition,
|
||
|
|
normalizeRiskDispositionError,
|
||
|
|
normalizeRiskObservation
|
||
|
|
} from '../src/services/riskObservations.js'
|
||
|
|
|
||
|
|
const evidenceCard = readFileSync(
|
||
|
|
fileURLToPath(new URL('../src/components/travel/RiskObservationEvidenceCard.vue', import.meta.url)),
|
||
|
|
'utf8'
|
||
|
|
)
|
||
|
|
const waiverDialog = readFileSync(
|
||
|
|
fileURLToPath(new URL('../src/components/travel/RiskWaiverActionDialog.vue', import.meta.url)),
|
||
|
|
'utf8'
|
||
|
|
)
|
||
|
|
const waiverRecord = readFileSync(
|
||
|
|
fileURLToPath(new URL('../src/components/travel/RiskWaiverRecord.vue', import.meta.url)),
|
||
|
|
'utf8'
|
||
|
|
)
|
||
|
|
|
||
|
|
function jsonResponse(payload) {
|
||
|
|
return {
|
||
|
|
ok: true,
|
||
|
|
status: 200,
|
||
|
|
async json() {
|
||
|
|
return payload
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
test('risk disposition normalizes server-authorized actions and complete waiver audit fields', () => {
|
||
|
|
const disposition = normalizeRiskDisposition({
|
||
|
|
id: 'disposition-1',
|
||
|
|
observation_id: 'risk-1',
|
||
|
|
lifecycle_status: 'waived',
|
||
|
|
version: 3,
|
||
|
|
available_actions: ['approve_waiver', 'reject_waiver', 'approve_waiver'],
|
||
|
|
read_only_reason: null,
|
||
|
|
waiver_requester_id: 'employee-requester',
|
||
|
|
waiver_requester_name: '申请人甲',
|
||
|
|
waiver_requested_at: '2026-07-16T08:00:00Z',
|
||
|
|
waiver_reason: '客户现场暂时无法取得原件',
|
||
|
|
waiver_scope: '仅限本次单据的行程确认材料',
|
||
|
|
waiver_expires_at: '2026-07-20T08:00:00Z',
|
||
|
|
waiver_conditions_json: ['三日内补交原件', '到期前复核', '三日内补交原件'],
|
||
|
|
waiver_decision: 'approved',
|
||
|
|
waiver_decider_id: 'employee-decider',
|
||
|
|
waiver_decider_name: '决定人乙',
|
||
|
|
waiver_decided_at: '2026-07-16T09:00:00Z',
|
||
|
|
waiver_decision_reason: '补偿控制充分,同意限时豁免',
|
||
|
|
events: [{
|
||
|
|
id: 'waiver-event-1',
|
||
|
|
action: 'request_waiver',
|
||
|
|
version: 2,
|
||
|
|
actor_id: 'employee-requester',
|
||
|
|
actor_name: '申请人甲',
|
||
|
|
request_id: 'waiver-request-001',
|
||
|
|
comment: '客户现场暂时无法取得原件',
|
||
|
|
after_json: { waiver_scope: '仅限本次单据的行程确认材料' },
|
||
|
|
created_at: '2026-07-16T08:00:00Z'
|
||
|
|
}]
|
||
|
|
})
|
||
|
|
|
||
|
|
assert.deepEqual(disposition.availableActions, ['approve_waiver', 'reject_waiver'])
|
||
|
|
assert.equal(disposition.readOnlyReason, '')
|
||
|
|
assert.equal(disposition.waiverRequesterName, '申请人甲')
|
||
|
|
assert.equal(disposition.waiverReason, '客户现场暂时无法取得原件')
|
||
|
|
assert.equal(disposition.waiverScope, '仅限本次单据的行程确认材料')
|
||
|
|
assert.deepEqual(disposition.waiverConditions, ['三日内补交原件', '到期前复核'])
|
||
|
|
assert.equal(disposition.waiverDeciderName, '决定人乙')
|
||
|
|
assert.equal(disposition.waiverDecisionReason, '补偿控制充分,同意限时豁免')
|
||
|
|
assert.equal(disposition.events[0].actorId, 'employee-requester')
|
||
|
|
assert.equal(disposition.events[0].requestId, 'waiver-request-001')
|
||
|
|
assert.equal(
|
||
|
|
disposition.events[0].afterState.waiver_scope,
|
||
|
|
'仅限本次单据的行程确认材料'
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('observation keeps server actions when the first disposition has not been created', () => {
|
||
|
|
const observation = normalizeRiskDisposition(null)
|
||
|
|
assert.equal(observation, null)
|
||
|
|
|
||
|
|
const normalized = normalizeRiskObservation({
|
||
|
|
id: 'risk-first-action',
|
||
|
|
disposition: null,
|
||
|
|
available_actions: ['confirm', 'false_positive'],
|
||
|
|
read_only_reason: ''
|
||
|
|
})
|
||
|
|
assert.equal(normalized.disposition, null)
|
||
|
|
assert.deepEqual(normalized.availableActions, ['confirm', 'false_positive'])
|
||
|
|
assert.equal(normalized.readOnlyReason, '')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('waiver request and decision payloads carry idempotency key and optimistic version', async (t) => {
|
||
|
|
const originalFetch = globalThis.fetch
|
||
|
|
const calls = []
|
||
|
|
t.after(() => {
|
||
|
|
globalThis.fetch = originalFetch
|
||
|
|
})
|
||
|
|
globalThis.fetch = async (url, options) => {
|
||
|
|
calls.push({ url: String(url), body: JSON.parse(options.body) })
|
||
|
|
return jsonResponse({
|
||
|
|
disposition: {
|
||
|
|
id: 'disposition-1',
|
||
|
|
observation_id: 'risk/1',
|
||
|
|
version: calls.length,
|
||
|
|
available_actions: []
|
||
|
|
},
|
||
|
|
event: { id: `event-${calls.length}` },
|
||
|
|
replayed: false
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
await executeRiskDispositionAction('risk/1', {
|
||
|
|
action: 'request_waiver',
|
||
|
|
expectedVersion: 2,
|
||
|
|
requestId: 'risk-request-waiver-001',
|
||
|
|
waiverReason: '材料暂缺',
|
||
|
|
waiverScope: '仅限当前单据',
|
||
|
|
waiverExpiresAt: '2026-07-20T08:00:00.000Z',
|
||
|
|
waiverConditions: ['三日内补件', '三日内补件', '', '财务复核']
|
||
|
|
})
|
||
|
|
await executeRiskDispositionAction('risk/1', {
|
||
|
|
action: 'approve_waiver',
|
||
|
|
expectedVersion: 3,
|
||
|
|
requestId: 'risk-approve-waiver-001',
|
||
|
|
comment: '补偿控制充分,同意限时豁免'
|
||
|
|
})
|
||
|
|
await executeRiskDispositionAction('risk/1', {
|
||
|
|
action: 'reject_waiver',
|
||
|
|
expectedVersion: 3,
|
||
|
|
requestId: 'risk-reject-waiver-001',
|
||
|
|
comment: '补偿控制不足,拒绝豁免'
|
||
|
|
})
|
||
|
|
|
||
|
|
assert.equal(calls[0].url, '/api/v1/risk-observations/risk%2F1/disposition/actions')
|
||
|
|
assert.deepEqual(calls[0].body, {
|
||
|
|
action: 'request_waiver',
|
||
|
|
expected_version: 2,
|
||
|
|
request_id: 'risk-request-waiver-001',
|
||
|
|
waiver_reason: '材料暂缺',
|
||
|
|
waiver_scope: '仅限当前单据',
|
||
|
|
waiver_expires_at: '2026-07-20T08:00:00.000Z',
|
||
|
|
waiver_conditions: ['三日内补件', '财务复核']
|
||
|
|
})
|
||
|
|
assert.deepEqual(calls[1].body, {
|
||
|
|
action: 'approve_waiver',
|
||
|
|
expected_version: 3,
|
||
|
|
request_id: 'risk-approve-waiver-001',
|
||
|
|
comment: '补偿控制充分,同意限时豁免'
|
||
|
|
})
|
||
|
|
assert.deepEqual(calls[2].body, {
|
||
|
|
action: 'reject_waiver',
|
||
|
|
expected_version: 3,
|
||
|
|
request_id: 'risk-reject-waiver-001',
|
||
|
|
comment: '补偿控制不足,拒绝豁免'
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
test('waiver errors are presented by conflict, permission, validation and timeout type', () => {
|
||
|
|
assert.deepEqual(
|
||
|
|
normalizeRiskDispositionError({
|
||
|
|
code: 'RISK_DISPOSITION_VERSION_CONFLICT',
|
||
|
|
message: '状态已变化'
|
||
|
|
}),
|
||
|
|
{
|
||
|
|
code: 'RISK_DISPOSITION_VERSION_CONFLICT',
|
||
|
|
title: '风险状态已更新',
|
||
|
|
message: '状态已变化',
|
||
|
|
shouldRefresh: true
|
||
|
|
}
|
||
|
|
)
|
||
|
|
assert.equal(
|
||
|
|
normalizeRiskDispositionError({ code: 'RISK_WAIVER_DECISION_FORBIDDEN' }).title,
|
||
|
|
'当前账号不能执行此操作'
|
||
|
|
)
|
||
|
|
assert.equal(normalizeRiskDispositionError({ status: 422 }).code, 'RISK_DISPOSITION_VALIDATION_FAILED')
|
||
|
|
assert.equal(normalizeRiskDispositionError({ code: 'REQUEST_TIMEOUT' }).shouldRefresh, true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('waiver UI is server-action driven and makes all three confirmations explicit', () => {
|
||
|
|
assert.match(evidenceCard, /currentDisposition\.value\.availableActions/)
|
||
|
|
assert.match(evidenceCard, /currentDisposition\.readOnlyReason/)
|
||
|
|
assert.match(evidenceCard, /批准豁免/)
|
||
|
|
assert.match(evidenceCard, /拒绝豁免/)
|
||
|
|
assert.match(evidenceCard, /风险豁免已拒绝,仍会阻断审批/)
|
||
|
|
assert.match(evidenceCard, /waiverExpiresAt/)
|
||
|
|
assert.match(evidenceCard, /scheduleWaiverExpiryRefresh/)
|
||
|
|
assert.match(evidenceCard, /onBeforeUnmount\(clearWaiverExpiryTimer\)/)
|
||
|
|
assert.doesNotMatch(evidenceCard, /roleCodes|role_codes|isAdmin|is_admin/)
|
||
|
|
|
||
|
|
assert.match(waiverDialog, /确认提交豁免申请/)
|
||
|
|
assert.match(waiverDialog, /确认批准豁免/)
|
||
|
|
assert.match(waiverDialog, /确认拒绝豁免/)
|
||
|
|
assert.match(waiverDialog, /请填写拒绝理由。/)
|
||
|
|
assert.match(waiverDialog, /补偿条件最多填写 20 项。/)
|
||
|
|
assert.match(waiverDialog, /拒绝后该风险仍会阻断审批/)
|
||
|
|
assert.match(waiverDialog, /:disabled="busy"/)
|
||
|
|
|
||
|
|
assert.match(waiverRecord, /申请人/)
|
||
|
|
assert.match(waiverRecord, /申请理由/)
|
||
|
|
assert.match(waiverRecord, /豁免范围/)
|
||
|
|
assert.match(waiverRecord, /有效期至/)
|
||
|
|
assert.match(waiverRecord, /补偿条件/)
|
||
|
|
assert.match(waiverRecord, /决定记录/)
|
||
|
|
assert.match(waiverRecord, /waiverDecisionReason/)
|
||
|
|
assert.match(waiverRecord, /豁免审计轨迹/)
|
||
|
|
assert.match(waiverRecord, /event\.requestId/)
|
||
|
|
})
|