feat: receive dropship inbound by source pallet

This commit is contained in:
2026-09-09 18:33:17 +08:00
parent c8ae626e87
commit 45e7c98b3f
13 changed files with 174 additions and 12 deletions
@@ -1067,11 +1067,14 @@ export async function run(context) {
`processStatus=${finalMeasurement?.processStatus ?? 'missing'}, measurementStatus=${finalMeasurement?.measurementStatus ?? 'missing'}, packages=${finalMeasurement?.actualPackageCount ?? 'missing'}, pallets=${weightEvidence.count}, grossWeight=${finalMeasurement?.actualGrossWeightKg ?? 'missing'}, netWeight=${finalMeasurement?.actualNetWeightKg ?? 'missing'}`, OPTIONS)
let sealedCartonMarks = []
let sealedSourcePalletNos = []
if (dropship && finalMeasurement?.processStatus === 'WAIT_PALLETIZE') {
const sealed = await completeDropshipPalletization(context, {
inboundId, inboundCode, warehouseId: asId(summary?.warehouseId), measurement: finalMeasurement,
})
sealedCartonMarks = sealed?.taskStatus === 'COMPLETED' ? sealed.sealedCartonMarks : []
sealedSourcePalletNos = sealed?.taskStatus === 'COMPLETED'
? (sealed.pallets || []).map((item) => item?.palletNo).filter(Boolean) : []
completed = await waitForCompletedMeasurement(context, measurementPath, ['WAIT_SPLIT'], 10)
finalResponse = completed.response
finalMeasurement = context.apiSuccess(MODULE, '合托后回查一件代发入仓单', finalResponse,
@@ -1093,7 +1096,7 @@ export async function run(context) {
OPTIONS)
context.createdResources.inboundMeasurement = {
sealedCartonMarks,
sealedCartonMarks, sealedSourcePalletNos,
marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId),
boxCode, finalStatus: finalMeasurement?.measurementStatus || null,
processStatus: finalMeasurement?.processStatus || null,
@@ -506,6 +506,7 @@ export async function run(context) {
cartonMark: fixture.cartonMark || null,
inboundCode: fixture.inboundCode || null,
sealedCartonMarks: Array.isArray(fixture.sealedCartonMarks) ? fixture.sealedCartonMarks : [],
sealedSourcePalletNos: Array.isArray(fixture.sealedSourcePalletNos) ? fixture.sealedSourcePalletNos : [],
cartonMarks: Array.isArray(fixture.cartonMarks) ? fixture.cartonMarks : [],
skuCodes: Array.isArray(fixture.skuCodes) ? fixture.skuCodes : [],
skuLines: Array.isArray(fixture.skuLines) ? fixture.skuLines : [],
@@ -1607,6 +1607,7 @@ export async function run(context) {
context.createdResources.phase3Loading = {
inboundCode: initial?.summary?.inboundCode || source?.inboundCode,
sealedCartonMarks: Array.isArray(source?.sealedCartonMarks) ? source.sealedCartonMarks : [],
sealedSourcePalletNos: Array.isArray(source?.sealedSourcePalletNos) ? source.sealedSourcePalletNos : [],
tenantId, orderId: id(source?.orderId), inboundId, preloadPlanId: planId,
cartonMark: source?.boxCode || source?.cartonMark || null,
cartonMarks: stableLoadedCartonMarks(stagedTask?.items),
@@ -86,7 +86,7 @@ test('PDA module is injected, token-scoped, traceable, and contains no direct ne
test('PDA module covers the container-number receiving contract and excludes outbound domains', () => {
for (const token of ['/candidates', '/prepare', '/current/unload-photos/files', '/current/sign',
'/current/cartons/resolve', '/current/pallets/resolve',
'/current/receipts/bulk', '/current/receipts/carton', '/current/complete']) assert.ok(source.includes(token), token)
'/current/receipts/bulk', '/current/receipts/carton', '/current/receipts/pallet', '/current/complete']) assert.ok(source.includes(token), token)
for (const token of ['/inventory/resolve', '/locations/resolve', '/pallets/resolve', '/operations?page=1&size=50']) {
assert.ok(source.includes(token), token)
}
@@ -564,6 +564,37 @@ async function receiveOverseas(context, token, inbounds, pallets, cartonFixtures
return true
}
async function receiveDropship(context, token, producer, inbounds) {
const sourcePalletNos = Array.isArray(producer?.sealedSourcePalletNos)
? producer.sealedSourcePalletNos.map(id).filter(nonBlank) : []
const expectedPending = inbounds.reduce((total, inbound) => total + Number(inbound.pendingPackages || 0), 0)
context.assert(MODULE, '一件代发来源托盘快照已从国内封托完整传递', null,
sourcePalletNos.length > 0 && new Set(sourcePalletNos).size === sourcePalletNos.length,
`sourcePallets=${sourcePalletNos.length}, pendingPackages=${expectedPending}`, OPTIONS)
if (!sourcePalletNos.length) return false
let accepted = 0
for (const [index, sourcePalletNo] of sourcePalletNos.entries()) {
const body = { sourcePalletNo, idempotencyKey: key(context, `dropship-pallet-${index}`) }
const first = await pda(context, token, 'POST', `一件代发整托实收${index + 1}`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/pallet', body)
if (!success(first.response)) return false
accepted += Number(first.data?.acceptedPackages || 0)
const replay = await pda(context, token, 'POST', `一件代发整托${index + 1}同键同载荷重放`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/pallet', body)
context.assert(MODULE, `一件代发整托${index + 1}幂等结果一致`, replay.response,
success(replay.response) && JSON.stringify(replay.data) === JSON.stringify(first.data), trace(replay.response), OPTIONS)
await expected(context, token, 'POST', `一件代发整托${index + 1}同键异载荷冲突`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/pallet',
{ ...body, sourcePalletNo: `${sourcePalletNo}-MUTATED` }, conflict)
await expected(context, token, 'POST', `一件代发来源托盘重复收货被拒绝${index + 1}`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/pallet',
{ ...body, idempotencyKey: key(context, `dropship-pallet-duplicate-${index}`) }, conflict)
}
context.assert(MODULE, '一件代发整托实收箱数与待收箱数守恒', null,
accepted === expectedPending, `accepted=${accepted}, expected=${expectedPending}`, OPTIONS)
return accepted === expectedPending
}
async function runReceiving(context, token, secondaryToken, merchantToken, preferredProducer = null, runAuthorizationProbes = true) {
if (runAuthorizationProbes && context.overseasToken) {
const denied = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, context.overseasToken)
@@ -666,7 +697,8 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
}
const fbaInbounds = current.data.inbounds.filter((item) => item.destinationType === 'FBA')
const overseasInbounds = current.data.inbounds.filter((item) => item.destinationType === 'OVERSEAS')
if (fbaInbounds.length && fbaInbounds.every((item) => pallets.has(item.inboundCode))) {
const strictDropship = preferredProducer?.fulfillmentType === 'ONE_CLICK_DROPSHIP'
if (!strictDropship && fbaInbounds.length && fbaInbounds.every((item) => pallets.has(item.inboundCode))) {
const firstFba = fbaInbounds[0]
const fbaCarton = cartonFixtures.find((item) => item.resolved.destinationType === 'FBA'
&& item.inbound.inboundCode === firstFba.inboundCode)
@@ -681,10 +713,10 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
for (const [index, inbound] of fbaInbounds.entries()) {
await receiveFba(context, token, inbound, pallets.get(inbound.inboundCode), cartonFixtures, index)
}
} else if (preferredProducer?.destinationType !== 'OVERSEAS') {
} else if (!strictDropship && preferredProducer?.destinationType !== 'OVERSEAS') {
blockerGap(context, 'FBA按箱数链', '当前FBA柜没有带真实托盘的 FBA 入仓码')
}
if (overseasInbounds.length && overseasInbounds.every((item) => pallets.has(item.inboundCode))) {
if (!strictDropship && overseasInbounds.length && overseasInbounds.every((item) => pallets.has(item.inboundCode))) {
const firstOverseas = overseasInbounds[0]
const pallet = pallets.get(firstOverseas.inboundCode)
await expected(context, token, 'POST', 'OVERSEAS模式硬拒绝批量收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', {
@@ -692,9 +724,32 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
packageCount: 1, idempotencyKey: key(context, 'overseas-bulk-forbidden'),
}, (response) => a0409Message(response, /^OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden$/))
await receiveOverseas(context, token, overseasInbounds, pallets, cartonFixtures)
} else if (preferredProducer?.destinationType !== 'FBA') {
} else if (!strictDropship && preferredProducer?.destinationType !== 'FBA') {
blockerGap(context, 'OVERSEAS逐箱链', '当前OVERSEAS柜没有带真实冻结箱唛与托盘的 OVERSEAS 入仓码')
}
if (strictDropship) {
const dropshipInbounds = [...fbaInbounds, ...overseasInbounds]
const firstInbound = dropshipInbounds[0]
const pallet = firstInbound ? pallets.get(firstInbound.inboundCode) : null
if (!firstInbound || !pallet) {
blockerGap(context, '一件代发整托链', '当前一件代发柜没有带预留实体托盘的入仓码')
return
}
if (firstInbound.destinationType === 'OVERSEAS') {
await expected(context, token, 'POST', '一件代发硬拒绝逐箱收货',
'/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', {
cartonMark: cartonFixtures[0]?.cartonMark,
palletCode: pallet.palletCode, idempotencyKey: key(context, 'dropship-carton-forbidden'),
}, (response) => a0409Message(response, /^One-click dropship inbound must be operated by complete source pallet$/))
} else {
await expected(context, token, 'POST', '一件代发硬拒绝按箱数收货',
'/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', {
inboundCode: firstInbound.inboundCode, palletCode: pallet.palletCode,
packageCount: 1, idempotencyKey: key(context, 'dropship-bulk-forbidden'),
}, (response) => a0409Message(response, /^One-click dropship inbound must be picked by complete source pallet$/))
}
await receiveDropship(context, token, preferredProducer, dropshipInbounds)
}
const refreshed = await pda(context, token, 'GET', '回查柜内逐入仓码实收进度', '/api/pda/v1/last-leg/receiving-containers/current')
if (!success(refreshed.response) || Number(refreshed.data?.pendingPackages) !== 0) {
blockerGap(context, '完成当前柜', `柜内实收尚未精确核平;pendingPackages=${refreshed.data?.pendingPackages ?? 'missing'}`, 'receiving-incomplete', refreshed.response)