const MODULE = 'coload-groups' const OPTIONS = { scenario: 'indivisible-coload-group', deploymentBlocker: true } 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)) } 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 createDifferentRouteFixture(context, base) { 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 try { for (let attempt = 1; attempt <= 3; attempt += 1) { context.marker = `${originalMarker}-ROUTE-${attempt}` context.context.orderProfile = { destinationType: 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: 1, packageCountPerSku: 2, packageCounts: [2], totalPackageCount: 2, 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.waitSplitRoute = { originWarehouseName: base.receivingWarehouseName || '', randomize: true, 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 differs = candidate.warehouseId !== base.warehouseId || candidate.channelCode !== base.channelCode || candidate.destinationType !== base.destinationType || candidate.destinationWarehouseId !== id(base.destinationWarehouseId) || candidate.destinationWarehouseCode !== id(base.destinationWarehouseCode) if (candidate.orderId && candidate.inboundId && candidate.processStatus === 'WAIT_SPLIT' && differs) 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 } } 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 >= 4, `count=${usable.length}, marker=${context.marker}`, OPTIONS) if (usable.length < 4) return const [first, second, third, fourth] = usable const sameRoute = [second, third, fourth].every((item) => item.warehouseId === first.warehouseId && item.channelCode === first.channelCode && item.destinationType === first.destinationType && id(item.destinationWarehouseId) === id(first.destinationWarehouseId) && id(item.destinationWarehouseCode) === id(first.destinationWarehouseCode)) context.assert(MODULE, '正向样本属于同一仓库渠道和目的仓港', null, sameRoute, `warehouse=${first.warehouseId}, channel=${first.channelCode}, destination=${first.destinationType}/${id(first.destinationWarehouseId) || id(first.destinationWarehouseCode)}`, 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 differentRoute = await createDifferentRouteFixture(context, first) context.assert(MODULE, '创建本轮不同仓库渠道或目的仓港反向样本', null, Boolean(differentRoute), differentRoute ? `orderId=${differentRoute.orderId}, warehouse=${differentRoute.warehouseId}, channel=${differentRoute.channelCode}, destination=${differentRoute.destinationType}/${differentRoute.destinationWarehouseId || differentRoute.destinationWarehouseCode}` : '三次正式下单均未取得不同线路,未借用历史订单', 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 zeroDriftResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups/batch-query', { orderIds: [third.orderId, differentRoute.orderId] }) const zeroDrift = context.apiSuccess(MODULE, '回读跨线路拒绝后的组事实', zeroDriftResponse, evidence(zeroDriftResponse), OPTIONS) || [] context.assert(MODULE, '跨线路拒绝后两个订单均无组事实', zeroDriftResponse, zeroDrift.length === 0, `count=${zeroDrift.length}`, OPTIONS) } const queryResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups/batch-query', { orderIds: [fourth.orderId, second.orderId, first.orderId, third.orderId] }) const queried = context.apiSuccess(MODULE, '按订单批量回读组事实', queryResponse, evidence(queryResponse), OPTIONS) || [] context.assert(MODULE, '批查只返回组内订单且长ID保持字符串', queryResponse, queried.length === 2 && queried.every((item) => groupOrders.includes(id(item?.orderId)) && id(item?.group?.groupId) === groupId && typeof item?.group?.groupId === 'string'), `count=${queried.length}, orderIds=${queried.map((item) => id(item?.orderId)).join(',')}`, OPTIONS) const simulationResponse = await context.request('POST', '/api/platform/v1/containers', { channelCode: first.channelCode, warehouseId: first.warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, }) const simulation = context.apiSuccess(MODULE, '创建本轮组装柜模拟柜', simulationResponse, evidence(simulationResponse), OPTIONS) const 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) const 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) 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))) context.assert(MODULE, '整组分配同计划且每票均为满箱数', assignResponse, assigned.length === 2 && assigned.every((item) => [first, second].some((fixture) => fixture.inboundId === id(item?.inboundId) && fixture.packages === Number(item?.packageCount))), `items=${assigned.map((item) => `${id(item?.inboundId)}:${Number(item?.packageCount)}`).join(',')}`, OPTIONS) const lockedQueryResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups/batch-query', { orderIds: groupOrders }) const lockedQuery = context.apiSuccess(MODULE, '回读整组锁定事实', lockedQueryResponse, evidence(lockedQueryResponse), OPTIONS) || [] context.assert(MODULE, '整组只锁定到当前计划一次', lockedQueryResponse, lockedQuery.length === 2 && lockedQuery.every((item) => item?.group?.status === 'LOCKED' && id(item?.group?.lockedPlanId) === planId), `status=${lockedQuery[0]?.group?.status || 'missing'}, lockedPlanId=${id(lockedQuery[0]?.group?.lockedPlanId)}`, 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(lockedQuery[0]?.group?.version), 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 activeQueryResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups/batch-query', { orderIds: groupOrders }) const activeQuery = context.apiSuccess(MODULE, '回读整组解锁事实', activeQueryResponse, evidence(activeQueryResponse), OPTIONS) || [] context.assert(MODULE, '整组移除后恢复ACTIVE且不残留计划锁', activeQueryResponse, activeQuery.length === 2 && activeQuery.every((item) => item?.group?.status === 'ACTIVE' && item?.group?.lockedPlanId == null), `status=${activeQuery[0]?.group?.status || 'missing'}, lockedPlanId=${id(activeQuery[0]?.group?.lockedPlanId) || 'null'}`, OPTIONS) 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-recommend`.slice(0, 128), }) plan = context.apiSuccess(MODULE, '智能推荐按超级项处理同柜组', recommendResponse, evidence(recommendResponse), OPTIONS) const recommendedIds = new Set((plan?.items || []).map((item) => id(item?.inboundId))) const recommendedMembers = [first.inboundId, second.inboundId].filter((value) => recommendedIds.has(value)) context.assert(MODULE, '智能推荐对整组全取或全不取', recommendResponse, recommendedMembers.length === 0 || recommendedMembers.length === 2, `recommendedMembers=${recommendedMembers.join(',') || 'none'}`, OPTIONS) if (recommendedMembers.length === 0) { 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) context.assert(MODULE, '推送后整组仍保持同计划满量事实', pushResponse, [first, second].every((fixture) => (pushed?.items || []).some((item) => id(item?.inboundId) === fixture.inboundId && Number(item?.packageCount) === fixture.packages)), `planId=${planId}, itemCount=${pushed?.items?.length ?? 'missing'}`, OPTIONS) context.createdResources.coloadGroupAcceptance = { marker: context.marker, groupId, groupNo: group?.groupNo || null, planId, bookingId, orderIds: groupOrders, inboundIds: [first.inboundId, second.inboundId], publicBypassResult: 'individual assignment/adjust/delete rejected before push', } }