Files
baiyue-firstleg/scripts/admin-test-env-modules/coload-groups.mjs
T

323 lines
21 KiB
JavaScript
Raw Normal View History

2026-08-29 11:10:28 +08:00
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',
}
}