Files
baiyue-firstleg/scripts/admin-test-env-modules/phase3-container-documents.mjs
T

148 lines
8.7 KiB
JavaScript
Raw Normal View History

2026-08-26 14:56:33 +08:00
const MODULE = 'phase3-container-documents'
const OPTIONS = { scenario: 'phase3-container-four-documents', deploymentBlocker: true }
const CONTAINERS = '/api/platform/v1/containers'
const FIRST_LEG = '/api/platform/v1/first-leg'
export const DOCUMENT_TYPES = Object.freeze([
'DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER',
])
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function success(response) { return response?.http === 200 && codeOf(response) === '200' }
function conflict(response) { return response?.http === 200 && codeOf(response) === 'A0409' }
function traced(response) { return Boolean(response?.requestId) && Boolean(response?.traceId) }
function key(context, label) { return `${context.marker}-four-documents-${label}`.slice(0, 128) }
function validDocument(document, type, completed) {
return Boolean(document?.id) && Boolean(document?.fileId) && document?.documentType === type
&& /^[0-9a-f]{64}$/i.test(String(document?.fileHash || ''))
&& Number.isInteger(Number(document?.version))
&& (completed ? Boolean(document?.completedAt) : document?.completedAt == null)
}
function documentForm(context, type, expectedVersion, operationReason, altered = false) {
const form = new FormData()
const suffix = altered ? '-altered' : ''
form.append('file', new Blob([`${context.marker}-${type}${suffix}`], { type: 'text/plain' }),
`${type.toLowerCase()}${suffix}.txt`)
form.append('expectedVersion', String(expectedVersion))
form.append('operationReason', operationReason)
return form
}
async function upload(context, path, type, expectedVersion, operationReason, idempotencyKey, altered = false) {
return context.requestMultipart('POST', path,
documentForm(context, type, expectedVersion, operationReason, altered), context.token,
{ 'Idempotency-Key': idempotencyKey })
}
async function uploadAndVerify(context, { type, uploadPath, readPath, completePath = null }) {
const operationReason = `${context.marker} 四类货柜资料全链验收`
const uploadKey = key(context, `${type}-upload`)
const response = await upload(context, uploadPath, type, 0, operationReason, uploadKey)
let document = context.apiSuccess(MODULE, `上传${type}`, response, `documentType=${type}`, OPTIONS)
context.assert(MODULE, `${type}上传返回归属、文件哈希和草稿版本`, response,
validDocument(document, type, false) && traced(response),
`documentId=${id(document?.id) || 'missing'}, fileId=${id(document?.fileId) || 'missing'}, version=${document?.version ?? 'missing'}`, OPTIONS)
if (!validDocument(document, type, false)) return null
const replay = await upload(context, uploadPath, type, 0, operationReason, uploadKey)
const replayed = context.apiSuccess(MODULE, `${type}同键同载荷重放`, replay, '', OPTIONS)
context.assert(MODULE, `${type}上传重放不新增资料或递增版本`, replay,
id(replayed?.id) === id(document.id) && id(replayed?.fileId) === id(document.fileId)
&& Number(replayed?.version) === Number(document.version),
`documentId=${id(replayed?.id) || 'missing'}, version=${replayed?.version ?? 'missing'}`, OPTIONS)
const hashConflict = await upload(context, uploadPath, type, 0, operationReason, uploadKey, true)
context.addStep(MODULE, `${type}同键异文件返回A0409`, hashConflict,
conflict(hashConflict) && traced(hashConflict),
`HTTP=${hashConflict.http}, code=${codeOf(hashConflict)}, requestId=${hashConflict.requestId || 'missing'}, traceId=${hashConflict.traceId || 'missing'}`, OPTIONS)
if (completePath) {
const completeKey = key(context, `${type}-complete`)
const completeBody = { expectedVersion: Number(document.version), operationReason }
const completeResponse = await context.request('POST', completePath, completeBody, context.token,
{ 'Idempotency-Key': completeKey })
document = context.apiSuccess(MODULE, `完成${type}`, completeResponse, '', OPTIONS)
context.assert(MODULE, `${type}完成后锁定且版本递增`, completeResponse,
validDocument(document, type, true) && Number(document.version) === Number(completeBody.expectedVersion) + 1,
`completedAt=${document?.completedAt || 'missing'}, version=${document?.version ?? 'missing'}`, OPTIONS)
const completeReplay = await context.request('POST', completePath, completeBody, context.token,
{ 'Idempotency-Key': completeKey })
const replayedComplete = context.apiSuccess(MODULE, `${type}完成同键重放`, completeReplay, '', OPTIONS)
context.assert(MODULE, `${type}完成重放保持同一终态`, completeReplay,
id(replayedComplete?.id) === id(document.id)
&& Number(replayedComplete?.version) === Number(document.version)
&& replayedComplete?.completedAt === document.completedAt,
`documentId=${id(replayedComplete?.id) || 'missing'}, version=${replayedComplete?.version ?? 'missing'}`, OPTIONS)
const completeConflict = await context.request('POST', completePath,
{ ...completeBody, operationReason: `${operationReason}-altered` }, context.token,
{ 'Idempotency-Key': completeKey })
context.addStep(MODULE, `${type}完成同键异载荷返回A0409`, completeConflict,
conflict(completeConflict) && traced(completeConflict),
`HTTP=${completeConflict.http}, code=${codeOf(completeConflict)}`, OPTIONS)
}
const readResponse = await context.request('GET', readPath)
const stored = context.apiSuccess(MODULE, `回读${type}`, readResponse, '', OPTIONS)
const expectedCompleted = Boolean(completePath)
context.assert(MODULE, `${type}负向请求后持久事实零漂移`, readResponse,
validDocument(stored, type, expectedCompleted) && id(stored.id) === id(document.id)
&& id(stored.fileId) === id(document.fileId) && Number(stored.version) === Number(document.version),
`documentId=${id(stored?.id) || 'missing'}, version=${stored?.version ?? 'missing'}`, OPTIONS)
return stored
}
export async function run(context) {
const fixture = context.createdResources?.phase3Loading
const containerId = id(fixture?.containerBookingId)
const planId = id(fixture?.loadingPlanId)
const warehouseId = id(fixture?.warehouseId)
if (fixture?.marker !== context.marker || fixture?.finalizeSucceeded !== true
|| fixture?.finalStatus !== 'LOADED' || !containerId || !planId || !warehouseId) {
context.addStep(MODULE, '取得本轮LOADED柜次四类资料前置', null, false,
`marker=${fixture?.marker || 'missing'}, status=${fixture?.finalStatus || 'missing'}, containerBookingId=${containerId || 'missing'}, loadingPlanId=${planId || 'missing'}, warehouseId=${warehouseId || 'missing'}`,
OPTIONS)
return
}
const specs = [
{
type: 'DESTINATION_CLEARANCE',
uploadPath: `${CONTAINERS}/${containerId}/clearance/document/upload`,
readPath: `${CONTAINERS}/${containerId}/clearance`,
completePath: `${CONTAINERS}/${containerId}/clearance/complete`,
},
{
type: 'CUSTOMS_DECLARATION',
uploadPath: `${FIRST_LEG}/loading-lists/${planId}/customs/document/upload?warehouseId=${encodeURIComponent(warehouseId)}`,
readPath: `${FIRST_LEG}/loading-lists/${planId}/customs?warehouseId=${encodeURIComponent(warehouseId)}`,
completePath: `${FIRST_LEG}/loading-lists/${planId}/customs/complete?warehouseId=${encodeURIComponent(warehouseId)}`,
},
...['DELIVERY_ORDER', 'PICKUP_ORDER'].map((type) => ({
type,
uploadPath: `${CONTAINERS}/${containerId}/documents/${type}/upload`,
readPath: `${CONTAINERS}/${containerId}/documents/${type}`,
})),
]
const documents = []
for (const spec of specs) documents.push(await uploadAndVerify(context, spec))
const overviewResponse = await context.request('GET', `${CONTAINERS}/${containerId}/overview`)
const overview = context.apiSuccess(MODULE, '回查货柜四类资料聚合', overviewResponse, '', OPTIONS)
const storedTypes = new Set((overview?.documents || []).map((item) => item?.documentType))
context.assert(MODULE, '同一LOADED柜次聚合返回四类资料且身份未漂移', overviewResponse,
id(overview?.container?.id) === containerId && documents.every(Boolean)
&& DOCUMENT_TYPES.every((type) => storedTypes.has(type)),
`containerBookingId=${containerId}, documentTypes=${[...storedTypes].join('|') || 'none'}`, OPTIONS)
if (documents.every(Boolean) && DOCUMENT_TYPES.every((type) => storedTypes.has(type))) {
context.createdResources.phase3ContainerDocuments = {
marker: context.marker, containerBookingId: containerId, loadingPlanId: planId,
warehouseId, finalStatus: 'COMPLETED', documentTypes: [...DOCUMENT_TYPES],
documentIds: Object.fromEntries(documents.map((item) => [item.documentType, id(item.id)])),
}
}
}