const MODULE = 'phase4-tail-order-controls' const SCENARIO = 'phase4-tail-order-controls-lifecycle' const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true } const ORDERS = '/api/platform/v1/last-leg/orders' const PLANS = '/api/platform/v1/last-leg/inbound-plans' const NODE_CODES = Object.freeze([ 'DOMESTIC_CUSTOMS_COMPLETED', 'ESTIMATED_DEPARTURE', 'ACTUAL_DEPARTURE', 'ESTIMATED_ARRIVAL', 'OVERSEAS_CUSTOMS_COMPLETED', 'ACTUAL_ARRIVAL_WAITING_UNLOAD', 'PICKUP_APPOINTED', 'UNLOADED', ]) function id(value) { return value == null ? '' : String(value) } function codeOf(response) { return response?.body?.code == null ? '' : String(response.body.code) } function success(response) { return response?.http === 200 && codeOf(response) === '200' } function conflict(response) { return response?.http === 409 || (response?.http === 200 && codeOf(response) === 'A0409') } function records(value) { return Array.isArray(value?.records) ? value.records : [] } function nonBlank(value) { return typeof value === 'string' && value.trim().length > 0 } function localIso(value) { return value.toISOString().slice(0, 19) } function trace(response) { return `HTTP=${response?.http ?? 'missing'}, code=${codeOf(response) || 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` } function query(path, values) { const params = new URLSearchParams() for (const [key, value] of Object.entries(values || {})) { if (value !== undefined && value !== null && String(value) !== '') params.set(key, String(value)) } return params.size ? `${path}?${params}` : path } async function api(context, method, name, path, body, headers = {}) { const response = await context.request(method, path, body, context.token, headers) const data = context.apiSuccess(MODULE, name, response, trace(response), OPTIONS) return { response, data } } async function domesticApi(context, name, path) { const response = await context.request('GET', path) const data = context.apiSuccess(MODULE, name, response, trace(response), OPTIONS) return { response, data } } function blocker(context, name, evidence, response = null) { context.addStep(MODULE, name, response, false, `${evidence}; ${trace(response)}`, OPTIONS) context.addCoverageGap(MODULE, SCENARIO, name, evidence, 'fixture', { deploymentBlocker: true }) } function targetProducer(context) { const matrix = context.createdResources?.phase4InboundProducerFixtures if (matrix?.marker !== context.marker) return null return matrix.containers?.find((item) => item?.fulfillmentType === 'ONE_CLICK_DROPSHIP') || matrix.byDestinationType?.FBA?.[0] || matrix.byDestinationType?.OVERSEAS?.[0] || null } async function markerOrders(context, producer) { let last = null for (let attempt = 1; attempt <= 60; attempt += 1) { last = await api(context, 'GET', `等待本轮尾程订单落地(${attempt}/60)`, query(ORDERS, { page: 1, size: 200, containerNo: producer.containerNo })) const matched = records(last.data).filter((row) => id(row?.containerNo) === id(producer.containerNo) && id(row?.customerId) === id(producer.tenantId) && id(row?.id)) if (matched.length > 0) return { rows: matched, response: last.response } if (attempt < 60) await new Promise((resolve) => setTimeout(resolve, 1_000)) } return { rows: [], response: last?.response || null } } async function detail(context, row, name = '读取尾程订单即时详情') { return api(context, 'GET', name, query(`${ORDERS}/${encodeURIComponent(id(row.id))}`, { customerId: id(row.customerId) })) } async function ensureCurrentUsdRate(context) { const month = new Date().toISOString().slice(0, 7) const path = `/api/platform/v1/last-leg/master-data/exchange-rates?page=1&size=20&monthFrom=${month}&monthTo=${month}` const listed = await api(context, 'GET', '查询尾程费用业务月汇率', path) const existing = records(listed.data).find((row) => row?.month === month) if (existing?.status === 'ACTIVE' && Number(existing?.usdToCnyAverage) > 0) return month if (existing) { blocker(context, '尾程费用业务月汇率', `业务月${month}汇率存在但不是ACTIVE,拒绝覆盖既有主数据`, listed.response) return null } const created = await api(context, 'POST', '补齐尾程费用业务月汇率', '/api/platform/v1/last-leg/master-data/exchange-rates', { month, usdToCnyAverage: 7.1, remark: `${context.marker} acceptance rate`, }) context.assert(MODULE, '尾程费用业务月汇率可用于USD兑CNY', created.response, created.data?.month === month && created.data?.status === 'ACTIVE' && Number(created.data?.usdToCnyAverage) > 0, `month=${month}, status=${created.data?.status || 'missing'}`, OPTIONS) return created.data?.status === 'ACTIVE' ? month : null } async function verifyReceivingWarehouseChange(context, producer, details) { const bookingId = id(producer.containerBookingId) if (!bookingId) { blocker(context, '取得本轮来源柜预订ID', `containerNo=${producer.containerNo}`) return false } const progressPath = `${ORDERS}/container-summaries/${encodeURIComponent(bookingId)}` const current = await api(context, 'GET', '读取本轮柜级入仓计划进度', progressPath) const summary = current.data?.summary const originalWarehouseId = id(summary?.overseasWarehouseId) if (!success(current.response) || !originalWarehouseId || summary?.canChangeReceivingWarehouse !== true) { blocker(context, '柜级接柜仓可变更前置', `bookingId=${bookingId}, warehouseId=${originalWarehouseId || 'missing'}, canChange=${summary?.canChangeReceivingWarehouse}`, current.response) return false } const warehouses = await domesticApi(context, '查询可用公共海外接柜仓', '/api/platform/v1/warehouse-admin?tenantId=0&page=1&size=200&capabilityCode=RECEIVING&warehouseType=OVERSEAS&status=ACTIVE') const alternate = records(warehouses.data).find((row) => id(row?.id) && id(row.id) !== originalWarehouseId && row?.warehouseType === 'OVERSEAS' && row?.status === 'ACTIVE') if (!alternate) { blocker(context, '柜级接柜仓真实切换候选', `originalWarehouseId=${originalWarehouseId}; 至少需要2个ACTIVE公共OVERSEAS接柜仓`, warehouses.response) return false } const changePath = `${progressPath}/receiving-warehouse` const changeKey = `${context.marker}-receiving-warehouse-change`.slice(0, 128) const changeBody = { overseasWarehouseId: id(alternate.id), expectedVersion: Number(summary.assignmentVersion), remark: context.marker } const changed = await api(context, 'PUT', '整柜切换到另一公共接柜仓', changePath, changeBody, { 'Idempotency-Key': changeKey }) context.assert(MODULE, '接柜仓变更原子推进版本并保持柜内订单数', changed.response, id(changed.data?.overseasWarehouseId) === id(alternate.id) && Number(changed.data?.assignmentVersion) === Number(summary.assignmentVersion) + 1 && Number(changed.data?.totalOrderCount) === details.length, trace(changed.response), OPTIONS) if (!success(changed.response)) return false const replay = await api(context, 'PUT', '接柜仓变更同键同载荷重放', changePath, changeBody, { 'Idempotency-Key': changeKey }) context.assert(MODULE, '接柜仓变更重放版本零漂移', replay.response, Number(replay.data?.assignmentVersion) === Number(changed.data?.assignmentVersion) && id(replay.data?.overseasWarehouseId) === id(alternate.id), trace(replay.response), OPTIONS) const hashConflict = await context.request('PUT', changePath, { ...changeBody, remark: `${context.marker}-DIFFERENT` }, context.token, { 'Idempotency-Key': changeKey }) context.addStep(MODULE, '接柜仓变更同键异载荷返回A0409', hashConflict, conflict(hashConflict), trace(hashConflict), OPTIONS) const stale = await context.request('PUT', changePath, { overseasWarehouseId: originalWarehouseId, expectedVersion: Number(summary.assignmentVersion), remark: context.marker }, context.token, { 'Idempotency-Key': `${context.marker}-receiving-warehouse-stale`.slice(0, 128) }) context.addStep(MODULE, '接柜仓变更旧版本返回A0409', stale, conflict(stale), trace(stale), OPTIONS) const restored = await api(context, 'PUT', '整柜恢复原公共接柜仓', changePath, { overseasWarehouseId: originalWarehouseId, expectedVersion: Number(changed.data.assignmentVersion), remark: `${context.marker}-restore` }, { 'Idempotency-Key': `${context.marker}-receiving-warehouse-restore`.slice(0, 128) }) context.assert(MODULE, '接柜仓恢复后供后续PDA消费', restored.response, id(restored.data?.overseasWarehouseId) === originalWarehouseId && Number(restored.data?.assignmentVersion) === Number(changed.data.assignmentVersion) + 1, trace(restored.response), OPTIONS) return success(restored.response) } async function verifyDestinationMergeFeeAndExport(context, details, producer) { const fbaResult = await domesticApi(context, '查询公共ACTIVE FBA目的仓', '/api/platform/v1/warehouse-admin?tenantId=0&page=1&size=200&warehouseType=FBA&status=ACTIVE') const fba = records(fbaResult.data).find((row) => id(row?.id) && nonBlank(row?.warehouseCode) && nonBlank(row?.warehouseName) && row?.warehouseType === 'FBA' && row?.status === 'ACTIVE') if (!fba) { blocker(context, '尾程目的仓变更权威主档', '国内Warehouse未返回公共ACTIVE FBA仓', fbaResult.response) return false } const destination = { warehouseId: id(fba.id), warehouseCode: fba.warehouseCode, warehouseName: fba.warehouseName, remark: context.marker } const representative = details[0] const one = await api(context, 'PUT', '单票变更FBA目的仓', query(`${ORDERS}/${encodeURIComponent(id(representative.id))}/destination`, { customerId: id(representative.customerId) }), destination) context.assert(MODULE, '单票目的仓变更返回1', one.response, Number(one.data) === 1, trace(one.response), OPTIONS) if (!success(one.response)) return false const batchOrders = details.slice(1).length > 0 ? details.slice(1) : [representative] const batch = await api(context, 'PUT', '批量变更FBA目的仓', query(`${ORDERS}/destination`, { customerId: id(representative.customerId) }), { ...destination, orderIds: batchOrders.map((row) => id(row.id)) }) context.assert(MODULE, '批量目的仓变更数量精确', batch.response, Number(batch.data) === batchOrders.length, trace(batch.response), OPTIONS) if (!success(batch.response)) return false const mergePath = query(`${ORDERS}/${encodeURIComponent(id(representative.id))}/pallet-merge-required`, { customerId: id(representative.customerId), }) const marked = await api(context, 'PUT', '单票开启PDA合托提示', mergePath, { palletMergeRequired: true }) context.assert(MODULE, '单票合托标记写入成功', marked.response, marked.data == null, trace(marked.response), OPTIONS) const markedRead = await detail(context, representative, '回查单票合托标记') context.assert(MODULE, '单票合托标记为REQUIRED', markedRead.response, markedRead.data?.order?.palletMergeFlag === 'REQUIRED', trace(markedRead.response), OPTIONS) const unmarked = await api(context, 'PUT', '单票取消PDA合托提示', mergePath, { palletMergeRequired: false }) context.assert(MODULE, '单票合托标记可取消', unmarked.response, unmarked.data == null, trace(unmarked.response), OPTIONS) const allIds = details.map((row) => id(row.id)) const batchMergePath = query(`${ORDERS}/pallet-merge-required/batch`, { customerId: id(representative.customerId) }) const batchMarked = await api(context, 'PUT', '批量开启PDA合托分组', batchMergePath, { orderIds: allIds, palletMergeRequired: true }) context.assert(MODULE, '批量合托分组写入成功', batchMarked.response, batchMarked.data == null, trace(batchMarked.response), OPTIONS) const groupRead = await detail(context, representative, '回查批量合托关系') context.assert(MODULE, '批量合托关系包含本轮订单', groupRead.response, groupRead.data?.order?.palletMergeFlag === 'REQUIRED' && (allIds.length === 1 || nonBlank(groupRead.data?.order?.palletMergeBatchId)), `orders=${allIds.length}, batchId=${groupRead.data?.order?.palletMergeBatchId || 'single-order'}; ${trace(groupRead.response)}`, OPTIONS) const batchCleared = await api(context, 'PUT', '批量取消PDA合托分组', batchMergePath, { orderIds: allIds, palletMergeRequired: false }) context.assert(MODULE, '批量合托分组取消成功', batchCleared.response, batchCleared.data == null, trace(batchCleared.response), OPTIONS) const feeMonth = await ensureCurrentUsdRate(context) if (!feeMonth) return false const feeItem = `${context.marker}-TAIL-FEE`.slice(0, 128) const fee = await api(context, 'POST', '新增本轮尾程订单费用', query(`${ORDERS}/${encodeURIComponent(id(representative.id))}/fees`, { customerId: id(representative.customerId) }), { feeItemCode: feeItem.slice(0, 64), feeItemName: feeItem, direction: 'RECEIVABLE', currency: 'USD', amount: 0.01, businessDate: `${feeMonth}-01` }, { 'Idempotency-Key': `${context.marker}-tail-fee`.slice(0, 128) }) context.assert(MODULE, '尾程费用冻结币种金额和审核状态', fee.response, id(fee.data?.orderId) === id(representative.id) && fee.data?.feeItemName === feeItem && fee.data?.originalCurrency === 'USD' && Number(fee.data?.originalAmount) === 0.01 && fee.data?.currency === 'CNY' && Number(fee.data?.amount) > 0 && fee.data?.reviewStatus === 'PENDING', trace(fee.response), OPTIONS) const feeRead = await detail(context, representative, '回查尾程订单费用明细') context.assert(MODULE, '尾程费用详情可追溯', feeRead.response, feeRead.data?.fees?.some((row) => row?.feeItemName === feeItem && row?.originalCurrency === 'USD' && Number(row?.originalAmount) === 0.01 && row?.currency === 'CNY' && Number(row?.amount) > 0), trace(feeRead.response), OPTIONS) const exported = await context.requestBinary('GET', query(`${ORDERS}/export`, { customerId: id(representative.customerId), containerNo: producer.containerNo, })) const csv = new TextDecoder().decode(exported.bytes || new Uint8Array()) context.addStep(MODULE, '导出本轮尾程订单CSV', exported, exported?.http === 200 && String(exported?.contentType || '').toLowerCase().includes('csv') && csv.includes(producer.containerNo) && csv.includes('尾程单号'), `bytes=${exported?.bytes?.length ?? 0}, containerNo=${producer.containerNo}; ${trace(exported)}`, OPTIONS) return success(batchCleared.response) && success(fee.response) && exported?.http === 200 } function manualPlanBody(order, version, marker) { return { customerId: id(order.customerId), orderId: id(order.id), expectedOrderVersion: Number(version), inboundCode: order.inboundCode, palletCount: Math.min(999, Math.max(1, Number(order.palletCount) || 1)), palletType: 'STANDARD', assigned: false, mergeRequired: false, urgent: true, remark: marker, } } function completeManualNodes(marker) { const base = Date.now() - 8 * 60 * 60_000 return NODE_CODES.map((nodeCode, index) => ({ nodeCode, nodeAt: localIso(new Date(base + index * 60 * 60_000)), })).map((node, index) => index === 3 ? { ...node, nodeAt: null } : node) } function previewGroupRank(group) { return ({ FBA: 0, ONE_CLICK_DROPSHIP: 1, PRIVATE_ADDRESS: 2 })[group?.destinationType] ?? 99 } function previewGroupsSorted(groups) { const rows = Array.isArray(groups) ? groups : [] return rows.every((group, index) => { if (index === 0) return true const previous = rows[index - 1] const rankDifference = previewGroupRank(previous) - previewGroupRank(group) return rankDifference < 0 || (rankDifference === 0 && String(previous?.destinationWarehouseCode || '').localeCompare( String(group?.destinationWarehouseCode || '')) <= 0) }) } async function convertContainerPlans(context, producer, details) { const bookingId = id(producer.containerBookingId) const arrival = await api(context, 'POST', '按柜转换前读取预计到仓时间', `${ORDERS}/container-arrival-times/query`, { sourceContainerBookingIds: [bookingId] }) const currentArrival = Array.isArray(arrival.data) ? arrival.data.find(row => id(row.sourceContainerBookingId) === bookingId) : null if (!success(arrival.response) || !currentArrival || !Number.isInteger(Number(currentArrival.version))) return null if (!nonBlank(currentArrival.estimatedWarehouseArrivalAt)) { const expected = await api(context, 'PUT', '按柜转换前补齐预计到仓时间', `${ORDERS}/container-summaries/${encodeURIComponent(bookingId)}/expected-arrival`, { estimatedWarehouseArrivalAt: new Date(Date.now() + 2 * 86400000).toISOString().slice(0, 19), expectedVersion: Number(currentArrival.version), remark: `${context.marker}-container-plan-arrival`, }, { 'Idempotency-Key': `${context.marker}-container-plan-arrival`.slice(0, 128) }) if (!success(expected.response) || id(expected.data?.sourceContainerBookingId) !== bookingId || !nonBlank(expected.data?.estimatedWarehouseArrivalAt)) return null } const previewPath = query(`${PLANS}/container-preview`, { sourceContainerBookingId: bookingId }) const preview = await api(context, 'GET', '预览整柜待生成入仓计划清单', previewPath) const groups = Array.isArray(preview.data?.groups) ? preview.data.groups : [] const previewOrders = groups.flatMap((group) => Array.isArray(group?.orders) ? group.orders : []) const pendingPalletCount = Number(preview.data?.pendingPalletCount) context.assert(MODULE, '柜级预览列清尾程单、入仓码、目的仓、业务类型与建议托数', preview.response, preview.data?.ready === true && id(preview.data?.sourceContainerBookingId) === bookingId && previewOrders.length === details.length && pendingPalletCount > 0 && previewOrders.every((order) => nonBlank(order?.businessOrderNo) && nonBlank(order?.inboundCode) && nonBlank(order?.destinationType) && nonBlank(order?.destinationWarehouseCode) && Number(order?.suggestedPalletCount) > 0 && order?.readiness === 'READY' && Array.isArray(order?.blockers) && order.blockers.length === 0), `orders=${previewOrders.length}, pendingPallets=${pendingPalletCount}, groups=${groups.length}; ${trace(preview.response)}`, OPTIONS) context.assert(MODULE, '柜级预览按FBA、一件代发、私人地址及目的仓稳定排序', preview.response, previewGroupsSorted(groups), groups.map((group) => `${group.destinationType}:${group.destinationWarehouseCode}`).join(','), OPTIONS) context.assert(MODULE, '一件代发的FBA目的仓快照保留给自动托盘', preview.response, previewOrders.filter((order) => order?.destinationType === 'ONE_CLICK_DROPSHIP' && nonBlank(order?.destinationWarehouseCode)).every((order) => nonBlank(order?.fbaWarehouseCode)), `dropshipFba=${previewOrders.filter((order) => nonBlank(order?.fbaWarehouseCode)).length}`, OPTIONS) if (!success(preview.response) || preview.data?.ready !== true || pendingPalletCount <= 0) return null const body = { sourceContainerBookingId: bookingId, snapshotVersion: Number(preview.data.snapshotVersion), snapshotHash: preview.data.snapshotHash } const key = `${context.marker}-container-plan-confirm`.slice(0, 128) const converted = await api(context, 'POST', '按柜确认并自动生成入仓计划与实体托盘', `${PLANS}/convert-container`, body, { 'Idempotency-Key': key }) const plans = Array.isArray(converted.data) ? converted.data : [] const pallets = plans.flatMap((plan) => Array.isArray(plan?.pallets) ? plan.pallets.map((pallet) => ({ plan, pallet })) : []) context.assert(MODULE, '按柜确认逐票生成独立计划且托盘总数与预览一致', converted.response, plans.length === details.length && pallets.length === pendingPalletCount && plans.every((plan) => plan?.creationMode === 'AUTO' && nonBlank(plan?.planNo) && Array.isArray(plan?.lines) && plan.lines.length === 1) && pallets.every(({ pallet }) => nonBlank(pallet?.palletNo) && id(pallet?.warehousePalletId) && id(pallet?.warehouseLocationId) && pallet?.assignmentStatus === 'RESERVED'), `plans=${plans.length}, pallets=${pallets.length}, expectedPallets=${pendingPalletCount}; ${trace(converted.response)}`, OPTIONS) if (!success(converted.response)) return null for (const { plan, pallet } of pallets) { const line = plan.lines[0] const palletPath = query(`/api/platform/v1/overseas-warehouse-resources/${encodeURIComponent(id(preview.data.overseasWarehouseId))}/pallets`, { page: 1, size: 10, keyword: pallet.palletNo, }) const listed = await api(context, 'GET', `回查自动托盘${pallet.palletNo}`, palletPath) const stored = records(listed.data).find((row) => row?.palletCode === pallet.palletNo) context.assert(MODULE, '自动托盘进入托盘列表并保留FBA目的仓', listed.response, id(stored?.id) === id(pallet.warehousePalletId) && stored?.status === 'OCCUPIED' && (!nonBlank(line?.fbaWarehouseCode) || stored?.fbaWarehouseCode === line.fbaWarehouseCode), `pallet=${pallet.palletNo}, expectedFba=${line?.fbaWarehouseCode || 'none'}, actualFba=${stored?.fbaWarehouseCode || 'none'}; ${trace(listed.response)}`, OPTIONS) } const replay = await api(context, 'POST', '按柜确认同键同快照幂等重放', `${PLANS}/convert-container`, body, { 'Idempotency-Key': key }) context.assert(MODULE, '按柜确认重放不新增计划或托盘', replay.response, JSON.stringify(replay.data) === JSON.stringify(converted.data), trace(replay.response), OPTIONS) const after = await api(context, 'GET', '回查整柜计划与托盘均已生成', previewPath) context.assert(MODULE, '确认后预览待生成票数和托数归零', after.response, Number(after.data?.pendingOrderCount) === 0 && Number(after.data?.pendingPalletCount) === 0 && Number(after.data?.totalPlannedPalletCount) >= pendingPalletCount, `pendingOrders=${after.data?.pendingOrderCount}, pendingPallets=${after.data?.pendingPalletCount}, totalPlannedPallets=${after.data?.totalPlannedPalletCount}; ${trace(after.response)}`, OPTIONS) return plans } export async function verifyReceivedOrderHold(context) { const owned = context.createdResources.phase4TailOrderControls if (owned?.marker !== context.marker || !owned.representativeOrderId) throw new Error('缺少本轮暂扣目标订单') owned.holdVerifiedAfterReceipt = false delete owned.holdInventoryProof const producer = targetProducer(context) const loaded = await detail(context, { id: owned.representativeOrderId, customerId: producer?.tenantId }, '入库后读取本轮暂扣目标') const representative = loaded.data?.order if (!success(loaded.response) || id(representative?.id) !== id(owned.representativeOrderId) || id(representative?.customerId) !== id(producer?.tenantId) || representative?.currentStatus !== 'INBOUND_COMPLETED') throw new Error('暂扣正向验收必须使用本轮已完成入库订单') const require = (name, passed) => { context.assert(MODULE, name, null, Boolean(passed), `orderId=${representative.id}`, OPTIONS) if (!passed) throw new Error(name) } const customerId = id(representative.customerId), orderId = id(representative.id) const attemptKey = `${context.marker}-V${representative.version}` const pda = context.createdResources.phase4OverseasPda require('暂扣库存前置必须为本轮已验证收货证据', pda?.marker === context.marker) const matches = Object.values(pda?.receivingCompletionEvidence || {}).filter(value => value.status === 'VERIFIED' && value.proof?.passed === true && id(value.scope?.customerId) === customerId && value.scope?.containerNo === producer.containerNo && value.stages?.AFTER_COMPLETE?.captured === true && value.stages.AFTER_COMPLETE.evidence?.stockReceipts?.some(row => id(row.orderId) === orderId)) require('暂扣目标必须唯一命中本轮不可变实收事实', matches.length === 1) const evidence = matches[0].stages.AFTER_COMPLETE.evidence const warehouseId = id(matches[0].scope.warehouseId) require('暂扣目标实际仓与收货证据一致', warehouseId && warehouseId === id(representative.overseasWarehouseId)) const receipts = evidence.stockReceipts.filter(row => id(row.orderId) === orderId) require('暂扣实收事实具有完整客户来源和原始库存标识', receipts.length > 0 && receipts.every(row => id(row.customerId) === customerId && row.inboundCode === representative.inboundCode && id(row.inventoryBalanceId) && id(row.inventoryPositionId))) const balanceIds = [...new Set(receipts.map(row => id(row.inventoryBalanceId)))].sort() const positionIds = [...new Set(receipts.map(row => id(row.inventoryPositionId)))].sort() require('暂扣收货证据包含原始余额与位置全集', Array.isArray(evidence.balances) && Array.isArray(evidence.positions)) const originalBalances = evidence.balances.filter(row => id(row.inboundOrderId) === orderId) const originalPositions = evidence.positions.filter(row => balanceIds.includes(id(row.balanceId))) require('暂扣原始ID不得由公共SKU汇总推导或漏掉位置', sameHoldFacts(originalBalances.map(row=>id(row.id)),balanceIds) && sameHoldFacts(originalPositions.map(row=>id(row.id)),positionIds) && originalBalances.every(row=>id(row.customerId)===customerId && id(row.overseasWarehouseId)===warehouseId) && originalPositions.every(row=>id(row.customerId)===customerId && id(row.overseasWarehouseId)===warehouseId)) const proof = owned.holdInventoryProof = { marker:context.marker, orderId, customerId, warehouseId, balanceIds, positionIds, snapshots:{}, status:'RUNNING', restored:false } const orderPath = query(`${ORDERS}/${encodeURIComponent(orderId)}/hold`, {customerId}) const inventoryPath = '/api/platform/v1/last-leg/inventory' let uncertain = false, otherApplied = false, active = null, baseline, otherHeld, otherBody const request = async (method,name,path,body,headers={}) => { let response try { response = await context.request(method,path,body,context.token,headers) } catch(error) { if(method!=='GET') uncertain=true;throw error } context.addStep(MODULE,name,response,success(response),trace(response),OPTIONS) if (!success(response)) { if(method!=='GET' && (!response?.http || response.http>=500)) uncertain=true throw new Error(`${name}: ${trace(response)}`) } return response.body.data } const reject = async (name,path,body,headers,method) => { let response try { response=await context.request(method,path,body,context.token,headers) } catch(error) {uncertain=true;throw error} if(!response?.http || response.http>=500 || success(response)) uncertain=true context.addStep(MODULE,name,response,conflict(response),trace(response),OPTIONS) require(`${name}必须明确拒绝`,conflict(response) && nonBlank(response.requestId) && nonBlank(response.traceId)) return response } const read = async label => { const order = (await request('GET','暂扣回查订单事实',query(`${ORDERS}/${orderId}`,{customerId}))).order require('暂扣回查订单身份和入库状态稳定', id(order?.id)===orderId && id(order.customerId)===customerId && id(order.overseasWarehouseId)===warehouseId && order.currentStatus==='INBOUND_COMPLETED') const balances=[],positions=[] for(const balanceId of balanceIds) { const balance=await request('GET','暂扣读取原始余额详情',query(`${inventoryPath}/balances/${balanceId}`,{customerId})) require('暂扣原始余额归属正确',id(balance?.id)===balanceId && id(balance.inboundOrderId)===orderId && id(balance.customerId)===customerId && id(balance.overseasWarehouseId)===warehouseId) balances.push(holdNumbers(balance,HOLD_BALANCE_BUCKETS)) const rows=await request('GET','暂扣读取原始余额所有位置',query(`${inventoryPath}/balances/${balanceId}/positions`,{customerId})) require('暂扣位置归属正确',Array.isArray(rows) && rows.every(row=>id(row.balanceId)===balanceId && id(row.inboundOrderId)===orderId && id(row.customerId)===customerId && id(row.overseasWarehouseId)===warehouseId)) positions.push(...rows.map(row=>holdNumbers(row,HOLD_POSITION_BUCKETS))) } require('暂扣位置集合完整且无重复',sameHoldFacts(positions.map(row=>id(row.id)),positionIds)) for(const balance of balances) for(const field of HOLD_POSITION_BUCKETS) { require(`暂扣余额位置核平${field}`,balance[field]===positions.filter(row=>id(row.balanceId)===id(balance.id)).reduce((sum,row)=>sum+row[field],0)) } const value={order,balances,positions};proof.snapshots[label]=value;return value } const stock = value => ({balances:value.balances.map(holdStableStock),positions:value.positions.map(holdStableStock)}) const expectStock = (actual,expected,label) => require(label,sameHoldFacts(stock(actual),stock(expected))) const orderCommand = async (held,version,suffix) => { const body={held,chargeable:held,reason:`${context.marker} ${suffix}`,expectedVersion:Number(version)} const headers={'Idempotency-Key':`${attemptKey}-${suffix}`} const data=await request('PUT',held?'暂扣本轮单票尾程订单':'解除本轮单票尾程暂扣',orderPath,body,headers) // A malformed successful response is not sufficient authority for a recovery mutation. if(id(data?.orderId)!==orderId || data.held!==held || Number(data.version)!==Number(version)+1) uncertain=true require('暂扣命令返回本订单预期版本及计费状态',!uncertain && data.chargeable===held && (held || data.reason==null)) active=held?{version:Number(data.version),reason:body.reason}:null return {body,headers,data} } const releaseOther = async () => { await request('POST','释放本轮独立库存暂扣',`${inventoryPath}/release`,{...otherBody, idempotencyKey:`${attemptKey}-F32-OTHER-RELEASE`,remark:`${context.marker} independent release`}) otherApplied=false } try { baseline=await read('BEFORE') require('暂扣前货物可用且无现存业务冻结或出库占用',baseline.order.detained===false && baseline.positions.every(row=>row.holdPackages===0 && row.allocatedPackages===0 && row.pickedPackages===0 && row.receivingPackages===0) && baseline.balances.every(row=>row.inTransitPackages===0 && row.procurementPackages===0)) for(const row of baseline.balances) { const original=originalBalances.find(ref=>id(ref.id)===id(row.id)) require('暂扣当前余额仍等于本轮实收入账',HOLD_BALANCE_BUCKETS.every(field=>row[field]===holdInteger(original[field]))) } for(const row of baseline.positions) { const original=originalPositions.find(ref=>id(ref.id)===id(row.id)) require('暂扣原始位置仍绑定实收托盘库位批次', ['balanceId','palletId','palletNo','batchNo','warehouseLocationId','warehouseLocationCode','destinationWarehouseCode'] .every(field=>id(row[field])===id(original[field])) && receipts.filter(ref=>id(ref.inventoryPositionId)===id(row.id)).every(ref=>id(ref.inventoryBalanceId)===id(row.balanceId))) require('暂扣当前位置仍等于本轮实收入账',HOLD_POSITION_BUCKETS.every(field=>row[field]===holdInteger(original[field])) && row.availablePackages===receipts.filter(ref=>id(ref.inventoryPositionId)===id(row.id)).reduce((sum,ref)=>sum+holdInteger(ref.receivedPackages),0)) } const selected=baseline.positions.find(row=>row.availablePackages>=2) require('暂扣必须有足够独立实收库存保留另一业务一箱',Boolean(selected)) otherBody={positionId:selected.id,packages:1,businessOrderNo:`${context.marker}-F32-OTHER`, sourceEventId:`${context.marker}-F32-OTHER`,idempotencyKey:`${attemptKey}-F32-OTHER-HOLD`,remark:`${context.marker} independent hold`} await request('POST','建立本轮独立库存暂扣一箱',`${inventoryPath}/hold`,otherBody);otherApplied=true otherHeld=shiftHoldStock(baseline,selected.id,false) expectStock(await read('OTHER_HELD'),otherHeld,'独立暂扣只移动目标位置一箱且所有桶守恒') const allHeld=shiftHoldStock(otherHeld,null,true) for(let cycle=1;cycle<=2;cycle++) { const before=await read(`C${cycle}_BEFORE`) const held=await orderCommand(true,before.order.version,`F32-C${cycle}-HOLD`) const during=await read(`C${cycle}_HELD`) require('暂扣只推进单票版本并保留计费原因',during.order.detained===true && Number(during.order.version)===Number(held.data.version) && during.order.holdReason===held.body.reason && during.order.heldChargeable===true) expectStock(during,allHeld,`第${cycle}轮暂扣全部原始位置可用转HOLD`) await request('PUT','暂扣命令同键同载荷重放',orderPath,held.body,held.headers) require('暂扣重放版本及全部库存桶零漂移',sameHoldFacts(await read(`C${cycle}_HOLD_REPLAY`),during)) await reject('暂扣同键异载荷返回A0409',orderPath,{...held.body,reason:`${held.body.reason}-DIFFERENT`},held.headers,'PUT') require('暂扣异载荷拒绝且事实不变',sameHoldFacts(await read(`C${cycle}_CONFLICT`),during)) await reject('暂扣尾程订单不能生成人工计划',`${PLANS}/manual`,manualPlanBody(representative,held.data.version,context.marker), {'Idempotency-Key':`${attemptKey}-F32-C${cycle}-PLAN`},'POST') require('人工计划拒绝后暂扣与库存不变',sameHoldFacts(await read(`C${cycle}_PLAN_REJECTED`),during)) const released=await orderCommand(false,during.order.version,`F32-C${cycle}-RELEASE`) const after=await read(`C${cycle}_RELEASED`) require('解除暂扣清空计费与原因并推进版本',after.order.detained===false && after.order.heldChargeable===false && after.order.holdReason==null && Number(after.order.version)===Number(released.data.version)) expectStock(after,otherHeld,`第${cycle}轮解除保留另一业务HOLD一箱`) await request('PUT','解除暂扣原键重放',orderPath,released.body,released.headers) require('解除重放订单余额及所有位置零漂移',sameHoldFacts(await read(`C${cycle}_RELEASE_REPLAY`),after)) } await releaseOther() const final=await read('AFTER') expectStock(final,baseline,'最终库存完整恢复到本轮实收入账基线') require('两轮暂扣后订单未冻结且版本精确增加四',final.order.detained===false && Number(final.order.version)===Number(baseline.order.version)+4) proof.restored=true;proof.status='VERIFIED';owned.holdVerifiedAfterReceipt=true } catch(error) {proof.status='FAILED';proof.error=error.message;throw error} finally { proof.uncertainWrite=uncertain if(proof.status==='FAILED' && (uncertain || active || otherApplied)) { try { if(uncertain) throw new Error('写入响应不明,保留样本,禁止自动释放') const current=await read('RECOVERY_BEFORE') if(active) { require('只恢复本轮已确认且版本原因一致的暂扣',current.order.detained===true && Number(current.order.version)===active.version && current.order.holdReason===active.reason) await orderCommand(false,active.version,'F32-RECOVERY-RELEASE') } else require('恢复独立冻结前订单必须未暂扣',current.order.detained===false) if(otherApplied) { // Never release one arbitrary HOLD: all buckets must still equal our known independent hold. expectStock(await read('RECOVERY_OTHER_CHECK'),otherHeld,'恢复只释放本轮已确认独立一箱') await releaseOther() } const recovered=await read('RECOVERED');expectStock(recovered,baseline,'失败恢复后库存完整回到原基线') require('失败恢复后订单未暂扣',recovered.order.detained===false);proof.restored=true } catch(recoveryError) {proof.recoveryError=recoveryError.message} proof.uncertainWrite=uncertain } } } const HOLD_POSITION_BUCKETS=['availablePackages','holdPackages','allocatedPackages','pickedPackages','receivingPackages'] const HOLD_BALANCE_BUCKETS=[...HOLD_POSITION_BUCKETS,'inTransitPackages','procurementPackages'] function holdInteger(value) { if(!((typeof value==='number' || (typeof value==='string' && /^(0|[1-9][0-9]*)$/.test(value))) && Number.isSafeInteger(Number(value)) && Number(value)>=0)) throw new Error('暂扣库存数量不是非负安全整数') return Number(value) } function holdNumbers(row,fields) { const value={...row} for(const field of [...fields,'totalPackages']) value[field]=holdInteger(row[field]) if(value.totalPackages!==fields.reduce((sum,field)=>sum+value[field],0)) throw new Error('暂扣库存总量与完整数量桶不守恒') return value } function holdStableStock({updatedAt,version,...row}) {return row} function sameHoldFacts(a,b) { const normalize=value=>Array.isArray(value)?value.map(normalize).sort((x,y)=>JSON.stringify(x).localeCompare(JSON.stringify(y))) :value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(key=>[key,normalize(value[key])])):value return JSON.stringify(normalize(a))===JSON.stringify(normalize(b)) } function shiftHoldStock(source,positionId,all) { const value=structuredClone(source) for(const position of value.positions) { const quantity=all?position.availablePackages:id(position.id)===id(positionId)?1:0 position.availablePackages-=quantity;position.holdPackages+=quantity const balance=value.balances.find(row=>id(row.id)===id(position.balanceId)) balance.availablePackages-=quantity;balance.holdPackages+=quantity } return value } export async function run(context) { context.createdResources.phase4TailOrderControls = { marker: context.marker, containerNo: null, orderIds: [], planNos: [], representativeOrderId: null, finalStatus: null, cleanup: 'Automatically generated container inbound plans and pallets remain as marker-owned prerequisites consumed by overseas PDA receiving', } const producer = targetProducer(context) if (!producer?.containerNo || !producer?.tenantId) { blocker(context, '取得本轮尾程控制目标柜', '缺少本轮phase4入仓生产者,禁止借历史尾程订单') return } const listed = await markerOrders(context, producer) if (listed.rows.length === 0) { blocker(context, '取得本轮尾程订单', `containerNo=${producer.containerNo}, customerId=${producer.tenantId}`, listed.response) return } const details = [] for (const row of listed.rows) { const current = await detail(context, row) if (success(current.response) && id(current.data?.order?.id) === id(row.id)) details.push(current.data.order) } if (details.length !== listed.rows.length || details.some((order) => !order.inboundCode || !order.overseasWarehouseId)) { blocker(context, '尾程订单人工计划冻结事实完整', `listed=${listed.rows.length}, detailed=${details.length}`) return } if (!await verifyReceivingWarehouseChange(context, producer, details)) return if (!await verifyDestinationMergeFeeAndExport(context, details, producer)) return const refreshedDetails = [] for (const original of details) { const refreshed = await detail(context, original, `管理命令后刷新订单${id(original.id)}版本`) if (!success(refreshed.response) || !refreshed.data?.order) return refreshedDetails.push(refreshed.data.order) } const representative = refreshedDetails[0] const rejectedHold = await context.request('PUT', query( `${ORDERS}/${encodeURIComponent(id(representative.id))}/hold`, { customerId: id(representative.customerId) }), { held: true, chargeable: true, reason: `${context.marker} 入库前禁止暂扣`, expectedVersion: Number(representative.version) }, undefined, { 'Idempotency-Key': `${context.marker}-pre-receipt-hold` }) context.addStep(MODULE, '入库前暂扣必须拒绝', rejectedHold, conflict(rejectedHold), trace(rejectedHold), OPTIONS) const afterHold = await detail(context, representative, '入库前暂扣拒绝后回查订单') const unchanged = success(afterHold.response) && JSON.stringify(afterHold.data?.order) === JSON.stringify(representative) context.assert(MODULE, '拒绝暂扣后订单事实零漂移', afterHold.response, unchanged, trace(afterHold.response), OPTIONS) if (!conflict(rejectedHold) || !unchanged) return // The positive hold/release lifecycle runs only after PDA receiving. const nodesPath = query(`${ORDERS}/${encodeURIComponent(id(representative.id))}/manual-nodes`, { customerId: id(representative.customerId), }) const nodesKey = `${context.marker}-tail-manual-nodes`.slice(0, 128) const nodesBody = { expectedVersion: Number(representative.version), nodes: completeManualNodes(context.marker) } const nodes = await api(context, 'PUT', '保存尾程八个人工节点快照', nodesPath, nodesBody, { 'Idempotency-Key': nodesKey }) context.assert(MODULE, '人工节点永久切换MANUAL且完整返回八行', nodes.response, nodes.data?.manualOverrideEnabled === true && nodes.data?.nodes?.length === 8 && nodes.data.nodes.every((node) => node?.source === 'MANUAL') && Number(nodes.data?.version) === Number(representative.version) + 1, `nodes=${nodes.data?.nodes?.length ?? 'missing'}; ${trace(nodes.response)}`, OPTIONS) if (!success(nodes.response)) return const nodesReplay = await api(context, 'PUT', '人工节点同键同载荷重放', nodesPath, nodesBody, { 'Idempotency-Key': nodesKey }) context.assert(MODULE, '人工节点重放版本与八行快照零漂移', nodesReplay.response, JSON.stringify(nodesReplay.data) === JSON.stringify(nodes.data), trace(nodesReplay.response), OPTIONS) const nodesConflict = await context.request('PUT', nodesPath, { ...nodesBody, nodes: nodesBody.nodes.map((node, index) => index === 0 ? { ...node, nodeAt: localIso(new Date()) } : node) }, context.token, { 'Idempotency-Key': nodesKey }) context.addStep(MODULE, '人工节点同键异载荷返回A0409', nodesConflict, conflict(nodesConflict), trace(nodesConflict), OPTIONS) const nodesRead = await api(context, 'GET', '回查尾程人工节点来源', nodesPath) context.assert(MODULE, '人工节点读取固定为MANUAL且不回退柜级节点', nodesRead.response, nodesRead.data?.manualOverrideEnabled === true && nodesRead.data?.nodes?.length === 8 && nodesRead.data.nodes.every((node) => node?.source === 'MANUAL'), trace(nodesRead.response), OPTIONS) const generated = await convertContainerPlans(context, producer, refreshedDetails) if (!generated) return context.createdResources.phase4TailOrderControls = { ...context.createdResources.phase4TailOrderControls, containerNo: producer.containerNo, orderIds: refreshedDetails.map((order) => id(order.id)), planNos: generated.map((plan) => plan.planNo), representativeOrderId: id(representative.id), generatedPlans: generated, finalStatus: 'AUTO_CONTAINER_PLANS_READY', } }