test(acceptance): reuse fixed customers and align native fulfillment flows

This commit is contained in:
2026-10-23 04:48:02 +08:00
parent b92f7fe991
commit 139f6b1932
51 changed files with 1076 additions and 228 deletions
@@ -1,5 +1,6 @@
import { verifyReceivedOrderHold } from './phase4-tail-order-controls.mjs'
import { verifyPdaWarehouseRevocation } from './unified-pda-session-revocation.mjs'
import { readCurrentBoxedInventory } from './current-boxed-inventory.mjs'
const MODULE = 'phase4-overseas-pda-operations'
export const RELABEL_CHARGE_AMOUNT = 0.01
@@ -364,14 +365,17 @@ export function selectPhysicalRelabelSources(candidates, preferredBalanceId) {
const seen = new Set()
for (const source of ordered) {
if (!id(source?.inventoryBalanceId) || !id(source?.inboundOrderId)) continue
for (const position of source.positions || []) {
const usedByPosition = new Map()
for (const carton of source.cartons || []) {
const position = (source.positions || []).find(row => id(row.positionId) === id(carton.positionId))
const positionId = id(position?.positionId)
if (!positionId || seen.has(positionId) || !nonBlank(position?.palletNo)
const cartonKey = `${id(source.inboundOrderId)}:${carton.cartonMark}`
if (!carton.cartonMark || seen.has(cartonKey) || !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 })
const used = usedByPosition.get(positionId) || 0
if (used >= Number(position.availablePackages)) continue
seen.add(cartonKey); usedByPosition.set(positionId, used + 1)
sourceLines.push({ source, position, carton, packageCount: 1 })
if (sourceLines.length === 3) return sourceLines
}
}
@@ -388,25 +392,11 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
return false
}
if (!await fundRelabelWallet(context, customerId)) return false
// 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.request('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 boxed = await readCurrentBoxedInventory(context, merchantToken, fixture.overseasWarehouseId, MODULE)
// 同一次收货产生的真实有箱库存;优先选一个具备三箱来源的入仓单,避免借用历史库存。
const candidate = boxed.find(row => selectPhysicalRelabelSources([row]).length === 3)
const inventoryCandidates = candidate ? [candidate] : []
const inventoryCandidatesResponse = null
const sourceLines = selectPhysicalRelabelSources(inventoryCandidates, inventory.balanceId)
const requiredPackages = 3 - sourceLines.length
context.addStep(MODULE, 'P5客户库存候选返回换标权威来源', inventoryCandidatesResponse,
@@ -426,7 +416,7 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
contactName: 'PDA换标验收', contactPhone: '13800000006', chargeAmount: RELABEL_CHARGE_AMOUNT, remark: context.marker,
idempotencyKey: key(context, 'RELABEL-CREATE'),
deliveryMethod: 'TRUCK', printShippingLabel: false,
items: sourceLines.map(({ source, position, packageCount }, index) => {
items: sourceLines.map(({ source, position, carton, packageCount }, index) => {
const newSkuCode = `${source.skuCode}-NEW-${markerSuffix}-${index + 1}`.slice(0, 128)
const changesCartonLabel = nonBlank(source.destinationFbaWarehouseCode)
return {
@@ -437,11 +427,11 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
shipmentNo: `SHIP-${context.marker}-${index + 1}`.slice(0, 128),
newSkuCode,
labelRequirement: context.marker,
cartonMark: nonBlank(source.cartonMark) ? source.cartonMark : `CM-${String(context.marker).slice(-28)}-${index + 1}`,
cartonMark: carton.cartonMark,
fbaCartonMark: changesCartonLabel ? `FBA-${String(context.marker).slice(-27)}-${index + 1}` : null,
destinationFbaWarehouseCode: changesCartonLabel ? source.destinationFbaWarehouseCode : null,
operationType: changesCartonLabel ? 'CARTON_AND_SKU' : 'SKU_LABEL',
skuItems: [{ skuCode: source.skuCode, newSkuCode, quantity: packageCount, remark: context.marker }],
skuItems: carton.items.map(item => ({ skuCode: item.skuCode, newSkuCode, quantity: Number(item.quantity), remark: context.marker })),
}
}),
}