const MODULE = 'coload-groups' const OPTIONS = { scenario: 'indivisible-coload-group', deploymentBlocker: true } const CONTAINERS = '/api/platform/v1/containers' const SMART = '/api/platform/v1/first-leg/smart-loading' const FIRST_LEG = '/api/platform/v1/first-leg' const PDA = '/api/platform/v1/first-leg/pda-loading' import { oceanBookingDraft } from './ocean-booking-fixture.mjs' import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs' function id(value) { return value == null ? '' : String(value) } function code(response) { return id(response?.body?.code) || 'missing' } function evidence(response, extra = '') { return `HTTP=${response?.http ?? 'missing'}, code=${code(response)}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}${extra ? `, ${extra}` : ''}` } function conflict(context, name, response, extra = '') { const passed = response?.http === 200 && code(response) === 'A0409' context.assert(MODULE, name, response, passed, evidence(response, extra), OPTIONS) return passed } function planSignature(plan) { return JSON.stringify({ id: id(plan?.id), version: Number(plan?.version), status: plan?.status || null, items: [...(plan?.items || [])].map((item) => ({ id: id(item?.id), accountId: id(item?.accountId), inboundId: id(item?.inboundId), packageCount: Number(item?.packageCount), version: Number(item?.version), })).sort((left, right) => left.id.localeCompare(right.id)), }) } function memberFacts(group) { return [...(group?.members || [])].map((item) => ({ orderId: id(item?.orderId), inboundId: id(item?.inboundId), fullPackageCount: Number(item?.fullPackageCount), })).sort((left, right) => left.orderId.localeCompare(right.orderId)) } function taskSignature(task) { return JSON.stringify({ id: id(task?.id), version: Number(task?.version), taskStatus: task?.taskStatus || null, workflowStatus: task?.workflowStatus || null, items: [...(task?.items || [])].map((item) => ({ id: id(item?.id), inboundId: id(item?.inboundId), version: Number(item?.version), planned: Number(item?.plannedPackageCount), picked: Number(item?.pickedPackageCount), status: item?.itemStatus || null, })).sort((left, right) => left.id.localeCompare(right.id)), }) } function treeSignature(tree) { return JSON.stringify({ parentOrderId: id(tree?.parentOrderId), originalPackageCount: Number(tree?.originalPackageCount), cumulativeLoadedPackageCount: Number(tree?.cumulativeLoadedPackageCount), currentRemainderPackageCount: Number(tree?.currentRemainderPackageCount), children: [...(tree?.children || [])].map((item) => ({ orderId: id(item?.orderId), sourceOrderId: id(item?.sourceOrderId), orderNo: item?.orderNo || null, splitRole: item?.splitRole || null, packageCount: Number(item?.packageCount), loadingPlanId: id(item?.loadingPlanId), containerBookingId: id(item?.containerBookingId), })).sort((left, right) => `${left.orderId}|${left.splitRole}`.localeCompare(`${right.orderId}|${right.splitRole}`)), }) } async function readPlan(context, planId, name) { const response = await context.request('GET', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}`) return { response, data: context.apiSuccess(MODULE, name, response, `planId=${planId}`, OPTIONS) } } async function readGroupSummary(context, inboundCode, name) { const response = await context.request('GET', `/api/platform/v1/first-leg/coload-groups/by-inbound-code/${encodeURIComponent(inboundCode)}`) return { response, data: context.apiSuccess(MODULE, name, response, evidence(response, `inboundCode=${inboundCode}`), OPTIONS) } } function summaryInboundCodes(summary) { return [...(summary?.members || [])].map((member) => String(member?.inboundCode || '')) .filter(Boolean).sort() } async function createSecondGroup(context, third, fourth) { const response = await context.request('POST', '/api/platform/v1/first-leg/coload-groups', { orderIds: [third.orderId, fourth.orderId] }, undefined, { 'Idempotency-Key': `${context.marker}-coload-second-create`.slice(0, 128) }) const group = context.apiSuccess(MODULE, '创建第二个同柜不可拆组用于足量完成链', response, evidence(response), OPTIONS) const frozen = memberFacts(group) context.assert(MODULE, '第二组冻结两票权威入库单与满箱数', response, Boolean(group?.groupId) && group?.status === 'ACTIVE' && frozen.length === 2 && frozen.every((member) => [third, fourth].some((fixture) => fixture.orderId === member.orderId && fixture.inboundId === member.inboundId && fixture.packages === member.fullPackageCount)), `groupId=${id(group?.groupId) || 'missing'}, members=${JSON.stringify(frozen)}`, OPTIONS) return group } async function createAssignedPushedPlan(context, fixture, groupId, suffix) { const simulationResponse = await context.request('POST', CONTAINERS, { channelCode: fixture.channelCode, warehouseId: fixture.warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, }) const simulation = context.apiSuccess(MODULE, `${suffix} 创建组装柜模拟柜`, simulationResponse, evidence(simulationResponse), OPTIONS) const bookingId = id(simulation?.id) if (!bookingId) return null const planResponse = await context.request('POST', `${SMART}/plans`, { bookingId, remark: `${context.marker}-${suffix}`, }) let plan = context.apiSuccess(MODULE, `${suffix} 创建组装柜计划`, planResponse, evidence(planResponse), OPTIONS) const planId = id(plan?.id) if (!planId) return null const assignResponse = await context.request('POST', `${SMART}/plans/${encodeURIComponent(planId)}/group-assignments`, { inboundId: fixture.inboundId, expectedVersion: Number(plan?.version), operationId: `${context.marker}-${suffix}-assign`.slice(0, 128), }) plan = context.apiSuccess(MODULE, `${suffix} 整组原子分配`, assignResponse, evidence(assignResponse, `groupId=${groupId}`), OPTIONS) const pushResponse = await context.request('POST', `${SMART}/plans/${encodeURIComponent(planId)}/push-preload`, { expectedVersion: Number(plan?.version), }, undefined, { 'Idempotency-Key': `${context.marker}-${suffix}-push`.slice(0, 128) }) const pushed = context.apiSuccess(MODULE, `${suffix} 整组完整推送预装`, pushResponse, evidence(pushResponse), OPTIONS) return pushed ? { containerBookingId: bookingId, planId, plan: pushed, simulation } : null } async function createNormalPushedPlan(context, fixture, suffix) { const simulationResponse = await context.request('POST', CONTAINERS, { channelCode: fixture.channelCode, warehouseId: fixture.warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, }) const simulation = context.apiSuccess(MODULE, `${suffix} 创建普通单模拟柜`, simulationResponse, evidence(simulationResponse), OPTIONS) const containerBookingId = id(simulation?.id) if (!containerBookingId) return null const planResponse = await context.request('POST', `${SMART}/plans`, { bookingId: containerBookingId, remark: `${context.marker}-${suffix}`, }) let plan = context.apiSuccess(MODULE, `${suffix} 创建普通单装柜计划`, planResponse, evidence(planResponse), OPTIONS) const planId = id(plan?.id) if (!planId) return null const assignedCount = Math.max(1, Number(fixture.packages) - 1) const assignResponse = await context.request('POST', `${SMART}/plans/${encodeURIComponent(planId)}/inbound-assignments`, { inboundId: fixture.inboundId, packageCount: assignedCount, expectedVersion: Number(plan?.version), operationId: `${context.marker}-${suffix}-assign`.slice(0, 128), }) plan = context.apiSuccess(MODULE, `${suffix} 未成组普通单按数量分配`, assignResponse, evidence(assignResponse), OPTIONS) const assignedTotal = (plan?.items || []).filter((item) => id(item?.inboundId) === fixture.inboundId) .reduce((sum, item) => sum + Number(item?.packageCount || 0), 0) context.assert(MODULE, `${suffix} 普通单维持既有部分分配语义`, assignResponse, assignedTotal === assignedCount && assignedCount < Number(fixture.packages), `assigned=${assignedTotal}, source=${fixture.packages}`, OPTIONS) const pushResponse = await context.request('POST', `${SMART}/plans/${encodeURIComponent(planId)}/push-preload`, { expectedVersion: Number(plan?.version), }, undefined, { 'Idempotency-Key': `${context.marker}-${suffix}-push`.slice(0, 128) }) const pushed = context.apiSuccess(MODULE, `${suffix} 未成组普通单推送预装不回归`, pushResponse, evidence(pushResponse), OPTIONS) return pushed ? { containerBookingId, planId, plan: pushed, simulation, assignedCount } : null } async function createReleasedBookingAndLink(context, fixture, chains) { const inboundResponse = await context.request('GET', `/api/platform/inbounds/${encodeURIComponent(fixture.inboundId)}?tenantId=${encodeURIComponent(fixture.tenantId)}`) const inbound = context.apiSuccess(MODULE, 'PDA链读取订舱主数据', inboundResponse, evidence(inboundResponse), OPTIONS) const agentResponse = await context.request('GET', '/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&carrierCode=MATSON&page=1&size=20') const agentPage = context.apiSuccess(MODULE, '读取已关联MATSON的订舱代理', agentResponse, evidence(agentResponse), OPTIONS) const agent = (agentPage?.records || []).find((item) => item?.category === 'BOOKING' && item?.status === 'ACTIVE') const carrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS) context.assert(MODULE, 'PDA链订舱代理与船公司主数据齐全', agentResponse, Boolean(agent?.id && agent?.name && carrier?.id), `agentId=${id(agent?.id) || 'missing'}, carrierId=${id(carrier?.id) || 'missing'}`, OPTIONS) if (!agent?.id || !carrier?.id) return null const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18) const master = { customerId: fixture.tenantId, customerName: inbound?.summary?.customerName || `验收客户-${fixture.tenantId}`, channelCode: fixture.channelCode, channelName: fixture.channelName || fixture.channelCode, warehouseId: fixture.warehouseId, warehouseName: fixture.receivingWarehouseName || inbound?.summary?.warehouseName, } const draft = oceanBookingDraft(context, suffix, master, { bookingAgentId: id(agent.id), bookingAgentName: agent.name, shippingCompanyId: id(carrier.id), shippingCompanyName: carrier.name, carrierCode: 'MATSON', containerRequirements: [{ containerType: '40HC', containerKind: 'FCL', quantity: chains.length, capacityCbm: 76, maxWeightKg: 28000 }], }) const createResponse = await context.request('POST', '/api/platform/v1/ocean-bookings', { expectedVersion: 0, data: draft }, undefined, { 'Idempotency-Key': `${context.marker}-coload-pda-booking-create`.slice(0, 128) }) const created = context.apiSuccess(MODULE, 'PDA链创建订舱草稿', createResponse, evidence(createResponse), OPTIONS) const oceanBookingId = id(created?.oceanBooking?.id) if (!oceanBookingId) return null const releaseResponse = await context.request('POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/release`, { expectedVersion: Number(created?.oceanBooking?.version), reason: context.marker, }, undefined, { 'Idempotency-Key': `${context.marker}-coload-pda-booking-release`.slice(0, 128) }) const released = context.apiSuccess(MODULE, 'PDA链放舱并冻结两柜需求', releaseResponse, evidence(releaseResponse), OPTIONS) const requirement = (released?.containerRequirements || []).find((item) => item?.containerType === '40HC') context.assert(MODULE, '放舱需求数量覆盖短拣与足量两柜', releaseResponse, released?.oceanBooking?.status === 'RELEASED' && Number(requirement?.quantity) === chains.length, `status=${released?.oceanBooking?.status || 'missing'}, quantity=${requirement?.quantity ?? 'missing'}`, OPTIONS) if (!requirement?.id) return null for (let index = 0; index < chains.length; index += 1) { const chain = chains[index] const detailResponse = await context.request('GET', `${CONTAINERS}/${chain.containerBookingId}`) const detail = context.apiSuccess(MODULE, `${chain.label} 关联前读取柜版本`, detailResponse, evidence(detailResponse), OPTIONS) const containerNo = `CL${suffix}${index + 1}`.slice(0, 64) const linkResponse = await context.request('POST', `${CONTAINERS}/${chain.containerBookingId}/ocean-booking-link`, { oceanBookingId, requirementId: id(requirement.id), containerNo, expectedVersion: Number(detail?.version), }) const linked = context.apiSuccess(MODULE, `${chain.label} 关联RELEASED订舱并补真实柜号`, linkResponse, evidence(linkResponse), OPTIONS) context.assert(MODULE, `${chain.label} 保持原计划柜次身份`, linkResponse, id(linked?.id) === chain.containerBookingId && id(linked?.oceanBookingId) === oceanBookingId && linked?.containerNo === containerNo, `containerBookingId=${id(linked?.id) || 'missing'}, containerNo=${linked?.containerNo || 'missing'}`, OPTIONS) chain.containerNo = containerNo } return oceanBookingId } async function confirmAndClaimTask(context, chain, fixture) { const key = chain.key || chain.label.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 32) || 'chain' const detailResponse = await context.request('GET', `${FIRST_LEG}/loading-plans/${encodeURIComponent(chain.planId)}?warehouseId=${encodeURIComponent(fixture.warehouseId)}`) const detail = context.apiSuccess(MODULE, `${chain.label} 读取预装确认门禁`, detailResponse, evidence(detailResponse), OPTIONS) context.assert(MODULE, `${chain.label} 订舱关联后整份计划可确认`, detailResponse, detail?.summary?.status === 'EDITING' && detail?.summary?.readiness?.canConfirm === true, `status=${detail?.summary?.status || 'missing'}, canConfirm=${detail?.summary?.readiness?.canConfirm}`, OPTIONS) if (detail?.summary?.readiness?.canConfirm !== true) return null const confirmResponse = await context.request('POST', `${FIRST_LEG}/loading-plans/${encodeURIComponent(chain.planId)}/confirm`, { expectedVersion: Number(detail?.summary?.version), }, undefined, { 'Idempotency-Key': `${context.marker}-${key}-confirm`.slice(0, 128) }) const confirmed = context.apiSuccess(MODULE, `${chain.label} 原子确认并生成PDA任务`, confirmResponse, evidence(confirmResponse), OPTIONS) const taskId = id(confirmed?.readiness?.pdaTaskId) if (!taskId) return null const taskResponse = await context.request('GET', `${PDA}/tasks?loadingPlanId=${encodeURIComponent(chain.planId)}&warehouseId=${encodeURIComponent(fixture.warehouseId)}`) const task = context.apiSuccess(MODULE, `${chain.label} 读取唯一待领取PDA任务`, taskResponse, evidence(taskResponse), OPTIONS) const claimResponse = await context.request('POST', `${PDA}/tasks/${taskId}/claim`, { expectedAssignmentVersion: Number(task?.assignment?.version || 0), operationId: `${context.marker}-${key}-claim`.slice(0, 128), }) const claimed = context.apiSuccess(MODULE, `${chain.label} 领取PDA任务`, claimResponse, evidence(claimResponse), OPTIONS) const inventoryResponse = await context.request('GET', `${PDA}/tasks/${taskId}/inventory-snapshot?warehouseId=${encodeURIComponent(fixture.warehouseId)}`) const inventory = context.apiSuccess(MODULE, `${chain.label} 读取实时库存快照`, inventoryResponse, evidence(inventoryResponse), OPTIONS) context.assert(MODULE, `${chain.label} PDA任务包含同柜组全部入库码`, taskResponse, chain.inboundIds.every((inboundId) => (claimed?.items || []).some((item) => id(item?.inboundId) === inboundId)) && typeof inventory?.snapshotToken === 'string' && inventory.snapshotToken.length === 64, `taskId=${taskId}, inboundIds=${[...new Set((claimed?.items || []).map((item) => id(item?.inboundId)))].join(',')}`, OPTIONS) return { task: claimed, inventory, taskId } } async function fulfillmentTrees(context, fixtures, name) { const snapshots = [] for (const fixture of fixtures) { const response = await context.request('GET', `/api/platform/v1/orders/${encodeURIComponent(fixture.orderId)}/fulfillment-tree?tenantId=${encodeURIComponent(fixture.tenantId)}`) const tree = context.apiSuccess(MODULE, `${name} ${fixture.orderId} 履约树`, response, evidence(response), OPTIONS) snapshots.push({ fixture, response, tree, signature: treeSignature(tree) }) } return snapshots } async function uploadCompletionEvidence(context, fixture, label) { const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130]) for (const evidenceType of ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']) { const path = `/api/platform/v1/files/loading-evidence/${fixture.tenantId}/${fixture.inboundId}?evidenceType=${encodeURIComponent(evidenceType)}` const form = new FormData() form.append('file', new Blob([png], { type: 'image/png' }), `${context.marker}-${label}-${evidenceType}.png`) let uploadResponse try { const raw = await fetch(`${context.options.gateway}${path}`, { method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` }, body: form, signal: AbortSignal.timeout(60_000), }) const body = await raw.json() uploadResponse = { method: 'POST', path, http: raw.status, body, requestId: body?.requestId || raw.headers.get('x-request-id'), traceId: body?.traceId || raw.headers.get('x-trace-id') } } catch (error) { uploadResponse = { method: 'POST', path, http: 0, body: null, raw: String(error) } } const uploaded = context.apiSuccess(MODULE, `${label} 上传 ${evidenceType} 柜级证据`, uploadResponse, evidence(uploadResponse), OPTIONS) const fileId = id(uploaded?.fileId || uploaded?.id) if (!fileId) return false const bindResponse = await context.request('POST', `/api/platform/v1/inbounds/${fixture.inboundId}/loading/evidence?tenantId=${encodeURIComponent(fixture.tenantId)}`, { expectedVersion: 0, evidenceType, fileId, sealNo: evidenceType === 'SEAL' ? `SEAL-${context.marker}-${label}`.slice(0, 64) : null, remark: context.marker, }, undefined, { 'Idempotency-Key': `${context.marker}-${label}-${evidenceType}`.slice(0, 128) }) const bound = context.apiSuccess(MODULE, `${label} 绑定 ${evidenceType} 柜级证据`, bindResponse, evidence(bindResponse), OPTIONS) if (!bound) return false } return true } async function pollCompletedTask(context, taskId, warehouseId, label) { let response = null let task = null for (let attempt = 1; attempt <= 90; attempt += 1) { response = await context.request('GET', `${PDA}/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(warehouseId)}`) if (code(response) === '200') task = response.body?.data if (task?.taskStatus === 'COMPLETED' && task?.workflowStatus === 'COMPLETED') break if (task?.taskStatus === 'COMPLETION_FAILED') break await new Promise((resolve) => setTimeout(resolve, 1_000)) } context.assert(MODULE, `${label} 异步完成最终收敛`, response, task?.taskStatus === 'COMPLETED' && task?.workflowStatus === 'COMPLETED', evidence(response, `taskStatus=${task?.taskStatus || 'missing'}, workflow=${task?.workflowStatus || 'missing'}`), OPTIONS) return { response, task } } async function createRouteVariantFixture(context, base, variant) { const orders = await import(new URL('./orders.mjs', import.meta.url)) const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url)) const originalMarker = context.marker const originalOrder = context.createdResources.orders const originalMeasurement = context.createdResources.inboundMeasurement const originalProfile = context.context.orderProfile const originalDestination = context.context.orderDestinationType const originalFulfillment = context.context.orderFulfillmentType const originalRoute = context.context.waitSplitRoute const originalVariant = context.context.waitSplitVariant const originalDetailCount = context.context.phase3LoadingIncrementalDetailCount const originalPackageCount = context.context.phase3LoadingIncrementalPackageCount try { for (let attempt = 1; attempt <= 6; attempt += 1) { context.marker = `${originalMarker}-${variant === 'DESTINATION' ? 'DEST' : 'ROUTE'}-${attempt}` context.context.orderProfile = { destinationType: variant === 'DESTINATION' && attempt >= 3 ? 'OVERSEAS' : 'FBA', fulfillmentType: 'STANDARD', pickupMethod: 'PICKUP', taxMode: 'BUY_DECLARATION', containerMode: 'LCL', customsInspection: false, } context.context.orderDestinationType = context.context.orderProfile.destinationType context.context.orderFulfillmentType = 'STANDARD' context.context.waitSplitVariant = { sequence: 90 + attempt, skuCount: 4, packageCountPerSku: 1, packageCounts: [1, 1, 1, 1], totalPackageCount: 4, palletCount: 1, grossWeightPerPalletKg: 45, measurementProfile: { code: 'SMALL', first: { length: 29.1, width: 19.1, height: 18.1 }, replacement: { length: 29.5, width: 19.5, height: 18.5 }, outer: { length: 34, width: 24, height: 24 } }, } context.context.phase3LoadingIncrementalDetailCount = 4 context.context.phase3LoadingIncrementalPackageCount = 1 context.context.waitSplitRoute = { originWarehouseName: base.receivingWarehouseName || '', randomize: true, channelCode: variant === 'DESTINATION' ? base.channelCode : '', randomIndex: 50 + attempt * 31, destinationIndex: 80 + attempt * 37, taxModeIndex: attempt, } await orders.run(context) await measurement.run(context) const order = context.createdResources.orders || {} const inbound = context.createdResources.inboundMeasurement || {} const candidate = { marker: context.marker, orderId: id(order.orderId), inboundId: id(inbound.inboundId), tenantId: id(order.tenantId), warehouseId: id(order.receivingWarehouseId), receivingWarehouseName: order.receivingWarehouseName || null, channelCode: order.channelCode || null, destinationType: order.destinationType || null, destinationWarehouseId: id(order.destinationWarehouseId), destinationWarehouseCode: order.destinationWarehouseCode || null, packages: (order.skuLines || []).reduce((sum, item) => sum + Number(item.packageCount || 0), 0), processStatus: inbound.processStatus, } const sameLoadingRoute = candidate.warehouseId === base.warehouseId && candidate.channelCode === base.channelCode const differentDestination = candidate.destinationType !== base.destinationType || candidate.destinationWarehouseId !== id(base.destinationWarehouseId) || candidate.destinationWarehouseCode !== id(base.destinationWarehouseCode) const matched = variant === 'DESTINATION' ? sameLoadingRoute && differentDestination : !sameLoadingRoute if (candidate.orderId && candidate.inboundId && candidate.processStatus === 'WAIT_SPLIT' && matched) return candidate } return null } finally { context.marker = originalMarker if (originalOrder === undefined) delete context.createdResources.orders else context.createdResources.orders = originalOrder if (originalMeasurement === undefined) delete context.createdResources.inboundMeasurement else context.createdResources.inboundMeasurement = originalMeasurement context.context.orderProfile = originalProfile context.context.orderDestinationType = originalDestination context.context.orderFulfillmentType = originalFulfillment context.context.waitSplitRoute = originalRoute context.context.waitSplitVariant = originalVariant context.context.phase3LoadingIncrementalDetailCount = originalDetailCount context.context.phase3LoadingIncrementalPackageCount = originalPackageCount } } export async function run(context) { const fixtures = (context.createdResources?.waitSplitFixtures || []).map((item) => ({ ...item, orderId: id(item.orderId), inboundId: id(item.inboundId), tenantId: id(item.tenantId), warehouseId: id(item.receivingWarehouseId), packages: Number(item.totalPackageCount), })) const usable = fixtures.filter((item) => item.orderId && item.inboundId && item.tenantId && item.warehouseId && item.channelCode && item.packages > 0) context.assert(MODULE, '取得至少八票本轮 WAIT_SPLIT 新数据', null, usable.length >= 8, `count=${usable.length}, marker=${context.marker}`, OPTIONS) if (usable.length < 8) return const [first, second, third, fourth, fifth, sixth, seventh, eighth] = usable const sameRoute = [second, third, fourth, fifth, sixth, seventh].every((item) => item.warehouseId === first.warehouseId && item.channelCode === first.channelCode) && eighth.warehouseId === first.warehouseId && eighth.channelCode === first.channelCode context.assert(MODULE, '正向样本只要求同一国内仓和渠道', null, sameRoute, `warehouse=${first.warehouseId}, channel=${first.channelCode}`, OPTIONS) if (!sameRoute) return const groupOrders = [first.orderId, second.orderId] const createKey = `${context.marker}-coload-create`.slice(0, 128) const createdResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups', { orderIds: groupOrders }, undefined, { 'Idempotency-Key': createKey }) const group = context.apiSuccess(MODULE, '创建同柜不可拆组', createdResponse, evidence(createdResponse, `orders=${groupOrders.join(',')}`), OPTIONS) const groupId = id(group?.groupId) const frozen = memberFacts(group) context.assert(MODULE, '组冻结两票权威入库单与满箱数', createdResponse, groupId && group?.status === 'ACTIVE' && frozen.length === 2 && frozen.every((member) => [first, second].some((item) => item.orderId === member.orderId && item.inboundId === member.inboundId && item.packages === member.fullPackageCount)), `groupId=${groupId || 'missing'}, status=${group?.status || 'missing'}, members=${JSON.stringify(frozen)}`, OPTIONS) if (!groupId) return const replayResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups', { orderIds: groupOrders }, undefined, { 'Idempotency-Key': createKey }) const replay = context.apiSuccess(MODULE, '创建同键同载荷精确重放', replayResponse, evidence(replayResponse, `groupId=${id(replayResponse?.body?.data?.groupId)}`), OPTIONS) context.assert(MODULE, '创建重放不生成第二组', replayResponse, id(replay?.groupId) === groupId && replay?.version === group?.version && JSON.stringify(memberFacts(replay)) === JSON.stringify(frozen), `original=${groupId}, replay=${id(replay?.groupId)}, version=${replay?.version ?? 'missing'}`, OPTIONS) const changedPayloadResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups', { orderIds: [first.orderId, third.orderId] }, undefined, { 'Idempotency-Key': createKey }) conflict(context, '创建同键异载荷返回并发冲突', changedPayloadResponse) const differentDestination = await createRouteVariantFixture(context, first, 'DESTINATION') context.assert(MODULE, '创建同国内仓同渠道但不同目的仓港的正向样本', null, Boolean(differentDestination), differentDestination ? `orderId=${differentDestination.orderId}, warehouse=${differentDestination.warehouseId}, channel=${differentDestination.channelCode}, destination=${differentDestination.destinationType}/${differentDestination.destinationWarehouseId || differentDestination.destinationWarehouseCode}` : '六次正式下单均未取得跨目的仓样本,未借用历史订单', OPTIONS) if (differentDestination) { const crossDestinationResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups', { orderIds: [fifth.orderId, differentDestination.orderId] }, undefined, { 'Idempotency-Key': `${context.marker}-coload-cross-destination`.slice(0, 128) }) const crossDestinationGroup = context.apiSuccess(MODULE, '不同目的仓港仍可创建同柜不可拆组', crossDestinationResponse, evidence(crossDestinationResponse), OPTIONS) const crossDestinationFacts = memberFacts(crossDestinationGroup) context.assert(MODULE, '跨目的仓组完整冻结两票当前入仓片段', crossDestinationResponse, crossDestinationFacts.length === 2 && [fifth.inboundId, differentDestination.inboundId] .every((inboundId) => crossDestinationFacts.some((member) => member.inboundId === inboundId)), `members=${JSON.stringify(crossDestinationFacts)}`, OPTIONS) if (crossDestinationGroup?.groupId) { const cleanupResponse = await context.request('POST', `/api/platform/v1/first-leg/coload-groups/${encodeURIComponent(crossDestinationGroup.groupId)}/cancel`, { expectedVersion: Number(crossDestinationGroup.version), reason: '释放跨目的仓正向验收组成员', }, undefined, { 'Idempotency-Key': `${context.marker}-coload-cross-destination-cancel`.slice(0, 128) }) context.apiSuccess(MODULE, '跨目的仓正向组整组取消并释放成员', cleanupResponse, evidence(cleanupResponse), OPTIONS) } } const differentRoute = await createRouteVariantFixture(context, first, 'LOADING_ROUTE') context.assert(MODULE, '创建本轮不同国内仓或渠道反向样本', null, Boolean(differentRoute), differentRoute ? `orderId=${differentRoute.orderId}, warehouse=${differentRoute.warehouseId}, channel=${differentRoute.channelCode}` : '六次正式下单均未取得不同国内仓或渠道,未借用历史订单', OPTIONS) if (differentRoute) { const routeConflictResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups', { orderIds: [third.orderId, differentRoute.orderId] }, undefined, { 'Idempotency-Key': `${context.marker}-coload-route-conflict`.slice(0, 128) }) conflict(context, '跨国内仓或跨渠道创建整组被拒绝且不落组', routeConflictResponse, `baseOrder=${third.orderId}, mismatchOrder=${differentRoute.orderId}`) const zeroDriftResponses = await Promise.all([third, differentRoute].map((fixture) => context.request('GET', `/api/platform/v1/first-leg/coload-groups/by-inbound-code/${encodeURIComponent(fixture.inboundCode)}`))) context.assert(MODULE, '跨线路拒绝后两个入仓码均无组事实', zeroDriftResponses[0], zeroDriftResponses.every((response) => code(response) === 'A0404'), `codes=${zeroDriftResponses.map((response) => code(response)).join('|')}`, OPTIONS) } const queried = await readGroupSummary(context, first.inboundCode, '按入仓码回读组业务摘要') const frozenCodes = [first.inboundCode, second.inboundCode].sort() context.assert(MODULE, '业务摘要只返回状态、成员数和完整入仓码', queried.response, queried.data?.status === 'ACTIVE' && Number(queried.data?.memberCount) === 2 && JSON.stringify(summaryInboundCodes(queried.data)) === JSON.stringify(frozenCodes), `status=${queried.data?.status || 'missing'}, members=${summaryInboundCodes(queried.data).join('|')}`, OPTIONS) const simulationResponse = await context.request('POST', '/api/platform/v1/containers', { channelCode: first.channelCode, warehouseId: first.warehouseId, containerType: '20GP', capacityCbm: 33, maxWeightKg: 28000, }) const simulation = context.apiSuccess(MODULE, '创建容量拒绝专用20GP模拟柜', simulationResponse, evidence(simulationResponse), OPTIONS) let bookingId = id(simulation?.id) if (!bookingId) return const planResponse = await context.request('POST', '/api/platform/v1/first-leg/smart-loading/plans', { bookingId, remark: `${context.marker} 同柜不可拆组验收`, }) let plan = context.apiSuccess(MODULE, '创建容量拒绝专用空计划', planResponse, evidence(planResponse, `bookingId=${bookingId}`), OPTIONS) let planId = id(plan?.id) if (!planId) return const candidateResponse = await context.request('GET', `/api/platform/v1/first-leg/smart-loading/inbound-candidates?bookingId=${encodeURIComponent(bookingId)}&warehouseId=${encodeURIComponent(first.warehouseId)}`) const candidates = context.apiSuccess(MODULE, '读取整票智能装柜候选', candidateResponse, evidence(candidateResponse), OPTIONS) || [] const groupedCandidates = candidates.filter((item) => id(item?.coLoadGroupId) === groupId) context.assert(MODULE, '每个组成员候选携带相同整组事实', candidateResponse, groupedCandidates.length === 2 && groupedCandidates.every((item) => Number(item?.memberCount) === 2 && Number(item?.groupTotalBoxes) === first.packages + second.packages && item?.locked !== true), `count=${groupedCandidates.length}, groupTotal=${first.packages + second.packages}`, OPTIONS) let isolatedCandidates = candidates const competing = [...new Set(isolatedCandidates.filter((item) => item?.locked !== true) .map((item) => id(item?.coLoadGroupId)).filter((value) => value && value !== groupId))] let isolatedCandidateResponse = candidateResponse if (competing.length > 0) { isolatedCandidateResponse = await context.request('GET', `${SMART}/inbound-candidates?bookingId=${encodeURIComponent(bookingId)}&warehouseId=${encodeURIComponent(first.warehouseId)}`) isolatedCandidates = context.apiSuccess(MODULE, '清理后重新隔离读取目标组候选', isolatedCandidateResponse, evidence(isolatedCandidateResponse), OPTIONS) || [] } const candidateGroupIds = [...new Set(isolatedCandidates.filter((item) => item?.locked !== true) .map((item) => id(item?.coLoadGroupId)).filter(Boolean))] const targetCandidateInbounds = isolatedCandidates.filter((item) => id(item?.coLoadGroupId) === groupId) .map((item) => id(item?.inboundId)).sort() context.assert(MODULE, '推荐候选仅含当前目标同柜组且成员身份精确', isolatedCandidateResponse, candidateGroupIds.length === 1 && candidateGroupIds[0] === groupId && JSON.stringify(targetCandidateInbounds) === JSON.stringify([first.inboundId, second.inboundId].sort()), `groupId=${groupId}, candidateGroupIds=${candidateGroupIds.join('|') || 'none'}, members=${targetCandidateInbounds.join('|')}, capacity=33cbm/28000kg`, OPTIONS) const allCandidateVolume = isolatedCandidates .reduce((sum, item) => sum + Number(item?.availableVolumeCbm || 0), 0) context.assert(MODULE, '推荐候选总量形成容量竞争场景', isolatedCandidateResponse, allCandidateVolume > 33 * 1.1, `targetGroupId=${groupId}, candidateVolume=${allCandidateVolume}, limit=${33 * 1.1}`, OPTIONS) const beforeRecommend = plan const recommendResponse = await context.request('POST', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/recommend`, { expectedVersion: Number(plan?.version), operationId: `${context.marker}-coload-capacity-recommend`.slice(0, 128), }) const capacityRecommendation = context.apiSuccess(MODULE, '容量竞争时智能推荐不因整组容量失败', recommendResponse, evidence(recommendResponse, `targetGroupId=${groupId}`), OPTIONS) const capacityGroupItems = (capacityRecommendation?.items || []) .filter((item) => [first.inboundId, second.inboundId].includes(id(item?.inboundId))) const capacityTotals = new Map() for (const item of capacityGroupItems) { capacityTotals.set(id(item?.inboundId), (capacityTotals.get(id(item?.inboundId)) || 0) + Number(item?.packageCount || 0)) } const capacityTookNone = capacityGroupItems.length === 0 const capacityTookWhole = [first, second].every((fixture) => capacityTotals.get(fixture.inboundId) === fixture.packages) context.assert(MODULE, '容量竞争时同柜组只能整组全取或全不取', recommendResponse, capacityTookNone || capacityTookWhole, `targetGroupId=${groupId}, assignedMembers=${capacityGroupItems.map((item) => id(item?.inboundId)).join('|') || 'none'}, before=${planSignature(beforeRecommend)}, after=${planSignature(capacityRecommendation)}`, OPTIONS) const recommendSimulationResponse = await context.request('POST', '/api/platform/v1/containers', { channelCode: first.channelCode, warehouseId: first.warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, }) const recommendSimulation = context.apiSuccess(MODULE, '创建智能推荐整组成功专用40HC模拟柜', recommendSimulationResponse, evidence(recommendSimulationResponse), OPTIONS) const recommendBookingId = id(recommendSimulation?.id) if (!recommendBookingId) return const recommendPlanResponse = await context.request('POST', `${SMART}/plans`, { bookingId: recommendBookingId, remark: `${context.marker} 智能推荐整组全取`, }) let recommendPlan = context.apiSuccess(MODULE, '创建智能推荐整组成功专用计划', recommendPlanResponse, evidence(recommendPlanResponse), OPTIONS) const recommendPlanId = id(recommendPlan?.id) if (!recommendPlanId) return const recommendSuccessResponse = await context.request('POST', `${SMART}/plans/${encodeURIComponent(recommendPlanId)}/recommend`, { expectedVersion: Number(recommendPlan?.version), operationId: `${context.marker}-coload-success-recommend`.slice(0, 128), }) recommendPlan = context.apiSuccess(MODULE, '第二柜智能推荐请求成功', recommendSuccessResponse, evidence(recommendSuccessResponse, `groupId=${groupId}`), OPTIONS) const recommendedTotals = new Map() for (const item of recommendPlan?.items || []) { recommendedTotals.set(id(item?.inboundId), (recommendedTotals.get(id(item?.inboundId)) || 0) + Number(item?.packageCount || 0)) } const recommendTookNone = [first, second].every((fixture) => !recommendedTotals.has(fixture.inboundId)) const recommendTookWhole = [first, second] .every((fixture) => recommendedTotals.get(fixture.inboundId) === fixture.packages) context.assert(MODULE, '智能推荐不会只取同柜组部分成员或部分箱数', recommendSuccessResponse, recommendTookNone || recommendTookWhole, `groupId=${groupId}, inboundTotals=${JSON.stringify(Object.fromEntries(recommendedTotals))}`, OPTIONS) if (recommendTookWhole) { const recommendRemoveResponse = await context.request('DELETE', `${SMART}/plans/${encodeURIComponent(recommendPlanId)}/group-assignments/${encodeURIComponent(groupId)}`, { expectedPlanVersion: Number(recommendPlan?.version), operationId: `${context.marker}-coload-success-recommend-remove`.slice(0, 128), }) context.apiSuccess(MODULE, '智能推荐整组验收后原子移除并释放组锁', recommendRemoveResponse, evidence(recommendRemoveResponse), OPTIONS) } else { const recommendGroup = await readGroupSummary(context, first.inboundCode, '智能推荐全不取后回读组状态') context.assert(MODULE, '智能推荐全不取后整组仍ACTIVE', recommendGroup.response, recommendGroup.data?.status === 'ACTIVE', `status=${recommendGroup.data?.status || 'missing'}`, OPTIONS) } const workingSimulationResponse = await context.request('POST', '/api/platform/v1/containers', { channelCode: first.channelCode, warehouseId: first.warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, }) const workingSimulation = context.apiSuccess(MODULE, '创建PDA主链40HC模拟柜', workingSimulationResponse, evidence(workingSimulationResponse), OPTIONS) bookingId = id(workingSimulation?.id) if (!bookingId) return const workingPlanResponse = await context.request('POST', `${SMART}/plans`, { bookingId, remark: `${context.marker} 同柜不可拆组PDA主链`, }) plan = context.apiSuccess(MODULE, '创建PDA主链同柜装柜计划', workingPlanResponse, evidence(workingPlanResponse, `bookingId=${bookingId}`), OPTIONS) planId = id(plan?.id) if (!planId) return const beforeIndividual = await readPlan(context, planId, '读取单票绕过前计划快照') const individualResponse = await context.request('POST', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/inbound-assignments`, { inboundId: first.inboundId, packageCount: first.packages, expectedVersion: Number(beforeIndividual.data?.version), operationId: `${context.marker}-coload-individual`.slice(0, 128), }) conflict(context, '组成员不能通过单票入口分配', individualResponse) const afterIndividual = await readPlan(context, planId, '回读单票拒绝后计划快照') context.assert(MODULE, '单票分配拒绝后计划零漂移', afterIndividual.response, planSignature(beforeIndividual.data) === planSignature(afterIndividual.data), `before=${planSignature(beforeIndividual.data)}, after=${planSignature(afterIndividual.data)}`, OPTIONS) const assignResponse = await context.request('POST', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/group-assignments`, { inboundId: first.inboundId, expectedVersion: Number(afterIndividual.data?.version), operationId: `${context.marker}-coload-group-assign`.slice(0, 128), }) plan = context.apiSuccess(MODULE, '整组原子分配全部成员', assignResponse, evidence(assignResponse, `groupId=${groupId}`), OPTIONS) const assigned = (plan?.items || []).filter((item) => [first.inboundId, second.inboundId].includes(id(item?.inboundId))) const assignedTotals = new Map() for (const item of assigned) assignedTotals.set(id(item?.inboundId), (assignedTotals.get(id(item?.inboundId)) || 0) + Number(item?.packageCount || 0)) context.assert(MODULE, '整组分配同计划且每票均为满箱数', assignResponse, [first, second].every((fixture) => assignedTotals.get(fixture.inboundId) === fixture.packages), `inboundTotals=${JSON.stringify(Object.fromEntries(assignedTotals))}`, OPTIONS) const lockedQuery = await readGroupSummary(context, first.inboundCode, '回读整组锁定事实') context.assert(MODULE, '整组业务状态只单调推进至LOCKED', lockedQuery.response, lockedQuery.data?.status === 'LOCKED', `status=${lockedQuery.data?.status || 'missing'}`, OPTIONS) const targetItem = assigned[0] const beforeMutation = planSignature(plan) const adjustResponse = await context.request('PATCH', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/items/${encodeURIComponent(id(targetItem?.id))}`, { packageCount: Math.max(1, Number(targetItem?.packageCount) - 1), expectedPlanVersion: Number(plan?.version), expectedItemVersion: Number(targetItem?.version), operationId: `${context.marker}-coload-adjust`.slice(0, 128), }) conflict(context, '组成员不能单独调整箱数', adjustResponse) const deleteResponse = await context.request('DELETE', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/items/${encodeURIComponent(id(targetItem?.id))}`, { expectedPlanVersion: Number(plan?.version), expectedItemVersion: Number(targetItem?.version), operationId: `${context.marker}-coload-delete`.slice(0, 128), }) conflict(context, '组成员不能单独删除', deleteResponse) const afterMutation = await readPlan(context, planId, '回读组成员变更拒绝后计划') context.assert(MODULE, '单成员调整删除拒绝后整组零漂移', afterMutation.response, beforeMutation === planSignature(afterMutation.data), `before=${beforeMutation}, after=${planSignature(afterMutation.data)}`, OPTIONS) const cancelLockedResponse = await context.request('POST', `/api/platform/v1/first-leg/coload-groups/${encodeURIComponent(groupId)}/cancel`, { expectedVersion: Number(group?.version) + 1, reason: '锁定组取消反向验收', }, undefined, { 'Idempotency-Key': `${context.marker}-coload-cancel-locked`.slice(0, 128) }) conflict(context, '已锁定组不能取消', cancelLockedResponse) const removeResponse = await context.request('DELETE', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/group-assignments/${encodeURIComponent(groupId)}`, { expectedPlanVersion: Number(afterMutation.data?.version), operationId: `${context.marker}-coload-group-remove`.slice(0, 128), }) plan = context.apiSuccess(MODULE, '整组原子移除', removeResponse, evidence(removeResponse), OPTIONS) context.assert(MODULE, '整组移除后两个成员同时消失', removeResponse, !(plan?.items || []).some((item) => [first.inboundId, second.inboundId].includes(id(item?.inboundId))), `remainingItems=${plan?.items?.length ?? 'missing'}`, OPTIONS) const activeQuery = await readGroupSummary(context, first.inboundCode, '回读整组解锁事实') context.assert(MODULE, '整组移除后业务状态恢复ACTIVE', activeQuery.response, activeQuery.data?.status === 'ACTIVE', `status=${activeQuery.data?.status || 'missing'}`, OPTIONS) const manualGroupResponse = await context.request('POST', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/group-assignments`, { inboundId: first.inboundId, expectedVersion: Number(plan?.version), operationId: `${context.marker}-coload-reassign`.slice(0, 128), }) plan = context.apiSuccess(MODULE, '整组移除后再次完整回配', manualGroupResponse, evidence(manualGroupResponse), OPTIONS) if (!plan) return const pushKey = `${context.marker}-coload-push`.slice(0, 128) const pushResponse = await context.request('POST', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}/push-preload`, { expectedVersion: Number(plan.version), }, undefined, { 'Idempotency-Key': pushKey }) const pushed = context.apiSuccess(MODULE, '整组完整时推送预装成功', pushResponse, evidence(pushResponse), OPTIONS) const pushedTotals = new Map() for (const item of pushed?.items || []) pushedTotals.set(id(item?.inboundId), (pushedTotals.get(id(item?.inboundId)) || 0) + Number(item?.packageCount || 0)) context.assert(MODULE, '推送后整组仍保持同计划满量事实', pushResponse, [first, second].every((fixture) => pushedTotals.get(fixture.inboundId) === fixture.packages), `planId=${planId}, inboundTotals=${JSON.stringify(Object.fromEntries(pushedTotals))}`, OPTIONS) const secondGroup = await createSecondGroup(context, third, fourth) const secondGroupId = id(secondGroup?.groupId) if (!secondGroupId) return const fullChainBase = await createAssignedPushedPlan(context, third, secondGroupId, 'coload-full') const normalChainBase = await createNormalPushedPlan(context, seventh, 'normal-split') if (!fullChainBase || !normalChainBase) return const cancelCreateResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups', { orderIds: [fifth.orderId, sixth.orderId] }, undefined, { 'Idempotency-Key': `${context.marker}-coload-cancel-create`.slice(0, 128) }) const cancelGroup = context.apiSuccess(MODULE, '创建仅用于ACTIVE取消的同柜组', cancelCreateResponse, evidence(cancelCreateResponse), OPTIONS) const cancelGroupId = id(cancelGroup?.groupId) if (!cancelGroupId) return const cancelResponse = await context.request('POST', `/api/platform/v1/first-leg/coload-groups/${encodeURIComponent(cancelGroupId)}/cancel`, { expectedVersion: Number(cancelGroup?.version), reason: '验收ACTIVE整组取消', }, undefined, { 'Idempotency-Key': `${context.marker}-coload-active-cancel`.slice(0, 128) }) const cancelled = context.apiSuccess(MODULE, 'ACTIVE同柜组整组取消成功', cancelResponse, evidence(cancelResponse), OPTIONS) const cancelledQueryResponse = await context.request('GET', `/api/platform/v1/first-leg/coload-groups/by-inbound-code/${encodeURIComponent(fifth.inboundCode)}`) context.assert(MODULE, '取消组进入CANCELLED且成员不再受活动组约束', cancelResponse, cancelled?.status === 'CANCELLED' && code(cancelledQueryResponse) === 'A0404', `status=${cancelled?.status || 'missing'}, queryCode=${code(cancelledQueryResponse)}`, OPTIONS) const partialChain = { label: '同柜短拣链', key: 'coload-partial', groupId, containerBookingId: bookingId, planId, fixtures: [first, second], inboundIds: [first.inboundId, second.inboundId], } const fullChain = { label: '同柜足量链', key: 'coload-full', groupId: secondGroupId, ...fullChainBase, fixtures: [third, fourth], inboundIds: [third.inboundId, fourth.inboundId], } const normalChain = { label: '普通单拆分链', key: 'normal-split', ...normalChainBase, fixtures: [seventh], inboundIds: [seventh.inboundId], } const oceanBookingId = await createReleasedBookingAndLink(context, first, [partialChain, fullChain, normalChain]) if (!oceanBookingId) return const partialPda = await confirmAndClaimTask(context, partialChain, first) const fullPda = await confirmAndClaimTask(context, fullChain, third) const normalPda = await confirmAndClaimTask(context, normalChain, seventh) if (!partialPda || !fullPda || !normalPda) return const partialTreesBefore = await fulfillmentTrees(context, partialChain.fixtures, '短拣前') const partialTarget = (partialPda.task?.items || []).find((item) => Number(item?.plannedPackageCount) > 0) context.assert(MODULE, '短拣链存在可记录SKIP的组成员明细', null, Boolean(partialTarget?.id), `planned=${partialTarget?.plannedPackageCount ?? 'missing'}, taskItems=${partialPda.task?.items?.length ?? 0}`, OPTIONS) if (!partialTarget?.id) return const skippedInboundId = id(partialTarget.inboundId) const partialBatchItems = (partialPda.task.items || []).map((item) => id(item.inboundId) === skippedInboundId ? { itemId: id(item.id), action: 'SKIP', pickedPackageCount: null, finalPickedPackageCount: null, releaseMode: null, locationIds: [], version: Number(item.version), reason: '同柜不可拆组短拣验收' } : { itemId: id(item.id), action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount), finalPickedPackageCount: null, releaseMode: 'MANUAL', locationIds: [], version: Number(item.version), reason: null }) const partialBatchResponse = await context.request('POST', `${PDA}/tasks/${partialPda.taskId}/batch`, { warehouseId: first.warehouseId, loadingPlanId: partialChain.planId, containerBookingId: partialChain.containerBookingId, expectedVersion: Number(partialPda.task.version), inventorySnapshotToken: partialPda.inventory.snapshotToken, items: partialBatchItems, }, undefined, { 'Idempotency-Key': `${context.marker}-coload-partial-batch`.slice(0, 128) }) const partialBatch = context.apiSuccess(MODULE, '同柜组PDA原子批次记录短拣异常', partialBatchResponse, evidence(partialBatchResponse), OPTIONS) context.assert(MODULE, '短拣批次只进入待确认且保留SKIP事实', partialBatchResponse, partialBatch?.taskStatus === 'LOADING' && partialBatch?.workflowStatus === 'AWAITING_REVIEW' && partialBatch?.items?.filter((item) => id(item?.inboundId) === skippedInboundId) .every((item) => item?.itemStatus === 'SKIPPED' && Number(item?.pickedPackageCount) === 0), `taskStatus=${partialBatch?.taskStatus || 'missing'}, workflow=${partialBatch?.workflowStatus || 'missing'}`, OPTIONS) const partialTaskBeforeComplete = taskSignature(partialBatch) const partialCompleteResponse = await context.request('POST', `${PDA}/tasks/${partialPda.taskId}/complete`, { expectedTaskVersion: Number(partialBatch?.version), operationId: `${context.marker}-coload-partial-complete`.slice(0, 128), remark: '同柜不可拆组短拣不得完成', }) conflict(context, '同柜组任一成员短拣时完成被拒绝', partialCompleteResponse, `message=${partialCompleteResponse?.body?.message || 'missing'}`) context.assert(MODULE, '短拣完成拒绝明确命中全组箱数守恒', partialCompleteResponse, String(partialCompleteResponse?.body?.message || '').includes('全组箱数守恒'), evidence(partialCompleteResponse, `message=${partialCompleteResponse?.body?.message || 'missing'}`), OPTIONS) const partialTaskAfterResponse = await context.request('GET', `${PDA}/tasks/${partialPda.taskId}?warehouseId=${encodeURIComponent(first.warehouseId)}`) const partialTaskAfter = context.apiSuccess(MODULE, '短拣完成拒绝后回读PDA任务', partialTaskAfterResponse, evidence(partialTaskAfterResponse), OPTIONS) const partialTreesAfter = await fulfillmentTrees(context, partialChain.fixtures, '短拣拒绝后') const partialGroup = await readGroupSummary(context, first.inboundCode, '短拣拒绝后回读组状态') context.assert(MODULE, '短拣完成拒绝后任务、父子树和组锁零漂移', partialTaskAfterResponse, partialTaskBeforeComplete === taskSignature(partialTaskAfter) && partialTreesBefore.every((before, index) => before.signature === partialTreesAfter[index]?.signature && (partialTreesAfter[index]?.tree?.children || []).length === 0) && partialGroup.data?.status === 'LOCKED', `taskStable=${partialTaskBeforeComplete === taskSignature(partialTaskAfter)}, children=${partialTreesAfter.map((item) => item.tree?.children?.length ?? -1).join('|')}, groupStatus=${partialGroup.data?.status || 'missing'}`, OPTIONS) const fullTreesBefore = await fulfillmentTrees(context, fullChain.fixtures, '足量完成前') const fullBatchItems = (fullPda.task?.items || []).map((item) => ({ itemId: id(item.id), action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount), finalPickedPackageCount: null, releaseMode: 'MANUAL', locationIds: [], version: Number(item.version), reason: null, })) const fullBatchResponse = await context.request('POST', `${PDA}/tasks/${fullPda.taskId}/batch`, { warehouseId: third.warehouseId, loadingPlanId: fullChain.planId, containerBookingId: fullChain.containerBookingId, expectedVersion: Number(fullPda.task.version), inventorySnapshotToken: fullPda.inventory.snapshotToken, items: fullBatchItems, }, undefined, { 'Idempotency-Key': `${context.marker}-coload-full-batch`.slice(0, 128) }) const fullBatch = context.apiSuccess(MODULE, '同柜组PDA全成员足量批量拣货', fullBatchResponse, evidence(fullBatchResponse), OPTIONS) context.assert(MODULE, '足量批次全明细进入待确认', fullBatchResponse, fullBatch?.workflowStatus === 'AWAITING_REVIEW' && fullBatch?.items?.every((item) => Number(item?.pickedPackageCount) === Number(item?.plannedPackageCount)), `workflow=${fullBatch?.workflowStatus || 'missing'}, items=${fullBatch?.items?.length ?? 0}`, OPTIONS) if (!await uploadCompletionEvidence(context, third, 'coload-full')) return const fullCompleteResponse = await context.request('POST', `${PDA}/tasks/${fullPda.taskId}/complete`, { expectedTaskVersion: Number(fullBatch?.version), operationId: `${context.marker}-coload-full-complete`.slice(0, 128), remark: context.marker, }) const acceptedFull = context.apiSuccess(MODULE, '同柜组足量完成命令可靠受理', fullCompleteResponse, evidence(fullCompleteResponse), OPTIONS) context.assert(MODULE, '足量完成先返回PROCESSING或已完成事实', fullCompleteResponse, ['PROCESSING', 'COMPLETED'].includes(acceptedFull?.workflowStatus), `taskStatus=${acceptedFull?.taskStatus || 'missing'}, workflow=${acceptedFull?.workflowStatus || 'missing'}`, OPTIONS) const fullFinal = await pollCompletedTask(context, fullPda.taskId, third.warehouseId, '同柜组足量') if (fullFinal.task?.taskStatus !== 'COMPLETED') return const fullTreesAfter = await fulfillmentTrees(context, fullChain.fixtures, '足量完成后') const consumedGroup = await readGroupSummary(context, third.inboundCode, '足量完成后回读组消费事实') const completedInboundStates = [] for (const fixture of fullChain.fixtures) { const response = await context.request('GET', `/api/platform/inbounds/${encodeURIComponent(fixture.inboundId)}?tenantId=${encodeURIComponent(fixture.tenantId)}`) const inbound = context.apiSuccess(MODULE, `足量完成后回读入库单 ${fixture.inboundId}`, response, evidence(response), OPTIONS) completedInboundStates.push(inbound?.summary?.processStatus) } const fullRemainders = fullTreesAfter.flatMap((item) => item?.tree?.children || []) .filter((item) => item?.splitRole === 'REMAINDER') context.assert(MODULE, '足量完成整组已消费、全部入库完成且不生成余量子单', consumedGroup.response, consumedGroup.data?.status === 'CONSUMED' && completedInboundStates.every((status) => status === 'LOADED') && fullRemainders.length === 0, `groupStatus=${consumedGroup.data?.status || 'missing'}, inboundStates=${completedInboundStates.join('|')}, remainderChildren=${fullRemainders.length}, childRoles=${fullTreesAfter.flatMap((item) => item?.tree?.children || []).map((item) => item?.splitRole).join('|') || 'none'}`, OPTIONS) const normalTreeBefore = await fulfillmentTrees(context, normalChain.fixtures, '普通单完成前') const normalBatchItems = (normalPda.task?.items || []).map((item) => ({ itemId: id(item.id), action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount), finalPickedPackageCount: null, releaseMode: 'MANUAL', locationIds: [], version: Number(item.version), reason: null, })) const normalBatchResponse = await context.request('POST', `${PDA}/tasks/${normalPda.taskId}/batch`, { warehouseId: seventh.warehouseId, loadingPlanId: normalChain.planId, containerBookingId: normalChain.containerBookingId, expectedVersion: Number(normalPda.task.version), inventorySnapshotToken: normalPda.inventory.snapshotToken, items: normalBatchItems, }, undefined, { 'Idempotency-Key': `${context.marker}-normal-batch`.slice(0, 128) }) const normalBatch = context.apiSuccess(MODULE, '未成组普通单PDA批量拣货不回归', normalBatchResponse, evidence(normalBatchResponse), OPTIONS) if (!await uploadCompletionEvidence(context, seventh, 'normal-split')) return const normalCompleteResponse = await context.request('POST', `${PDA}/tasks/${normalPda.taskId}/complete`, { expectedTaskVersion: Number(normalBatch?.version), operationId: `${context.marker}-normal-complete`.slice(0, 128), remark: context.marker, }) context.apiSuccess(MODULE, '未成组普通单完成命令受理', normalCompleteResponse, evidence(normalCompleteResponse), OPTIONS) const normalFinal = await pollCompletedTask(context, normalPda.taskId, seventh.warehouseId, '未成组普通单') if (normalFinal.task?.taskStatus !== 'COMPLETED') return const normalTreeAfter = await fulfillmentTrees(context, normalChain.fixtures, '普通单完成后') const normalTree = normalTreeAfter[0]?.tree const remainder = (normalTree?.children || []).find((item) => item?.splitRole === 'REMAINDER') context.assert(MODULE, '未成组普通单仍按既有规则生成唯一余量子单', normalTreeAfter[0]?.response, normalTreeBefore[0]?.tree?.children?.length === 0 && Boolean(remainder?.orderId) && Number(normalTree?.cumulativeLoadedPackageCount) === Number(normalChain.assignedCount) && Number(normalTree?.currentRemainderPackageCount) === Number(seventh.packages) - Number(normalChain.assignedCount), `children=${normalTree?.children?.length ?? 0}, remainderOrderId=${id(remainder?.orderId) || 'missing'}`, OPTIONS) if (!remainder?.orderId) return const splitTreeBeforeGroup = treeSignature(normalTree) const splitGroupResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups', { orderIds: [id(remainder.orderId), eighth.orderId] }, undefined, { 'Idempotency-Key': `${context.marker}-coload-split-allow`.slice(0, 128) }) const splitGroup = context.apiSuccess(MODULE, '拆分余量子单仍可与同仓同渠道订单创建同柜组', splitGroupResponse, evidence(splitGroupResponse, `rootOrderId=${seventh.orderId}, remainderOrderId=${id(remainder.orderId)}`), OPTIONS) const splitMembers = memberFacts(splitGroup) context.assert(MODULE, '拆分身份不影响当前WAIT_SPLIT片段完整纳组', splitGroupResponse, splitMembers.length >= 2 && splitMembers.some((item) => item.orderId === id(remainder.orderId)) && splitMembers.some((item) => item.orderId === eighth.orderId) && splitMembers.every((item) => item.fullPackageCount > 0), `members=${JSON.stringify(splitMembers)}`, OPTIONS) const splitSummary = await readGroupSummary(context, eighth.inboundCode, '拆分组按入仓码返回完整成员') context.assert(MODULE, '拆分组摘要返回全部当前有效成员入仓码且不重复', splitSummary.response, Number(splitSummary.data?.memberCount) === splitMembers.length && new Set(summaryInboundCodes(splitSummary.data)).size === splitMembers.length, `memberCount=${splitSummary.data?.memberCount ?? 'missing'}, members=${summaryInboundCodes(splitSummary.data).join('|')}`, OPTIONS) const splitTreeAfterResponse = await context.request('GET', `/api/platform/v1/orders/${encodeURIComponent(seventh.orderId)}/fulfillment-tree?tenantId=${encodeURIComponent(seventh.tenantId)}`) const splitTreeAfter = context.apiSuccess(MODULE, '拆分余量建组后回读根履约树', splitTreeAfterResponse, evidence(splitTreeAfterResponse), OPTIONS) context.assert(MODULE, '拆分余量建组不改写既有父子数量事实', splitTreeAfterResponse, splitTreeBeforeGroup === treeSignature(splitTreeAfter), `treeStable=${splitTreeBeforeGroup === treeSignature(splitTreeAfter)}`, OPTIONS) if (splitGroup?.groupId) { const splitCancelResponse = await context.request('POST', `/api/platform/v1/first-leg/coload-groups/${encodeURIComponent(splitGroup.groupId)}/cancel`, { expectedVersion: Number(splitGroup.version), reason: '完成拆分余量正向验收', }, undefined, { 'Idempotency-Key': `${context.marker}-coload-split-cancel`.slice(0, 128) }) context.apiSuccess(MODULE, '拆分余量正向组整组取消', splitCancelResponse, evidence(splitCancelResponse), OPTIONS) } let uiAcceptance = null if (differentDestination) { const uiGroupResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups', { orderIds: [fifth.orderId, differentDestination.orderId] }, undefined, { 'Idempotency-Key': `${context.marker}-coload-ui-preserve`.slice(0, 128) }) const uiGroup = context.apiSuccess(MODULE, '为Admin页面保留ACTIVE未分配跨目的仓组', uiGroupResponse, evidence(uiGroupResponse), OPTIONS) const uiSimulationResponse = await context.request('POST', CONTAINERS, { channelCode: fifth.channelCode, warehouseId: fifth.warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, }) const uiSimulation = context.apiSuccess(MODULE, '为Admin页面保留可编辑智能装柜柜次', uiSimulationResponse, evidence(uiSimulationResponse), OPTIONS) const uiBookingId = id(uiSimulation?.id) const uiPlanResponse = uiBookingId ? await context.request('POST', `${SMART}/plans`, { bookingId: uiBookingId, remark: `${context.marker} Admin整组候选人工验收保留`, }) : null const uiPlan = uiPlanResponse ? context.apiSuccess(MODULE, '为Admin页面保留未分配空计划', uiPlanResponse, evidence(uiPlanResponse, `bookingId=${uiBookingId}`), OPTIONS) : null const uiMembers = memberFacts(uiGroup) context.assert(MODULE, '页面验收保留组仍为ACTIVE且未锁定', uiGroupResponse, uiGroup?.status === 'ACTIVE' && !uiGroup?.lockedPlanId && uiMembers.length === 2 && Boolean(uiBookingId) && Boolean(uiPlan?.id), `groupId=${id(uiGroup?.groupId)}, groupNo=${uiGroup?.groupNo || 'missing'}, bookingId=${uiBookingId}, planId=${id(uiPlan?.id)}, members=${JSON.stringify(uiMembers)}`, OPTIONS) uiAcceptance = { groupId: id(uiGroup?.groupId), groupNo: uiGroup?.groupNo || null, orderIds: [fifth.orderId, differentDestination.orderId], inboundIds: [fifth.inboundId, differentDestination.inboundId], bookingId: uiBookingId, planId: id(uiPlan?.id), } } context.createdResources.coloadGroupAcceptance = { marker: context.marker, groupId, groupNo: group?.groupNo || null, planId, bookingId, orderIds: groupOrders, inboundIds: [first.inboundId, second.inboundId], fullGroupId: secondGroupId, fullTaskId: fullPda.taskId, fullPlanId: fullChain.planId, partialTaskId: partialPda.taskId, normalTaskId: normalPda.taskId, oceanBookingId, cancelledGroupId: cancelGroupId, remainderOrderId: id(remainder.orderId), splitGroupId: id(splitGroup?.groupId), crossDestinationOrderId: id(differentDestination?.orderId), uiAcceptance, publicBypassResult: 'individual assignment/adjust/delete and short-pick completion rejected; full group consumed', } }