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 }