Files

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
}