fix: allocate pallet locations on weight confirmation
This commit is contained in:
@@ -122,10 +122,11 @@ node scripts/admin-test-env-acceptance.mjs --modules merchant-qualification-fixt
|
||||
```
|
||||
|
||||
整票库位块专项可在只跑 `orders,inbound-measurement` 时追加
|
||||
`--verify-pallet-location-guide true`。它在测量提交后核对
|
||||
`--verify-pallet-location-guide true`。它在测量仍可编辑、正式提交前核对
|
||||
`palletLocationGuide` 计数、每槽 `blockId/slotNo/reservationId/locationCode/slotStatus`、
|
||||
称重结果按 `weightResultId` 显式绑定槽位,并真实验证删除图片后原 reservation/location
|
||||
保留且转为 `RESERVED_EMPTY`。脚本只观测自然出现的共享 A0;不会为制造普通位不足批量停用库位。
|
||||
保留且转为 `RESERVED_EMPTY`,再上传替代图片并确认复用原槽后继续提交与审核。脚本只观测自然出现的共享 A0;
|
||||
不会为制造普通位不足批量停用库位。
|
||||
|
||||
只验证真实集装箱号晚补门禁时,使用正式可选参数 `LATE_CONTAINER_GATE_ONLY=1` 并仍从总入口执行 `phase3-loading`。该模式使用本轮 marker 独立造数,固定验证无号 push、订舱关联、整份预装确认、PDA claim/batch、无号 complete `A0409`、LOADING 阶段首次补号和补号后 complete 受理;随后允许后台命令停在 `CABINET_EVIDENCE/A0409`,以证明柜号门禁已经消失且下一门禁来自未上传的五类柜级证据。该模式不继续完整报关或 LOADED 链,也不得与完整链同时解释为同一终态验收。
|
||||
|
||||
|
||||
@@ -67,6 +67,9 @@ test('opt-in HTTP acceptance verifies whole-ticket slot identity and delete-to-e
|
||||
assert.match(source, /\['RESERVED_EMPTY', 'BOUND', 'TRANSFERRED', 'RELEASED'\]/)
|
||||
assert.match(source, /deleteSucceeded/)
|
||||
assert.match(source, /retainedSlot\?\.slotStatus === 'RESERVED_EMPTY'/)
|
||||
assert.match(source, /recreateConfirmedWeightEvidence/)
|
||||
assert.match(source, /reboundSlot\?\.slotStatus === 'BOUND'/)
|
||||
assert.match(source, /replacementWeightResultId/)
|
||||
assert.match(source, /不批量停用普通库位制造容量不足/)
|
||||
})
|
||||
|
||||
|
||||
@@ -237,6 +237,47 @@ function verifyPalletLocationGuide(context, response, workspace, expectedWeightR
|
||||
return { guide, slots, weights, identityComplete: Boolean(identityComplete), bindingsStable }
|
||||
}
|
||||
|
||||
async function recreateConfirmedWeightEvidence(context, {
|
||||
inboundId, tenantId, palletId, grossWeightKg,
|
||||
}) {
|
||||
const uploadResponse = await context.requestMultipart('POST',
|
||||
`/api/platform/v1/files/measurements/${tenantId}/${inboundId}`, {
|
||||
content: WEIGHT_IMAGE, type: 'image/png', name: `weight-${inboundId}-replacement.png`,
|
||||
})
|
||||
const upload = context.apiSuccess(MODULE, '重新上传替代称重图片', uploadResponse,
|
||||
`inboundId=${inboundId}`, OPTIONS)
|
||||
const fileId = asId(upload?.fileId)
|
||||
if (!fileId) return null
|
||||
|
||||
const createResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results?${query(tenantId)}`, {
|
||||
fileId, palletId, ocrProvider: 'ADMIN_ACCEPTANCE', ocrRawText: null,
|
||||
ocrWeightValue: null, ocrWeightUnit: null, confidence: null,
|
||||
})
|
||||
const created = context.apiSuccess(MODULE, '登记替代称重图片与托盘快照', createResponse,
|
||||
`fileId=${fileId}, palletId=${palletId}`, OPTIONS)
|
||||
if (!created?.id) return null
|
||||
|
||||
const confirmResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, {
|
||||
version: Number(created.version), adjustedWeightValue: Number(grossWeightKg),
|
||||
adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 删除后替代称重确认`,
|
||||
})
|
||||
const confirmed = context.apiSuccess(MODULE, '确认替代称重图片并复用空槽', confirmResponse,
|
||||
`resultId=${asId(created.id)}, palletId=${palletId}`, OPTIONS)
|
||||
const valid = confirmed?.recognitionStatus === 'CONFIRMED'
|
||||
&& Number.isFinite(Number(confirmed?.grossWeightKg))
|
||||
&& Number.isFinite(Number(confirmed?.netWeightKg))
|
||||
context.assert(MODULE, '替代图片形成新的已确认称重结果', confirmResponse, valid,
|
||||
`resultId=${asId(confirmed?.id) || 'missing'}, status=${confirmed?.recognitionStatus || 'missing'}`,
|
||||
OPTIONS)
|
||||
return valid ? {
|
||||
id: asId(confirmed.id), palletId: asId(confirmed.palletId),
|
||||
grossWeightKg: Number(confirmed.grossWeightKg), netWeightKg: Number(confirmed.netWeightKg),
|
||||
version: Number(confirmed.version),
|
||||
} : null
|
||||
}
|
||||
|
||||
function findIsolatedInbound(context) {
|
||||
const roots = [context.createdResources, context.context]
|
||||
const visited = new Set()
|
||||
@@ -641,21 +682,20 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const submitResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/submit?${query(tenantId)}`,
|
||||
{ measurementId: draft.measurementId, version: Number(draft.version) })
|
||||
const submitted = context.apiSuccess(MODULE, '提交测量审核', submitResponse,
|
||||
`measurementId=${asId(draft.measurementId)}`, OPTIONS)
|
||||
context.assert(MODULE, '提交后进入待测量审核状态', submitResponse,
|
||||
submitted?.processStatus === 'MEASURE_PENDING' && submitted?.measureReviewStatus === 'PENDING',
|
||||
`processStatus=${submitted?.processStatus ?? 'missing'}, reviewStatus=${submitted?.measureReviewStatus ?? 'missing'}`, OPTIONS)
|
||||
|
||||
if (context.options?.verifyPalletLocationGuide) {
|
||||
const guideResponse = await context.request('GET', measurementPath)
|
||||
const guideWorkspace = context.apiSuccess(MODULE, '提交后读取整票库位块与称重图片槽绑定', guideResponse,
|
||||
`inboundId=${inboundId}`, OPTIONS)
|
||||
const guideWorkspace = context.apiSuccess(MODULE, '提交前读取整票库位块与称重图片槽绑定', guideResponse,
|
||||
`inboundId=${inboundId}, processStatus=${guideResponse.body?.data?.processStatus ?? 'missing'}`, OPTIONS)
|
||||
const beforeDelete = verifyPalletLocationGuide(context, guideResponse, guideWorkspace,
|
||||
weightEvidence.confirmedResults.map((item) => item.id), '删除前')
|
||||
if (!beforeDelete.identityComplete || !beforeDelete.bindingsStable) {
|
||||
context.createdResources.inboundMeasurementLocationGuide = {
|
||||
marker: context.marker, inboundId, tenantId, blockId: asId(beforeDelete.guide?.blockId),
|
||||
preSubmitGuideAvailable: false, processStatus: guideWorkspace?.processStatus || null,
|
||||
}
|
||||
finishScenario(context, start)
|
||||
return
|
||||
}
|
||||
const overflowSlots = beforeDelete.slots.filter((slot) => slot?.sharedOverflow === true)
|
||||
context.addStep(MODULE, '正式数据 A0 共享溢出观测', guideResponse, true,
|
||||
overflowSlots.length > 0
|
||||
@@ -664,48 +704,92 @@ export async function run(context) {
|
||||
{ scenario: SCENARIO })
|
||||
|
||||
const deletedWeight = weightEvidence.confirmedResults[0]
|
||||
const previousSlot = beforeDelete.slots.find((slot) =>
|
||||
asId(slot?.boundWeightResultId) === deletedWeight.id)
|
||||
const deleteResponse = await context.request('DELETE',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${deletedWeight.id}?${query(tenantId)}&version=${deletedWeight.version}`)
|
||||
const deleteSucceeded = deleteResponse.http === 200 && String(deleteResponse.body?.code) === '200'
|
||||
context.assert(MODULE, '删除称重图片后保留原 reservation/location 并释放为空槽', deleteResponse,
|
||||
deleteSucceeded,
|
||||
context.assert(MODULE, '可编辑状态删除一张已确认称重图片', deleteResponse, deleteSucceeded,
|
||||
`HTTP=${deleteResponse.http}, code=${deleteResponse.body?.code ?? 'missing'}, message=${deleteResponse.body?.message ?? 'missing'}, requestId=${deleteResponse.requestId || 'missing'}, traceId=${deleteResponse.traceId || 'missing'}`,
|
||||
OPTIONS)
|
||||
if (deleteSucceeded) {
|
||||
const afterResponse = await context.request('GET', measurementPath)
|
||||
const afterWorkspace = context.apiSuccess(MODULE, '删除后回查整票库位块', afterResponse,
|
||||
`deletedWeightResultId=${deletedWeight.id}`, OPTIONS)
|
||||
const afterDelete = verifyPalletLocationGuide(context, afterResponse, afterWorkspace,
|
||||
weightEvidence.confirmedResults.slice(1).map((item) => item.id), '删除后')
|
||||
const previousSlot = beforeDelete.slots.find((slot) => asId(slot?.boundWeightResultId) === deletedWeight.id)
|
||||
const retainedSlot = afterDelete.slots.find((slot) =>
|
||||
asId(slot?.reservationId) === asId(previousSlot?.reservationId)
|
||||
&& asId(slot?.locationId) === asId(previousSlot?.locationId))
|
||||
context.assert(MODULE, '删除只解绑图片且原槽变为 RESERVED_EMPTY', afterResponse,
|
||||
Boolean(previousSlot) && retainedSlot?.slotStatus === 'RESERVED_EMPTY'
|
||||
&& !asId(retainedSlot?.boundWeightResultId)
|
||||
&& Number(afterDelete.guide?.reservedSlotCount) === Number(beforeDelete.guide?.reservedSlotCount),
|
||||
`reservationId=${asId(retainedSlot?.reservationId) || 'missing'}, locationId=${asId(retainedSlot?.locationId) || 'missing'}, slotStatus=${retainedSlot?.slotStatus || 'missing'}`,
|
||||
OPTIONS)
|
||||
if (!deleteSucceeded) {
|
||||
context.createdResources.inboundMeasurementLocationGuide = {
|
||||
marker: context.marker, inboundId, tenantId, blockId: asId(afterDelete.guide?.blockId),
|
||||
deletedWeightResultId: deletedWeight.id, deleteSucceeded: true,
|
||||
sharedOverflowObserved: afterDelete.slots.some((slot) => slot?.sharedOverflow === true),
|
||||
marker: context.marker, inboundId, tenantId, blockId: asId(beforeDelete.guide?.blockId),
|
||||
deletedWeightResultId: deletedWeight.id, deleteSucceeded: false,
|
||||
deleteCode: deleteResponse.body?.code || null, deleteMessage: deleteResponse.body?.message || null,
|
||||
deleteRequestId: deleteResponse.requestId || null, deleteTraceId: deleteResponse.traceId || null,
|
||||
sharedOverflowObserved: overflowSlots.length > 0,
|
||||
}
|
||||
finishScenario(context, start)
|
||||
return
|
||||
}
|
||||
|
||||
const afterDeleteResponse = await context.request('GET', measurementPath)
|
||||
const afterDeleteWorkspace = context.apiSuccess(MODULE, '删除后回查整票库位块', afterDeleteResponse,
|
||||
`deletedWeightResultId=${deletedWeight.id}`, OPTIONS)
|
||||
const afterDelete = verifyPalletLocationGuide(context, afterDeleteResponse, afterDeleteWorkspace,
|
||||
weightEvidence.confirmedResults.slice(1).map((item) => item.id), '删除后')
|
||||
const retainedSlot = afterDelete.slots.find((slot) =>
|
||||
asId(slot?.reservationId) === asId(previousSlot?.reservationId)
|
||||
&& asId(slot?.locationId) === asId(previousSlot?.locationId))
|
||||
const retainedEmpty = Boolean(previousSlot) && retainedSlot?.slotStatus === 'RESERVED_EMPTY'
|
||||
&& !asId(retainedSlot?.boundWeightResultId)
|
||||
&& Number(afterDelete.guide?.reservedSlotCount) === Number(beforeDelete.guide?.reservedSlotCount)
|
||||
context.assert(MODULE, '删除只解绑图片且原 reservation/location 变为 RESERVED_EMPTY', afterDeleteResponse,
|
||||
retainedEmpty,
|
||||
`reservationId=${asId(retainedSlot?.reservationId) || 'missing'}, locationId=${asId(retainedSlot?.locationId) || 'missing'}, slotStatus=${retainedSlot?.slotStatus || 'missing'}`,
|
||||
OPTIONS)
|
||||
if (!retainedEmpty) {
|
||||
finishScenario(context, start)
|
||||
return
|
||||
}
|
||||
|
||||
const replacementWeight = await recreateConfirmedWeightEvidence(context, {
|
||||
inboundId, tenantId, palletId: deletedWeight.palletId, grossWeightKg: deletedWeight.grossWeightKg,
|
||||
})
|
||||
if (!replacementWeight) {
|
||||
finishScenario(context, start)
|
||||
return
|
||||
}
|
||||
const reboundResponse = await context.request('GET', measurementPath)
|
||||
const reboundWorkspace = context.apiSuccess(MODULE, '替代图片确认后回查空槽复用', reboundResponse,
|
||||
`replacementWeightResultId=${replacementWeight.id}`, OPTIONS)
|
||||
const activeResults = [...weightEvidence.confirmedResults.slice(1), replacementWeight]
|
||||
const rebound = verifyPalletLocationGuide(context, reboundResponse, reboundWorkspace,
|
||||
activeResults.map((item) => item.id), '重绑后')
|
||||
const reboundSlot = rebound.slots.find((slot) =>
|
||||
asId(slot?.boundWeightResultId) === replacementWeight.id)
|
||||
const reusedSlot = reboundSlot?.slotStatus === 'BOUND'
|
||||
&& asId(reboundSlot?.reservationId) === asId(previousSlot?.reservationId)
|
||||
&& asId(reboundSlot?.locationId) === asId(previousSlot?.locationId)
|
||||
&& Number(rebound.guide?.reservedEmptySlotCount) === Number(beforeDelete.guide?.reservedEmptySlotCount)
|
||||
context.assert(MODULE, '替代图片按新 resultId 复用原空槽', reboundResponse, reusedSlot,
|
||||
`oldResultId=${deletedWeight.id}, newResultId=${replacementWeight.id}, reservationId=${asId(reboundSlot?.reservationId) || 'missing'}, locationId=${asId(reboundSlot?.locationId) || 'missing'}, slotStatus=${reboundSlot?.slotStatus || 'missing'}`,
|
||||
OPTIONS)
|
||||
if (!reusedSlot) {
|
||||
finishScenario(context, start)
|
||||
return
|
||||
}
|
||||
weightEvidence.confirmedResults = activeResults
|
||||
weightEvidence.grossWeightKg = activeResults.reduce((sum, item) => sum + item.grossWeightKg, 0)
|
||||
weightEvidence.netWeightKg = activeResults.reduce((sum, item) => sum + item.netWeightKg, 0)
|
||||
context.createdResources.inboundMeasurementLocationGuide = {
|
||||
marker: context.marker, inboundId, tenantId, blockId: asId(beforeDelete.guide?.blockId),
|
||||
deletedWeightResultId: deletedWeight.id, deleteSucceeded: false,
|
||||
deleteCode: deleteResponse.body?.code || null,
|
||||
deleteMessage: deleteResponse.body?.message || null,
|
||||
deleteRequestId: deleteResponse.requestId || null,
|
||||
deleteTraceId: deleteResponse.traceId || null,
|
||||
sharedOverflowObserved: overflowSlots.length > 0,
|
||||
marker: context.marker, inboundId, tenantId, blockId: asId(rebound.guide?.blockId),
|
||||
deletedWeightResultId: deletedWeight.id, replacementWeightResultId: replacementWeight.id,
|
||||
deleteSucceeded: true, slotReused: true, reservationId: asId(reboundSlot?.reservationId),
|
||||
locationId: asId(reboundSlot?.locationId), sharedOverflowObserved: overflowSlots.length > 0,
|
||||
}
|
||||
}
|
||||
|
||||
const submitResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/submit?${query(tenantId)}`,
|
||||
{ measurementId: draft.measurementId, version: Number(draft.version) })
|
||||
const submitted = context.apiSuccess(MODULE, '提交测量审核', submitResponse,
|
||||
`measurementId=${asId(draft.measurementId)}`, OPTIONS)
|
||||
context.assert(MODULE, '提交后进入待测量审核状态', submitResponse,
|
||||
submitted?.processStatus === 'MEASURE_PENDING' && submitted?.measureReviewStatus === 'PENDING',
|
||||
`processStatus=${submitted?.processStatus ?? 'missing'}, reviewStatus=${submitted?.measureReviewStatus ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const rejectedScanResponse = await multipartRequest(context, scanPath, {
|
||||
boxCode, ...replacement, machineNo: 'MJ-ADMIN-E2E-03', measuredAt: new Date().toISOString().slice(0, 19),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user