2026-08-08 21:14:39 +08:00
|
|
|
const MODULE = 'phase3-loading'
|
|
|
|
|
const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true }
|
2026-08-11 11:41:41 +08:00
|
|
|
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
|
|
|
|
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
2026-08-08 21:14:39 +08:00
|
|
|
|
|
|
|
|
function id(value) { return value == null ? '' : String(value) }
|
|
|
|
|
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
|
|
|
|
function failure(response) {
|
|
|
|
|
return [400, 401, 403, 404, 409, 503].includes(response?.http)
|
|
|
|
|
|| (response?.http === 200 && codeOf(response) !== '200')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function request(context, method, path, body, headers = {}) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${context.options.gateway}${path}`, {
|
|
|
|
|
method,
|
|
|
|
|
headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers },
|
|
|
|
|
body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000),
|
|
|
|
|
})
|
|
|
|
|
const raw = await response.text(); let payload = null
|
|
|
|
|
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
|
|
|
|
|
return { method, path, http: response.status, body: payload, raw: payload === null ? raw.slice(0, 500) : null,
|
|
|
|
|
requestId: payload?.requestId || response.headers.get('x-request-id'), traceId: payload?.traceId || response.headers.get('x-trace-id') }
|
|
|
|
|
} catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
async function uploadEvidence(context, tenantId, inboundId, evidenceType) {
|
|
|
|
|
const path = `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}?evidenceType=${encodeURIComponent(evidenceType)}`
|
2026-08-08 21:14:39 +08:00
|
|
|
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])
|
2026-08-08 22:42:49 +08:00
|
|
|
const fileName = `${context.marker}-${evidenceType.toLowerCase()}.png`
|
|
|
|
|
form.append('file', new Blob([png], { type: 'image/png' }), fileName)
|
2026-08-08 21:14:39 +08:00
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${context.options.gateway}${path}`, {
|
|
|
|
|
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
|
|
|
|
|
body: form, signal: AbortSignal.timeout(60_000),
|
|
|
|
|
})
|
|
|
|
|
const raw = await response.text(); let payload = null
|
|
|
|
|
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
|
|
|
|
|
return { method: 'POST', path, http: response.status, body: payload,
|
|
|
|
|
raw: payload === null ? raw.slice(0, 500) : null,
|
|
|
|
|
requestId: payload?.requestId || response.headers.get('x-request-id'),
|
|
|
|
|
traceId: payload?.traceId || response.headers.get('x-trace-id') }
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { method: 'POST', path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
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) })
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 21:14:39 +08:00
|
|
|
function fixture(context) {
|
|
|
|
|
return context.createdResources?.orchestrationFixtures?.inboundMeasurement
|
|
|
|
|
|| context.createdResources?.inboundMeasurement
|
|
|
|
|
|| context.createdResources?.orders
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function run(context) {
|
|
|
|
|
const source = fixture(context)
|
|
|
|
|
const tenantId = id(source?.tenantId)
|
|
|
|
|
const inboundId = id(source?.inboundId)
|
|
|
|
|
if (!tenantId || !inboundId) {
|
|
|
|
|
context.addStep(MODULE, '取得本轮 WAIT_SPLIT 隔离入库单', null, false,
|
|
|
|
|
'未找到 orders/inbound-measurement 本轮隔离 fixture,未使用历史入库单', OPTIONS)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const detailPath = `/api/platform/inbounds/${inboundId}?tenantId=${encodeURIComponent(tenantId)}`
|
|
|
|
|
const initialResponse = await context.request('GET', detailPath)
|
|
|
|
|
const initial = context.apiSuccess(MODULE, '读取三期隔离入库单状态与版本', initialResponse, `tenantId=${tenantId}, inboundId=${inboundId}`, OPTIONS)
|
|
|
|
|
const initialStatus = initial?.summary?.processStatus
|
|
|
|
|
const initialVersion = Number(initial?.summary?.version)
|
|
|
|
|
context.assert(MODULE, '隔离入库单处于智能分柜门禁状态', initialResponse,
|
|
|
|
|
['INBOUNDED', 'WAIT_SPLIT'].includes(initialStatus) && Number.isInteger(initialVersion),
|
|
|
|
|
`status=${initialStatus || 'missing'}, version=${initial?.summary?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (!['INBOUNDED', 'WAIT_SPLIT'].includes(initialStatus) || !Number.isInteger(initialVersion)) return
|
|
|
|
|
|
|
|
|
|
const base = `/api/platform/v1/inbounds/${inboundId}`
|
|
|
|
|
const suffix = context.marker.replace(/\D/g, '').slice(-10)
|
2026-08-09 11:21:38 +08:00
|
|
|
const warehouseId = id(initial?.summary?.warehouseId)
|
|
|
|
|
const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading'
|
|
|
|
|
const pdaBase = '/api/platform/v1/first-leg/pda-loading'
|
|
|
|
|
const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS)
|
|
|
|
|
const candidate = Array.isArray(candidates) ? candidates.find((item) => id(item.inboundId) === inboundId
|
|
|
|
|
&& Number(item.availablePackageCount) > 0) : null
|
|
|
|
|
context.assert(MODULE, '柜次装柜候选与入库单、客户和仓库一致', candidateResponse,
|
|
|
|
|
Boolean(candidate?.inboundDetailId) && id(candidate?.accountId) === tenantId
|
|
|
|
|
&& id(candidate?.warehouseId) === warehouseId,
|
|
|
|
|
`candidate=${Boolean(candidate)}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
|
|
|
|
|
if (!candidate?.inboundDetailId || !warehouseId) return
|
|
|
|
|
|
|
|
|
|
const containerNo = `P3B-${suffix}`
|
|
|
|
|
const containerResponse = await request(context, 'POST', '/api/platform/v1/containers', {
|
|
|
|
|
containerNo, cabinetNo: `P3BI-${suffix}`, warehouseId,
|
|
|
|
|
warehouseName: initial?.summary?.warehouseName || null, billOfLadingNo: `P3BL-${suffix}`,
|
|
|
|
|
vesselName: `P3-BRIDGE-${suffix}`, voyageNo: `P3V-${suffix}`, containerType: '40HQ',
|
|
|
|
|
shippingCompanyName: `P3-SHIPPING-${suffix}`, departurePortCode: 'CNSZX', departurePortName: '深圳',
|
|
|
|
|
destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
|
|
|
|
|
destinationWarehouseCode: candidate.destinationWarehouseCode || null,
|
|
|
|
|
destinationWarehouseName: candidate.destinationWarehouseName || null,
|
|
|
|
|
capacityCbm: 100, maxWeightKg: 100000, loadingAt: '2026-09-02T08:00:00',
|
|
|
|
|
estimatedSailingAt: '2026-09-03T08:00:00', estimatedPortArrivalAt: '2026-09-20T08:00:00',
|
|
|
|
|
estimatedWarehouseArrivalAt: '2026-09-21T08:00:00',
|
|
|
|
|
})
|
|
|
|
|
const container = context.apiSuccess(MODULE, '创建与入库预装串联的柜次', containerResponse, '', OPTIONS)
|
|
|
|
|
const containerBookingId = id(container?.id)
|
|
|
|
|
if (!containerBookingId) return
|
|
|
|
|
|
|
|
|
|
const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, {
|
|
|
|
|
bookingId: containerBookingId, remark: context.marker,
|
|
|
|
|
})
|
|
|
|
|
const loadingPlan = context.apiSuccess(MODULE, '创建柜次级智能装柜计划', loadingPlanResponse, '', OPTIONS)
|
|
|
|
|
const loadingPlanId = id(loadingPlan?.id)
|
|
|
|
|
if (!loadingPlanId) return
|
|
|
|
|
const assignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
|
|
|
|
|
inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
|
|
|
|
|
expectedVersion: Number(loadingPlan.version), operationId: `${context.marker}-bridge-assign`.slice(0, 128),
|
|
|
|
|
})
|
|
|
|
|
const assignedLoadingPlan = context.apiSuccess(MODULE, '将本轮入库货物分配到柜次计划', assignmentResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '柜次计划明细和版本真实持久化', assignmentResponse,
|
|
|
|
|
assignedLoadingPlan?.items?.some?.((item) => id(item.inboundId) === inboundId && Number(item.packageCount) === 1)
|
|
|
|
|
&& Number(assignedLoadingPlan?.version) === 1,
|
|
|
|
|
`loadingPlanId=${loadingPlanId}, version=${assignedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
|
2026-08-08 21:14:39 +08:00
|
|
|
const smart = await request(context, 'POST', `${base}/split/smart?tenantId=${encodeURIComponent(tenantId)}`, {
|
|
|
|
|
expectedVersion: 0,
|
|
|
|
|
items: [{ containerCode: `P3-${suffix}`, cargoReference: `REF-${suffix}`, packageCount: 1, volumeCbm: 0.01, remark: context.marker }],
|
|
|
|
|
remark: context.marker,
|
|
|
|
|
}, { 'Idempotency-Key': `${context.marker}-p3-smart`.slice(0, 128) })
|
|
|
|
|
const smartData = context.apiSuccess(MODULE, '智能分柜并生成预装清单', smart, `inboundId=${inboundId}`, OPTIONS)
|
|
|
|
|
const planId = id(smartData?.preloadPlan?.id)
|
|
|
|
|
context.assert(MODULE, '智能分柜预装计划 PENDING、版本和明细正确', smart,
|
|
|
|
|
smartData?.mainStatus === 'INBOUNDED' && Boolean(planId) && smartData?.preloadPlan?.status === 'PENDING'
|
|
|
|
|
&& Number(smartData?.preloadPlan?.version) === 0 && smartData?.preloadPlan?.items?.length === 1,
|
|
|
|
|
`mainStatus=${smartData?.mainStatus || 'missing'}, planId=${planId || 'missing'}, status=${smartData?.preloadPlan?.status || 'missing'}`, OPTIONS)
|
|
|
|
|
if (!planId) return
|
|
|
|
|
|
|
|
|
|
const replay = await request(context, 'POST', `${base}/split/smart?tenantId=${encodeURIComponent(tenantId)}`, {
|
|
|
|
|
expectedVersion: 0, items: [], remark: 'replay must not replace',
|
|
|
|
|
}, { 'Idempotency-Key': `${context.marker}-p3-smart`.slice(0, 128) })
|
|
|
|
|
context.assert(MODULE, '智能分柜幂等重放返回同一预装计划', replay,
|
|
|
|
|
codeOf(replay) === '200' && id(replay.body?.data?.preloadPlan?.id) === planId
|
|
|
|
|
&& replay.body?.data?.preloadPlan?.items?.length === 1,
|
|
|
|
|
`HTTP=${replay.http}, code=${codeOf(replay)}, planId=${id(replay.body?.data?.preloadPlan?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
|
2026-08-09 11:21:38 +08:00
|
|
|
const bindResponse = await request(context, 'POST',
|
|
|
|
|
`${base}/preload/loading-plans/${loadingPlanId}?tenantId=${encodeURIComponent(tenantId)}`, {
|
|
|
|
|
expectedPreloadVersion: Number(smartData.preloadPlan.version),
|
|
|
|
|
expectedLoadingPlanVersion: Number(assignedLoadingPlan.version),
|
|
|
|
|
}, { 'Idempotency-Key': `${context.marker}-bridge-bind`.slice(0, 128) })
|
|
|
|
|
const link = context.apiSuccess(MODULE, '原子关联入库预装与柜次装柜计划', bindResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '桥接双版本递增且关联归属唯一', bindResponse,
|
|
|
|
|
id(link?.inboundId) === inboundId && id(link?.loadingPlanId) === loadingPlanId
|
|
|
|
|
&& id(link?.preloadPlanId) === planId && Number(link?.allocatedPackageCount) === 1
|
|
|
|
|
&& Number(link?.preloadVersion) === 1 && Number(link?.loadingPlanVersion) === 2,
|
|
|
|
|
`linkId=${id(link?.id) || 'missing'}, preloadVersion=${link?.preloadVersion ?? 'missing'}, loadingVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (!link?.id) return
|
|
|
|
|
|
|
|
|
|
const pushResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
|
|
|
|
|
{ expectedVersion: Number(link.loadingPlanVersion) },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) })
|
|
|
|
|
const pushedLoadingPlan = context.apiSuccess(MODULE, '桥接后推送柜次计划并生成真实 PDA 任务', pushResponse, '', OPTIONS)
|
2026-08-11 15:11:50 +08:00
|
|
|
context.assert(MODULE, '推送后计划保持 EDITING 并生成真实 PDA 任务', pushResponse,
|
|
|
|
|
pushedLoadingPlan?.status === 'EDITING', `status=${pushedLoadingPlan?.status || 'missing'}`, OPTIONS)
|
|
|
|
|
if (pushedLoadingPlan?.status !== 'EDITING') return
|
2026-08-08 21:14:39 +08:00
|
|
|
|
|
|
|
|
const preload = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`)
|
2026-08-11 15:11:50 +08:00
|
|
|
const preloadData = context.apiSuccess(MODULE, '回查预装清单', preload, '', OPTIONS)
|
2026-08-08 21:14:39 +08:00
|
|
|
context.assert(MODULE, '预装清单归属、明细和 marker 持久化', preload,
|
|
|
|
|
id(preloadData?.id) === planId && id(preloadData?.inboundId) === inboundId && id(preloadData?.tenantId) === tenantId
|
|
|
|
|
&& preloadData?.items?.[0]?.containerCode === `P3-${suffix}` && preloadData?.remark === context.marker,
|
|
|
|
|
`planId=${id(preloadData?.id) || 'missing'}, items=${preloadData?.items?.length ?? 'n/a'}`, OPTIONS)
|
|
|
|
|
|
2026-08-09 11:21:38 +08:00
|
|
|
const canonicalTaskResponse = await context.request('GET',
|
|
|
|
|
`${pdaBase}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
const canonicalTask = context.apiSuccess(MODULE, '读取柜次级 PDA 唯一事实任务', canonicalTaskResponse, '', OPTIONS)
|
|
|
|
|
const canonicalTaskId = id(canonicalTask?.id)
|
|
|
|
|
const canonicalItem = canonicalTask?.items?.find?.((item) => id(item.inboundId) === inboundId)
|
|
|
|
|
context.assert(MODULE, '柜次 PDA 任务与桥接入库明细一致', canonicalTaskResponse,
|
|
|
|
|
Boolean(canonicalTaskId) && id(canonicalTask?.loadingPlanId) === loadingPlanId
|
|
|
|
|
&& canonicalTask?.workflowStatus === 'UNASSIGNED' && Boolean(canonicalItem?.id),
|
|
|
|
|
`taskId=${canonicalTaskId || 'missing'}, itemId=${id(canonicalItem?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
if (!canonicalTaskId || !canonicalItem?.id) return
|
|
|
|
|
|
|
|
|
|
const claim = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/claim`, {
|
|
|
|
|
expectedAssignmentVersion: 0, operationId: `${context.marker}-bridge-claim`.slice(0, 128),
|
|
|
|
|
})
|
|
|
|
|
const claimed = context.apiSuccess(MODULE, '领取柜次 PDA 任务', claim, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '柜次 PDA 任务绑定当前作业员', claim,
|
|
|
|
|
claimed?.workflowStatus === 'IN_PROGRESS' && Boolean(claimed?.assignment?.assigneeId),
|
|
|
|
|
`workflow=${claimed?.workflowStatus || 'missing'}`, OPTIONS)
|
|
|
|
|
|
2026-08-11 15:11:50 +08:00
|
|
|
const claimedPlanResponse = await request(context, 'GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
|
|
|
|
const claimedPlan = context.apiSuccess(MODULE, '领取后回查柜次计划进入 LOADING', claimedPlanResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '领取后计划进入 LOADING', claimedPlanResponse,
|
|
|
|
|
claimedPlan?.status === 'LOADING', `status=${claimedPlan?.status || 'missing'}`, OPTIONS)
|
|
|
|
|
if (claimedPlan?.status !== 'LOADING') return
|
|
|
|
|
|
|
|
|
|
const confirming = await request(context, 'POST', `${base}/preload/confirming?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
{ expectedVersion: Number(link.preloadVersion) }, { 'Idempotency-Key': `${context.marker}-p3-confirming`.slice(0, 128) })
|
|
|
|
|
const confirmingData = context.apiSuccess(MODULE, '提交预装清单确认', confirming, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '预装清单进入 CONFIRMING 且版本递增', confirming,
|
|
|
|
|
confirmingData?.preloadPlan?.status === 'CONFIRMING' && Number(confirmingData?.preloadPlan?.version) === 2,
|
|
|
|
|
`status=${confirmingData?.preloadPlan?.status || 'missing'}, version=${confirmingData?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (confirmingData?.preloadPlan?.status !== 'CONFIRMING') return
|
|
|
|
|
|
|
|
|
|
const confirm = await request(context, 'POST', `${base}/preload/confirm?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
{ expectedVersion: Number(confirmingData.preloadPlan.version) }, { 'Idempotency-Key': `${context.marker}-p3-confirm`.slice(0, 128) })
|
|
|
|
|
const confirmed = context.apiSuccess(MODULE, '确认预装并进入装柜中', confirm, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '预装确认推进主状态 LOADING 和计划 CONFIRMED', confirm,
|
|
|
|
|
confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMED'
|
|
|
|
|
&& Number(confirmed?.preloadPlan?.version) === 3,
|
|
|
|
|
`mainStatus=${confirmed?.mainStatus || 'missing'}, planStatus=${confirmed?.preloadPlan?.status || 'missing'}, version=${confirmed?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (confirmed?.mainStatus !== 'LOADING') return
|
|
|
|
|
|
|
|
|
|
const loadingResponse = await context.request('GET', detailPath)
|
|
|
|
|
const loading = context.apiSuccess(MODULE, '读取装柜中入库单版本', loadingResponse, '', OPTIONS)
|
|
|
|
|
const loadingVersion = Number(loading?.summary?.version)
|
|
|
|
|
context.assert(MODULE, '入库单已进入 LOADING 且版本有效', loadingResponse,
|
|
|
|
|
loading?.summary?.processStatus === 'LOADING' && Number.isInteger(loadingVersion),
|
|
|
|
|
`status=${loading?.summary?.processStatus || 'missing'}, version=${loading?.summary?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (!Number.isInteger(loadingVersion)) return
|
|
|
|
|
|
|
|
|
|
const blockedBeforePicking = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
{ expectedVersion: loadingVersion, remark: 'must require picking' },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-picking`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, '未完成 PDA 拣货时拒绝完成装柜', blockedBeforePicking, failure(blockedBeforePicking),
|
|
|
|
|
`HTTP=${blockedBeforePicking.http}, code=${codeOf(blockedBeforePicking)}`, OPTIONS)
|
|
|
|
|
|
2026-08-11 11:41:41 +08:00
|
|
|
const batchPath = `${pdaBase}/tasks/${canonicalTaskId}/batch`
|
|
|
|
|
const batchKey = `${context.marker}-bridge-batch`.slice(0, 128)
|
|
|
|
|
const batchPayload = {
|
|
|
|
|
warehouseId, containerNo,
|
|
|
|
|
expectedVersion: Number(claimed?.version),
|
|
|
|
|
items: [{
|
|
|
|
|
itemId: id(canonicalItem.id), action: 'PICK', pickedPackageCount: 1,
|
|
|
|
|
releaseMode: 'MANUAL', locationIds: [], version: Number(canonicalItem.version),
|
|
|
|
|
}],
|
|
|
|
|
}
|
2026-08-10 12:23:43 +08:00
|
|
|
|
2026-08-11 11:41:41 +08:00
|
|
|
const staleBatch = await request(context, 'POST', batchPath,
|
|
|
|
|
{ ...batchPayload, expectedVersion: Number(batchPayload.expectedVersion) + 1 },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-bridge-batch-stale`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, 'PDA 原子批次旧任务版本返回冲突', staleBatch,
|
|
|
|
|
staleBatch.http === 200 && codeOf(staleBatch) === 'A0409',
|
|
|
|
|
`HTTP=${staleBatch.http}, code=${codeOf(staleBatch)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const invalidBatch = await request(context, 'POST', batchPath, {
|
|
|
|
|
...batchPayload,
|
|
|
|
|
items: [...batchPayload.items, {
|
|
|
|
|
itemId: '999999999999999999', action: 'SKIP', version: 0, reason: 'forged-item-must-rollback',
|
|
|
|
|
}],
|
|
|
|
|
}, { 'Idempotency-Key': `${context.marker}-bridge-batch-rollback`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, 'PDA 原子批次任一非法明细时整批拒绝', invalidBatch,
|
|
|
|
|
invalidBatch.http === 200 && codeOf(invalidBatch) === 'A0409',
|
|
|
|
|
`HTTP=${invalidBatch.http}, code=${codeOf(invalidBatch)}`, OPTIONS)
|
|
|
|
|
const unchangedAfterInvalidResponse = await context.request('GET',
|
|
|
|
|
`${pdaBase}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
|
|
|
|
const unchangedAfterInvalid = context.apiSuccess(MODULE, '回查失败批次后的 PDA 任务快照', unchangedAfterInvalidResponse, '', OPTIONS)
|
|
|
|
|
const unchangedItem = unchangedAfterInvalid?.items?.find?.((item) => id(item.id) === id(canonicalItem.id))
|
|
|
|
|
context.assert(MODULE, '失败批次整批回滚且无明细部分写入', unchangedAfterInvalidResponse,
|
|
|
|
|
unchangedAfterInvalid?.taskStatus === 'LOADING'
|
|
|
|
|
&& Number(unchangedAfterInvalid?.version) === Number(batchPayload.expectedVersion)
|
|
|
|
|
&& unchangedItem?.itemStatus === 'PENDING' && Number(unchangedItem?.pickedPackageCount) === 0
|
|
|
|
|
&& Number(unchangedItem?.version) === Number(canonicalItem.version),
|
|
|
|
|
`taskVersion=${unchangedAfterInvalid?.version ?? 'missing'}, itemStatus=${unchangedItem?.itemStatus || 'missing'}, picked=${unchangedItem?.pickedPackageCount ?? 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const merchantLogin = await context.request('POST', '/api/auth/login', {
|
|
|
|
|
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
|
|
|
|
|
}, '')
|
|
|
|
|
const merchantToken = merchantLogin.http === 200 && codeOf(merchantLogin) === '200'
|
|
|
|
|
? merchantLogin.body?.data?.accessToken : null
|
|
|
|
|
context.addStep(MODULE, '取得客户侧隔离令牌用于 PDA 批次越权验证', merchantLogin,
|
|
|
|
|
Boolean(merchantToken), `username=${MERCHANT_USERNAME}`, OPTIONS)
|
|
|
|
|
const customerIsolatedBatch = merchantToken
|
|
|
|
|
? await request(context, 'POST', batchPath, batchPayload, {
|
|
|
|
|
Authorization: `Bearer ${merchantToken}`,
|
|
|
|
|
'Idempotency-Key': `${context.marker}-bridge-batch-customer`.slice(0, 128),
|
2026-08-09 11:21:38 +08:00
|
|
|
})
|
2026-08-11 11:41:41 +08:00
|
|
|
: merchantLogin
|
|
|
|
|
context.addStep(MODULE, '客户身份无权提交平台 PDA 原子批次', customerIsolatedBatch,
|
|
|
|
|
customerIsolatedBatch.http === 403 && codeOf(customerIsolatedBatch) === 'A0403'
|
|
|
|
|
&& Boolean(customerIsolatedBatch.requestId) && Boolean(customerIsolatedBatch.traceId),
|
|
|
|
|
`HTTP=${customerIsolatedBatch.http}, code=${codeOf(customerIsolatedBatch)}, requestId=${customerIsolatedBatch.requestId || 'missing'}, traceId=${customerIsolatedBatch.traceId || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const currentAssigneeId = Number(claimed?.assignment?.assigneeId)
|
|
|
|
|
const reassigned = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/assign`, {
|
|
|
|
|
assigneeId: currentAssigneeId + 1000000, assigneeName: `${context.marker}-other-picker`,
|
|
|
|
|
expectedAssignmentVersion: Number(claimed?.assignment?.version),
|
|
|
|
|
operationId: `${context.marker}-bridge-assign-other`.slice(0, 128),
|
|
|
|
|
})
|
|
|
|
|
const otherAssignment = context.apiSuccess(MODULE, '将 PDA 任务改派给另一作业员', reassigned, '', OPTIONS)
|
|
|
|
|
const operatorIsolatedBatch = await request(context, 'POST', batchPath, batchPayload,
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-bridge-batch-other-operator`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, '非任务作业员提交 PDA 原子批次返回冲突', operatorIsolatedBatch,
|
|
|
|
|
operatorIsolatedBatch.http === 200 && codeOf(operatorIsolatedBatch) === 'A0409',
|
|
|
|
|
`HTTP=${operatorIsolatedBatch.http}, code=${codeOf(operatorIsolatedBatch)}`, OPTIONS)
|
|
|
|
|
const restoredAssignmentResponse = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/assign`, {
|
|
|
|
|
assigneeId: currentAssigneeId, assigneeName: claimed?.assignment?.assigneeName || 'admin',
|
|
|
|
|
expectedAssignmentVersion: Number(otherAssignment?.assignment?.version),
|
|
|
|
|
operationId: `${context.marker}-bridge-assign-restore`.slice(0, 128),
|
|
|
|
|
})
|
|
|
|
|
const restoredAssignment = context.apiSuccess(MODULE, '恢复 PDA 任务到当前作业员', restoredAssignmentResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '作业员隔离验证后任务归属恢复', restoredAssignmentResponse,
|
|
|
|
|
id(restoredAssignment?.assignment?.assigneeId) === id(currentAssigneeId),
|
|
|
|
|
`assigneeId=${id(restoredAssignment?.assignment?.assigneeId) || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const canonicalPick = await request(context, 'POST', batchPath, batchPayload,
|
|
|
|
|
{ 'Idempotency-Key': batchKey })
|
|
|
|
|
const pickedCanonical = context.apiSuccess(MODULE, '原子提交柜次 PDA 整批拣货', canonicalPick, '', OPTIONS)
|
2026-08-11 10:10:18 +08:00
|
|
|
const directPdaCompletion = pickedCanonical?.taskStatus === 'COMPLETED'
|
|
|
|
|
&& pickedCanonical?.workflowStatus === 'COMPLETED'
|
2026-08-11 11:41:41 +08:00
|
|
|
context.assert(MODULE, 'PDA 原子批次成功并推进任务终态', canonicalPick,
|
|
|
|
|
directPdaCompletion && pickedCanonical?.items?.every?.((item) => item.itemStatus !== 'PENDING'),
|
2026-08-11 10:10:18 +08:00
|
|
|
`taskStatus=${pickedCanonical?.taskStatus || 'missing'}, workflow=${pickedCanonical?.workflowStatus || 'missing'}`, OPTIONS)
|
2026-08-09 11:21:38 +08:00
|
|
|
|
2026-08-11 11:41:41 +08:00
|
|
|
const canonicalBatchReplay = await request(context, 'POST', batchPath, batchPayload,
|
|
|
|
|
{ 'Idempotency-Key': batchKey })
|
2026-08-11 11:44:34 +08:00
|
|
|
const atomicBatchReplayVerified = canonicalBatchReplay.http === 200 && codeOf(canonicalBatchReplay) === '200'
|
2026-08-11 11:41:41 +08:00
|
|
|
&& id(canonicalBatchReplay.body?.data?.id) === canonicalTaskId
|
|
|
|
|
&& canonicalBatchReplay.body?.data?.taskStatus === 'COMPLETED'
|
2026-08-11 11:44:34 +08:00
|
|
|
&& Number(canonicalBatchReplay.body?.data?.version) === Number(pickedCanonical?.version)
|
|
|
|
|
context.assert(MODULE, 'PDA 原子批次同参同键重放返回同一终态', canonicalBatchReplay,
|
|
|
|
|
atomicBatchReplayVerified,
|
2026-08-11 11:41:41 +08:00
|
|
|
`HTTP=${canonicalBatchReplay.http}, code=${codeOf(canonicalBatchReplay)}, taskVersion=${canonicalBatchReplay.body?.data?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
|
2026-08-11 10:10:18 +08:00
|
|
|
// The deployed settlement contract completes a one-item task during pick. A
|
|
|
|
|
// review call is therefore a negative compatibility assertion, never a second
|
|
|
|
|
// positive action that can poison the marker fixture with A0409.
|
2026-08-09 11:21:38 +08:00
|
|
|
const canonicalReview = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/review`, {
|
|
|
|
|
expectedTaskVersion: Number(pickedCanonical?.version),
|
|
|
|
|
operationId: `${context.marker}-bridge-review`.slice(0, 128), remark: context.marker,
|
|
|
|
|
})
|
2026-08-11 10:10:18 +08:00
|
|
|
context.addStep(MODULE, '柜次 PDA 已完成后拒绝重复复核', canonicalReview,
|
2026-08-11 11:11:57 +08:00
|
|
|
directPdaCompletion
|
|
|
|
|
? canonicalReview.http === 200 && codeOf(canonicalReview) === 'A0409'
|
|
|
|
|
: canonicalReview.http === 200 && codeOf(canonicalReview) === '200',
|
2026-08-11 10:10:18 +08:00
|
|
|
`HTTP=${canonicalReview.http}, code=${codeOf(canonicalReview)}`, OPTIONS)
|
2026-08-09 11:21:38 +08:00
|
|
|
|
|
|
|
|
const forgedPickingStart = await request(context, 'POST', `${base}/loading/picking/start?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
{ expectedVersion: 0, expectedCount: 1, pickedCount: 0, remark: 'forged-client-count' },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-picking-forged-start`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, '入库级 PDA 汇总拒绝客户端伪造数量', forgedPickingStart, failure(forgedPickingStart),
|
|
|
|
|
`HTTP=${forgedPickingStart.http}, code=${codeOf(forgedPickingStart)}`, OPTIONS)
|
|
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
const pickingStart = await request(context, 'POST', `${base}/loading/picking/start?tenantId=${encodeURIComponent(tenantId)}`,
|
2026-08-09 11:21:38 +08:00
|
|
|
{ expectedVersion: 0, expectedCount: 1, pickedCount: 1, remark: context.marker },
|
2026-08-08 22:42:49 +08:00
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-picking-start`.slice(0, 128) })
|
2026-08-11 10:10:18 +08:00
|
|
|
const pickingStarted = directPdaCompletion
|
|
|
|
|
? null
|
|
|
|
|
: context.apiSuccess(MODULE, '开始本轮 PDA 拣货任务', pickingStart, '', OPTIONS)
|
|
|
|
|
context.addStep(MODULE, directPdaCompletion
|
|
|
|
|
? '柜次 PDA 已完成后拒绝旧入库级启动入口' : '开始本轮 PDA 拣货任务', pickingStart,
|
2026-08-11 11:11:57 +08:00
|
|
|
directPdaCompletion
|
|
|
|
|
? pickingStart.http === 200 && codeOf(pickingStart) === 'A0409'
|
|
|
|
|
: Boolean(pickingStarted),
|
2026-08-11 10:10:18 +08:00
|
|
|
`HTTP=${pickingStart.http}, code=${codeOf(pickingStart)}, status=${pickingStarted?.status || 'n/a'}`, OPTIONS)
|
|
|
|
|
if (!directPdaCompletion) {
|
|
|
|
|
context.assert(MODULE, 'PDA 拣货任务处于 IN_PROGRESS 且计划数量正确', pickingStart,
|
|
|
|
|
pickingStarted?.status === 'IN_PROGRESS' && Number(pickingStarted?.version) === 0
|
|
|
|
|
&& Number(pickingStarted?.expectedCount) === 1 && Number(pickingStarted?.pickedCount) === 1,
|
|
|
|
|
`status=${pickingStarted?.status || 'missing'}, version=${pickingStarted?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
}
|
2026-08-08 21:14:39 +08:00
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
const pickingMismatch = await request(context, 'POST', `${base}/loading/picking/complete?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
{ expectedVersion: 0, expectedCount: 1, pickedCount: 0, remark: context.marker },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-picking-mismatch`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, 'PDA 拣货数量不一致时拒绝完成', pickingMismatch, failure(pickingMismatch),
|
|
|
|
|
`HTTP=${pickingMismatch.http}, code=${codeOf(pickingMismatch)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const pickingComplete = await request(context, 'POST', `${base}/loading/picking/complete?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
{ expectedVersion: 0, expectedCount: 1, pickedCount: 1, remark: context.marker },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-picking-complete`.slice(0, 128) })
|
2026-08-11 10:10:18 +08:00
|
|
|
const pickingCompleted = directPdaCompletion
|
|
|
|
|
? null
|
|
|
|
|
: context.apiSuccess(MODULE, '完成本轮 PDA 拣货复核', pickingComplete, '', OPTIONS)
|
|
|
|
|
context.addStep(MODULE, directPdaCompletion
|
|
|
|
|
? '柜次 PDA 已完成后拒绝旧入库级完成入口' : '完成本轮 PDA 拣货复核', pickingComplete,
|
2026-08-11 11:11:57 +08:00
|
|
|
directPdaCompletion
|
|
|
|
|
? pickingComplete.http === 200 && codeOf(pickingComplete) === 'A0409'
|
|
|
|
|
: Boolean(pickingCompleted),
|
2026-08-11 10:10:18 +08:00
|
|
|
`HTTP=${pickingComplete.http}, code=${codeOf(pickingComplete)}, status=${pickingCompleted?.status || 'n/a'}`, OPTIONS)
|
|
|
|
|
if (!directPdaCompletion) {
|
|
|
|
|
context.assert(MODULE, 'PDA 拣货任务 COMPLETED 且数量一致', pickingComplete,
|
|
|
|
|
pickingCompleted?.status === 'COMPLETED' && Number(pickingCompleted?.version) === 1
|
|
|
|
|
&& Number(pickingCompleted?.expectedCount) === 1 && Number(pickingCompleted?.pickedCount) === 1,
|
|
|
|
|
`status=${pickingCompleted?.status || 'missing'}, version=${pickingCompleted?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
}
|
2026-08-08 22:42:49 +08:00
|
|
|
|
|
|
|
|
const evidenceTypes = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']
|
|
|
|
|
const evidenceItems = []
|
|
|
|
|
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
|
|
|
|
|
&& 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)
|
|
|
|
|
if (!evidenceItem?.id) return
|
|
|
|
|
evidenceItems.push(evidenceItem)
|
|
|
|
|
|
|
|
|
|
const download = await context.requestBinary('GET', `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}/${fileId}`)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
if (evidenceType === 'PHOTO_STEP_4') {
|
|
|
|
|
const missingSeal = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
{ expectedVersion: loadingVersion, remark: 'must require seal' },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-seal`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, '缺铅封照片和锁码时拒绝完成装柜', missingSeal, failure(missingSeal),
|
|
|
|
|
`HTTP=${missingSeal.http}, code=${codeOf(missingSeal)}`, OPTIONS)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const firstPhoto = evidenceItems.find((item) => item.evidenceType === 'PHOTO_STEP_1')
|
|
|
|
|
const sealEvidence = evidenceItems.find((item) => item.evidenceType === 'SEAL')
|
2026-08-10 11:28:57 +08:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
Boolean(appendedFileId) && appendedItems.length >= 2
|
|
|
|
|
&& appendedItems.some((item) => id(item.fileId) === id(firstPhoto?.fileId))
|
|
|
|
|
&& appendedItems.some((item) => id(item.fileId) === appendedFileId),
|
|
|
|
|
`photoStep1Count=${appendedItems.length}, original=${id(firstPhoto?.fileId) || 'missing'}, appended=${appendedFileId || 'missing'}`, OPTIONS)
|
|
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
const wrongType = await bindEvidence(context, base, tenantId, 'PHOTO_STEP_2', id(firstPhoto?.fileId), suffix, 'wrong-type')
|
|
|
|
|
context.addStep(MODULE, '不同证据类型的真实 fileId 不可交叉绑定', wrongType, failure(wrongType),
|
|
|
|
|
`HTTP=${wrongType.http}, code=${codeOf(wrongType)}`, OPTIONS)
|
2026-08-08 21:14:39 +08:00
|
|
|
|
|
|
|
|
const forged = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
|
2026-08-08 22:42:49 +08:00
|
|
|
expectedVersion: Number(sealEvidence?.version), evidenceType: 'SEAL', fileId: '999999999999999999',
|
2026-08-08 21:14:39 +08:00
|
|
|
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),
|
|
|
|
|
`HTTP=${forged.http}, code=${codeOf(forged)}`, OPTIONS)
|
|
|
|
|
|
2026-08-10 11:28:57 +08:00
|
|
|
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) })
|
|
|
|
|
context.addStep(MODULE, '带锁码照片缺少锁码文本时拒绝绑定', missingSealText, failure(missingSealText),
|
|
|
|
|
`HTTP=${missingSealText.http}, code=${codeOf(missingSealText)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const voidTarget = appendedItems.find((item) => id(item.fileId) === appendedFileId)
|
|
|
|
|
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) })
|
|
|
|
|
: 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`),
|
|
|
|
|
`evidenceId=${id(voidTarget?.id) || 'missing'}, status=${voidTarget?.status || 'missing'}`, OPTIONS)
|
|
|
|
|
|
2026-08-08 21:14:39 +08:00
|
|
|
const evidenceList = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
|
2026-08-08 22:42:49 +08:00
|
|
|
const evidenceListData = context.apiSuccess(MODULE, '回查五类装柜证据列表', evidenceList, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '五类装柜证据完整且铅封号持久化', evidenceList,
|
2026-08-10 11:28:57 +08:00
|
|
|
evidenceListData?.evidences?.length >= evidenceTypes.length
|
2026-08-08 22:42:49 +08:00
|
|
|
&& evidenceTypes.every((type) => evidenceListData.evidences.some((item) => item.evidenceType === type))
|
|
|
|
|
&& evidenceListData.evidences.some((item) => item.evidenceType === 'SEAL' && item.sealNo === `SEAL-${suffix}`),
|
2026-08-08 21:14:39 +08:00
|
|
|
`evidences=${evidenceListData?.evidences?.length ?? 'n/a'}`, OPTIONS)
|
|
|
|
|
|
2026-08-10 12:23:43 +08:00
|
|
|
const blockedBeforeDownstream = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
|
|
|
|
|
{ expectedVersion: loadingVersion, remark: `${context.marker}-before-downstream` },
|
|
|
|
|
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-downstream`.slice(0, 128) })
|
|
|
|
|
context.addStep(MODULE, '订舱拖车报关未齐备时 LOADED 硬门禁拒绝', blockedBeforeDownstream,
|
|
|
|
|
failure(blockedBeforeDownstream),
|
|
|
|
|
`HTTP=${blockedBeforeDownstream.http}, code=${codeOf(blockedBeforeDownstream)}`, OPTIONS)
|
2026-08-08 21:14:39 +08:00
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
context.createdResources.phase3Loading = {
|
|
|
|
|
tenantId, inboundId, preloadPlanId: planId, evidenceIds: evidenceItems.map((item) => id(item.id)),
|
2026-08-10 12:23:43 +08:00
|
|
|
voidedEvidenceId: id(voidTarget?.id), evidenceTypes,
|
2026-08-09 11:21:38 +08:00
|
|
|
loadingPlanId, containerBookingId, containerNo, warehouseId, canonicalPdaTaskId: canonicalTaskId,
|
2026-08-11 10:10:18 +08:00
|
|
|
pickingTaskId: canonicalTaskId, inboundVersion: loadingVersion,
|
2026-08-11 11:44:34 +08:00
|
|
|
atomicBatchVerified: directPdaCompletion && atomicBatchReplayVerified,
|
2026-08-10 12:23:43 +08:00
|
|
|
marker: context.marker, inboundStatus: 'LOADING', loadingPlanStatus: 'COMPLETED', finalStatus: 'COMPLETED',
|
2026-08-08 22:42:49 +08:00
|
|
|
}
|
2026-08-08 21:14:39 +08:00
|
|
|
}
|