This commit is contained in:
2026-09-20 19:57:47 +08:00
parent db07111e63
commit bd03ed5671
17 changed files with 210 additions and 97 deletions
@@ -1,3 +1,5 @@
import { verifyReceivedOrderHold } from './phase4-tail-order-controls.mjs'
const MODULE = 'phase4-overseas-pda-operations'
const SCENARIO = 'phase4-overseas-pda-p2-p5'
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
@@ -460,8 +462,20 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
return false
}
const unitPath = `${pdaBase}/${encodeURIComponent(relabelNo)}/units`
if (item.operationType === 'CARTON_AND_SKU') {
const carton = await request(context, token, 'POST', 'P5正常换标先确认箱标阶段',
`${pdaBase}/${encodeURIComponent(relabelNo)}/cartons/scan`, {
cartonMark: item.cartonMark, expectedVersion: Number(detail.version),
idempotencyKey: key(context, 'RELABEL-NORMAL-CARTON'),
})
if (!success(carton.response)) return false
detail = carton.data
}
const scanBody = { itemId: id(item.itemId), oldLabel: item.cartonMark,
newLabel: item.fbaCartonMark || item.newSkuCode,
skuVerifications: (item.skuItems || []).map(sku => ({
skuCode: sku.skuCode, newSkuCode: sku.newSkuCode, quantity: Number(sku.quantity),
})),
expectedVersion: Number(detail.version), idempotencyKey: key(context, 'RELABEL-NORMAL') }
const scanned = await request(context, token, 'POST', 'P5正常换标一箱', unitPath, scanBody)
const scanReplay = await request(context, token, 'POST', 'P5正常换标同键重放', unitPath, scanBody)
@@ -516,43 +530,45 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
if (!photoId) return false
const completePath = `${pdaBase}/${encodeURIComponent(relabelNo)}/complete`
const completeBody = { photoFileIds: [photoId], expectedVersion: Number(detail.version),
idempotencyKey: key(context, 'RELABEL-COMPLETE-1'), remark: `${context.marker}-complete-1` }
const completed = await request(context, token, 'POST', 'P5首次完成换标进入WAIT_REVIEW', completePath, completeBody)
manualCompletion: true, idempotencyKey: key(context, 'RELABEL-COMPLETE-1'), remark: `${context.marker}-complete-1` }
const completed = await request(context, token, 'POST', 'P5异常手动完成换标进入WAIT_REVIEW', completePath, completeBody)
if (!success(completed.response)) return false
const completeReplay = await request(context, token, 'POST', 'P5首次完成同键重放', completePath, completeBody)
context.assert(MODULE, 'P5首次完成重放保持WAIT_REVIEW', completeReplay.response,
completeReplay.data?.taskStatus === 'WAIT_REVIEW', trace(completeReplay.response), OPTIONS)
const reviewPath = `${pdaBase}/${encodeURIComponent(relabelNo)}/review`
const reworkBody = { decision: 'REWORK', expectedVersion: Number(completed.data?.version),
const reviewPath = `${relabelBase}/${encodeURIComponent(relabelNo)}/rework`
const reworkBody = { decision: 'REWORK', expectedVersion: Number(completed.data.version),
idempotencyKey: key(context, 'RELABEL-REWORK'), remark: `${context.marker}-rework` }
const reworked = await request(context, token, 'POST', 'P5复核退回REWORK', reviewPath, reworkBody)
const reworkReplay = await request(context, token, 'POST', 'P5 REWORK同键重放', reviewPath, reworkBody)
const reworked = await request(context, context.overseasToken, 'POST', 'P5平台复核退回REWORK', reviewPath, reworkBody)
if (!success(reworked.response)) return false
const reworkReplay = await request(context, context.overseasToken, 'POST', 'P5 REWORK同键重放', reviewPath, reworkBody)
context.assert(MODULE, 'P5 REWORK重放保持PROCESSING', reworkReplay.response,
reworkReplay.data?.taskStatus === 'PROCESSING', trace(reworkReplay.response), OPTIONS)
const secondCompleteBody = { photoFileIds: [photoId], expectedVersion: Number(reworked.data?.version),
idempotencyKey: key(context, 'RELABEL-COMPLETE-2'), remark: `${context.marker}-complete-2` }
const secondCompleted = await request(context, token, 'POST', 'P5返工后再次完成WAIT_REVIEW',
completePath, secondCompleteBody)
const secondReplay = await request(context, token, 'POST', 'P5再次完成同键重放', completePath, secondCompleteBody)
context.assert(MODULE, 'P5再次完成重放保持WAIT_REVIEW', secondReplay.response,
secondReplay.data?.taskStatus === 'WAIT_REVIEW', trace(secondReplay.response), OPTIONS)
const approveBody = { decision: 'APPROVE', expectedVersion: Number(secondCompleted.data?.version),
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 PDA批准后等待客户确认', approveReplay.response,
approved.data?.taskStatus === 'COMPLETED' && approved.data?.applicationStatus === 'CUSTOMER_CONFIRM_PENDING'
&& approveReplay.data?.taskStatus === 'COMPLETED', trace(approveReplay.response), OPTIONS)
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',
`${merchantRelabelBase}/${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
reworkReplay.data?.taskStatus === 'PROCESSING' && Number(reworkReplay.data.relabelledPackages) === 0,
trace(reworkReplay.response), OPTIONS)
detail = reworked.data
for (const redoItem of detail.items || []) {
if (redoItem.operationType === 'CARTON_AND_SKU') {
const carton = await request(context, token, 'POST', 'P5返工重新确认箱标阶段',
`${pdaBase}/${encodeURIComponent(relabelNo)}/cartons/scan`, {
cartonMark: redoItem.cartonMark, expectedVersion: Number(detail.version),
idempotencyKey: key(context, `RELABEL-REDO-CARTON-${redoItem.itemId}`),
})
if (!success(carton.response)) return false
detail = carton.data
}
const redo = await request(context, token, 'POST', 'P5返工重新完成SKU阶段', unitPath, {
itemId: id(redoItem.itemId), oldLabel: redoItem.cartonMark, newLabel: redoItem.newSkuCode,
skuVerifications: (redoItem.skuItems || []).map(sku => ({
skuCode: sku.skuCode, newSkuCode: sku.newSkuCode, quantity: Number(sku.quantity),
})),
expectedVersion: Number(detail.version), idempotencyKey: key(context, `RELABEL-REDO-SKU-${redoItem.itemId}`),
})
if (!success(redo.response)) return false
detail = redo.data
}
const bindingResponse = await context.requestOverseas('GET', `${relabelBase}/${encodeURIComponent(relabelNo)}`)
const bindingDetail = success(bindingResponse) ? bindingResponse.body.data : null
if (!Array.isArray(bindingDetail?.items) || bindingDetail.items.length !== 3) return false
const operationBase = '/api/pda/v1/last-leg/pallet-operations'
const printKeyA = key(context, 'RELABEL-PRINT-A')
@@ -574,7 +590,7 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
const targets = [printedA.data, printedB.data]
const beforeBySource = new Map()
const movedFacts = []
const sourceCodes = confirmed.items.map((itemDetail) => {
const sourceCodes = bindingDetail.items.map((itemDetail) => {
const sourcePositionId = id(itemDetail?.sourceInventoryPositionId)
return sourceLines.find((line) => id(line.position.positionId) === sourcePositionId)?.position?.palletNo || ''
})
@@ -584,12 +600,12 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
}, new Map())
const splitSource = [...sourceFrequency.entries()].find(([, count]) => count >= 2)?.[0] || ''
let splitSourceMoves = 0
for (const [index, itemDetail] of confirmed.items.entries()) {
for (const [index, itemDetail] of bindingDetail.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)
blocker(context, 'P5换标拆托来源快照', `item=${index + 1}缺少来源托盘或箱唛`, bindingResponse)
return false
}
const sourceBefore = (await resolvePallet(context, token, sourcePalletCode,
@@ -670,6 +686,27 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
&& relabelLedgers.every((row) => String(row?.remark || '').startsWith('换标拆托')),
`rows=${relabelLedgers.length}, deltas=${relabelLedgers.map((row) => row.deltaPackages).join(',')}; ${trace(ledgerResponse)}`, OPTIONS)
const afterBinding = await request(context, token, 'GET', 'P5组托后刷新任务版本',
`${pdaBase}/${encodeURIComponent(relabelNo)}`)
if (!success(afterBinding.response)) return false
const finalBody = { photoFileIds: [photoId], expectedVersion: Number(afterBinding.data.version),
idempotencyKey: key(context, 'RELABEL-COMPLETE-2'), remark: `${context.marker}-complete-2` }
const approved = await request(context, token, 'POST', 'P5返工组托后提交完工等待客户确认', completePath, finalBody)
if (!success(approved.response) || approved.data?.applicationStatus !== 'CUSTOMER_CONFIRM_PENDING') return false
const finalReplay = await request(context, token, 'POST', 'P5返工完工同键重放', completePath, finalBody)
context.assert(MODULE, 'P5返工完工重放保持待客户确认', finalReplay.response,
finalReplay.data?.taskStatus === 'WAIT_REVIEW' && finalReplay.data?.applicationStatus === 'CUSTOMER_CONFIRM_PENDING',
trace(finalReplay.response), OPTIONS)
const confirmBody = { idempotencyKey: key(context, 'RELABEL-CUSTOMER-CONFIRM'),
remark: `${context.marker}-customer-confirmed` }
const confirmedResponse = await context.requestOverseas('POST',
`${merchantRelabelBase}/${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
context.createdResources.phase4OverseasRelabel = {
marker: context.marker, relabelNo, customerId, overseasWarehouseId: id(fixture.overseasWarehouseId),
targetPalletIds: [...new Set(movedFacts.map((fact) => fact.targetPalletId))],
@@ -719,6 +756,7 @@ export async function run(context) {
return
}
fixture.overseasWarehouseId = id(handoff.overseasWarehouseId)
await verifyReceivedOrderHold(context)
await verifyStocktake(context, token, fixture)
await verifyInventoryMove(context, token, fixture)
await verifyPalletOperations(context, token, fixture)