test: prove dispatch split across FBA destinations

This commit is contained in:
2026-09-15 12:54:09 +08:00
parent a7a6d78568
commit 9914d40593
2 changed files with 45 additions and 20 deletions
@@ -95,7 +95,7 @@ async function runRelabelDispatch(context, relabel) {
export async function run(context) {
context.createdResources.phase4OverseasDispatch = {
marker: context.marker, dispatchNo: null, dispatchId: null, draftToken: null,
planPalletId: null, finalStatus: null,
planPalletId: null, planPalletIds: [], destinationWarehouseCodes: [], finalStatus: null,
cleanup: 'PENDING_DEPARTURE dispatch is retained as marker-owned acceptance evidence; outbound, PDA and delivery APIs are excluded',
}
@@ -122,16 +122,26 @@ export async function run(context) {
const candidateResult = await api(context, 'GET', '查询本轮已收货托盘智能预排候选',
query(`${BASE}/candidates`, { sourceStage: 'RECEIVED' }))
const candidates = Array.isArray(candidateResult.data) ? candidateResult.data : []
const candidate = candidates.find((value) => fixture.palletIds.has(id(value?.warehousePalletId))
const markerCandidates = candidates.filter((value) => fixture.palletIds.has(id(value?.warehousePalletId))
&& id(value?.overseasWarehouseId) === id(fixture.overseasWarehouseId)
&& id(value?.planId) && id(value?.planPalletId))
context.assert(MODULE, '候选严格命中本轮PDA库存托盘与实际海外仓', candidateResult.response,
Boolean(candidate) && candidate.sourceStage === 'RECEIVED' && Number(candidate.packageCount) > 0,
`candidate=${Boolean(candidate)}, pdaPallets=${[...fixture.palletIds].join(',')}, warehouseId=${id(fixture.overseasWarehouseId)}; ${trace(candidateResult.response)}`, OPTIONS)
if (!candidate) return
&& id(value?.planId) && id(value?.planPalletId)
&& value?.sourceStage === 'RECEIVED' && Number(value?.packageCount) > 0)
const candidatesByDestination = new Map()
for (const value of markerCandidates) {
const destination = String(value?.destinationWarehouseCode || value?.fbaWarehouseCode || '').trim().toUpperCase()
if (destination && !candidatesByDestination.has(destination)) candidatesByDestination.set(destination, value)
}
const selectedCandidates = [...candidatesByDestination.values()]
const candidate = selectedCandidates[0]
context.assert(MODULE, '候选严格命中本轮PDA库存托盘且覆盖多个FBA仓', candidateResult.response,
selectedCandidates.length >= 2,
`candidates=${selectedCandidates.length}, destinations=${[...candidatesByDestination.keys()].join(',')}, pdaPallets=${[...fixture.palletIds].join(',')}, warehouseId=${id(fixture.overseasWarehouseId)}; ${trace(candidateResult.response)}`, OPTIONS)
if (selectedCandidates.length < 2) return
const planId = id(candidate.planId)
const planPalletId = id(candidate.planPalletId)
const planIds = [...new Set(selectedCandidates.map(value => id(value.planId)))]
const planPalletIds = selectedCandidates.map(value => id(value.planPalletId))
// Exercise removal on a disposable dispatch, then reuse the released current-run pallet.
const removable = await api(context, 'POST', '创建本轮托盘移除对照排车', `${BASE}/pre-arrange`, {
planPalletIds: [planPalletId], route: `${context.marker}-REMOVE`,
@@ -158,9 +168,11 @@ export async function run(context) {
context.createdResources.phase4OverseasDispatch.removalDispatchNo = removableNo
const draftToken = `${context.marker}-DISPATCH-DRAFT`.slice(0, 64)
const containersJson = JSON.stringify([{ key: 'C1', capacity: Math.max(1, Number(candidate.packageCount)) }])
const snapshotBody = { draftToken, planIds: [planId], containersJson }
const snapshotBody = { draftToken, planIds, containersJson }
context.createdResources.phase4OverseasDispatch.draftToken = draftToken
context.createdResources.phase4OverseasDispatch.planPalletId = planPalletId
context.createdResources.phase4OverseasDispatch.planPalletIds = planPalletIds
context.createdResources.phase4OverseasDispatch.destinationWarehouseCodes = [...candidatesByDestination.keys()]
const snapshot = await api(context, 'POST', '保存智能预排草稿快照', `${BASE}/drafts/snapshot`, snapshotBody)
context.assert(MODULE, 'Void草稿快照允许data为空且请求成功', snapshot.response,
@@ -173,7 +185,7 @@ export async function run(context) {
const draftRows = Array.isArray(draftsResult.data) ? draftsResult.data : []
const matchingDrafts = draftRows.filter((value) => value?.draftToken === draftToken)
context.assert(MODULE, '同token同载荷快照只保留一份活动草稿', draftsResult.response,
matchingDrafts.length === 1 && String(matchingDrafts[0].planIdsJson || '').split(',').includes(planId)
matchingDrafts.length === 1 && planIds.every(value => String(matchingDrafts[0].planIdsJson || '').split(',').includes(value))
&& matchingDrafts[0].containersJson === containersJson,
`matches=${matchingDrafts.length}, planIds=${matchingDrafts[0]?.planIdsJson || 'missing'}; ${trace(draftsResult.response)}`, OPTIONS)
@@ -198,24 +210,29 @@ export async function run(context) {
Array.isArray(deletedDrafts.data) && !deletedDrafts.data.find(draft => draft.draftToken === draftToken)?.containers?.some(container => id(container.id) === containerId), trace(deletedDrafts.response), OPTIONS)
const finalContainer = await api(context, 'POST', '创建正式预排使用的草稿柜', `${BASE}/drafts/containers`, { draftToken, capacity: 15 })
if (!success(finalContainer.response) || !finalContainer.data?.id) return
const lockBody = { planPalletId, draftToken, draftContainerId: id(finalContainer.data.id) }
const locked = await api(context, 'POST', '锁定本轮预排托盘', `${BASE}/draft-locks/lock`, lockBody)
if (!success(locked.response)) return
const lockBodies = planPalletIds.map(value => ({ planPalletId: value, draftToken, draftContainerId: id(finalContainer.data.id) }))
for (const [index, body] of lockBodies.entries()) {
const locked = await api(context, 'POST', `锁定本轮预排托盘${index + 1}`, `${BASE}/draft-locks/lock`, body)
if (!success(locked.response)) return
}
const lockBody = lockBodies[0]
const lockReplay = await api(context, 'POST', '同草稿重复锁定托盘幂等成功', `${BASE}/draft-locks/lock`, lockBody)
context.assert(MODULE, '重复锁定不创建冲突且Void响应可为空', lockReplay.response,
success(lockReplay.response) && lockReplay.response.body?.data == null, trace(lockReplay.response), OPTIONS)
const visibleInDraft = await api(context, 'GET', '当前草稿仍可查看自身锁定候选',
query(`${BASE}/candidates`, { sourceStage: 'RECEIVED', draftToken }))
const visible = Array.isArray(visibleInDraft.data) && visibleInDraft.data.some((value) => id(value?.planPalletId) === planPalletId)
const visibleIds = new Set((Array.isArray(visibleInDraft.data) ? visibleInDraft.data : []).map(value => id(value?.planPalletId)))
const visible = planPalletIds.every(value => visibleIds.has(value))
context.assert(MODULE, 'draftToken仅放行当前草稿自己的锁', visibleInDraft.response, visible,
`planPalletId=${planPalletId}; ${trace(visibleInDraft.response)}`, OPTIONS)
`planPalletIds=${planPalletIds.join(',')}; ${trace(visibleInDraft.response)}`, OPTIONS)
const hiddenResult = await api(context, 'GET', '无草稿标识时隐藏已锁托盘',
query(`${BASE}/candidates`, { sourceStage: 'RECEIVED' }))
const hidden = Array.isArray(hiddenResult.data) && !hiddenResult.data.some((value) => id(value?.planPalletId) === planPalletId)
const hiddenIds = new Set((Array.isArray(hiddenResult.data) ? hiddenResult.data : []).map(value => id(value?.planPalletId)))
const hidden = planPalletIds.every(value => !hiddenIds.has(value))
context.assert(MODULE, '活动草稿锁阻止其他预排重复选择', hiddenResult.response, hidden,
`planPalletId=${planPalletId}; ${trace(hiddenResult.response)}`, OPTIONS)
`planPalletIds=${planPalletIds.join(',')}; ${trace(hiddenResult.response)}`, OPTIONS)
const foreignToken = `${context.marker}-DISPATCH-FOREIGN`.slice(0, 64)
const foreignLock = await context.requestOverseas('POST', `${BASE}/draft-locks/lock`, {
@@ -224,7 +241,7 @@ export async function run(context) {
context.addStep(MODULE, '其他草稿锁定同一托盘返回A0409', foreignLock, conflict(foreignLock), trace(foreignLock), OPTIONS)
const preArrangeBody = {
planPalletIds: [planPalletId], route: `${context.marker}-ROUTE`, fbaAppointmentNo: null,
planPalletIds, route: `${context.marker}-ROUTE`, fbaAppointmentNo: null,
fbaAppointmentStatus: null, vehicleAppointmentStatus: null, draftToken,
}
const arranged = await api(context, 'POST', '将草稿锁原子转换为待出库预排', `${BASE}/pre-arrange`, preArrangeBody)
@@ -232,9 +249,9 @@ export async function run(context) {
context.assert(MODULE, '智能预排仅创建PENDING_DEPARTURE排车并冻结托盘', arranged.response,
Boolean(dispatchNo) && arranged.data?.status === 'PENDING_DEPARTURE'
&& Array.isArray(arranged.data?.items)
&& arranged.data.items.some((item) => id(item?.planPalletId) === planPalletId)
&& Number(arranged.data?.palletCount) === 1,
`dispatchNo=${dispatchNo || 'missing'}, status=${arranged.data?.status || 'missing'}; ${trace(arranged.response)}`, OPTIONS)
&& planPalletIds.every(value => arranged.data.items.some(item => id(item?.planPalletId) === value))
&& Number(arranged.data?.palletCount) === planPalletIds.length,
`dispatchNo=${dispatchNo || 'missing'}, status=${arranged.data?.status || 'missing'}, pallets=${arranged.data?.palletCount ?? 'missing'}, destinations=${[...candidatesByDestination.keys()].join(',')}; ${trace(arranged.response)}`, OPTIONS)
if (!dispatchNo) return
context.createdResources.phase4OverseasDispatch.dispatchNo = dispatchNo
context.createdResources.phase4OverseasDispatch.dispatchId = id(arranged.data?.id)