|
|
|
@@ -77,15 +77,20 @@ async function request(context, method, path, body, headers = {}) {
|
|
|
|
|
} catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function uploadEvidence(context, tenantId, inboundId, evidenceType) {
|
|
|
|
|
const path = `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}?evidenceType=${encodeURIComponent(evidenceType)}`
|
|
|
|
|
async function uploadEvidence(context, taskId, warehouseId, evidenceType, expectedVersion, keySuffix,
|
|
|
|
|
{ sealNo = null, remark = context.marker, fileSuffix = evidenceType } = {}) {
|
|
|
|
|
const query = new URLSearchParams({ warehouseId: String(warehouseId), expectedVersion: String(expectedVersion),
|
|
|
|
|
evidenceType, remark })
|
|
|
|
|
if (sealNo != null) query.set('sealNo', sealNo)
|
|
|
|
|
const path = `/api/platform/v1/first-leg/pda-loading/tasks/${taskId}/evidence?${query}`
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130])
|
|
|
|
|
const fileName = `${context.marker}-${evidenceType.toLowerCase()}.png`
|
|
|
|
|
const fileName = `${context.marker}-${String(fileSuffix).toLowerCase()}.png`
|
|
|
|
|
form.append('file', new Blob([png], { type: 'image/png' }), fileName)
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${context.options.gateway}${path}`, {
|
|
|
|
|
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
|
|
|
|
|
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`,
|
|
|
|
|
'Idempotency-Key': `${context.marker}-p3-evidence-${keySuffix}`.slice(0, 128) },
|
|
|
|
|
body: form, signal: AbortSignal.timeout(60_000),
|
|
|
|
|
})
|
|
|
|
|
const raw = await response.text(); let payload = null
|
|
|
|
@@ -99,17 +104,6 @@ async function uploadEvidence(context, tenantId, inboundId, evidenceType) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function bindEvidence(context, base, tenantId, evidenceType, fileId, suffix, keySuffix = evidenceType) {
|
|
|
|
|
return request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
|
|
|
|
|
expectedVersion: 0,
|
|
|
|
|
evidenceType,
|
|
|
|
|
fileId,
|
|
|
|
|
fileName: 'client-must-not-override.png',
|
|
|
|
|
sealNo: evidenceType === 'SEAL' ? `SEAL-${suffix}` : null,
|
|
|
|
|
remark: context.marker,
|
|
|
|
|
}, { 'Idempotency-Key': `${context.marker}-p3-evidence-${keySuffix}`.slice(0, 128) })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fixture(context) {
|
|
|
|
|
return context.createdResources?.orchestrationFixtures?.phase3Loading
|
|
|
|
|
|| context.createdResources?.orchestrationFixtures?.inboundMeasurement
|
|
|
|
@@ -440,8 +434,52 @@ export async function run(context) {
|
|
|
|
|
const bookingDraftResponse = context.apiSuccess(MODULE, '创建三期 DRAFT 订舱(不生成真实柜)', bookingCreateResponse, '', OPTIONS)
|
|
|
|
|
const bookingId = id(bookingDraftResponse?.oceanBooking?.id)
|
|
|
|
|
const draftVersion = Number(bookingDraftResponse?.oceanBooking?.version)
|
|
|
|
|
context.assert(MODULE, '三期 DRAFT 订舱返回稳定身份和初始版本', bookingCreateResponse,
|
|
|
|
|
Boolean(bookingId) && bookingDraftResponse?.oceanBooking?.status === 'DRAFT'
|
|
|
|
|
&& Number.isInteger(draftVersion) && draftVersion >= 0,
|
|
|
|
|
`bookingId=${bookingId || 'missing'}, status=${bookingDraftResponse?.oceanBooking?.status || 'missing'}, version=${bookingDraftResponse?.oceanBooking?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (!bookingId || !Number.isInteger(draftVersion) || draftVersion < 0) return
|
|
|
|
|
const bookingDocumentKey = `${context.marker}-p3-booking-document`.slice(0, 128)
|
|
|
|
|
const bookingDocumentForm = () => {
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append('expectedVersion', String(draftVersion))
|
|
|
|
|
form.append('documentType', 'BOOKING_CONFIRMATION')
|
|
|
|
|
form.append('file', new Blob([`${context.marker}-BOOKING_CONFIRMATION`], { type: 'text/plain' }),
|
|
|
|
|
`booking-confirmation-${context.marker}.txt`)
|
|
|
|
|
return form
|
|
|
|
|
}
|
|
|
|
|
const bookingDocumentResponse = await context.requestMultipart('POST',
|
|
|
|
|
`/api/platform/v1/ocean-bookings/${bookingId}/documents/upload`, bookingDocumentForm(), context.token,
|
|
|
|
|
{ 'Idempotency-Key': bookingDocumentKey })
|
|
|
|
|
const bookingDocument = context.apiSuccess(MODULE, '三期订舱上传当前有效订舱确认文件', bookingDocumentResponse, '', OPTIONS)
|
|
|
|
|
const releaseVersion = Number(bookingDocument?.bookingVersion)
|
|
|
|
|
context.assert(MODULE, '订舱文件归属当前订舱且上传递增版本', bookingDocumentResponse,
|
|
|
|
|
Boolean(bookingDocument?.document?.id) && id(bookingDocument?.document?.oceanBookingId) === bookingId
|
|
|
|
|
&& bookingDocument?.document?.documentType === 'BOOKING_CONFIRMATION'
|
|
|
|
|
&& bookingDocument?.document?.valid === true && Number.isInteger(releaseVersion)
|
|
|
|
|
&& releaseVersion > draftVersion,
|
|
|
|
|
`documentId=${id(bookingDocument?.document?.id) || 'missing'}, oceanBookingId=${id(bookingDocument?.document?.oceanBookingId) || 'missing'}, draftVersion=${draftVersion}, bookingVersion=${bookingDocument?.bookingVersion ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (!bookingId || !Number.isInteger(releaseVersion) || releaseVersion <= draftVersion) return
|
|
|
|
|
|
|
|
|
|
const bookingDocumentReplayResponse = await context.requestMultipart('POST',
|
|
|
|
|
`/api/platform/v1/ocean-bookings/${bookingId}/documents/upload`, bookingDocumentForm(), context.token,
|
|
|
|
|
{ 'Idempotency-Key': bookingDocumentKey })
|
|
|
|
|
const bookingDocumentReplay = context.apiSuccess(MODULE, '订舱文件同键同载荷重放', bookingDocumentReplayResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '订舱文件精确重放不新增文件或版本', bookingDocumentReplayResponse,
|
|
|
|
|
id(bookingDocumentReplay?.document?.id) === id(bookingDocument?.document?.id)
|
|
|
|
|
&& id(bookingDocumentReplay?.document?.oceanBookingId) === bookingId
|
|
|
|
|
&& Number(bookingDocumentReplay?.bookingVersion) === releaseVersion,
|
|
|
|
|
`firstDocumentId=${id(bookingDocument?.document?.id) || 'missing'}, replayDocumentId=${id(bookingDocumentReplay?.document?.id) || 'missing'}, firstVersion=${releaseVersion}, replayVersion=${bookingDocumentReplay?.bookingVersion ?? 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const bookingAfterDocumentResponse = await context.request('GET', `/api/platform/v1/ocean-bookings/${bookingId}`)
|
|
|
|
|
const bookingAfterDocument = context.apiSuccess(MODULE, '回查订舱文件有效归属', bookingAfterDocumentResponse, '', OPTIONS)
|
|
|
|
|
const activeBookingDocuments = (bookingAfterDocument?.documents || []).filter((document) => document?.valid === true)
|
|
|
|
|
context.assert(MODULE, '同键重放后当前订舱仅保留一条目标有效文件', bookingAfterDocumentResponse,
|
|
|
|
|
activeBookingDocuments.filter((document) => id(document?.id) === id(bookingDocument?.document?.id)
|
|
|
|
|
&& id(document?.oceanBookingId) === bookingId && document?.documentType === 'BOOKING_CONFIRMATION').length === 1,
|
|
|
|
|
`activeDocuments=${activeBookingDocuments.length}, targetDocumentId=${id(bookingDocument?.document?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`,
|
|
|
|
|
{ expectedVersion: draftVersion, reason: context.marker },
|
|
|
|
|
{ expectedVersion: releaseVersion, reason: context.marker },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-booking-release`.slice(0, 128) })
|
|
|
|
|
const bookingConfirmed = context.apiSuccess(MODULE, '三期订舱 RELEASED 冻结运输快照', releaseResponse, '', OPTIONS)
|
|
|
|
|
const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
|
|
|
|
@@ -851,46 +889,45 @@ export async function run(context) {
|
|
|
|
|
|
|
|
|
|
const evidenceTypes = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']
|
|
|
|
|
const evidenceItems = []
|
|
|
|
|
const evidencePath = `${pdaBase}/tasks/${canonicalTaskId}/evidence`
|
|
|
|
|
const normalizedEvidence = (item) => item ? { ...item, id: id(item.evidenceId) } : null
|
|
|
|
|
for (const evidenceType of evidenceTypes) {
|
|
|
|
|
const upload = await uploadEvidence(context, tenantId, inboundId, evidenceType)
|
|
|
|
|
const uploaded = context.apiSuccess(MODULE, `上传本轮 ${evidenceType} 真实证据文件`, upload, '', OPTIONS)
|
|
|
|
|
const fileId = id(uploaded?.fileId)
|
|
|
|
|
context.assert(MODULE, `${evidenceType} 文件归属类型和媒体元数据正确`, upload,
|
|
|
|
|
Boolean(fileId) && uploaded?.businessType === `LOADING_${evidenceType}`
|
|
|
|
|
&& uploaded?.contentType === 'image/png' && Number(uploaded?.sizeBytes) > 0,
|
|
|
|
|
`fileId=${fileId || 'missing'}, businessType=${uploaded?.businessType || 'missing'}`, OPTIONS)
|
|
|
|
|
if (!fileId) return
|
|
|
|
|
|
|
|
|
|
const evidence = await bindEvidence(context, base, tenantId, evidenceType, fileId, suffix)
|
|
|
|
|
const evidenceData = context.apiSuccess(MODULE, `绑定本轮 ${evidenceType} 装柜证据`, evidence, '', OPTIONS)
|
|
|
|
|
const evidenceItem = evidenceData?.evidences?.evidences?.find?.((item) => item.evidenceType === evidenceType)
|
|
|
|
|
context.assert(MODULE, `${evidenceType} 证据归属、marker 和版本正确`, evidence,
|
|
|
|
|
evidenceData?.mainStatus === 'LOADING' && id(evidenceItem?.fileId) === fileId
|
|
|
|
|
const sealNo = evidenceType === 'SEAL' ? `SEAL-${suffix}` : null
|
|
|
|
|
const upload = await uploadEvidence(context, canonicalTaskId, warehouseId, evidenceType, 0,
|
|
|
|
|
evidenceType, { sealNo })
|
|
|
|
|
const evidenceData = context.apiSuccess(MODULE, `通过 PDA task 原子上传并绑定 ${evidenceType} 证据`, upload, '', OPTIONS)
|
|
|
|
|
const evidenceItem = normalizedEvidence(evidenceData?.evidences?.find?.((item) =>
|
|
|
|
|
item.evidenceType === evidenceType && item.status !== 'VOID'))
|
|
|
|
|
const fileId = id(evidenceItem?.fileId)
|
|
|
|
|
context.assert(MODULE, `${evidenceType} 证据归属 task、柜次、marker 和版本`, upload,
|
|
|
|
|
id(evidenceData?.taskId) === canonicalTaskId
|
|
|
|
|
&& id(evidenceData?.containerBookingId) === containerBookingId
|
|
|
|
|
&& Boolean(evidenceItem?.id) && Boolean(fileId)
|
|
|
|
|
&& evidenceItem?.fileName === `${context.marker}-${evidenceType.toLowerCase()}.png`
|
|
|
|
|
&& evidenceItem?.remark === context.marker && Number(evidenceItem?.version) === 0
|
|
|
|
|
&& (evidenceType !== 'SEAL' || evidenceItem?.sealNo === `SEAL-${suffix}`),
|
|
|
|
|
`mainStatus=${evidenceData?.mainStatus || 'missing'}, evidenceId=${id(evidenceItem?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
&& (evidenceType !== 'SEAL' || evidenceItem?.sealNo === sealNo),
|
|
|
|
|
`taskId=${id(evidenceData?.taskId) || 'missing'}, containerBookingId=${id(evidenceData?.containerBookingId) || 'missing'}, evidenceId=${id(evidenceItem?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
if (!evidenceItem?.id) return
|
|
|
|
|
evidenceItems.push(evidenceItem)
|
|
|
|
|
|
|
|
|
|
if (evidenceType === 'PHOTO_STEP_1') {
|
|
|
|
|
const replay = await bindEvidence(context, base, tenantId, evidenceType, fileId, suffix)
|
|
|
|
|
const replayData = context.apiSuccess(MODULE, '装柜证据绑定同键同参幂等重放', replay, '', OPTIONS)
|
|
|
|
|
const replayItems = replayData?.evidences?.evidences?.filter?.((item) => item.evidenceType === evidenceType) || []
|
|
|
|
|
context.assert(MODULE, '证据绑定重放返回同一记录且不重复追加', replay,
|
|
|
|
|
replayItems.length === 1 && id(replayItems[0]?.id) === id(evidenceItem.id),
|
|
|
|
|
`evidenceId=${id(replayItems[0]?.id) || 'missing'}, count=${replayItems.length}`, OPTIONS)
|
|
|
|
|
const conflict = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
|
|
|
|
|
expectedVersion: 0, evidenceType, fileId,
|
|
|
|
|
fileName: 'client-must-not-override.png', sealNo: null, remark: `${context.marker}-different`,
|
|
|
|
|
}, { 'Idempotency-Key': `${context.marker}-p3-evidence-${evidenceType}`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, '装柜证据绑定同键异参返回冲突', conflict,
|
|
|
|
|
const replay = await uploadEvidence(context, canonicalTaskId, warehouseId, evidenceType, 0,
|
|
|
|
|
evidenceType)
|
|
|
|
|
const replayData = context.apiSuccess(MODULE, '装柜证据 task 上传同键同载荷重放', replay, '', OPTIONS)
|
|
|
|
|
const replayItems = replayData?.evidences?.filter?.((item) => item.evidenceType === evidenceType) || []
|
|
|
|
|
context.assert(MODULE, '证据 task 上传重放返回同一记录且不重复追加', replay,
|
|
|
|
|
replayItems.length === 1 && id(replayItems[0]?.evidenceId) === id(evidenceItem.id),
|
|
|
|
|
`evidenceId=${id(replayItems[0]?.evidenceId) || 'missing'}, count=${replayItems.length}`, OPTIONS)
|
|
|
|
|
const conflict = await uploadEvidence(context, canonicalTaskId, warehouseId, evidenceType, 0,
|
|
|
|
|
evidenceType, { remark: `${context.marker}-different` })
|
|
|
|
|
context.addStep(MODULE, '装柜证据 task 上传同键异载荷返回冲突', conflict,
|
|
|
|
|
conflict.http === 200 && codeOf(conflict) === 'A0409'
|
|
|
|
|
&& Boolean(conflict.requestId) && Boolean(conflict.traceId),
|
|
|
|
|
`HTTP=${conflict.http}, code=${codeOf(conflict)}, requestId=${conflict.requestId || 'missing'}, traceId=${conflict.traceId || 'missing'}`, OPTIONS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const download = await context.requestBinary('GET', `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}/${fileId}`)
|
|
|
|
|
const download = await context.requestBinary('GET',
|
|
|
|
|
`${evidencePath}/${encodeURIComponent(evidenceItem.id)}/content?warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
context.assert(MODULE, `下载本轮 ${evidenceType} 证据文件`, download,
|
|
|
|
|
download.http === 200 && download.contentType.toLowerCase().includes('image/png') && download.bytes.length > 0,
|
|
|
|
|
`bytes=${download.bytes.length}, contentType=${download.contentType || 'missing'}`, OPTIONS)
|
|
|
|
@@ -904,95 +941,100 @@ export async function run(context) {
|
|
|
|
|
const firstPhoto = evidenceItems.find((item) => item.evidenceType === 'PHOTO_STEP_1')
|
|
|
|
|
const sealEvidence = evidenceItems.find((item) => item.evidenceType === 'SEAL')
|
|
|
|
|
|
|
|
|
|
const appendedUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_1')
|
|
|
|
|
const appendedFile = context.apiSuccess(MODULE, '追加同类型第二张装柜证据文件', appendedUpload, '', OPTIONS)
|
|
|
|
|
const appendedFileId = id(appendedFile?.fileId)
|
|
|
|
|
const appended = appendedFileId
|
|
|
|
|
? await bindEvidence(context, base, tenantId, 'PHOTO_STEP_1', appendedFileId, suffix, 'PHOTO_STEP_1_APPEND')
|
|
|
|
|
: appendedUpload
|
|
|
|
|
const appendedData = context.apiSuccess(MODULE, '追加同类型第二张装柜证据', appended, '', OPTIONS)
|
|
|
|
|
const appendedItems = appendedData?.evidences?.evidences?.filter?.((item) => item.evidenceType === 'PHOTO_STEP_1') || []
|
|
|
|
|
context.assert(MODULE, '同类型装柜证据支持多图追加且不替换历史', appended,
|
|
|
|
|
const appended = await uploadEvidence(context, canonicalTaskId, warehouseId, 'PHOTO_STEP_1',
|
|
|
|
|
Number(firstPhoto?.version), 'PHOTO_STEP_1_APPEND', { fileSuffix: 'PHOTO_STEP_1_APPEND' })
|
|
|
|
|
const appendedData = context.apiSuccess(MODULE, '追加同类型第二张 task 装柜证据', appended, '', OPTIONS)
|
|
|
|
|
const appendedItems = appendedData?.evidences?.filter?.((item) => item.evidenceType === 'PHOTO_STEP_1') || []
|
|
|
|
|
const appendedActive = normalizedEvidence(appendedItems.find((item) => item.status !== 'VOID'))
|
|
|
|
|
const appendedFileId = id(appendedActive?.fileId)
|
|
|
|
|
context.assert(MODULE, '同类型装柜证据追加保留历史并唯一激活新图', appended,
|
|
|
|
|
Boolean(appendedFileId) && appendedItems.length >= 2
|
|
|
|
|
&& appendedItems.some((item) => id(item.fileId) === id(firstPhoto?.fileId))
|
|
|
|
|
&& appendedItems.some((item) => id(item.fileId) === appendedFileId),
|
|
|
|
|
&& appendedItems.some((item) => id(item.fileId) === id(firstPhoto?.fileId) && item.status === 'VOID')
|
|
|
|
|
&& id(appendedActive?.fileId) === appendedFileId,
|
|
|
|
|
`photoStep1Count=${appendedItems.length}, original=${id(firstPhoto?.fileId) || 'missing'}, appended=${appendedFileId || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const wrongType = await bindEvidence(context, base, tenantId, 'PHOTO_STEP_2', id(firstPhoto?.fileId), suffix, 'wrong-type')
|
|
|
|
|
context.addStep(MODULE, '不同证据类型的真实 fileId 不可交叉绑定', wrongType, failure(wrongType),
|
|
|
|
|
const wrongType = await uploadEvidence(context, canonicalTaskId, warehouseId, 'PHOTO_STEP_9', 0,
|
|
|
|
|
'wrong-type', { fileSuffix: 'wrong-type' })
|
|
|
|
|
context.addStep(MODULE, '不在白名单的证据类型不可上传绑定', wrongType, failure(wrongType),
|
|
|
|
|
`HTTP=${wrongType.http}, code=${codeOf(wrongType)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const forged = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
|
|
|
|
|
expectedVersion: Number(sealEvidence?.version), evidenceType: 'SEAL', fileId: '999999999999999999',
|
|
|
|
|
fileName: `${context.marker}-forged.png`, sealNo: `FORGED-${suffix}`, remark: context.marker,
|
|
|
|
|
}, { 'Idempotency-Key': `${context.marker}-p3-forged-evidence`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, '伪造装柜证据 fileId 归属校验拒绝', forged, failure(forged),
|
|
|
|
|
const forged = await context.request('GET',
|
|
|
|
|
`${evidencePath}/999999999999999999/content?warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
context.addStep(MODULE, '伪造装柜 evidenceId 无法读取任务证据内容', forged,
|
|
|
|
|
(forged.http === 200 && codeOf(forged) === 'A0404') || forged.http === 404,
|
|
|
|
|
`HTTP=${forged.http}, code=${codeOf(forged)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const missingSealText = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
|
|
|
|
|
expectedVersion: Number(sealEvidence?.version), evidenceType: 'SEAL', fileId: id(sealEvidence?.fileId),
|
|
|
|
|
fileName: `${context.marker}-missing-seal-text.png`, sealNo: ' ', remark: context.marker,
|
|
|
|
|
}, { 'Idempotency-Key': `${context.marker}-p3-missing-seal-text`.slice(0, 128) })
|
|
|
|
|
const beforeMissingSealResponse = await context.request('GET',
|
|
|
|
|
`${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
const beforeMissingSeal = beforeMissingSealResponse.body?.data?.evidences || []
|
|
|
|
|
const missingSealText = await uploadEvidence(context, canonicalTaskId, warehouseId, 'SEAL',
|
|
|
|
|
Number(sealEvidence?.version), 'missing-seal-text', { sealNo: ' ', fileSuffix: 'missing-seal-text' })
|
|
|
|
|
context.addStep(MODULE, '带锁码照片缺少锁码文本时拒绝绑定', missingSealText, failure(missingSealText),
|
|
|
|
|
`HTTP=${missingSealText.http}, code=${codeOf(missingSealText)}`, OPTIONS)
|
|
|
|
|
const afterMissingSealResponse = await context.request('GET',
|
|
|
|
|
`${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
context.assert(MODULE, '缺锁码文本失败后 task 证据零漂移', afterMissingSealResponse,
|
|
|
|
|
JSON.stringify(afterMissingSealResponse.body?.data?.evidences || []) === JSON.stringify(beforeMissingSeal),
|
|
|
|
|
`before=${beforeMissingSeal.length}, after=${afterMissingSealResponse.body?.data?.evidences?.length ?? 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const voidTarget = appendedItems.find((item) => id(item.fileId) === appendedFileId)
|
|
|
|
|
const voidTarget = appendedActive
|
|
|
|
|
const voidResponse = voidTarget
|
|
|
|
|
? await request(context, 'POST', `${base}/loading/evidence/${encodeURIComponent(id(voidTarget.id))}/void?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
{ expectedVersion: Number(voidTarget.version), reason: `${context.marker}-wrong-photo` },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void`.slice(0, 128) })
|
|
|
|
|
? await request(context, 'POST', `${evidencePath}/${encodeURIComponent(id(voidTarget.id))}/void`,
|
|
|
|
|
{ warehouseId, expectedVersion: Number(voidTarget.version), reason: `${context.marker}-wrong-photo` })
|
|
|
|
|
: appended
|
|
|
|
|
const voidData = context.apiSuccess(MODULE, '作废错误装柜证据并保留审计', voidResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '错图作废后原记录保留且不再计入有效证据', voidResponse,
|
|
|
|
|
Boolean(voidTarget) && voidData?.evidences?.evidences?.some?.((item) => id(item.id) === id(voidTarget.id)
|
|
|
|
|
&& (item.status === 'VOID' || item.valid === false) && item.voidReason === `${context.marker}-wrong-photo`),
|
|
|
|
|
Boolean(voidTarget) && voidData?.evidences?.some?.((item) => id(item.evidenceId) === id(voidTarget.id)
|
|
|
|
|
&& item.status === 'VOID' && Boolean(item.updatedAt)),
|
|
|
|
|
`evidenceId=${id(voidTarget?.id) || 'missing'}, status=${voidTarget?.status || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const restoredPhotoResponse = await uploadEvidence(context, canonicalTaskId, warehouseId, 'PHOTO_STEP_1', 0,
|
|
|
|
|
'PHOTO_STEP_1_RESTORE', { fileSuffix: 'PHOTO_STEP_1_RESTORE' })
|
|
|
|
|
const restoredPhotoData = context.apiSuccess(MODULE, '错图作废后恢复 PHOTO_STEP_1 有效证据', restoredPhotoResponse, '', OPTIONS)
|
|
|
|
|
const restoredPhoto = normalizedEvidence(restoredPhotoData?.evidences?.find?.((item) =>
|
|
|
|
|
item.evidenceType === 'PHOTO_STEP_1' && item.status !== 'VOID'))
|
|
|
|
|
context.assert(MODULE, 'PHOTO_STEP_1 恢复后旧记录保留且新记录唯一有效', restoredPhotoResponse,
|
|
|
|
|
Boolean(restoredPhoto?.id) && restoredPhotoData?.evidences?.some?.((item) =>
|
|
|
|
|
id(item.evidenceId) === id(voidTarget?.id) && item.status === 'VOID'),
|
|
|
|
|
`restoredEvidenceId=${id(restoredPhoto?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const missingTypeTarget = evidenceItems.find((item) => item.evidenceType === 'PHOTO_STEP_4')
|
|
|
|
|
const missingTypePath = `${base}/loading/evidence/${encodeURIComponent(id(missingTypeTarget?.id))}/void?tenantId=${encodeURIComponent(tenantId)}`
|
|
|
|
|
const missingTypePath = `${evidencePath}/${encodeURIComponent(id(missingTypeTarget?.id))}/void`
|
|
|
|
|
const staleMissingTypeVoid = missingTypeTarget
|
|
|
|
|
? await request(context, 'POST', missingTypePath,
|
|
|
|
|
{ expectedVersion: Number(missingTypeTarget.version) + 1, reason: `${context.marker}-stale-version` },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void-stale`.slice(0, 128) })
|
|
|
|
|
{ warehouseId, expectedVersion: Number(missingTypeTarget.version) + 1, reason: `${context.marker}-stale-version` })
|
|
|
|
|
: voidResponse
|
|
|
|
|
context.addStep(MODULE, '装柜证据作废旧版本返回冲突', staleMissingTypeVoid,
|
|
|
|
|
Boolean(missingTypeTarget) && staleMissingTypeVoid.http === 200 && codeOf(staleMissingTypeVoid) === 'A0409',
|
|
|
|
|
`HTTP=${staleMissingTypeVoid.http}, code=${codeOf(staleMissingTypeVoid)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const missingTypeVoidKey = `${context.marker}-p3-evidence-void-missing-type`.slice(0, 128)
|
|
|
|
|
const missingTypeVoidBody = {
|
|
|
|
|
expectedVersion: Number(missingTypeTarget?.version), reason: `${context.marker}-temporary-missing-type`,
|
|
|
|
|
warehouseId, expectedVersion: Number(missingTypeTarget?.version), reason: `${context.marker}-temporary-missing-type`,
|
|
|
|
|
}
|
|
|
|
|
const missingTypeVoid = missingTypeTarget
|
|
|
|
|
? await request(context, 'POST', missingTypePath, missingTypeVoidBody,
|
|
|
|
|
{ 'Idempotency-Key': missingTypeVoidKey })
|
|
|
|
|
? await request(context, 'POST', missingTypePath, missingTypeVoidBody)
|
|
|
|
|
: voidResponse
|
|
|
|
|
const missingTypeVoidData = context.apiSuccess(MODULE, '临时作废唯一 PHOTO_STEP_4 证据', missingTypeVoid, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '临时作废后 PHOTO_STEP_4 不再计入有效证据', missingTypeVoid,
|
|
|
|
|
Boolean(missingTypeTarget) && missingTypeVoidData?.evidences?.evidences?.some?.((item) =>
|
|
|
|
|
id(item.id) === id(missingTypeTarget.id) && item.status === 'VOID'),
|
|
|
|
|
Boolean(missingTypeTarget) && missingTypeVoidData?.evidences?.some?.((item) =>
|
|
|
|
|
id(item.evidenceId) === id(missingTypeTarget.id) && item.status === 'VOID'),
|
|
|
|
|
`evidenceId=${id(missingTypeTarget?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const missingTypeVoidReplay = missingTypeTarget
|
|
|
|
|
? await request(context, 'POST', missingTypePath, missingTypeVoidBody,
|
|
|
|
|
{ 'Idempotency-Key': missingTypeVoidKey })
|
|
|
|
|
? await request(context, 'POST', missingTypePath, missingTypeVoidBody)
|
|
|
|
|
: voidResponse
|
|
|
|
|
context.assert(MODULE, '证据作废同键同参重放保持同一 VOID 结果', missingTypeVoidReplay,
|
|
|
|
|
missingTypeVoidReplay.http === 200 && codeOf(missingTypeVoidReplay) === '200'
|
|
|
|
|
&& missingTypeVoidReplay.body?.data?.evidences?.evidences?.some?.((item) =>
|
|
|
|
|
id(item.id) === id(missingTypeTarget?.id) && item.status === 'VOID'),
|
|
|
|
|
context.assert(MODULE, '已作废 task 证据重复作废返回冲突', missingTypeVoidReplay,
|
|
|
|
|
missingTypeVoidReplay.http === 200 && codeOf(missingTypeVoidReplay) === 'A0409',
|
|
|
|
|
`HTTP=${missingTypeVoidReplay.http}, code=${codeOf(missingTypeVoidReplay)}`, OPTIONS)
|
|
|
|
|
const missingTypeVoidConflict = missingTypeTarget
|
|
|
|
|
? await request(context, 'POST', missingTypePath,
|
|
|
|
|
{ ...missingTypeVoidBody, reason: `${context.marker}-different-void-reason` },
|
|
|
|
|
{ 'Idempotency-Key': missingTypeVoidKey })
|
|
|
|
|
{ ...missingTypeVoidBody, reason: `${context.marker}-different-void-reason` })
|
|
|
|
|
: voidResponse
|
|
|
|
|
context.addStep(MODULE, '证据作废同键异参返回冲突', missingTypeVoidConflict,
|
|
|
|
|
context.addStep(MODULE, '已作废 task 证据异参重试仍返回冲突', missingTypeVoidConflict,
|
|
|
|
|
missingTypeVoidConflict.http === 200 && codeOf(missingTypeVoidConflict) === 'A0409',
|
|
|
|
|
`HTTP=${missingTypeVoidConflict.http}, code=${codeOf(missingTypeVoidConflict)}`, OPTIONS)
|
|
|
|
|
const duplicateVoid = missingTypeTarget
|
|
|
|
|
? await request(context, 'POST', missingTypePath, missingTypeVoidBody,
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void-duplicate`.slice(0, 128) })
|
|
|
|
|
? await request(context, 'POST', missingTypePath, missingTypeVoidBody)
|
|
|
|
|
: voidResponse
|
|
|
|
|
context.addStep(MODULE, '已作废证据使用新请求重复作废返回冲突', duplicateVoid,
|
|
|
|
|
duplicateVoid.http === 200 && codeOf(duplicateVoid) === 'A0409',
|
|
|
|
@@ -1001,51 +1043,53 @@ export async function run(context) {
|
|
|
|
|
context.addStep(MODULE, '作废唯一证据类型后不提前占用异步完成命令', null, true,
|
|
|
|
|
'PHOTO_STEP_4 已真实作废并回读;补传恢复后才允许最终阶段提交唯一完成命令', OPTIONS)
|
|
|
|
|
|
|
|
|
|
const replacementUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_4')
|
|
|
|
|
const replacementFile = context.apiSuccess(MODULE, '补传缺失 PHOTO_STEP_4 真实文件', replacementUpload, '', OPTIONS)
|
|
|
|
|
const replacementFileId = id(replacementFile?.fileId)
|
|
|
|
|
const replacementBind = replacementFileId
|
|
|
|
|
? await bindEvidence(context, base, tenantId, 'PHOTO_STEP_4', replacementFileId, suffix, 'PHOTO_STEP_4_RESTORE')
|
|
|
|
|
: replacementUpload
|
|
|
|
|
const replacementData = context.apiSuccess(MODULE, '补传恢复 PHOTO_STEP_4 有效证据', replacementBind, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '补传后五类有效证据恢复且旧 VOID 记录保留', replacementBind,
|
|
|
|
|
const replacementUpload = await uploadEvidence(context, canonicalTaskId, warehouseId, 'PHOTO_STEP_4', 0,
|
|
|
|
|
'PHOTO_STEP_4_RESTORE', { fileSuffix: 'PHOTO_STEP_4_RESTORE' })
|
|
|
|
|
const replacementData = context.apiSuccess(MODULE, '补传恢复 PHOTO_STEP_4 有效证据', replacementUpload, '', OPTIONS)
|
|
|
|
|
const replacementItem = normalizedEvidence(replacementData?.evidences?.find?.((item) =>
|
|
|
|
|
item.evidenceType === 'PHOTO_STEP_4' && item.status !== 'VOID'))
|
|
|
|
|
const replacementFileId = id(replacementItem?.fileId)
|
|
|
|
|
context.assert(MODULE, '补传后五类有效证据恢复且旧 VOID 记录保留', replacementUpload,
|
|
|
|
|
Boolean(replacementFileId)
|
|
|
|
|
&& replacementData?.evidences?.evidences?.some?.((item) => item.evidenceType === 'PHOTO_STEP_4'
|
|
|
|
|
&& replacementData?.evidences?.some?.((item) => item.evidenceType === 'PHOTO_STEP_4'
|
|
|
|
|
&& id(item.fileId) === replacementFileId && item.status !== 'VOID')
|
|
|
|
|
&& replacementData?.evidences?.evidences?.some?.((item) => id(item.id) === id(missingTypeTarget?.id)
|
|
|
|
|
&& replacementData?.evidences?.some?.((item) => id(item.evidenceId) === id(missingTypeTarget?.id)
|
|
|
|
|
&& item.status === 'VOID'),
|
|
|
|
|
`replacementFileId=${replacementFileId || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const customerEvidence = merchantToken
|
|
|
|
|
? await request(context, 'GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
? await request(context, 'GET', `${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`,
|
|
|
|
|
undefined, { Authorization: `Bearer ${merchantToken}` })
|
|
|
|
|
: merchantLogin
|
|
|
|
|
context.addStep(MODULE, '客户身份无权访问平台装柜证据', customerEvidence,
|
|
|
|
|
customerEvidence.http === 403 && codeOf(customerEvidence) === 'A0403'
|
|
|
|
|
&& Boolean(customerEvidence.requestId) && Boolean(customerEvidence.traceId),
|
|
|
|
|
`HTTP=${customerEvidence.http}, code=${codeOf(customerEvidence)}, requestId=${customerEvidence.requestId || 'missing'}, traceId=${customerEvidence.traceId || 'missing'}`, OPTIONS)
|
|
|
|
|
const wrongTenantEvidence = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(Number(tenantId) + 1)}`)
|
|
|
|
|
context.addStep(MODULE, '错误客户作用域不可读取本轮装柜证据', wrongTenantEvidence,
|
|
|
|
|
wrongTenantEvidence.http === 200 && codeOf(wrongTenantEvidence) === 'A0404',
|
|
|
|
|
`HTTP=${wrongTenantEvidence.http}, code=${codeOf(wrongTenantEvidence)}`, OPTIONS)
|
|
|
|
|
const wrongInboundEvidence = await context.request('GET',
|
|
|
|
|
`/api/platform/v1/inbounds/999999999999999999/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
|
|
|
|
|
context.addStep(MODULE, '错误入库单不可读取本轮装柜证据', wrongInboundEvidence,
|
|
|
|
|
wrongInboundEvidence.http === 200 && codeOf(wrongInboundEvidence) === 'A0404',
|
|
|
|
|
`HTTP=${wrongInboundEvidence.http}, code=${codeOf(wrongInboundEvidence)}`, OPTIONS)
|
|
|
|
|
const wrongWarehouseEvidence = await context.request('GET',
|
|
|
|
|
`${evidencePath}?warehouseId=${encodeURIComponent(Number(warehouseId) + 1)}`)
|
|
|
|
|
context.addStep(MODULE, '错误仓库作用域不可读取本轮 task 装柜证据', wrongWarehouseEvidence,
|
|
|
|
|
wrongWarehouseEvidence.http === 403 && codeOf(wrongWarehouseEvidence) === 'A0403',
|
|
|
|
|
`HTTP=${wrongWarehouseEvidence.http}, code=${codeOf(wrongWarehouseEvidence)}`, OPTIONS)
|
|
|
|
|
const wrongTaskEvidence = await context.request('GET',
|
|
|
|
|
`${pdaBase}/tasks/999999999999999999/evidence?warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
context.addStep(MODULE, '错误 taskId 不可读取本轮装柜证据', wrongTaskEvidence,
|
|
|
|
|
wrongTaskEvidence.http === 200 && codeOf(wrongTaskEvidence) === 'A0404',
|
|
|
|
|
`HTTP=${wrongTaskEvidence.http}, code=${codeOf(wrongTaskEvidence)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const postLoadedUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_2')
|
|
|
|
|
const postLoadedFile = context.apiSuccess(MODULE, '预置 LOADED 后新增证据边界文件', postLoadedUpload, '', OPTIONS)
|
|
|
|
|
const postLoadedFileId = id(postLoadedFile?.fileId)
|
|
|
|
|
|
|
|
|
|
const evidenceList = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
|
|
|
|
|
const evidenceList = await context.request('GET',
|
|
|
|
|
`${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
const evidenceListData = context.apiSuccess(MODULE, '回查五类装柜证据列表', evidenceList, '', OPTIONS)
|
|
|
|
|
const activeEvidenceList = evidenceListData?.evidences?.filter?.((item) => item.status !== 'VOID') || []
|
|
|
|
|
context.assert(MODULE, '五类装柜证据完整且铅封号持久化', evidenceList,
|
|
|
|
|
evidenceListData?.evidences?.length >= evidenceTypes.length
|
|
|
|
|
&& evidenceTypes.every((type) => evidenceListData.evidences.some((item) => item.evidenceType === type))
|
|
|
|
|
&& evidenceListData.evidences.some((item) => item.evidenceType === 'SEAL' && item.sealNo === `SEAL-${suffix}`),
|
|
|
|
|
id(evidenceListData?.taskId) === canonicalTaskId
|
|
|
|
|
&& id(evidenceListData?.containerBookingId) === containerBookingId
|
|
|
|
|
&& evidenceTypes.every((type) => activeEvidenceList.some((item) => item.evidenceType === type))
|
|
|
|
|
&& activeEvidenceList.some((item) => item.evidenceType === 'SEAL' && item.sealNo === `SEAL-${suffix}`),
|
|
|
|
|
`evidences=${evidenceListData?.evidences?.length ?? 'n/a'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const immutablePhoto = restoredPhoto
|
|
|
|
|
const postLoadedFileId = null
|
|
|
|
|
|
|
|
|
|
const incrementalFixture = context.createdResources?.orchestrationFixtures?.phase3LoadingIncremental
|
|
|
|
|
const incrementalInboundId = id(incrementalFixture?.inboundId)
|
|
|
|
|
let skipBatchVerified = false
|
|
|
|
@@ -1197,7 +1241,8 @@ export async function run(context) {
|
|
|
|
|
const incrementedTask = context.apiSuccess(MODULE, '增量后回查稳定 PDA 四元身份', incrementedTaskResponse, '', OPTIONS)
|
|
|
|
|
const incrementalTaskItem = incrementedTask?.items?.find?.((item) =>
|
|
|
|
|
id(item?.loadingPlanItemId) === id(incrementalPlanItem?.id))
|
|
|
|
|
const evidenceAfterIncrementResponse = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
|
|
|
|
|
const evidenceAfterIncrementResponse = await context.request('GET',
|
|
|
|
|
`${evidencePath}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
const evidenceAfterIncrement = context.apiSuccess(MODULE, '增量后回查既有五类证据', evidenceAfterIncrementResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '增量保持稳定四元身份、LOADING 不回退并撤回最终完成门禁', incrementResponse,
|
|
|
|
|
id(incrementedPlan?.id) === loadingPlanId && incrementedPlan?.status === 'LOADING'
|
|
|
|
@@ -1373,7 +1418,7 @@ export async function run(context) {
|
|
|
|
|
evidenceIds: evidenceItems.map((item) => id(item.id)),
|
|
|
|
|
voidedEvidenceId: id(voidTarget?.id), evidenceTypes, sealNo: `SEAL-${suffix}`,
|
|
|
|
|
missingTypeVoidedEvidenceId: id(missingTypeTarget?.id), replacementEvidenceFileId: replacementFileId,
|
|
|
|
|
immutableEvidenceId: id(firstPhoto?.id), immutableEvidenceVersion: Number(firstPhoto?.version),
|
|
|
|
|
immutableEvidenceId: id(immutablePhoto?.id), immutableEvidenceVersion: Number(immutablePhoto?.version),
|
|
|
|
|
postLoadedFileId,
|
|
|
|
|
oceanBookingId: bookingId, loadingPlanId, containerBookingId, containerNo, warehouseId,
|
|
|
|
|
canonicalPdaTaskId: canonicalTaskId,
|
|
|
|
|