fix(fleet): reuse reviewed handover vehicle and check formal responses
This commit is contained in:
@@ -80,7 +80,7 @@ function thousandths(value) {
|
||||
}
|
||||
|
||||
/** Internal fresh-run helper, called only after the caller's real departure + replay and before signing. */
|
||||
export async function verifyFleetHandover(context, module, tripProof, pickerToken, dispatchDetails) {
|
||||
export async function verifyFleetHandover(context, module, tripProof, pickerToken, dispatchDetails, { reviewedVehicle } = {}) {
|
||||
const run = requireRun(context, tripProof)
|
||||
if (tripProof.handover || !pickerToken || !Array.isArray(dispatchDetails) || !dispatchDetails.length)
|
||||
throw new Error('交接验收缺少本轮PDA或排车依据,或已开始;保留原请求,不自动续跑')
|
||||
@@ -135,28 +135,53 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke
|
||||
const capacity = { maxPalletSpaces: detail.trip.maxPalletSpaces, maxLoadKg: detail.trip.maxLoadKg, maxVolumeCbm: detail.trip.maxVolumeCbm }
|
||||
check('使用正式原车辆容量及有效任务结束时间', Object.values(capacity).every(value => Number(value) > 0 && Number.isFinite(Number(value)))
|
||||
&& Date.parse(detail.trip.expectedDriverEndAt) > Date.now())
|
||||
const marker = `HO-${randomUUID().slice(0, 12)}`
|
||||
const marker = reviewedVehicle ? reviewedVehicle.snapshot.plateNumber : `HO-${randomUUID().slice(0, 12).toUpperCase()}`
|
||||
const vehicleBody = { plateNumber: marker, model: 'Box Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 0,
|
||||
region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', ...capacity, remark: `${context.marker} controlled TEST handover vehicle` }
|
||||
proof.replacement = { marker, vehicleRequest: vehicleBody }
|
||||
const vehicle = await request('POST', VEHICLES, vehicleBody, '建立本轮受控接替车辆')
|
||||
let vehicle
|
||||
if (reviewedVehicle) {
|
||||
check('显式沿用车辆具有已审阅成功请求和无占用证明', reviewedVehicle.creationRequest?.status === 'RESPONDED'
|
||||
&& reviewedVehicle.creationRequest.http === 200 && text(reviewedVehicle.creationRequest.code) === '200'
|
||||
&& reviewedVehicle.creationRequest.requestId && reviewedVehicle.creationRequest.traceId
|
||||
&& text(reviewedVehicle.occupation?.vehicleId) === text(reviewedVehicle.snapshot.id) && reviewedVehicle.occupation.assignmentCount === 0)
|
||||
vehicle = await request('GET', `${VEHICLES}/${encodeURIComponent(reviewedVehicle.snapshot.id)}`, undefined, '再次核对已创建接替车辆完整快照')
|
||||
check('已创建车辆未发生身份版本容量或状态变更', same(vehicle, reviewedVehicle.snapshot)
|
||||
&& text(vehicle.recordVersion) === '0' && vehicle.status === 'IN_SERVICE'
|
||||
&& vehicle.region === 'OVERSEAS' && vehicle.ownership === 'OWNED'
|
||||
&& Number(vehicle.maxPalletSpaces) === capacity.maxPalletSpaces && Number(vehicle.maxLoadKg) === capacity.maxLoadKg && Number(vehicle.maxVolumeCbm) === capacity.maxVolumeCbm)
|
||||
proof.replacement.vehicleReuse = clone(reviewedVehicle)
|
||||
} else vehicle = await request('POST', VEHICLES, vehicleBody, '建立本轮受控接替车辆')
|
||||
check('接替车辆返回真实身份及车牌', vehicle?.id && vehicle.plateNumber === marker)
|
||||
proof.replacement.vehicleId = text(vehicle.id); proof.replacement.vehiclePlate = vehicle.plateNumber
|
||||
const driver = await request('POST', `${FLEET}/drivers`, { name: `${marker}测试司机`, phone: '19900000000', ownership: 'OWNED', region: 'OVERSEAS',
|
||||
const driverBody = { name: `${marker}测试司机`, phone: '19900000000', ownership: 'OWNED', region: 'OVERSEAS',
|
||||
licenseNumber: `${marker}-TEST-NOT-REAL`, licenseType: 'TEST', licenseZone: 'UTC', licenseExpiresOn: new Date(Date.now() + 365 * 86400000).toISOString().slice(0, 10),
|
||||
remark: `${context.marker} TEST credential fixture; not a real driving licence` }, '建立本轮接替司机档案')
|
||||
check('接替司机返回身份和版本', driver?.id && validVersion(driver.recordVersion))
|
||||
remark: `${context.marker} TEST credential fixture; not a real driving licence` }
|
||||
const driver = await request('POST', `${FLEET}/drivers`, driverBody, '建立本轮接替司机档案')
|
||||
const profileFields = ['warehouseId', 'vehicleModels', 'languagesAndArea', 'medicalExpiresOn', 'trainingExpiresOn', 'insuranceExpiresOn']
|
||||
check('接替司机返回身份和版本', driver?.id && text(driver.recordVersion) === '0' && driver.active === true
|
||||
&& Object.entries(driverBody).every(([field, value]) => driver.details?.[field] === value)
|
||||
&& driver.details.licenseIssuedOn == null && driver.details.profile && !Array.isArray(driver.details.profile)
|
||||
&& profileFields.every(field => driver.details.profile[field] == null))
|
||||
proof.replacement.driverId = text(driver.id)
|
||||
const file = { name: `${marker}-TEST-NOT-REAL-LICENSE.png`, type: 'image/png', content: certificate }
|
||||
proof.replacement.certificate = { name: file.name, type: file.type, contentBase64: certificate.toString('base64'), sha256: createHash('sha256').update(certificate).digest('hex'), synthetic: true }
|
||||
const uploaded = await request('POST', `${FLEET}/drivers/${encodeURIComponent(driver.id)}/license?recordVersion=${driver.recordVersion}`, undefined, '正式上传非真实测试证照', { file })
|
||||
check('测试证照有正式文件归属', uploaded?.licenseFileId && validVersion(uploaded.recordVersion))
|
||||
check('测试证照有正式文件归属', text(uploaded?.id) === text(driver.id) && uploaded?.licenseFileId
|
||||
&& text(uploaded.recordVersion) === String(BigInt(driver.recordVersion) + 1n))
|
||||
proof.replacement.licenseFileId = text(uploaded.licenseFileId)
|
||||
const reviewed = await request('POST', `${FLEET}/drivers/${encodeURIComponent(driver.id)}/review`, { recordVersion: uploaded.recordVersion, decision: 'APPROVED', reason: '仅核验本轮合成测试证照' }, '正式审核本轮接替司机')
|
||||
check('接替司机已正式审核', reviewed?.reviewStatus === 'APPROVED')
|
||||
check('接替司机已正式审核', text(reviewed?.id) === text(driver.id) && reviewed.reviewStatus === 'APPROVED'
|
||||
&& text(reviewed.recordVersion) === String(BigInt(uploaded.recordVersion) + 1n) && reviewed.licenseFileId === uploaded.licenseFileId)
|
||||
proof.selection = { recordVersion: detail.trip.recordVersion, vehicleId: vehicle.id, driverId: driver.id,
|
||||
expectedDriverEndAt: detail.trip.expectedDriverEndAt, handedOverAt: new Date().toISOString() }
|
||||
const preview = await request('POST', `${base}/handovers/preview`, proof.selection, '预览真实剩余货物及资源资格')
|
||||
check('交接预览绑定原车次版本和确切新旧身份', preview?.tripNo === tripProof.tripNo
|
||||
&& text(preview.recordVersion) === text(proof.selection.recordVersion)
|
||||
&& text(preview.previous?.vehicleId) === proof.previous.vehicleId && text(preview.previous?.driverId) === proof.previous.driverId
|
||||
&& text(preview.replacement?.vehicleId) === text(vehicle.id) && text(preview.replacement?.driverId) === text(driver.id))
|
||||
check('交接预览容量与已核对车辆一致', Number(preview.capacity?.palletSpaces) === capacity.maxPalletSpaces
|
||||
&& Number(preview.capacity?.weightKg) === capacity.maxLoadKg && Number(preview.capacity?.volumeCbm) === capacity.maxVolumeCbm)
|
||||
const expectedCargo = original.confirmedLoad.cargo.map(row => ({ customerId: row.customerId, dispatchNo: row.dispatchNo, itemId: row.dispatchItemId,
|
||||
palletId: row.warehousePalletId, destinationKey: row.destinationKey, packages: row.packages }))
|
||||
check('预览精确包含全部未签收客户货物且资格有效', /^[0-9a-f]{64}$/.test(text(preview?.cargoHash)) && preview.driverQualification?.canDispatch === true
|
||||
@@ -171,7 +196,7 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke
|
||||
proof.preview = preview
|
||||
proof.confirmRequest = { selection: proof.selection, cargoHash: preview.cargoHash, location: `${context.marker}受控交接点`, reason: '本轮真实发运后整车资源交接', idempotencyKey: key() }
|
||||
const handover = await request('POST', `${base}/handovers`, proof.confirmRequest, '确认实际在途交接')
|
||||
check('交接保留原新资源和货物快照', handover?.handoverNo && handover.tripNo === tripProof.tripNo && validVersion(handover.recordVersion)
|
||||
check('交接保留原新资源和货物快照', handover?.handoverNo && handover.tripNo === tripProof.tripNo && text(handover.recordVersion) === '0'
|
||||
&& text(handover.snapshot?.previous?.vehicleId) === proof.previous.vehicleId && text(handover.snapshot.previous.driverId) === proof.previous.driverId
|
||||
&& text(handover.snapshot.replacement.vehicleId) === text(vehicle.id) && text(handover.snapshot.replacement.driverId) === text(driver.id)
|
||||
&& same(inspectionFacts(handover.snapshot.cargo), inspectionFacts(preview.cargo)) && handover.vehicleRelease == null && handover.driverRelease == null)
|
||||
@@ -194,11 +219,21 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke
|
||||
const releaseBase = `${base}/handovers/${encodeURIComponent(handover.handoverNo)}/resources/release`
|
||||
proof.vehicleReleaseRequest = { recordVersion: handover.recordVersion, resourceKind: 'VEHICLE', actualEndAt: new Date().toISOString(), location: `${context.marker}车辆任务结束点`, vehicleStatus: 'IN_SERVICE', idempotencyKey: key() }
|
||||
const vehicleReleased = await request('POST', releaseBase, proof.vehicleReleaseRequest, '单独结束交接前车辆任务')
|
||||
check('原车辆已结束而原司机仍在任务中', vehicleReleased.vehicleRelease?.actualEndAt && vehicleReleased.driverRelease == null)
|
||||
check('原车辆已结束而原司机仍在任务中', vehicleReleased.handoverNo === handover.handoverNo && vehicleReleased.tripNo === tripProof.tripNo
|
||||
&& text(vehicleReleased.recordVersion) === String(BigInt(proof.vehicleReleaseRequest.recordVersion) + 1n)
|
||||
&& Date.parse(vehicleReleased.vehicleRelease?.actualEndAt) === Date.parse(proof.vehicleReleaseRequest.actualEndAt)
|
||||
&& vehicleReleased.vehicleRelease?.idempotencyKey === proof.vehicleReleaseRequest.idempotencyKey
|
||||
&& vehicleReleased.vehicleRelease?.location === proof.vehicleReleaseRequest.location && vehicleReleased.vehicleRelease?.vehicleStatus === 'IN_SERVICE'
|
||||
&& vehicleReleased.driverRelease == null)
|
||||
await request('POST', `${pathFor(firstProbe.tripNo)}/assignment`, assignment, '只释放车辆后旧司机仍被占用', { conflict: /司机.*占用/ })
|
||||
proof.driverReleaseRequest = { recordVersion: vehicleReleased.recordVersion, resourceKind: 'DRIVER', actualEndAt: new Date().toISOString(), location: `${context.marker}司机任务结束点`, idempotencyKey: key() }
|
||||
const released = await request('POST', releaseBase, proof.driverReleaseRequest, '单独结束交接前司机任务')
|
||||
check('原车辆司机分别保留结束事实', released.vehicleRelease?.actualEndAt && released.driverRelease?.actualEndAt)
|
||||
check('原车辆司机分别保留结束事实', released.handoverNo === handover.handoverNo && released.tripNo === tripProof.tripNo
|
||||
&& text(released.recordVersion) === String(BigInt(proof.driverReleaseRequest.recordVersion) + 1n)
|
||||
&& same(released.vehicleRelease, vehicleReleased.vehicleRelease)
|
||||
&& Date.parse(released.driverRelease?.actualEndAt) === Date.parse(proof.driverReleaseRequest.actualEndAt)
|
||||
&& released.driverRelease?.idempotencyKey === proof.driverReleaseRequest.idempotencyKey
|
||||
&& released.driverRelease?.location === proof.driverReleaseRequest.location)
|
||||
const assigned = await request('POST', `${pathFor(firstProbe.tripNo)}/assignment`, assignment, '将已释放原资源分配给本轮无货计划')
|
||||
firstProbe.recordVersion = assigned.trip.recordVersion
|
||||
check('原资源真正可重新分配', text(assigned.trip.vehicleId) === proof.previous.vehicleId && text(assigned.trip.driverId) === proof.previous.driverId)
|
||||
@@ -208,7 +243,7 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke
|
||||
check('原司机结束重放不改记录', same(await request('POST', releaseBase, proof.driverReleaseRequest, '新计划占用后重放旧司机结束请求'), releaseSnapshot))
|
||||
const secondProbe = await createProbe(2)
|
||||
await request('POST', `${pathFor(secondProbe.tripNo)}/assignment`, { ...assignment, recordVersion: secondProbe.recordVersion }, '旧请求重放不能释放下一计划车辆', { conflict: /车辆.*占用/ })
|
||||
const spareBody = { ...vehicleBody, plateNumber: `HO-PROBE-${randomUUID().slice(0, 10)}` }
|
||||
const spareBody = { ...vehicleBody, plateNumber: `HO-PROBE-${randomUUID().slice(0, 10).toUpperCase()}` }
|
||||
const spare = await request('POST', VEHICLES, spareBody, '创建仅用于司机占用隔离的本轮车辆')
|
||||
check('隔离车辆已创建', spare?.id); proof.spareVehicleId = text(spare.id)
|
||||
await request('POST', `${pathFor(secondProbe.tripNo)}/assignment`, { ...assignment, vehicleId: spare.id, recordVersion: secondProbe.recordVersion }, '旧请求重放不能释放下一计划司机', { conflict: /司机.*占用/ })
|
||||
|
||||
Reference in New Issue
Block a user