116 lines
9.3 KiB
JavaScript
116 lines
9.3 KiB
JavaScript
import { randomUUID } from 'node:crypto'
|
|||
|
|
|
||
|
|
const MODULE = 'fleet-driver-qualification'
|
||
|
|
const BASE = '/api/platform/v1/last-leg/fleet/drivers'
|
||
|
|
const OPTIONS = { scenario: 'driver-license-and-qualification', deploymentBlocker: true }
|
||
|
|
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64')
|
||
|
|
|
||
|
|
/** Uses only new controlled TEST drivers, never real licence documents or existing driver assignments. */
|
||
|
|
export async function run(context) {
|
||
|
|
const proof = { status: 'CHECKING', drivers: [], requests: [], qualifications: [] }
|
||
|
|
context.createdResources.fleetDriverQualification = proof
|
||
|
|
const marker = `DQ-${randomUUID().slice(0, 12)}`
|
||
|
|
const day = offset => new Date(Date.now() + offset * 86400000).toISOString().slice(0, 10)
|
||
|
|
const expiry = day(15), nextDay = new Date(Date.parse(`${expiry}T00:00:00Z`) + 86400000).toISOString()
|
||
|
|
const endBefore = new Date(Date.parse(nextDay) - 1).toISOString()
|
||
|
|
const soonEnd = new Date(Date.now() + 86400000).toISOString()
|
||
|
|
const details = { name: `${marker}受控司机`, phone: '19900000000', ownership: 'OWNED', region: 'OVERSEAS',
|
||
|
|
licenseNumber: marker, licenseType: 'Class A', licenseZone: 'UTC', licenseExpiresOn: expiry,
|
||
|
|
remark: `${context.marker} TEST image, not a real driving licence`,
|
||
|
|
profile: { vehicleModels: 'Box Truck', languagesAndArea: 'TEST', medicalExpiresOn: day(-1), trainingExpiresOn: day(-1), insuranceExpiresOn: day(-1) } }
|
||
|
|
function check(label, response, condition) {
|
||
|
|
context.assert(MODULE, label, response, Boolean(condition), '', OPTIONS)
|
||
|
|
if (!condition) throw new Error(label)
|
||
|
|
}
|
||
|
|
async function request(method, path, body) {
|
||
|
|
const response = await context.request(method, path, body)
|
||
|
|
proof.requests.push({ method, path, http: response.http, code: response.body?.code, requestId: response.requestId, traceId: response.traceId })
|
||
|
|
return response
|
||
|
|
}
|
||
|
|
async function command(method, path, body, label) {
|
||
|
|
const response = await request(method, path, body)
|
||
|
|
const data = context.apiSuccess(MODULE, label, response, '', OPTIONS)
|
||
|
|
if (response.http !== 200 || String(response.body?.code) !== '200' || data == null) throw new Error(label)
|
||
|
|
return data
|
||
|
|
}
|
||
|
|
async function reject(method, path, body, code, label) {
|
||
|
|
const response = await request(method, path, body)
|
||
|
|
check(label, response, response.http === 200 && String(response.body?.code) === code)
|
||
|
|
}
|
||
|
|
async function qualification(id, end, reason, expiringSoon = undefined) {
|
||
|
|
const result = await command('GET', `${BASE}/${id}/qualification?expectedEnd=${encodeURIComponent(end)}`, undefined, `读取司机资格:${reason}`)
|
||
|
|
proof.qualifications.push({ driverId: String(id), expectedEnd: end, result })
|
||
|
|
check(`司机资格准确判定${reason}`, null, result.reason === reason && result.canDispatch === (reason === 'ELIGIBLE')
|
||
|
|
&& (expiringSoon === undefined || result.expiringSoon === expiringSoon))
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
async function download(id, fileId, label) {
|
||
|
|
const result = await context.requestBinary('GET', `${BASE}/${id}/license/${fileId}`)
|
||
|
|
check(label, result, result.http === 200 && result.contentType?.startsWith('image/png') && result.bytes instanceof Uint8Array && Buffer.from(result.bytes).equals(PNG))
|
||
|
|
}
|
||
|
|
let failure
|
||
|
|
try {
|
||
|
|
let driver = await command('POST', BASE, details, '建立本轮临近到期司机')
|
||
|
|
const own = { id: String(driver.id), driverNo: driver.driverNo, initialDetails: details, files: [] }
|
||
|
|
proof.drivers.push(own)
|
||
|
|
const id = own.id, path = `${BASE}/${id}`
|
||
|
|
await qualification(id, soonEnd, 'MISSING_LICENSE')
|
||
|
|
await reject('POST', `${path}/review`, { recordVersion: driver.recordVersion, decision: 'APPROVED' }, 'A0400', '缺驾驶证附件不能审核通过')
|
||
|
|
const uploaded = await context.requestMultipart('POST', `${path}/license?recordVersion=${driver.recordVersion}`, { name: `${marker}-TEST.png`, type: 'image/png', content: PNG })
|
||
|
|
driver = context.apiSuccess(MODULE, '上传本轮受控驾驶证图片', uploaded, '', OPTIONS)
|
||
|
|
check('上传图片保留待审而非直接通过', uploaded, driver?.licenseFileId && driver.reviewStatus === 'PENDING')
|
||
|
|
const originalFile = String(driver.licenseFileId); own.files.push(originalFile)
|
||
|
|
await qualification(id, soonEnd, 'PENDING_REVIEW')
|
||
|
|
await reject('POST', `${path}/review`, { recordVersion: driver.recordVersion, decision: 'REJECTED' }, 'A0400', '打回不能缺少原因')
|
||
|
|
driver = await command('POST', `${path}/review`, { recordVersion: driver.recordVersion, decision: 'REJECTED', reason: '核对本轮测试资料' }, '正式打回司机资料')
|
||
|
|
await qualification(id, soonEnd, 'REJECTED')
|
||
|
|
driver = await command('POST', `${path}/submit`, { recordVersion: driver.recordVersion }, '正式重提司机资料')
|
||
|
|
const beforeApproval = driver.recordVersion
|
||
|
|
driver = await command('POST', `${path}/review`, { recordVersion: driver.recordVersion, decision: 'APPROVED' }, '通过已补齐驾驶证的测试司机')
|
||
|
|
await qualification(id, soonEnd, 'ELIGIBLE', true)
|
||
|
|
await qualification(id, endBefore, 'ELIGIBLE', true)
|
||
|
|
await qualification(id, nextDay, 'EXPIRES_DURING_TRIP', true)
|
||
|
|
const missingEnd = await command('GET', path, undefined, '不填任务结束时间读取详情')
|
||
|
|
check('没有预计结束时间不默认为可派', null, !missingEnd.qualification.canDispatch && missingEnd.qualification.reason === 'MISSING_TRIP_END')
|
||
|
|
await qualification(id, new Date(Date.now() - 60000).toISOString(), 'INVALID_TRIP_END')
|
||
|
|
await reject('PUT', path, { recordVersion: beforeApproval, details }, 'A0409', '旧司机版本不能覆盖审核结果')
|
||
|
|
await download(id, originalFile, '本司机驾驶证下载字节一致')
|
||
|
|
const other = await command('POST', BASE, { ...details, name: `${marker}隔离司机`, licenseNumber: `${marker}-B` }, '建立附件归属隔离司机')
|
||
|
|
proof.drivers.push({ id: String(other.id), driverNo: other.driverNo, files: [] })
|
||
|
|
await reject('GET', `${BASE}/${other.id}/license/${originalFile}`, undefined, 'A0404', '其他司机不能通过自身路径下载本司机证照')
|
||
|
|
driver = await command('PUT', path, { recordVersion: driver.recordVersion, details: { ...details, remark: '仅补充备注' } }, '更正非资格信息')
|
||
|
|
check('备注更正不清除已通过的驾驶资格', null, driver.reviewStatus === 'APPROVED')
|
||
|
|
await qualification(id, soonEnd, 'ELIGIBLE', true)
|
||
|
|
const renewed = { ...details, licenseExpiresOn: day(90) }
|
||
|
|
driver = await command('PUT', path, { recordVersion: driver.recordVersion, details: renewed }, '更新证照有效期')
|
||
|
|
check('关键证照变化回到待审', null, driver.reviewStatus === 'PENDING')
|
||
|
|
await qualification(id, soonEnd, 'PENDING_REVIEW', false)
|
||
|
|
driver = await command('POST', `${path}/review`, { recordVersion: driver.recordVersion, decision: 'APPROVED' }, '复核新证照有效期')
|
||
|
|
await qualification(id, soonEnd, 'ELIGIBLE', false)
|
||
|
|
const replacement = await context.requestMultipart('POST', `${path}/license?recordVersion=${driver.recordVersion}`, { name: `${marker}-REPLACEMENT-TEST.png`, type: 'image/png', content: PNG })
|
||
|
|
driver = context.apiSuccess(MODULE, '替换本轮测试证照', replacement, '', OPTIONS)
|
||
|
|
check('替换附件必须重新审核', replacement, driver?.reviewStatus === 'PENDING' && driver.licenseFileId)
|
||
|
|
own.files.push(String(driver.licenseFileId))
|
||
|
|
await download(id, originalFile, '原证照仍可按本司机历史归属下载')
|
||
|
|
await download(id, driver.licenseFileId, '当前证照可下载')
|
||
|
|
driver = await command('PUT', path, { recordVersion: driver.recordVersion, details: { ...details, licenseExpiresOn: day(-1) } }, '登记已过期测试证照')
|
||
|
|
check('详情明确显示驾驶证已过期', null, driver.licenseState === 'EXPIRED' && !driver.qualification.canDispatch)
|
||
|
|
await reject('POST', `${path}/review`, { recordVersion: driver.recordVersion, decision: 'APPROVED' }, 'A0400', '过期驾驶证不能通过审核')
|
||
|
|
const history = await command('GET', `${path}/history?page=1&size=200`, undefined, '读取司机完整操作历史')
|
||
|
|
check('历史保留上传审核打回重提和旧证照', null, ['CREATE', 'UPLOAD_LICENSE', 'REJECTED', 'RESUBMIT', 'APPROVED', 'UPDATE'].every(action => history.records.some(row => row.action === action))
|
||
|
|
&& history.records.some(row => String(row.snapshot?.licenseFileId) === originalFile))
|
||
|
|
proof.status = 'VERIFIED'
|
||
|
|
} catch (error) { proof.status = 'FAILED'; proof.error = error instanceof Error ? error.message : String(error); failure = error }
|
||
|
|
finally {
|
||
|
|
for (const resource of proof.drivers) {
|
||
|
|
try {
|
||
|
|
const current = await command('GET', `${BASE}/${resource.id}`, undefined, '停用前读取本轮司机最新版本')
|
||
|
|
const ended = current.active ? await command('PATCH', `${BASE}/${resource.id}/status`, { recordVersion: current.recordVersion, active: false, reason: '本轮测试结束,停用专用司机并保留证据' }, '停用本轮测试司机') : current
|
||
|
|
check('测试司机已停用', null, ended.active === false); resource.cleanup = 'INACTIVE'
|
||
|
|
await qualification(resource.id, soonEnd, 'INACTIVE')
|
||
|
|
} catch (error) { resource.cleanup = 'FAILED'; proof.status = 'FAILED'; resource.cleanupError = error instanceof Error ? error.message : String(error); failure ??= error }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (failure) throw failure
|
||
|
|
}
|