test: complete overseas carton and sailing fixtures

This commit is contained in:
2026-08-24 23:28:24 +08:00
parent 985d952e54
commit 615ecb4ba1
6 changed files with 48 additions and 25 deletions
@@ -366,6 +366,7 @@ export async function run(context) {
sourceOrderId: orderId,
sourceInboundId: inboundId,
cartonMark: fixture.cartonMark || null,
cartonMarks: Array.isArray(fixture.cartonMarks) ? fixture.cartonMarks : [],
destinationType: fixture.destinationType || null,
destinationWarehouseId: id(fixture.destinationWarehouseId),
destinationWarehouseCode: fixture.destinationWarehouseCode || null,
@@ -1213,6 +1213,8 @@ export async function run(context) {
context.createdResources.phase3Loading = {
tenantId, orderId: id(source?.orderId), inboundId, preloadPlanId: planId,
cartonMark: source?.boxCode || source?.cartonMark || null,
cartonMarks: [...new Set((stagedTask?.items || []).flatMap((item) => item?.boxCodes || [])
.map((value) => String(value || '').trim()).filter(Boolean))],
destinationType: initial?.summary?.destinationType || source?.destinationType || null,
destinationWarehouseId: id(initial?.summary?.destinationWarehouseId || source?.destinationWarehouseId),
destinationWarehouseCode: initial?.summary?.destinationCode || source?.destinationWarehouseCode || null,
@@ -122,8 +122,7 @@ export async function run(context) {
|| context.createdResources?.phase3ContainerLoading
const ocean = await ensureOceanBooking(context, suffix, master, flow?.oceanBookingId)
if (!ocean?.id) return
const containerBookingId = ['COMPLETED', 'LOADED'].includes(flow?.finalStatus)
? id(flow.containerBookingId) : null
const containerBookingId = id(flow?.containerBookingId) || null
const knownAllocation = containerBookingId
&& ocean.allocatedContainerBookingIds?.includes(containerBookingId) ? containerBookingId : null
if (containerBookingId && !knownAllocation) {
@@ -401,6 +401,8 @@ test('preload and loading versions use immediate operation baselines and exact p
assert.match(loading, /shippingCompanyId: id\(shippingCarrier\.id\), shippingCompanyName: shippingCarrier\.name/)
assert.match(loading, /bookingAgentId: id\(supplier\.id\), bookingAgentName: supplier\.name/)
assert.doesNotMatch(loading, /shippingCompanyId: id\(supplier\.id\), shippingCompanyName: supplier\.name/)
assert.match(loading, /cartonMarks: \[\.\.\.new Set\(\(stagedTask\?\.items \|\| \[\]\)\.flatMap/)
assert.match(source('./phase3-loading-finalize.mjs'), /cartonMarks: Array\.isArray\(fixture\.cartonMarks\)/)
})
test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING audit with operator metadata', () => {
@@ -510,6 +512,8 @@ test('downstream container consumers prefer the phase3Loading fixture', () => {
assert.doesNotMatch(serviceCenter, /customerName: `服务中心链路客户-/)
const schedules = source('./phase3-port-schedules.mjs')
assert.match(schedules, /ensureOceanBooking\(context, suffix, master, flow\?\.oceanBookingId\)/)
assert.match(schedules, /const containerBookingId = id\(flow\?\.containerBookingId\) \|\| null/)
assert.doesNotMatch(schedules, /\['COMPLETED', 'LOADED'\]\.includes\(flow\?\.finalStatus\)/)
assert.match(schedules, /allocatedContainerBookingIds: \(detail\.containers \|\| \[\]\)/)
assert.match(schedules, /ocean\.allocatedContainerBookingIds\?\.includes\(containerBookingId\)/)
assert.match(schedules, /if \(containerBookingId && !knownAllocation\)/)
@@ -43,6 +43,8 @@ test('PDA module covers the container-number receiving contract and excludes out
assert.doesNotMatch(source, /const warehouseId = id\(inbound\?\.destinationWarehouseId\)/)
assert.match(source, /目的仓 .* 不得代替物理仓/)
assert.match(source, /retained as acceptance evidence/)
assert.match(source, /preferredProducer\?\.cartonMarks\?\.length/)
assert.match(source, /for \(const \[index, cartonMark\] of cartonMarks\.entries\(\)\)/)
})
test('missing WAREHOUSE_PICKER overseas authority is a deployment blocker', async () => {
@@ -202,27 +202,40 @@ async function receiveFba(context, token, inbound, pallet) {
return true
}
async function receiveOverseas(context, token, inbound, pallet, producerCartonMark = null) {
const cartonMark = producerCartonMark || context.createdResources?.inboundMeasurement?.boxCode
if (!nonBlank(cartonMark)) {
blockerGap(context, `OVERSEAS入仓码${inbound.inboundCode}逐箱`, '缺少本轮冻结箱唛,逐箱实收不能使用伪造箱号')
async function receiveOverseas(context, token, inbounds, pallets, producerCartonMarks = []) {
const cartonMarks = [...new Set((Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks])
.map(id).filter(nonBlank))]
if (!cartonMarks.length) {
blockerGap(context, 'OVERSEAS逐箱链', '缺少本轮PDA任务冻结箱唛,逐箱实收不能使用伪造箱号')
return false
}
const resolved = await pda(context, token, 'POST', '解析OVERSEAS冻结箱唛', '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark })
if (!success(resolved.response) || resolved.data?.inboundCode !== inbound.inboundCode) {
blockerGap(context, `OVERSEAS入仓码${inbound.inboundCode}箱唛归属`, `冻结箱唛未解析到目标入仓码;cartonMark=${cartonMark}`, 'fixture-mismatch', resolved.response)
return false
for (const [index, cartonMark] of cartonMarks.entries()) {
const resolved = await pda(context, token, 'POST', `解析OVERSEAS冻结箱唛${index + 1}`,
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark })
const inbound = inbounds.find((item) => item.inboundCode === resolved.data?.inboundCode)
const pallet = inbound ? pallets.get(inbound.inboundCode) : null
if (!success(resolved.response) || !inbound || !pallet) {
blockerGap(context, `OVERSEAS箱唛${cartonMark}归属`,
`冻结箱唛未解析到带真实托盘的本柜入仓码;inboundCode=${resolved.data?.inboundCode || 'missing'}`,
'fixture-mismatch', resolved.response)
return false
}
const body = { cartonMark, palletCode: pallet.palletCode,
idempotencyKey: key(context, 'overseas-carton', cartonMark) }
const first = await pda(context, token, 'POST', `OVERSEAS逐箱实收${index + 1}`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body)
if (!success(first.response)) return false
const replay = await pda(context, token, 'POST', `OVERSEAS逐箱${index + 1}同键同载荷重放`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body)
context.assert(MODULE, `OVERSEAS逐箱${index + 1}幂等结果一致`, replay.response,
success(replay.response) && JSON.stringify(replay.data) === JSON.stringify(first.data), trace(replay.response), OPTIONS)
await expected(context, token, 'POST', `OVERSEAS逐箱${index + 1}同键异载荷冲突`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/carton',
{ ...body, palletCode: `${pallet.palletCode}-MUTATED` }, conflict)
await expected(context, token, 'POST', `OVERSEAS重复箱新键被拒绝${index + 1}`,
'/api/pda/v1/last-leg/receiving-containers/current/receipts/carton',
{ ...body, idempotencyKey: key(context, 'overseas-duplicate', cartonMark) }, conflict)
}
const body = { cartonMark, palletCode: pallet.palletCode, idempotencyKey: key(context, 'overseas-carton') }
const first = await pda(context, token, 'POST', 'OVERSEAS逐箱实收', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body)
if (!success(first.response)) return false
const replay = await pda(context, token, 'POST', 'OVERSEAS逐箱同键同载荷重放', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body)
context.assert(MODULE, 'OVERSEAS逐箱幂等结果一致', replay.response,
success(replay.response) && JSON.stringify(replay.data) === JSON.stringify(first.data), trace(replay.response), OPTIONS)
await expected(context, token, 'POST', 'OVERSEAS逐箱同键异载荷冲突', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton',
{ ...body, palletCode: `${pallet.palletCode}-MUTATED` }, conflict)
await expected(context, token, 'POST', 'OVERSEAS重复箱新键被拒绝', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton',
{ ...body, idempotencyKey: key(context, 'overseas-duplicate') }, conflict)
return true
}
@@ -312,7 +325,7 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
success(resolved.response) && resolved.data?.compatible === true && resolved.data?.destinationType === inbound.destinationType, trace(resolved.response), OPTIONS)
}
const fbaInbounds = current.data.inbounds.filter((item) => item.destinationType === 'FBA')
const overseasInbound = current.data.inbounds.find((item) => item.destinationType === 'OVERSEAS')
const overseasInbounds = current.data.inbounds.filter((item) => item.destinationType === 'OVERSEAS')
if (fbaInbounds.length && fbaInbounds.every((item) => pallets.has(item.inboundCode))) {
const firstFba = fbaInbounds[0]
await expected(context, token, 'POST', 'FBA模式硬拒绝逐箱收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', {
@@ -323,13 +336,15 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
} else if (preferredProducer?.destinationType !== 'OVERSEAS') {
blockerGap(context, 'FBA SKU按箱数链', '当前FBA柜没有带真实托盘的 FBA 入仓码')
}
if (overseasInbound && pallets.has(overseasInbound.inboundCode)) {
const pallet = pallets.get(overseasInbound.inboundCode)
if (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', {
inboundCode: overseasInbound.inboundCode, skuCode: 'MODE-REJECT', palletCode: pallet.palletCode,
inboundCode: firstOverseas.inboundCode, skuCode: 'MODE-REJECT', palletCode: pallet.palletCode,
packageCount: 1, idempotencyKey: key(context, 'overseas-bulk-forbidden'),
}, (response) => conflictMessage(response, /OVERSEAS.*carton|bulk.*forbidden/i))
await receiveOverseas(context, token, overseasInbound, pallet, preferredProducer?.cartonMark)
await receiveOverseas(context, token, overseasInbounds, pallets,
preferredProducer?.cartonMarks?.length ? preferredProducer.cartonMarks : [preferredProducer?.cartonMark])
} else if (preferredProducer?.destinationType !== 'FBA') {
blockerGap(context, 'OVERSEAS逐箱链', '当前OVERSEAS柜没有带真实冻结箱唛与托盘的 OVERSEAS 入仓码')
}