|
|
|
@@ -363,22 +363,17 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
|
|
|
|
for (const source of orderedCandidates) {
|
|
|
|
|
if (!id(source?.inventoryBalanceId) || !id(source?.inboundOrderId)) continue
|
|
|
|
|
for (const position of source?.positions || []) {
|
|
|
|
|
if (!id(position?.positionId) || Number(position?.availablePackages) <= 0) continue
|
|
|
|
|
const packageCount = 1
|
|
|
|
|
sourceLines.push({ source, position, packageCount })
|
|
|
|
|
requiredPackages -= packageCount
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
for (const line of sourceLines) {
|
|
|
|
|
if (requiredPackages === 0) break
|
|
|
|
|
const remainingCapacity = Number(line.position.availablePackages) - line.packageCount
|
|
|
|
|
if (remainingCapacity <= 0) continue
|
|
|
|
|
const additionalPackages = Math.min(requiredPackages, remainingCapacity)
|
|
|
|
|
line.packageCount += additionalPackages
|
|
|
|
|
requiredPackages -= additionalPackages
|
|
|
|
|
}
|
|
|
|
|
context.addStep(MODULE, 'P5客户库存候选返回换标权威来源', inventoryCandidatesResponse,
|
|
|
|
|
requiredPackages === 0,
|
|
|
|
|
`sourceLines=${sourceLines.length}, selectedPackages=${3 - requiredPackages}; ${trace(inventoryCandidatesResponse)}`, OPTIONS)
|
|
|
|
@@ -394,6 +389,7 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
|
|
|
|
labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19),
|
|
|
|
|
contactName: 'PDA换标验收', contactPhone: '13800000006', chargeAmount: 0.01, remark: context.marker,
|
|
|
|
|
idempotencyKey: key(context, 'RELABEL-CREATE'),
|
|
|
|
|
deliveryMethod: 'TRUCK', printShippingLabel: false,
|
|
|
|
|
items: sourceLines.map(({ source, position, packageCount }, index) => {
|
|
|
|
|
const newSkuCode = `${source.skuCode}-NEW-${markerSuffix}-${index + 1}`.slice(0, 128)
|
|
|
|
|
const changesCartonLabel = nonBlank(source.destinationFbaWarehouseCode)
|
|
|
|
@@ -520,15 +516,135 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
|
|
|
|
|
idempotencyKey: key(context, 'RELABEL-APPROVE'), remark: `${context.marker}-approved` }
|
|
|
|
|
const approved = await request(context, token, 'POST', 'P5复核批准换标完成', reviewPath, approveBody)
|
|
|
|
|
const approveReplay = await request(context, token, 'POST', 'P5批准同键重放', reviewPath, approveBody)
|
|
|
|
|
context.assert(MODULE, 'P5最终任务和申请均COMPLETED', approveReplay.response,
|
|
|
|
|
approved.data?.taskStatus === 'COMPLETED' && approved.data?.applicationStatus === 'COMPLETED'
|
|
|
|
|
context.assert(MODULE, 'P5 PDA批准后等待客户确认', approveReplay.response,
|
|
|
|
|
approved.data?.taskStatus === 'COMPLETED' && approved.data?.applicationStatus === 'CUSTOMER_CONFIRM_PENDING'
|
|
|
|
|
&& approveReplay.data?.taskStatus === 'COMPLETED', trace(approveReplay.response), OPTIONS)
|
|
|
|
|
const settled = await poll(async () => context.requestOverseas('GET', `${relabelBase}/${encodeURIComponent(relabelNo)}`),
|
|
|
|
|
(response) => success(response) && response.body?.data?.application?.walletStatus === 'SETTLED')
|
|
|
|
|
context.addStep(MODULE, 'P5换标完成后钱包结算收敛', settled.result,
|
|
|
|
|
settled.result?.body?.data?.application?.walletStatus === 'SETTLED',
|
|
|
|
|
`pollRequests=${settled.attempts}, walletStatus=${settled.result?.body?.data?.application?.walletStatus || 'missing'}; ${trace(settled.result)}`, OPTIONS)
|
|
|
|
|
return success(approved.response) && settled.result?.body?.data?.application?.walletStatus === 'SETTLED'
|
|
|
|
|
if (approved.data?.applicationStatus !== 'CUSTOMER_CONFIRM_PENDING') return false
|
|
|
|
|
|
|
|
|
|
const confirmBody = { idempotencyKey: key(context, 'RELABEL-CUSTOMER-CONFIRM'),
|
|
|
|
|
remark: `${context.marker}-customer-confirmed` }
|
|
|
|
|
const confirmedResponse = await context.requestOverseas('POST',
|
|
|
|
|
`${relabelBase}/${encodeURIComponent(relabelNo)}/confirm-completion`, confirmBody, {}, merchantToken)
|
|
|
|
|
const confirmed = success(confirmedResponse) ? confirmedResponse.body?.data : null
|
|
|
|
|
context.addStep(MODULE, 'P5客户确认换标完成进入待排车', confirmedResponse,
|
|
|
|
|
confirmed?.application?.status === 'READY_FOR_OUTBOUND'
|
|
|
|
|
&& confirmed?.application?.fulfillmentStatus === 'WAIT_PRE_DISPATCH', trace(confirmedResponse), OPTIONS)
|
|
|
|
|
if (confirmed?.application?.status !== 'READY_FOR_OUTBOUND') return false
|
|
|
|
|
|
|
|
|
|
const operationBase = '/api/pda/v1/last-leg/pallet-operations'
|
|
|
|
|
const printKeyA = key(context, 'RELABEL-PRINT-A')
|
|
|
|
|
const printedA = await request(context, token, 'POST', 'P5打印换标新托盘A',
|
|
|
|
|
`${operationBase}/relabel-pallet-label`, { relabelNo, idempotencyKey: printKeyA })
|
|
|
|
|
const printedAReplay = await request(context, token, 'POST', 'P5换标新托盘A打印同键重放',
|
|
|
|
|
`${operationBase}/relabel-pallet-label`, { relabelNo, idempotencyKey: printKeyA })
|
|
|
|
|
context.assert(MODULE, 'P5新托盘标签幂等且返回可打印HTML', printedAReplay.response,
|
|
|
|
|
id(printedA.data?.palletId) === id(printedAReplay.data?.palletId)
|
|
|
|
|
&& printedA.data?.palletCode === printedAReplay.data?.palletCode
|
|
|
|
|
&& String(printedA.data?.printableHtml || '').includes(printedA.data?.palletCode || '__missing__'),
|
|
|
|
|
trace(printedAReplay.response), OPTIONS)
|
|
|
|
|
const printedB = await request(context, token, 'POST', 'P5打印换标新托盘B',
|
|
|
|
|
`${operationBase}/relabel-pallet-label`, {
|
|
|
|
|
relabelNo, idempotencyKey: key(context, 'RELABEL-PRINT-B'),
|
|
|
|
|
})
|
|
|
|
|
if (!printedA.data?.palletCode || !printedB.data?.palletCode) return false
|
|
|
|
|
|
|
|
|
|
const targets = [printedA.data, printedB.data]
|
|
|
|
|
const beforeBySource = new Map()
|
|
|
|
|
const movedFacts = []
|
|
|
|
|
for (const [index, itemDetail] of confirmed.items.entries()) {
|
|
|
|
|
const sourcePositionId = id(itemDetail?.sourceInventoryPositionId)
|
|
|
|
|
const sourceLine = sourceLines.find((line) => id(line.position.positionId) === sourcePositionId)
|
|
|
|
|
const sourcePalletCode = sourceLine?.position?.palletNo
|
|
|
|
|
if (!sourcePalletCode || !itemDetail?.cartonMark) {
|
|
|
|
|
blocker(context, 'P5换标拆托来源快照', `item=${index + 1}缺少来源托盘或箱唛`, confirmedResponse)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const sourceBefore = (await resolvePallet(context, token, sourcePalletCode,
|
|
|
|
|
`P5拆托${index + 1}前刷新来源托盘`)).data
|
|
|
|
|
if (!sourceBefore) return false
|
|
|
|
|
if (!beforeBySource.has(sourcePalletCode)) beforeBySource.set(sourcePalletCode, Number(sourceBefore.totalPackages))
|
|
|
|
|
// A receives one carton, then B receives one and is reused as an occupied merge target.
|
|
|
|
|
// If the first two cartons share a source, the same source is split across A and B.
|
|
|
|
|
const targetSeed = index === 0 ? targets[0] : targets[1]
|
|
|
|
|
const targetBefore = (await resolvePallet(context, token, targetSeed.palletCode,
|
|
|
|
|
`P5拆托${index + 1}前刷新目标托盘`)).data
|
|
|
|
|
const reason = index === 0 ? null : `${context.marker}-场景${index + 1}`
|
|
|
|
|
const body = {
|
|
|
|
|
idempotencyKey: key(context, `RELABEL-DISASSEMBLE-${index + 1}`), relabelNo,
|
|
|
|
|
source: { palletCode: sourceBefore.palletCode, expectedVersion: Number(sourceBefore.palletVersion) },
|
|
|
|
|
target: { palletCode: targetBefore.palletCode, expectedVersion: Number(targetBefore.palletVersion) },
|
|
|
|
|
cartonMarks: [itemDetail.cartonMark], reason,
|
|
|
|
|
}
|
|
|
|
|
const moved = await request(context, token, 'POST', `P5换标箱${index + 1}拆托`,
|
|
|
|
|
`${operationBase}/relabel-disassemble`, body)
|
|
|
|
|
const replay = await request(context, token, 'POST', `P5换标箱${index + 1}拆托同键重放`,
|
|
|
|
|
`${operationBase}/relabel-disassemble`, body)
|
|
|
|
|
const sourceAfter = operationPallet(moved.data, sourceBefore.palletCode)
|
|
|
|
|
const targetAfter = operationPallet(moved.data, targetBefore.palletCode)
|
|
|
|
|
context.assert(MODULE, `P5换标拆托${index + 1}源减目标增且重放稳定`, replay.response,
|
|
|
|
|
moved.data?.operationType === 'RELABEL_PALLET_DISASSEMBLE' && replay.data?.replayed === true
|
|
|
|
|
&& Number(sourceAfter?.totalPackages) === Number(sourceBefore.totalPackages) - 1
|
|
|
|
|
&& Number(targetAfter?.totalPackages) === Number(targetBefore.totalPackages) + 1,
|
|
|
|
|
`source=${sourceBefore.totalPackages}->${sourceAfter?.totalPackages}, target=${targetBefore.totalPackages}->${targetAfter?.totalPackages}; ${trace(replay.response)}`,
|
|
|
|
|
OPTIONS)
|
|
|
|
|
if (!success(moved.response)) return false
|
|
|
|
|
movedFacts.push({ sourcePalletCode, targetPalletId: id(targetAfter?.palletId),
|
|
|
|
|
targetPalletCode: targetAfter?.palletCode, cartonMark: itemDetail.cartonMark,
|
|
|
|
|
fbaCartonMark: itemDetail.fbaCartonMark, balanceId: id(itemDetail.sourceInventoryBalanceId),
|
|
|
|
|
inboundOrderId: id(itemDetail.sourceInboundOrderId), inboundOrderNo: itemDetail.sourceInboundOrderNo,
|
|
|
|
|
inboundCode: itemDetail.sourceInboundCode, packages: Number(itemDetail.notifiedPackageCount), reason })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [sourcePalletCode, totalBefore] of beforeBySource) {
|
|
|
|
|
const sourceAfter = (await resolvePallet(context, token, sourcePalletCode, 'P5拆托后核对来源托盘')).data
|
|
|
|
|
const movedPackages = movedFacts.filter((fact) => fact.sourcePalletCode === sourcePalletCode)
|
|
|
|
|
.reduce((sum, fact) => sum + fact.packages, 0)
|
|
|
|
|
context.assert(MODULE, 'P5来源托盘累计箱数按拆托批次减少', null,
|
|
|
|
|
Number(sourceAfter?.totalPackages) === totalBefore - movedPackages,
|
|
|
|
|
`pallet=${sourcePalletCode}, before=${totalBefore}, moved=${movedPackages}, after=${sourceAfter?.totalPackages}`, OPTIONS)
|
|
|
|
|
}
|
|
|
|
|
context.assert(MODULE, 'P5覆盖新托盘、既有托盘合托和同源分批拆托', null,
|
|
|
|
|
new Set(movedFacts.map((fact) => fact.targetPalletId)).size === 2
|
|
|
|
|
&& movedFacts.filter((fact) => fact.targetPalletId === id(printedB.data.palletId)).length >= 2
|
|
|
|
|
&& [...beforeBySource.keys()].some((source) => new Set(movedFacts.filter((fact) => fact.sourcePalletCode === source)
|
|
|
|
|
.map((fact) => fact.targetPalletId)).size >= 2),
|
|
|
|
|
`targets=${new Set(movedFacts.map((fact) => fact.targetPalletId)).size}, targetBMoves=${movedFacts.filter((fact) => fact.targetPalletId === id(printedB.data.palletId)).length}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
for (const fact of movedFacts) {
|
|
|
|
|
const positionsResponse = await context.requestOverseas('GET', query(
|
|
|
|
|
`/api/platform/v1/last-leg/inventory/balances/${encodeURIComponent(fact.balanceId)}/positions`,
|
|
|
|
|
{ customerId }), undefined)
|
|
|
|
|
const positions = success(positionsResponse) && Array.isArray(positionsResponse.body?.data)
|
|
|
|
|
? positionsResponse.body.data : []
|
|
|
|
|
const targetPosition = positions.find((position) => id(position?.palletId) === fact.targetPalletId)
|
|
|
|
|
context.addStep(MODULE, `P5换标箱${fact.cartonMark}保留客户和原入仓归属`, positionsResponse,
|
|
|
|
|
id(targetPosition?.customerId) === customerId
|
|
|
|
|
&& id(targetPosition?.inboundOrderId) === fact.inboundOrderId
|
|
|
|
|
&& targetPosition?.inboundOrderNo === fact.inboundOrderNo
|
|
|
|
|
&& targetPosition?.inboundCode === fact.inboundCode,
|
|
|
|
|
`targetPallet=${fact.targetPalletCode}, inbound=${targetPosition?.inboundOrderNo || 'missing'}; ${trace(positionsResponse)}`, OPTIONS)
|
|
|
|
|
}
|
|
|
|
|
const ledgerResponse = await context.requestOverseas('GET', query(
|
|
|
|
|
'/api/platform/v1/last-leg/inventory/ledger', {
|
|
|
|
|
customerId, page: 1, size: 100, businessType: 'RELABEL_PALLET_DISASSEMBLE',
|
|
|
|
|
occurredFrom: new Date(Date.now() - 10 * 60_000).toISOString().slice(0, 19),
|
|
|
|
|
}))
|
|
|
|
|
const ledgerRows = success(ledgerResponse) ? records(ledgerResponse.body?.data) : []
|
|
|
|
|
const relabelLedgers = ledgerRows.filter((row) => movedFacts.some((fact) =>
|
|
|
|
|
row?.palletNo === fact.sourcePalletCode || row?.palletNo === fact.targetPalletCode))
|
|
|
|
|
context.addStep(MODULE, 'P5库存流水记录换标拆托原因和正负变动', ledgerResponse,
|
|
|
|
|
relabelLedgers.some((row) => Number(row?.deltaPackages) < 0)
|
|
|
|
|
&& relabelLedgers.some((row) => Number(row?.deltaPackages) > 0)
|
|
|
|
|
&& relabelLedgers.every((row) => String(row?.remark || '').startsWith('换标拆托')),
|
|
|
|
|
`rows=${relabelLedgers.length}, deltas=${relabelLedgers.map((row) => row.deltaPackages).join(',')}; ${trace(ledgerResponse)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
context.createdResources.phase4OverseasRelabel = {
|
|
|
|
|
marker: context.marker, relabelNo, customerId, overseasWarehouseId: id(fixture.overseasWarehouseId),
|
|
|
|
|
targetPalletIds: [...new Set(movedFacts.map((fact) => fact.targetPalletId))],
|
|
|
|
|
relabelCartonMarks: movedFacts.map((fact) => fact.fbaCartonMark).filter(nonBlank),
|
|
|
|
|
sourceCartonMarks: movedFacts.map((fact) => fact.cartonMark), status: 'READY_FOR_OUTBOUND',
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function run(context) {
|
|
|
|
|