fix: align HTTP acceptance fixtures and pallet permissions
This commit is contained in:
@@ -87,7 +87,7 @@ async function expectConflict(context, token, method, name, path, body) {
|
||||
}
|
||||
|
||||
async function resolvePallet(context, token, palletCode, name = '扫码解析托盘') {
|
||||
return request(context, token, 'POST', name,
|
||||
return request(context, token, 'POST', `${name}(${palletCode})`,
|
||||
'/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: palletCode })
|
||||
}
|
||||
|
||||
@@ -103,6 +103,11 @@ async function resolvePublicLocation(context, fixture) {
|
||||
? { locationId: id(location.id), locationCode: location.locationCode } : null
|
||||
}
|
||||
|
||||
export function remainingRelabelItem(detail, usedItemIds) {
|
||||
const used = new Set(usedItemIds.map(id))
|
||||
return (detail?.items || []).find((item) => id(item?.itemId) && !used.has(id(item.itemId))) || null
|
||||
}
|
||||
|
||||
async function resolveReusableAuxiliaryPallet(context, token, fixture) {
|
||||
for (const candidate of fixture.auxiliaryPallets || []) {
|
||||
if (!nonBlank(candidate?.palletCode) || id(candidate?.palletId) === id(fixture.pallet?.palletId)) continue
|
||||
@@ -328,6 +333,27 @@ async function verifyPalletOperations(context, token, fixture) {
|
||||
return success(unbound.response)
|
||||
}
|
||||
|
||||
export function selectPhysicalRelabelSources(candidates, preferredBalanceId) {
|
||||
const ordered = [...candidates].sort((a, b) =>
|
||||
Number(id(b.inventoryBalanceId) === id(preferredBalanceId)) - Number(id(a.inventoryBalanceId) === id(preferredBalanceId)))
|
||||
const sourceLines = []
|
||||
const seen = new Set()
|
||||
for (const source of ordered) {
|
||||
if (!id(source?.inventoryBalanceId) || !id(source?.inboundOrderId)) continue
|
||||
for (const position of source.positions || []) {
|
||||
const positionId = id(position?.positionId)
|
||||
if (!positionId || seen.has(positionId) || !nonBlank(position?.palletNo)
|
||||
|| position.palletNo.toUpperCase() === 'UNBOUND') continue
|
||||
seen.add(positionId)
|
||||
const selected = Math.min(3 - sourceLines.length, Number(position.availablePackages))
|
||||
if (!Number.isInteger(selected) || selected <= 0) continue
|
||||
for (let index = 0; index < selected; index++) sourceLines.push({ source, position, packageCount: 1 })
|
||||
if (sourceLines.length === 3) return sourceLines
|
||||
}
|
||||
}
|
||||
return sourceLines
|
||||
}
|
||||
|
||||
async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
const customerId = id(fixture.order?.order?.customerId || context.createdResources?.phase4OverseasPda?.customerId)
|
||||
const customerOrderNo = fixture.order?.order?.orderNo || fixture.order?.order?.sourceOrderNo
|
||||
@@ -338,33 +364,27 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
return false
|
||||
}
|
||||
if (!await fundRelabelWallet(context, customerId)) return false
|
||||
const inventoryCandidatesResponse = await context.requestOverseas('GET', query(
|
||||
'/api/merchant/v1/last-leg/relabel-applications/inventory-candidates', {
|
||||
operationWarehouseId: fixture.overseasWarehouseId, inboundCode: fixture.inboundCode,
|
||||
}), undefined, {}, merchantToken)
|
||||
const inventoryCandidates = success(inventoryCandidatesResponse)
|
||||
&& Array.isArray(inventoryCandidatesResponse.body?.data) ? inventoryCandidatesResponse.body.data : []
|
||||
const orderedCandidates = [...inventoryCandidates].sort((left, right) => {
|
||||
const leftPreferred = id(left?.inventoryBalanceId) === id(inventory?.balanceId) ? 1 : 0
|
||||
const rightPreferred = id(right?.inventoryBalanceId) === id(inventory?.balanceId) ? 1 : 0
|
||||
return rightPreferred - leftPreferred
|
||||
})
|
||||
const sourceLines = []
|
||||
let requiredPackages = 3
|
||||
for (const source of orderedCandidates) {
|
||||
if (!id(source?.inventoryBalanceId) || !id(source?.inboundOrderId)) continue
|
||||
for (const position of source?.positions || []) {
|
||||
const available = Number(position?.availablePackages)
|
||||
if (!id(position?.positionId) || available <= 0) continue
|
||||
const selected = Math.min(requiredPackages, available)
|
||||
for (let packageIndex = 0; packageIndex < selected; packageIndex += 1) {
|
||||
sourceLines.push({ source, position, packageCount: 1 })
|
||||
}
|
||||
requiredPackages -= selected
|
||||
if (requiredPackages === 0) break
|
||||
}
|
||||
if (requiredPackages === 0) break
|
||||
// P4 intentionally leaves one carton UNBOUND. P5 requires physical source pallets,
|
||||
// and may combine only the current completed container's same-destination inbounds.
|
||||
const inboundCodes = [...new Set((context.context.phase4OverseasPdaOperations?.completedFixtures || [])
|
||||
.filter((item) => item.containerNo === fixture.containerNo && item.destinationType === fixture.destinationType
|
||||
&& item.fbaWarehouseCode === fixture.fbaWarehouseCode)
|
||||
.map((item) => item.inboundCode).filter(nonBlank))]
|
||||
const inventoryCandidates = []
|
||||
let inventoryCandidatesResponse = null
|
||||
for (const inboundCode of inboundCodes) {
|
||||
inventoryCandidatesResponse = await context.requestOverseas('GET', query(
|
||||
'/api/merchant/v1/last-leg/relabel-applications/inventory-candidates', {
|
||||
operationWarehouseId: fixture.overseasWarehouseId, inboundCode,
|
||||
}), undefined, {}, merchantToken)
|
||||
context.addStep(MODULE, 'P5读取本轮同柜同目的仓换标候选', inventoryCandidatesResponse,
|
||||
success(inventoryCandidatesResponse), trace(inventoryCandidatesResponse), OPTIONS)
|
||||
if (!success(inventoryCandidatesResponse)) return false
|
||||
inventoryCandidates.push(...records(inventoryCandidatesResponse.body?.data).filter((item) =>
|
||||
item.inboundCode === inboundCode && id(item.overseasWarehouseId) === id(fixture.overseasWarehouseId)))
|
||||
}
|
||||
const sourceLines = selectPhysicalRelabelSources(inventoryCandidates, inventory.balanceId)
|
||||
const requiredPackages = 3 - sourceLines.length
|
||||
context.addStep(MODULE, 'P5客户库存候选返回换标权威来源', inventoryCandidatesResponse,
|
||||
requiredPackages === 0,
|
||||
`sourceLines=${sourceLines.length}, selectedPackages=${3 - requiredPackages}; ${trace(inventoryCandidatesResponse)}`, OPTIONS)
|
||||
@@ -452,8 +472,13 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
await expectConflict(context, token, 'POST', 'P5正常换标旧版本A0409', unitPath,
|
||||
{ ...scanBody, oldLabel: `${context.marker}-OLD-STALE`, idempotencyKey: key(context, 'RELABEL-STALE') })
|
||||
detail = scanned.data
|
||||
if (!detail) return false
|
||||
const skipItem = Array.isArray(detail?.items)
|
||||
? detail.items.find((candidate) => id(candidate?.itemId) && id(candidate.itemId) !== id(item.itemId)) : null
|
||||
if (!skipItem) {
|
||||
blocker(context, 'P5跳过换标明细', '缺少独立的第二箱换标任务', scanned.response)
|
||||
return false
|
||||
}
|
||||
const skipBody = { itemId: id(skipItem?.itemId), oldLabel: skipItem?.cartonMark,
|
||||
reason: `${context.marker}-skip`, expectedVersion: Number(detail.version),
|
||||
idempotencyKey: key(context, 'RELABEL-SKIP') }
|
||||
@@ -462,7 +487,12 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
context.assert(MODULE, 'P5跳过换标重放不重复计数', skipReplay.response,
|
||||
Number(skipReplay.data?.skippedPackages) === 1, trace(skipReplay.response), OPTIONS)
|
||||
detail = skipped.data
|
||||
const exceptionItem = item
|
||||
if (!detail) return false
|
||||
const exceptionItem = remainingRelabelItem(detail, [item.itemId, skipItem.itemId])
|
||||
if (!exceptionItem) {
|
||||
blocker(context, 'P5异常换标明细', '缺少未被正常换标或跳过的第三箱任务', skipped.response)
|
||||
return false
|
||||
}
|
||||
const exceptionBody = { itemId: id(exceptionItem?.itemId), oldLabel: null,
|
||||
exceptionCode: 'DAMAGED', reason: `${context.marker}-damaged`, expectedVersion: Number(detail.version),
|
||||
idempotencyKey: key(context, 'RELABEL-EXCEPTION') }
|
||||
@@ -473,6 +503,7 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
||||
context.assert(MODULE, 'P5换标异常重放不重复计数', exceptionReplay.response,
|
||||
Number(exceptionReplay.data?.exceptionPackages) === 1, trace(exceptionReplay.response), OPTIONS)
|
||||
detail = exception.data
|
||||
if (!detail) return false
|
||||
|
||||
const photo = await context.requestOverseasMultipart('POST',
|
||||
`${pdaBase}/${encodeURIComponent(relabelNo)}/photos/files`, {
|
||||
|
||||
Reference in New Issue
Block a user