This commit is contained in:
2026-08-27 17:16:33 +08:00
parent 61a97cd057
commit 76c24867a4
19 changed files with 474 additions and 107 deletions
@@ -1,9 +1,9 @@
import {
CONTAINER_CONVERT_ENDPOINT,
CONTAINER_CONVERT_EXPECTED_ORDERS,
CONTAINER_SUMMARY_ENDPOINT,
validContainerBatchResult,
validOrderFact,
validPdaCandidate,
validContainerSummaryPage,
zeroDrift,
} from './container-inbound-plan-batch-contract.mjs'
@@ -15,15 +15,15 @@ const success = (response) => response?.http === 200 && String(response?.body?.c
const trace = (response) => `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
const rejected = (response, codes) => response?.http === 200 && codes.includes(String(response?.body?.code))
function gap(context, name, reason, priority = 'P0') {
context.addCoverageGap(MODULE, MODULE, name, reason, 'fixture', { deploymentBlocker: priority === 'P0' })
function gap(context, name, reason, priority = 'P1') {
context.addCoverageGap(MODULE, MODULE, name, reason, 'fixture', { deploymentBlocker: true })
context.addStep(MODULE, name, null, false, `${priority}: ${reason}`, OPTIONS)
throw new Error(`${priority} blocking coverage gap: ${name}: ${reason}`)
}
function fixture(context) {
const value = context.createdResources?.phase4ContainerInboundPlanFixture
if (value?.marker !== context.marker || !value?.normal?.containerNo
if (value?.marker !== context.marker || !value?.normal?.containerNo || !value?.normal?.sourceContainerBookingId
|| !Array.isArray(value.normal.orders) || value.normal.orders.length !== CONTAINER_CONVERT_EXPECTED_ORDERS) {
gap(context, '整柜15票新数据前置', '缺少本轮marker的15票同柜正常fixture,禁止借用历史已转换数据')
}
@@ -32,7 +32,6 @@ function fixture(context) {
gap(context, `${name}整柜前置`, `缺少本轮${name}专用fixture,不能验证整批零漂移`, 'P1')
}
}
if (!value.pdaToken) gap(context, 'PDA候选Token前置', '缺少绑定真实海外仓的WAREHOUSE_PICKER海外Token')
return value
}
@@ -63,32 +62,33 @@ async function snapshot(context, source) {
}
function convertBody(source) {
const anchor = source.orders[0]
return { orderId: id(anchor.orderId), customerId: id(anchor.customerId) }
return { sourceContainerBookingId: id(source.sourceContainerBookingId) }
}
async function convert(context, name, source, key) {
const response = await context.requestOverseas('POST', CONTAINER_CONVERT_ENDPOINT,
convertBody(source), {}, context.overseasToken, { 'Idempotency-Key': key })
convertBody(source), { 'Idempotency-Key': key }, context.overseasToken)
context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS)
return response
}
export async function run(context) {
const data = fixture(context)
const summaryResponse = await context.requestOverseas('GET',
`${CONTAINER_SUMMARY_ENDPOINT}?page=1&size=10&containerNo=${encodeURIComponent(data.normal.containerNo)}`)
context.assert(MODULE, 'P1服务端柜级摘要跨订单分页仍返回15票', summaryResponse,
success(summaryResponse) && validContainerSummaryPage(summaryResponse.body?.data, {
sourceContainerBookingId: data.normal.sourceContainerBookingId,
containerNo: data.normal.containerNo,
orderCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
inboundCodeCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
}), `orderPageSize=10, expectedOrderCount=15; ${trace(summaryResponse)}`, OPTIONS)
const normalList = await listOrders(context, data.normal.containerNo)
context.assert(MODULE, 'P0同柜15票列表前置', normalList.response,
context.assert(MODULE, 'P1同柜15票列表前置', normalList.response,
success(normalList.response) && normalList.rows.length === CONTAINER_CONVERT_EXPECTED_ORDERS
&& normalList.rows.every((row) => typeof row.id === 'string' && typeof row.customerId === 'string'),
`orders=${normalList.rows.length}; ${trace(normalList.response)}`, OPTIONS)
for (const expected of data.normal.orders) {
const actual = normalList.rows.find((row) => id(row.id) === id(expected.orderId))
context.assert(MODULE, `P2订单事实不猜算:${id(expected.orderId)}`, normalList.response,
Boolean(actual) && validOrderFact(actual, expected),
`customerId=${id(expected.customerId)}, inboundCode=${expected.inboundCode || 'missing'}`, { ...OPTIONS, deploymentBlocker: false })
}
const invalidBefore = await snapshot(context, data.invalid)
const invalid = await convert(context, 'P1任一非法票整柜转换拒绝', data.invalid, `${context.marker}-invalid-container`)
const invalidAfter = await snapshot(context, data.invalid)
@@ -103,9 +103,9 @@ export async function run(context) {
rejected(partial, ['A0409']) && zeroDrift(partialBefore, partialAfter), trace(partial), OPTIONS)
const key = `${context.marker}-container-convert`
const first = await convert(context, 'P0整柜一次转换并逐订单生成独立计划', data.normal, key)
const first = await convert(context, 'P1整柜一次转换并逐订单生成独立计划', data.normal, key)
const result = first?.body?.data
context.assert(MODULE, 'P0整柜响应覆盖全部订单和客户计划', first,
context.assert(MODULE, 'P1整柜响应覆盖全部订单和独立计划', first,
success(first) && validContainerBatchResult(result, data.normal.orders), trace(first), OPTIONS)
const replay = await convert(context, 'P1同键同请求重放', data.normal, key)
@@ -122,30 +122,9 @@ export async function run(context) {
concurrent.map(trace).join('; '), OPTIONS)
const after = await listOrders(context, data.normal.containerNo)
context.assert(MODULE, 'P0全部15票推进INBOUND_PLANNED', after.response,
context.assert(MODULE, 'P1全部15票推进INBOUND_PLANNED', after.response,
success(after.response) && after.rows.length === CONTAINER_CONVERT_EXPECTED_ORDERS
&& after.rows.every((row) => row.currentStatus === 'INBOUND_PLANNED' && row.inboundOrderNo),
`planned=${after.rows.filter((row) => row.currentStatus === 'INBOUND_PLANNED').length}; ${trace(after.response)}`, OPTIONS)
const candidateResponse = await context.requestOverseas('GET',
'/api/pda/v1/last-leg/receiving-containers/candidates', undefined, {}, data.pdaToken)
const candidate = Array.isArray(candidateResponse?.body?.data)
? candidateResponse.body.data.find((row) => row?.containerNo === data.normal.containerNo) : null
context.assert(MODULE, 'P0同柜PDA候选计划数与入仓码数一致', candidateResponse,
success(candidateResponse) && validPdaCandidate(candidate, {
containerNo: data.normal.containerNo,
planCount: data.normal.orders.length,
inboundCodeCount: data.normal.orders.length,
}), trace(candidateResponse), OPTIONS)
const carton = data.normal.orders.flatMap((order) => order.cartonMarks || [])
.map((cartonMark) => ({ cartonMark })).at(0)
if (!carton) gap(context, 'P3箱码命中fixture', '正常整柜没有可用于resolve的冻结箱码', 'P3')
const prepared = data.preparedSession
if (!prepared) gap(context, 'P3箱码命中会话', '需先由PDA合同完成prepare+三照片+sign,再执行resolve', 'P3')
const resolved = await context.requestOverseas('POST',
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', carton, {}, data.pdaToken)
context.assert(MODULE, 'P3箱码服务端命中当前柜唯一入仓码', resolved,
success(resolved) && data.normal.orders.some((order) => order.inboundCode === resolved?.body?.data?.inboundCode),
trace(resolved), { ...OPTIONS, deploymentBlocker: false })
}