fix: complete phase4 receiving acceptance chain
This commit is contained in:
@@ -22,7 +22,7 @@ const MODULE_DEPENDENCIES = new Map([
|
||||
['phase3-loading-finalize', ['phase3-loading', 'phase3-evidence-gate']],
|
||||
['phase3-customs', ['phase3-loading-finalize']],
|
||||
['phase3-port-schedules', ['ocean-bookings']],
|
||||
['phase3-outbound-dispatch', ['phase3-customs']],
|
||||
['phase3-outbound-dispatch', ['phase3-customs', 'phase3-port-schedules', 'service-center-flow']],
|
||||
['phase3-outbound-aggregate', ['phase3-outbound-dispatch']],
|
||||
['phase3-dispatch-gate', ['phase3-outbound-dispatch']],
|
||||
['phase3-waiting-for-loading', ['phase3-outbound-dispatch']],
|
||||
@@ -870,6 +870,7 @@ export class AcceptanceRun {
|
||||
tenantId: fixture.tenantId,
|
||||
orderId: fixture.orderId || this.createdResources.orders?.orderId,
|
||||
inboundId: fixture.inboundId,
|
||||
boxCode: fixture.boxCode,
|
||||
customerRemark: fixtureOrder.customerRemark,
|
||||
internalRemark: fixtureOrder.internalRemark,
|
||||
cargoRemark: fixtureOrder.cargoRemark,
|
||||
@@ -888,6 +889,7 @@ export class AcceptanceRun {
|
||||
tenantId: incrementalSource.tenantId,
|
||||
orderId: incrementalSource.orderId,
|
||||
inboundId: incrementalSource.inboundId,
|
||||
boxCode: incrementalSource.boxCode,
|
||||
} : { marker: originalMarker, failed: true }
|
||||
this.createdResources.orchestrationFixtures = {
|
||||
...(this.createdResources.orchestrationFixtures || {}),
|
||||
|
||||
@@ -365,6 +365,7 @@ export async function run(context) {
|
||||
containerBookingId,
|
||||
sourceOrderId: orderId,
|
||||
sourceInboundId: inboundId,
|
||||
cartonMark: fixture.cartonMark || null,
|
||||
destinationType: fixture.destinationType || null,
|
||||
destinationWarehouseId: id(fixture.destinationWarehouseId),
|
||||
destinationWarehouseCode: fixture.destinationWarehouseCode || null,
|
||||
|
||||
@@ -1180,6 +1180,7 @@ export async function run(context) {
|
||||
|
||||
context.createdResources.phase3Loading = {
|
||||
tenantId, orderId: id(source?.orderId), inboundId, preloadPlanId: planId,
|
||||
cartonMark: source?.boxCode || source?.cartonMark || null,
|
||||
destinationType: initial?.summary?.destinationType || source?.destinationType || null,
|
||||
destinationWarehouseId: id(initial?.summary?.destinationWarehouseId || source?.destinationWarehouseId),
|
||||
destinationWarehouseCode: initial?.summary?.destinationCode || source?.destinationWarehouseCode || null,
|
||||
|
||||
@@ -29,7 +29,8 @@ test('focused carrier node execution injects the complete outbound-confirm produ
|
||||
const modules = normalizeModuleOrder(['phase4-carrier-nodes'])
|
||||
const required = ['merchant-qualification-fixture', 'orders', 'inbound-measurement',
|
||||
'phase3-container-loading', 'phase3-loading', 'phase3-pda-convergence', 'phase3-evidence-gate',
|
||||
'phase3-loading-finalize', 'phase3-customs', 'phase3-outbound-dispatch',
|
||||
'phase3-loading-finalize', 'phase3-customs', 'ocean-bookings', 'phase3-port-schedules',
|
||||
'truck-appointments', 'service-center-flow', 'phase3-outbound-dispatch',
|
||||
'phase3-carrier-schedule-queries', 'phase4-carrier-nodes']
|
||||
for (const module of required) assert.ok(modules.includes(module), module)
|
||||
for (let index = 1; index < required.length; index += 1) {
|
||||
|
||||
@@ -15,8 +15,8 @@ function conflictMessage(response, pattern) { return conflict(response) && patte
|
||||
function notFound(response) { return (response?.http === 404 || (response?.http === 200 && ['404', 'A0404'].includes(code(response)))) && traced(response) }
|
||||
function forbidden(response) { return [401, 403].includes(response?.http) || (response?.http === 200 && ['A0401', 'A0403'].includes(code(response))) }
|
||||
function trace(response) { return `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` }
|
||||
function key(context, action, suffix = '') { return `${context.marker}-${action}-${suffix}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 120) }
|
||||
function markerSuffix(context) { return String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-16).toUpperCase() }
|
||||
function key(context, action, suffix = '') { return `${context.marker}-${context.pdaContainerSuffix || ''}-${action}-${suffix}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 120) }
|
||||
function markerSuffix(context) { return `${context.marker}${context.pdaContainerSuffix || ''}`.replace(/[^A-Za-z0-9]/g, '').slice(-16).toUpperCase() }
|
||||
|
||||
function blockerGap(context, name, reason, category = 'fixture-unavailable', response = null) {
|
||||
context.addCoverageGap(MODULE, SCENARIO, name, reason, category, { deploymentBlocker: true })
|
||||
@@ -183,8 +183,8 @@ async function receiveFba(context, token, inbound, pallet) {
|
||||
return true
|
||||
}
|
||||
|
||||
async function receiveOverseas(context, token, inbound, pallet) {
|
||||
const cartonMark = context.createdResources?.inboundMeasurement?.boxCode
|
||||
async function receiveOverseas(context, token, inbound, pallet, producerCartonMark = null) {
|
||||
const cartonMark = producerCartonMark || context.createdResources?.inboundMeasurement?.boxCode
|
||||
if (!nonBlank(cartonMark)) {
|
||||
blockerGap(context, `OVERSEAS入仓码${inbound.inboundCode}逐箱`, '缺少本轮冻结箱唛,逐箱实收不能使用伪造箱号')
|
||||
return false
|
||||
@@ -207,12 +207,12 @@ async function receiveOverseas(context, token, inbound, pallet) {
|
||||
return true
|
||||
}
|
||||
|
||||
async function runReceiving(context, token, secondaryToken, merchantToken) {
|
||||
if (context.overseasToken) {
|
||||
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)
|
||||
context.addStep(MODULE, '平台管理员海外令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS)
|
||||
}
|
||||
if (merchantToken) {
|
||||
if (runAuthorizationProbes && merchantToken) {
|
||||
const denied = await context.requestOverseas('GET', '/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, merchantToken)
|
||||
context.addStep(MODULE, '客户海外令牌不能冒充仓库PDA操作员', denied, forbidden(denied) && traced(denied), trace(denied), OPTIONS)
|
||||
}
|
||||
@@ -229,8 +229,16 @@ async function runReceiving(context, token, secondaryToken, merchantToken) {
|
||||
blockerGap(context, 'PDA柜号候选', `当前客户没有由三期装柜完成事件生成的待收入仓计划;${trace(candidates.response)}`, 'producer-chain', candidates.response)
|
||||
return
|
||||
}
|
||||
const preferredContainer = context.createdResources?.phase3Loading?.containerNo
|
||||
const candidate = candidates.data.find((item) => item.containerNo === preferredContainer) || candidates.data[0]
|
||||
const preferredContainer = id(preferredProducer?.containerNo)
|
||||
|| context.createdResources?.phase3Loading?.containerNo
|
||||
const candidate = preferredContainer
|
||||
? candidates.data.find((item) => item.containerNo === preferredContainer)
|
||||
: candidates.data[0]
|
||||
if (!candidate) {
|
||||
blockerGap(context, `本轮柜号${preferredContainer || 'missing'}候选`,
|
||||
'三期装柜完成生产者已落地,但海外候选尚未出现该柜;禁止回退处理历史或其他柜', 'producer-chain', candidates.response)
|
||||
return
|
||||
}
|
||||
const containerNo = id(candidate?.containerNo)
|
||||
context.assert(MODULE, '柜号候选来自本轮生产链且包含进度', candidates.response,
|
||||
nonBlank(containerNo) && Number(candidate?.totalPackages) > 0 && Number(candidate?.receivedPackages) >= 0,
|
||||
@@ -300,7 +308,7 @@ async function runReceiving(context, token, secondaryToken, merchantToken) {
|
||||
inboundCode: overseasInbound.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)
|
||||
await receiveOverseas(context, token, overseasInbound, pallet, preferredProducer?.cartonMark)
|
||||
} else blockerGap(context, 'OVERSEAS逐箱链', '当前柜没有带真实冻结箱唛与托盘的 OVERSEAS 入仓码')
|
||||
const refreshed = await pda(context, token, 'GET', '回查柜内逐入仓码实收进度', '/api/pda/v1/last-leg/receiving-containers/current')
|
||||
if (!success(refreshed.response) || Number(refreshed.data?.pendingPackages) !== 0) {
|
||||
@@ -321,7 +329,7 @@ async function runReceiving(context, token, secondaryToken, merchantToken) {
|
||||
export async function run(context) {
|
||||
context.createdResources.phase4OverseasPda = {
|
||||
marker: context.marker, containerNo: null, customerId: null,
|
||||
palletIds: [], locationIds: [], finalStatus: null, pickerMembershipIds: [],
|
||||
palletIds: [], locationIds: [], finalStatus: null, pickerMembershipIds: [], completedContainers: [],
|
||||
overseasWarehouseId: null, overseasWarehouseName: null,
|
||||
cleanup: 'Shared warehouse, pallets and locations used by completed inventory are retained as acceptance evidence',
|
||||
}
|
||||
@@ -345,8 +353,20 @@ export async function run(context) {
|
||||
blockerGap(context, '海外PDA双操作员账号', '未能创建并登录两个WAREHOUSE_PICKER平台账号,无法验证全局一柜锁')
|
||||
return
|
||||
}
|
||||
await runReceiving(context, primary.token, secondary.token, merchantToken)
|
||||
const matrix = context.createdResources?.phase4InboundProducerFixtures
|
||||
const producers = [matrix?.byDestinationType?.FBA?.[0], matrix?.byDestinationType?.OVERSEAS?.[0]]
|
||||
.filter((item) => nonBlank(item?.containerNo))
|
||||
for (const [index, producer] of (producers.length ? producers : [null]).entries()) {
|
||||
context.pdaContainerSuffix = producer?.containerNo || `CONTAINER-${index + 1}`
|
||||
context.createdResources.phase4OverseasPda.finalStatus = null
|
||||
await runReceiving(context, primary.token, secondary.token,
|
||||
index === 0 ? merchantToken : null, producer, index === 0)
|
||||
if (context.createdResources.phase4OverseasPda.finalStatus === 'COMPLETED') {
|
||||
context.createdResources.phase4OverseasPda.completedContainers.push(producer?.containerNo || null)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
delete context.pdaContainerSuffix
|
||||
await disablePicker(context, secondary, 2)
|
||||
await disablePicker(context, primary, 1)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ function contextFixture({ tenants = [{ id: '10001', status: 'ACTIVE' }], warehou
|
||||
if (requestPath.includes('/last-leg/orders/30001?')) return response({ order: { id: '30001' }, cargo: [], statusHistory: [] })
|
||||
if (requestPath.startsWith('/api/platform/v1/last-leg/relabel-applications?')) return response(page([{ id: '40001', relabelNo: 'RL-1', status: 'PENDING_PROCESSING' }]))
|
||||
if (requestPath.includes('/relabel-applications/RL-1?')) return response({ application: { relabelNo: 'RL-1' }, items: [], history: [] })
|
||||
if (requestPath.startsWith('/api/platform/v1/last-leg/inbound-plans?')) return response(page([{ id: '50001', sourceOrderId: '30001', planNo: 'IP-1', customerId: '10001', status: 'PENDING_ASSIGNMENT' }]))
|
||||
if (requestPath.startsWith('/api/platform/v1/last-leg/inbound-plans?')) return response(page([{ lineId: '50002', planId: '50001', orderId: '30001', planNo: 'IP-1', customerId: '10001', status: 'PENDING_ASSIGNMENT' }]))
|
||||
if (requestPath.includes('/inbound-plans/IP-1?')) return response({ planNo: 'IP-1', sourceOrderId: '30001', lines: [], pallets: [], allocations: [] })
|
||||
if (requestPath.includes('/inventory/summary?')) return response({ inboundOrderCount: 1, palletCount: 1, totalPackages: 10, availablePackages: 10, holdPackages: 0, inTransitPackages: 0, receivingPackages: 0, procurementPackages: 0 })
|
||||
if (requestPath.includes('/inventory/balances?')) return response(page([{ id: '60001', customerId: '10001', skuCode: 'SKU-1', totalPackages: 10, availablePackages: 10, updatedAt: '2026-08-21T10:00:00' }]))
|
||||
@@ -145,7 +145,9 @@ test('合同覆盖库存CSV并明确排除暂缓、离仓、PDA与内部路径',
|
||||
for (const forbidden of ['/cartons', '/products', '/outbound', '/dispatch', '/delivery', '/pda', '/internal']) assert.equal(source.includes(forbidden), false, forbidden)
|
||||
assert.match(source, /requestId=/)
|
||||
assert.match(source, /traceId=/)
|
||||
assert.match(source, /longId\(plan\.sourceOrderId\)/)
|
||||
for (const field of ['lineId', 'planId', 'orderId']) {
|
||||
assert.match(source, new RegExp(`longId\\(plan\\.${field}\\)`))
|
||||
}
|
||||
assert.match(source, /id\(pallet\.customerId\) === '0'/)
|
||||
assert.match(source, /创建本轮换标单/)
|
||||
assert.match(source, /换标单同幂等键不同请求返回A0409/)
|
||||
|
||||
@@ -107,8 +107,9 @@ export async function run(context) {
|
||||
const plan = records(plans.data)[0]
|
||||
if (plan) {
|
||||
context.assert(MODULE, '入仓计划关键字段正确', plans.response,
|
||||
longId(plan.id) && longId(plan.sourceOrderId) && nonBlank(plan.planNo) && id(plan.customerId) === customerId && nonBlank(plan.status),
|
||||
`id=${id(plan.id) || 'missing'}, sourceOrderId=${id(plan.sourceOrderId) || 'missing'}, planNo=${plan.planNo || 'missing'}, status=${plan.status || 'missing'}; ${trace(plans.response)}`, OPTIONS)
|
||||
longId(plan.lineId) && longId(plan.planId) && longId(plan.orderId)
|
||||
&& nonBlank(plan.planNo) && id(plan.customerId) === customerId && nonBlank(plan.status),
|
||||
`lineId=${id(plan.lineId) || 'missing'}, planId=${id(plan.planId) || 'missing'}, orderId=${id(plan.orderId) || 'missing'}, planNo=${plan.planNo || 'missing'}, status=${plan.status || 'missing'}; ${trace(plans.response)}`, OPTIONS)
|
||||
const detail = await overseasGet(context, '查询入仓计划详情', query(`/api/platform/v1/last-leg/inbound-plans/${encodeURIComponent(plan.planNo)}`, { customerId }))
|
||||
context.assert(MODULE, '入仓计划详情对应列表记录', detail.response,
|
||||
detail.data?.planNo === plan.planNo && longId(detail.data?.sourceOrderId)
|
||||
|
||||
Reference in New Issue
Block a user