test: prove dispatch split across FBA destinations
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user