115 lines
10 KiB
JavaScript
115 lines
10 KiB
JavaScript
const BASE = '/api/platform/v1/last-leg/fleet/vehicles'
|
|
const VEHICLES = '/api/platform/v1/last-leg/master-data/vehicles'
|
|
const OPTIONS = { scenario: 'vehicle-care-lifecycle', deploymentBlocker: true }
|
|
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64')
|
|
|
|
/** Run only from an authorized fresh TEST lifecycle. Keep every created record and its evidence. */
|
|
export async function verifyVehicleCare(context, module, vehicle, proof) {
|
|
const id = String(vehicle.id), base = `${BASE}/${encodeURIComponent(id)}/records`
|
|
proof.status = 'CHECKING'; proof.records = []
|
|
function check(label, response, condition) {
|
|
context.assert(module, label, response, Boolean(condition), '', OPTIONS)
|
|
if (!condition) throw new Error(label)
|
|
}
|
|
async function command(method, path, body, label) {
|
|
const response = await context.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
|
|
}
|
|
const occurredAt = new Date(Date.now() - 86400000).toISOString()
|
|
const maintenance = { recordType: 'MAINTENANCE', occurredAt, distanceUnit: 'KM', odometer: 80,
|
|
lastServiceOdometer: 20, nextServiceOdometer: 100, fuelQuantity: null, fuelUnit: null, verificationStatus: 'PENDING', remark: context.marker }
|
|
const createRequest = { idempotencyKey: `${context.marker}:CARE`, details: maintenance }
|
|
let record = await command('POST', base, createRequest, '建立本轮车辆保养记录')
|
|
proof.records.push({ recordNo: record.recordNo, createRequest })
|
|
check('保养记录属于本车且原始单位不变', null, record.vehicleId === id && record.details?.distanceUnit === 'KM' && Number(record.details.odometer) === 80)
|
|
const replay = await command('POST', base, createRequest, '重放同一保养创建请求')
|
|
check('保养创建重放没有重复记录', null, replay.recordNo === record.recordNo && Number(replay.recordVersion) === Number(record.recordVersion))
|
|
const originalVersion = Number(record.recordVersion)
|
|
const correction = { recordVersion: originalVersion, reason: '受控验收更正', details: { ...maintenance, odometer: 85 } }
|
|
record = await command('PUT', `${base}/${record.recordNo}`, correction, '更正本轮保养里程')
|
|
check('更正后业务值与版本均生效', null, Number(record.details.odometer) === 85 && Number(record.recordVersion) === originalVersion + 1)
|
|
const stale = await context.request('PUT', `${base}/${record.recordNo}`, correction)
|
|
check('旧版本不得覆盖运维记录', stale, stale?.http === 409 || (stale?.http === 200 && ['A0409', '409'].includes(String(stale.body?.code))))
|
|
const history = await command('GET', `${base}/${record.recordNo}/history?page=1&size=20`, undefined, '读取保养更正历史')
|
|
check('保养历史保留旧里程与更正原因', null, history.records?.length === 2 && history.records[0].reason === correction.reason
|
|
&& Number(history.records[1].snapshot?.details?.odometer) === 80)
|
|
const fuelRequest = { idempotencyKey: `${context.marker}:FUEL`, details: { ...maintenance, recordType: 'FUEL', distanceUnit: 'MI',
|
|
lastServiceOdometer: null, nextServiceOdometer: null, fuelQuantity: 28.6, fuelUnit: 'US_GAL' } }
|
|
let fuel = await command('POST', base, fuelRequest, '建立本轮加油记录')
|
|
proof.records.push({ recordNo: fuel.recordNo, createRequest: fuelRequest })
|
|
const uploadPath = `${base}/${fuel.recordNo}/files?recordVersion=${fuel.recordVersion}&idempotencyKey=${encodeURIComponent(`${context.marker}:PHOTO`)}`
|
|
const upload = () => context.requestMultipart('POST', uploadPath, { name: 'controlled-meter.png', type: 'image/png', content: PNG })
|
|
const first = await upload(); fuel = context.apiSuccess(module, '上传本轮受控运维照片', first, '', OPTIONS)
|
|
check('运维照片正式关联本记录', first, Boolean(fuel?.files?.length === 1 && fuel.files[0].fileId))
|
|
const repeated = await upload(), repeatedFuel = context.apiSuccess(module, '同键重放运维照片', repeated, '', OPTIONS)
|
|
check('照片重放不增加附件或版本', repeated, repeatedFuel?.files?.length === 1 && repeatedFuel.files[0].fileId === fuel.files[0].fileId
|
|
&& Number(repeatedFuel.recordVersion) === Number(fuel.recordVersion))
|
|
proof.photoId = fuel.files[0].fileId
|
|
const photo = await context.requestBinary('GET', `${base}/${fuel.recordNo}/files/${encodeURIComponent(proof.photoId)}`)
|
|
check('运维图片下载真实字节一致', photo, photo?.http === 200 && String(photo.contentType).startsWith('image/png')
|
|
&& photo.bytes instanceof Uint8Array && Buffer.from(photo.bytes).equals(PNG))
|
|
for (const type of ['REPAIR', 'BREAKDOWN', 'RESCUE', 'EXCEPTION']) {
|
|
const priced = ['REPAIR', 'RESCUE'].includes(type)
|
|
const details = { recordType: type, occurredAt, distanceUnit: null, odometer: null, lastServiceOdometer: null, nextServiceOdometer: null,
|
|
fuelQuantity: null, fuelUnit: null, verificationStatus: null, remark: context.marker,
|
|
incident: { description: `${context.marker} 受控故障处置记录`, item: priced ? '更换轮胎' : null, place: priced ? '本轮测试作业点' : null,
|
|
handler: null, processingStatus: 'PENDING', exceptionType: type === 'EXCEPTION' ? 'VEHICLE_BREAKDOWN' : null,
|
|
reportedCost: type === 'REPAIR' ? 125.5 : priced ? 0 : null, currency: priced ? 'USD' : null } }
|
|
const initial = { idempotencyKey: `${context.marker}:${type}`, details }
|
|
let incident = await command('POST', base, initial, `创建本轮${type}记录`)
|
|
proof.records.push({ recordNo: incident.recordNo, createRequest: initial })
|
|
check('事件项目地点和分类完整回显', null, incident.vehicleId === id && (incident.details?.incident?.item ?? null) === details.incident.item
|
|
&& (incident.details.incident.place ?? null) === details.incident.place && (incident.details.incident.exceptionType ?? null) === details.incident.exceptionType)
|
|
incident = await command('PUT', `${base}/${incident.recordNo}`, { recordVersion: Number(incident.recordVersion), reason: '受控任务实际处理完成',
|
|
details: { ...details, incident: { ...details.incident, handler: '本轮测试处理人', processingStatus: 'COMPLETED' } } }, `完成本轮${type}处理记录`)
|
|
check('处理完成保持独立的费用及付款事实', null, incident.details.incident.processingStatus === 'COMPLETED'
|
|
&& incident.paymentStatus == null
|
|
&& (!priced || (Number(incident.details.incident.reportedCost) === details.incident.reportedCost
|
|
&& incident.details.incident.currency === details.incident.currency)))
|
|
if (priced) {
|
|
const financial = await command('POST', `${base}/finance-status`, { recordNos: [incident.recordNo] }, '独立批量查询本轮处理记录付款事实')
|
|
check('处理完成未自动提交财务或标记付款', null, Array.isArray(financial) && financial.length === 1
|
|
&& financial[0].recordNo === incident.recordNo && financial[0].status === 'UNLINKED'
|
|
&& financial[0].sourceVersion == null && financial[0].submittedRecordVersion == null && financial[0].finance == null)
|
|
}
|
|
const refreshed = await command('GET', `${base}/${incident.recordNo}`, undefined, `重新读取本轮${type}记录`)
|
|
check('处理记录重新读取保持全部现场字段', null, (refreshed.details.incident.item ?? null) === details.incident.item
|
|
&& (refreshed.details.incident.place ?? null) === details.incident.place && refreshed.details.incident.handler === '本轮测试处理人')
|
|
const changes = await command('GET', `${base}/${incident.recordNo}/history?page=1&size=20`, undefined, `核对本轮${type}处理历史`)
|
|
check('处理历史保留原待处理事实及完成结果', null, changes.records?.length === 2
|
|
&& changes.records[0].snapshot?.details?.incident?.processingStatus === 'COMPLETED'
|
|
&& changes.records[1].snapshot?.details?.incident?.processingStatus === 'PENDING')
|
|
}
|
|
let other
|
|
try {
|
|
other = await command('POST', VEHICLES, { plateNumber: `CARE-${context.marker}`.slice(0, 64), model: 'Care isolation fixture',
|
|
fuelConsumptionPer100Km: 10, mileageKm: 0, region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', remark: context.marker }, '建立运维归属隔离测试车辆')
|
|
proof.otherVehicleId = String(other.id)
|
|
const foreignBase = `${BASE}/${encodeURIComponent(proof.otherVehicleId)}/records`
|
|
for (const [method, suffix, body] of [['GET', `/${record.recordNo}`, undefined], ['PUT', `/${record.recordNo}`, { ...correction, recordVersion: record.recordVersion }],
|
|
['GET', `/${fuel.recordNo}/files/${encodeURIComponent(proof.photoId)}`, undefined]]) {
|
|
const response = await context.request(method, foreignBase + suffix, body)
|
|
check('其他车辆不能读取或修改本车记录和照片', response, response?.http === 404 || (response?.http === 200 && ['404', 'A0404'].includes(String(response.body?.code))))
|
|
}
|
|
const foreign = await command('GET', `${foreignBase}?page=1&size=10`, undefined, '核对其他车辆记录列表')
|
|
check('其他车辆列表没有串入本车记录', null, Number(foreign.total) === 0 && foreign.records?.length === 0)
|
|
for (const type of ['MAINTENANCE', 'FUEL', 'REPAIR', 'BREAKDOWN', 'RESCUE', 'EXCEPTION']) {
|
|
const page = await command('GET', `${base}?recordType=${type}&page=1&size=10`, undefined, '按车辆和类型查询运维记录')
|
|
check('每类运维记录唯一且保留单位', null, Number(page.total) === 1 && page.records?.length === 1 && page.records[0].details.recordType === type
|
|
&& page.records[0].vehicleId === id && (type !== 'FUEL' || page.records[0].details.fuelUnit === 'US_GAL'))
|
|
}
|
|
const after = await command('GET', `${VEHICLES}/${encodeURIComponent(id)}`, undefined, '回查车辆主档未被运维记录改写')
|
|
check('运维保存不改车辆里程状态或主档版本', null, Number(after.mileageKm) === Number(vehicle.mileageKm)
|
|
&& after.status === vehicle.status && Number(after.recordVersion) === Number(vehicle.recordVersion))
|
|
} finally {
|
|
if (other?.id) {
|
|
const ended = await command('PATCH', `${VEHICLES}/${encodeURIComponent(String(other.id))}/status`, { recordVersion: Number(other.recordVersion), status: 'INACTIVE' }, '停用本轮隔离用车辆并保留记录')
|
|
check('隔离用车辆已停用', null, ended.status === 'INACTIVE'); proof.otherVehicleStatus = ended.status
|
|
}
|
|
}
|
|
proof.status = 'VERIFIED'
|
|
return proof
|
|
}
|