优化
This commit is contained in:
@@ -6,7 +6,7 @@ test('同柜不可拆组HTTP验收锁定公开接口与防绕过矩阵', async (
|
||||
const source = await readFile(new URL('./coload-groups.mjs', import.meta.url), 'utf8')
|
||||
for (const fragment of [
|
||||
'/api/platform/v1/first-leg/coload-groups',
|
||||
'/api/platform/v1/first-leg/coload-groups/batch-query',
|
||||
'/api/platform/v1/first-leg/coload-groups/by-inbound-code/',
|
||||
'/group-assignments',
|
||||
'/inbound-assignments',
|
||||
'/push-preload',
|
||||
@@ -14,11 +14,11 @@ test('同柜不可拆组HTTP验收锁定公开接口与防绕过矩阵', async (
|
||||
'创建同键异载荷返回并发冲突',
|
||||
'不同目的仓港仍可创建同柜不可拆组',
|
||||
'跨国内仓或跨渠道创建整组被拒绝且不落组',
|
||||
'批查只返回组内订单且长ID保持字符串',
|
||||
'业务摘要只返回状态、成员数和完整入仓码',
|
||||
'/recommend',
|
||||
'第二柜智能推荐请求成功',
|
||||
'智能推荐不会只取同柜组部分成员或部分箱数',
|
||||
'智能推荐全不取后整组仍ACTIVE且未锁定',
|
||||
'智能推荐全不取后整组仍ACTIVE',
|
||||
'组成员不能单独调整箱数',
|
||||
'组成员不能单独删除',
|
||||
'整组原子移除',
|
||||
|
||||
@@ -62,6 +62,18 @@ async function readPlan(context, planId, name) {
|
||||
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,
|
||||
@@ -475,23 +487,19 @@ export async function run(context) {
|
||||
{ '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 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 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'
|
||||
&& JSON.stringify(memberFacts(item?.group)) === JSON.stringify(frozen)),
|
||||
`count=${queried.length}, orderIds=${queried.map((item) => id(item?.orderId)).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',
|
||||
@@ -522,23 +530,6 @@ export async function run(context) {
|
||||
let isolatedCandidates = candidates
|
||||
const competing = [...new Set(isolatedCandidates.filter((item) => item?.locked !== true)
|
||||
.map((item) => id(item?.coLoadGroupId)).filter((value) => value && value !== groupId))]
|
||||
for (const competingGroupId of competing) {
|
||||
const competingOrders = isolatedCandidates.filter((item) => id(item?.coLoadGroupId) === competingGroupId)
|
||||
.map((item) => id(item?.orderId)).filter(Boolean)
|
||||
const competingQueryResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups/batch-query',
|
||||
{ orderIds: competingOrders })
|
||||
const competingQuery = context.apiSuccess(MODULE, '清理前回读旧验收活动组', competingQueryResponse,
|
||||
evidence(competingQueryResponse, `groupId=${competingGroupId}`), OPTIONS) || []
|
||||
const competingGroup = competingQuery.find((item) => id(item?.group?.groupId) === competingGroupId)?.group
|
||||
if (competingGroup?.status === 'ACTIVE') {
|
||||
const cleanupResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/coload-groups/${encodeURIComponent(competingGroupId)}/cancel`, {
|
||||
expectedVersion: Number(competingGroup.version), reason: '清理旧HTTP验收活动组以隔离容量候选',
|
||||
}, undefined, { 'Idempotency-Key': `${context.marker}-cleanup-${competingGroupId}`.slice(0, 128) })
|
||||
context.apiSuccess(MODULE, '通过公开接口取消旧验收活动组', cleanupResponse,
|
||||
evidence(cleanupResponse, `groupId=${competingGroupId}`), OPTIONS)
|
||||
}
|
||||
}
|
||||
let isolatedCandidateResponse = candidateResponse
|
||||
if (competing.length > 0) {
|
||||
isolatedCandidateResponse = await context.request('GET',
|
||||
@@ -622,14 +613,10 @@ export async function run(context) {
|
||||
context.apiSuccess(MODULE, '智能推荐整组验收后原子移除并释放组锁', recommendRemoveResponse,
|
||||
evidence(recommendRemoveResponse), OPTIONS)
|
||||
} else {
|
||||
const recommendGroupResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups/batch-query',
|
||||
{ orderIds: groupOrders })
|
||||
const recommendGroupRows = context.apiSuccess(MODULE, '智能推荐全不取后回读组锁', recommendGroupResponse,
|
||||
evidence(recommendGroupResponse), OPTIONS) || []
|
||||
context.assert(MODULE, '智能推荐全不取后整组仍ACTIVE且未锁定', recommendGroupResponse,
|
||||
recommendGroupRows.length === 2 && recommendGroupRows.every((item) => item?.group?.status === 'ACTIVE'
|
||||
&& !item?.group?.lockedPlanId),
|
||||
`rows=${recommendGroupRows.length}, status=${recommendGroupRows[0]?.group?.status || 'missing'}`, OPTIONS)
|
||||
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', {
|
||||
@@ -676,14 +663,10 @@ export async function run(context) {
|
||||
[first, second].every((fixture) => assignedTotals.get(fixture.inboundId) === fixture.packages),
|
||||
`inboundTotals=${JSON.stringify(Object.fromEntries(assignedTotals))}`, 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 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)
|
||||
@@ -706,7 +689,7 @@ export async function run(context) {
|
||||
|
||||
const cancelLockedResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/coload-groups/${encodeURIComponent(groupId)}/cancel`, {
|
||||
expectedVersion: Number(lockedQuery[0]?.group?.version), reason: '锁定组取消反向验收',
|
||||
expectedVersion: Number(group?.version) + 1, reason: '锁定组取消反向验收',
|
||||
}, undefined, { 'Idempotency-Key': `${context.marker}-coload-cancel-locked`.slice(0, 128) })
|
||||
conflict(context, '已锁定组不能取消', cancelLockedResponse)
|
||||
|
||||
@@ -721,14 +704,10 @@ export async function run(context) {
|
||||
!(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 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),
|
||||
@@ -772,13 +751,11 @@ export async function run(context) {
|
||||
}, 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('POST', '/api/platform/v1/first-leg/coload-groups/batch-query',
|
||||
{ orderIds: [fifth.orderId, sixth.orderId] })
|
||||
const cancelledQuery = context.apiSuccess(MODULE, '取消后回读成员活动组事实', cancelledQueryResponse,
|
||||
evidence(cancelledQueryResponse), 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' && cancelledQuery.length === 0,
|
||||
`status=${cancelled?.status || 'missing'}, activeRows=${cancelledQuery.length}`, OPTIONS)
|
||||
cancelled?.status === 'CANCELLED' && code(cancelledQueryResponse) === 'A0404',
|
||||
`status=${cancelled?.status || 'missing'}, queryCode=${code(cancelledQueryResponse)}`, OPTIONS)
|
||||
|
||||
const partialChain = {
|
||||
label: '同柜短拣链', key: 'coload-partial', groupId, containerBookingId: bookingId, planId,
|
||||
@@ -842,17 +819,13 @@ export async function run(context) {
|
||||
const partialTaskAfter = context.apiSuccess(MODULE, '短拣完成拒绝后回读PDA任务', partialTaskAfterResponse,
|
||||
evidence(partialTaskAfterResponse), OPTIONS)
|
||||
const partialTreesAfter = await fulfillmentTrees(context, partialChain.fixtures, '短拣拒绝后')
|
||||
const partialGroupResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups/batch-query',
|
||||
{ orderIds: partialChain.fixtures.map((fixture) => fixture.orderId) })
|
||||
const partialGroup = context.apiSuccess(MODULE, '短拣拒绝后回读组状态', partialGroupResponse,
|
||||
evidence(partialGroupResponse), OPTIONS) || []
|
||||
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.length === 2 && partialGroup.every((item) => item?.group?.status === 'LOCKED'
|
||||
&& id(item?.group?.lockedPlanId) === partialChain.planId),
|
||||
`taskStable=${partialTaskBeforeComplete === taskSignature(partialTaskAfter)}, children=${partialTreesAfter.map((item) => item.tree?.children?.length ?? -1).join('|')}, groupStatus=${partialGroup[0]?.group?.status || 'missing'}`, OPTIONS)
|
||||
&& 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) => ({
|
||||
@@ -884,10 +857,7 @@ export async function run(context) {
|
||||
const fullFinal = await pollCompletedTask(context, fullPda.taskId, third.warehouseId, '同柜组足量')
|
||||
if (fullFinal.task?.taskStatus !== 'COMPLETED') return
|
||||
const fullTreesAfter = await fulfillmentTrees(context, fullChain.fixtures, '足量完成后')
|
||||
const consumedGroupResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups/batch-query',
|
||||
{ orderIds: fullChain.fixtures.map((fixture) => fixture.orderId) })
|
||||
const consumedGroup = context.apiSuccess(MODULE, '足量完成后回读组消费事实', consumedGroupResponse,
|
||||
evidence(consumedGroupResponse), OPTIONS) || []
|
||||
const consumedGroup = await readGroupSummary(context, third.inboundCode, '足量完成后回读组消费事实')
|
||||
const completedInboundStates = []
|
||||
for (const fixture of fullChain.fixtures) {
|
||||
const response = await context.request('GET',
|
||||
@@ -898,11 +868,11 @@ export async function run(context) {
|
||||
}
|
||||
const fullRemainders = fullTreesAfter.flatMap((item) => item?.tree?.children || [])
|
||||
.filter((item) => item?.splitRole === 'REMAINDER')
|
||||
context.assert(MODULE, '足量完成整组已消费、全部入库完成且不生成余量子单', consumedGroupResponse,
|
||||
consumedGroup.length === 2 && consumedGroup.every((item) => item?.group?.status === 'CONSUMED')
|
||||
context.assert(MODULE, '足量完成整组已消费、全部入库完成且不生成余量子单', consumedGroup.response,
|
||||
consumedGroup.data?.status === 'CONSUMED'
|
||||
&& completedInboundStates.every((status) => status === 'LOADED')
|
||||
&& fullRemainders.length === 0,
|
||||
`groupStatus=${consumedGroup[0]?.group?.status || 'missing'}, inboundStates=${completedInboundStates.join('|')}, remainderChildren=${fullRemainders.length}, childRoles=${fullTreesAfter.flatMap((item) => item?.tree?.children || []).map((item) => item?.splitRole).join('|') || 'none'}`, OPTIONS)
|
||||
`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) => ({
|
||||
@@ -949,16 +919,12 @@ export async function run(context) {
|
||||
&& splitMembers.some((item) => item.orderId === eighth.orderId)
|
||||
&& splitMembers.every((item) => item.fullPackageCount > 0),
|
||||
`members=${JSON.stringify(splitMembers)}`, OPTIONS)
|
||||
const splitBatchResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups/batch-query',
|
||||
{ orderIds: [seventh.orderId, id(remainder.orderId), eighth.orderId] })
|
||||
const splitBatch = context.apiSuccess(MODULE, '拆分组按订单批查返回完整成员', splitBatchResponse,
|
||||
evidence(splitBatchResponse), OPTIONS) || []
|
||||
context.assert(MODULE, '批查只关联当前余量子单和同组订单且每行含完整members', splitBatchResponse,
|
||||
splitBatch.length === 2 && splitBatch.every((item) =>
|
||||
[id(remainder.orderId), eighth.orderId].includes(id(item?.orderId))
|
||||
&& id(item?.group?.groupId) === id(splitGroup?.groupId)
|
||||
&& JSON.stringify(memberFacts(item?.group)) === JSON.stringify(splitMembers)),
|
||||
`rows=${splitBatch.map((item) => id(item?.orderId)).join('|')}, members=${splitMembers.length}`, 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,
|
||||
|
||||
Reference in New Issue
Block a user