2026-08-27 16:34:30 +08:00
|
|
|
import {
|
|
|
|
|
CONTAINER_CONVERT_ENDPOINT,
|
|
|
|
|
CONTAINER_CONVERT_EXPECTED_ORDERS,
|
2026-08-27 17:16:33 +08:00
|
|
|
CONTAINER_SUMMARY_ENDPOINT,
|
2026-08-27 16:34:30 +08:00
|
|
|
validContainerBatchResult,
|
2026-08-27 17:16:33 +08:00
|
|
|
validContainerSummaryPage,
|
2026-08-27 16:34:30 +08:00
|
|
|
zeroDrift,
|
|
|
|
|
} from './container-inbound-plan-batch-contract.mjs'
|
|
|
|
|
|
|
|
|
|
const MODULE = 'phase4-container-inbound-plan-batch'
|
|
|
|
|
const OPTIONS = { scenario: MODULE, deploymentBlocker: true }
|
|
|
|
|
const id = (value) => value == null ? '' : String(value)
|
|
|
|
|
const records = (data) => Array.isArray(data?.records) ? data.records : Array.isArray(data) ? data : []
|
|
|
|
|
const success = (response) => response?.http === 200 && String(response?.body?.code) === '200'
|
|
|
|
|
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))
|
|
|
|
|
|
2026-08-27 17:16:33 +08:00
|
|
|
function gap(context, name, reason, priority = 'P1') {
|
|
|
|
|
context.addCoverageGap(MODULE, MODULE, name, reason, 'fixture', { deploymentBlocker: true })
|
2026-08-27 16:34:30 +08:00
|
|
|
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
|
2026-08-27 17:16:33 +08:00
|
|
|
if (value?.marker !== context.marker || !value?.normal?.containerNo || !value?.normal?.sourceContainerBookingId
|
2026-08-27 16:34:30 +08:00
|
|
|
|| !Array.isArray(value.normal.orders) || value.normal.orders.length !== CONTAINER_CONVERT_EXPECTED_ORDERS) {
|
|
|
|
|
gap(context, '整柜15票新数据前置', '缺少本轮marker的15票同柜正常fixture,禁止借用历史已转换数据')
|
|
|
|
|
}
|
|
|
|
|
for (const name of ['invalid', 'partial']) {
|
|
|
|
|
if (!value[name]?.containerNo || !Array.isArray(value[name]?.orders)) {
|
|
|
|
|
gap(context, `${name}整柜前置`, `缺少本轮${name}专用fixture,不能验证整批零漂移`, 'P1')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function listOrders(context, containerNo) {
|
|
|
|
|
const response = await context.requestOverseas('GET',
|
|
|
|
|
`/api/platform/v1/last-leg/orders?page=1&size=200&containerNo=${encodeURIComponent(containerNo)}`)
|
|
|
|
|
return { response, rows: records(response?.body?.data).filter((row) => row?.containerNo === containerNo) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function snapshot(context, source) {
|
|
|
|
|
const listed = await listOrders(context, source.containerNo)
|
|
|
|
|
const plans = []
|
|
|
|
|
for (const order of listed.rows) {
|
|
|
|
|
if (!order?.inboundCode) continue
|
|
|
|
|
const response = await context.requestOverseas('GET',
|
|
|
|
|
`/api/platform/v1/last-leg/inbound-plans?page=1&size=200&inboundCode=${encodeURIComponent(order.inboundCode)}`)
|
|
|
|
|
plans.push(...records(response?.body?.data).map((row) => ({
|
|
|
|
|
planId: id(row.planId), planNo: row.planNo, orderId: id(row.orderId), customerId: id(row.customerId), status: row.status,
|
|
|
|
|
})))
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
orders: listed.rows.map((row) => ({
|
|
|
|
|
id: id(row.id), customerId: id(row.customerId), currentStatus: row.currentStatus,
|
|
|
|
|
inboundOrderNo: row.inboundOrderNo ?? null, palletCount: row.palletCount ?? null,
|
|
|
|
|
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
|
|
|
plans: plans.sort((a, b) => `${a.planId}:${a.orderId}`.localeCompare(`${b.planId}:${b.orderId}`)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function convertBody(source) {
|
2026-08-27 17:16:33 +08:00
|
|
|
return { sourceContainerBookingId: id(source.sourceContainerBookingId) }
|
2026-08-27 16:34:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function convert(context, name, source, key) {
|
|
|
|
|
const response = await context.requestOverseas('POST', CONTAINER_CONVERT_ENDPOINT,
|
2026-08-27 17:16:33 +08:00
|
|
|
convertBody(source), { 'Idempotency-Key': key }, context.overseasToken)
|
2026-08-27 16:34:30 +08:00
|
|
|
context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS)
|
|
|
|
|
return response
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function run(context) {
|
|
|
|
|
const data = fixture(context)
|
2026-08-27 17:16:33 +08:00
|
|
|
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,
|
2026-08-28 13:58:49 +08:00
|
|
|
totalOrderCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
|
2026-08-27 17:16:33 +08:00
|
|
|
inboundCodeCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
|
2026-08-28 13:58:49 +08:00
|
|
|
missingInboundCodeCount: 0,
|
|
|
|
|
planCount: 0,
|
|
|
|
|
plannedInboundCodeCount: 0,
|
|
|
|
|
pendingPlanCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
|
|
|
|
|
allPlansReady: false,
|
2026-08-27 17:16:33 +08:00
|
|
|
}), `orderPageSize=10, expectedOrderCount=15; ${trace(summaryResponse)}`, OPTIONS)
|
2026-08-27 16:34:30 +08:00
|
|
|
const normalList = await listOrders(context, data.normal.containerNo)
|
2026-08-27 17:16:33 +08:00
|
|
|
context.assert(MODULE, 'P1同柜15票列表前置', normalList.response,
|
2026-08-27 16:34:30 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
context.assert(MODULE, 'P1非法票整批零漂移', invalid,
|
|
|
|
|
rejected(invalid, ['A0400', 'A0409']) && zeroDrift(invalidBefore, invalidAfter),
|
|
|
|
|
trace(invalid), OPTIONS)
|
|
|
|
|
|
|
|
|
|
const partialBefore = await snapshot(context, data.partial)
|
|
|
|
|
const partial = await convert(context, 'P1部分已转换整柜冲突', data.partial, `${context.marker}-partial-container`)
|
|
|
|
|
const partialAfter = await snapshot(context, data.partial)
|
|
|
|
|
context.assert(MODULE, 'P1部分转换冲突零漂移', partial,
|
|
|
|
|
rejected(partial, ['A0409']) && zeroDrift(partialBefore, partialAfter), trace(partial), OPTIONS)
|
|
|
|
|
|
|
|
|
|
const key = `${context.marker}-container-convert`
|
2026-08-27 17:16:33 +08:00
|
|
|
const first = await convert(context, 'P1整柜一次转换并逐订单生成独立计划', data.normal, key)
|
2026-08-27 16:34:30 +08:00
|
|
|
const result = first?.body?.data
|
2026-08-27 17:16:33 +08:00
|
|
|
context.assert(MODULE, 'P1整柜响应覆盖全部订单和独立计划', first,
|
2026-08-27 16:34:30 +08:00
|
|
|
success(first) && validContainerBatchResult(result, data.normal.orders), trace(first), OPTIONS)
|
|
|
|
|
|
|
|
|
|
const replay = await convert(context, 'P1同键同请求重放', data.normal, key)
|
|
|
|
|
context.assert(MODULE, 'P1重放计划号与明细零新增', replay,
|
|
|
|
|
success(replay) && zeroDrift(result, replay?.body?.data), trace(replay), OPTIONS)
|
|
|
|
|
|
|
|
|
|
const concurrentKey = `${context.marker}-container-double-click`
|
|
|
|
|
const concurrent = await Promise.all([
|
|
|
|
|
convert(context, 'P1并发双击A', data.normal, concurrentKey),
|
|
|
|
|
convert(context, 'P1并发双击B', data.normal, concurrentKey),
|
|
|
|
|
])
|
|
|
|
|
context.assert(MODULE, 'P1并发双击收敛同一批计划', concurrent[1],
|
|
|
|
|
concurrent.every(success) && zeroDrift(concurrent[0].body?.data, concurrent[1].body?.data),
|
|
|
|
|
concurrent.map(trace).join('; '), OPTIONS)
|
|
|
|
|
|
|
|
|
|
const after = await listOrders(context, data.normal.containerNo)
|
2026-08-27 17:16:33 +08:00
|
|
|
context.assert(MODULE, 'P1全部15票推进INBOUND_PLANNED', after.response,
|
2026-08-27 16:34:30 +08:00
|
|
|
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)
|
|
|
|
|
|
2026-08-28 13:58:49 +08:00
|
|
|
const readySummary = await context.requestOverseas('GET',
|
|
|
|
|
`${CONTAINER_SUMMARY_ENDPOINT}?page=1&size=10&containerNo=${encodeURIComponent(data.normal.containerNo)}`)
|
|
|
|
|
context.assert(MODULE, 'P0全柜每票独立计划完成后allPlansReady', readySummary,
|
|
|
|
|
success(readySummary) && validContainerSummaryPage(readySummary.body?.data, {
|
|
|
|
|
sourceContainerBookingId: data.normal.sourceContainerBookingId,
|
|
|
|
|
containerNo: data.normal.containerNo,
|
|
|
|
|
totalOrderCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
|
|
|
|
|
inboundCodeCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
|
|
|
|
|
missingInboundCodeCount: 0,
|
|
|
|
|
planCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
|
|
|
|
|
plannedInboundCodeCount: CONTAINER_CONVERT_EXPECTED_ORDERS,
|
|
|
|
|
pendingPlanCount: 0,
|
|
|
|
|
allPlansReady: true,
|
|
|
|
|
}), trace(readySummary), OPTIONS)
|
|
|
|
|
|
2026-08-27 16:34:30 +08:00
|
|
|
}
|