import { CONTAINER_CONVERT_ENDPOINT, CONTAINER_CONVERT_EXPECTED_ORDERS, validContainerBatchResult, validOrderFact, validPdaCandidate, 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)) function gap(context, name, reason, priority = 'P0') { context.addCoverageGap(MODULE, MODULE, name, reason, 'fixture', { deploymentBlocker: priority === 'P0' }) 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 || !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') } } if (!value.pdaToken) gap(context, 'PDA候选Token前置', '缺少绑定真实海外仓的WAREHOUSE_PICKER海外Token') 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) { const anchor = source.orders[0] return { orderId: id(anchor.orderId), customerId: id(anchor.customerId) } } async function convert(context, name, source, key) { const response = await context.requestOverseas('POST', CONTAINER_CONVERT_ENDPOINT, convertBody(source), {}, context.overseasToken, { 'Idempotency-Key': key }) context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS) return response } export async function run(context) { const data = fixture(context) const normalList = await listOrders(context, data.normal.containerNo) context.assert(MODULE, 'P0同柜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) 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` const first = await convert(context, 'P0整柜一次转换并逐订单生成独立计划', data.normal, key) const result = first?.body?.data context.assert(MODULE, 'P0整柜响应覆盖全部订单和客户计划', first, 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) context.assert(MODULE, 'P0全部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 }) }