test(firstleg): extend coload group acceptance
This commit is contained in:
@@ -313,7 +313,7 @@ async function pollCompletedTask(context, taskId, warehouseId, label) {
|
||||
return { response, task }
|
||||
}
|
||||
|
||||
async function createDifferentRouteFixture(context, base) {
|
||||
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
|
||||
@@ -327,10 +327,10 @@ async function createDifferentRouteFixture(context, base) {
|
||||
const originalDetailCount = context.context.phase3LoadingIncrementalDetailCount
|
||||
const originalPackageCount = context.context.phase3LoadingIncrementalPackageCount
|
||||
try {
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
context.marker = `${originalMarker}-ROUTE-${attempt}`
|
||||
for (let attempt = 1; attempt <= 6; attempt += 1) {
|
||||
context.marker = `${originalMarker}-${variant === 'DESTINATION' ? 'DEST' : 'ROUTE'}-${attempt}`
|
||||
context.context.orderProfile = {
|
||||
destinationType: attempt === 3 ? 'OVERSEAS' : 'FBA', fulfillmentType: 'STANDARD',
|
||||
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
|
||||
@@ -345,6 +345,7 @@ async function createDifferentRouteFixture(context, base) {
|
||||
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)
|
||||
@@ -361,12 +362,15 @@ async function createDifferentRouteFixture(context, base) {
|
||||
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
|
||||
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)
|
||||
if (candidate.orderId && candidate.inboundId && candidate.processStatus === 'WAIT_SPLIT' && differs) return candidate
|
||||
const matched = variant === 'DESTINATION'
|
||||
? sameLoadingRoute && differentDestination
|
||||
: !sameLoadingRoute
|
||||
if (candidate.orderId && candidate.inboundId && candidate.processStatus === 'WAIT_SPLIT' && matched) return candidate
|
||||
}
|
||||
return null
|
||||
} finally {
|
||||
@@ -392,17 +396,16 @@ export async function run(context) {
|
||||
}))
|
||||
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 >= 7,
|
||||
context.assert(MODULE, '取得至少八票本轮 WAIT_SPLIT 新数据', null, usable.length >= 8,
|
||||
`count=${usable.length}, marker=${context.marker}`, OPTIONS)
|
||||
if (usable.length < 7) return
|
||||
if (usable.length < 8) return
|
||||
|
||||
const [first, second, third, fourth, fifth, sixth, seventh] = usable
|
||||
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 && 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)}`,
|
||||
&& 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
|
||||
|
||||
@@ -434,16 +437,43 @@ export async function run(context) {
|
||||
{ orderIds: [first.orderId, third.orderId] }, undefined, { 'Idempotency-Key': createKey })
|
||||
conflict(context, '创建同键异载荷返回并发冲突', changedPayloadResponse)
|
||||
|
||||
const differentRoute = await createDifferentRouteFixture(context, first)
|
||||
context.assert(MODULE, '创建本轮不同仓库渠道或目的仓港反向样本', null, Boolean(differentRoute),
|
||||
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}, destination=${differentRoute.destinationType}/${differentRoute.destinationWarehouseId || differentRoute.destinationWarehouseCode}`
|
||||
: '三次正式下单均未取得不同线路,未借用历史订单', OPTIONS)
|
||||
? `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,
|
||||
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] })
|
||||
@@ -459,7 +489,8 @@ export async function run(context) {
|
||||
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'),
|
||||
&& 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 simulationResponse = await context.request('POST', '/api/platform/v1/containers', {
|
||||
@@ -530,9 +561,9 @@ export async function run(context) {
|
||||
`targetGroupId=${groupId}, volume=${targetCandidateVolume}, limit=${33 * 1.1}`, OPTIONS)
|
||||
const beforeRecommend = plan
|
||||
const recommendResponse = 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-capacity-assign`.slice(0, 128),
|
||||
`/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 capacityRejected = recommendResponse?.http === 200 && code(recommendResponse) === 'A0422'
|
||||
&& String(recommendResponse?.body?.message || '').includes('同柜不可拆组整体超过货柜')
|
||||
@@ -544,6 +575,44 @@ export async function run(context) {
|
||||
planSignature(beforeRecommend) === planSignature(afterRecommend.data),
|
||||
`before=${planSignature(beforeRecommend)}, after=${planSignature(afterRecommend.data)}`, 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))
|
||||
}
|
||||
context.assert(MODULE, '智能推荐不会只取同柜组部分成员或部分箱数', recommendSuccessResponse,
|
||||
[first, second].every((fixture) => recommendedTotals.get(fixture.inboundId) === fixture.packages),
|
||||
`groupId=${groupId}, inboundTotals=${JSON.stringify(Object.fromEntries(recommendedTotals))}`, OPTIONS)
|
||||
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)
|
||||
|
||||
const workingSimulationResponse = await context.request('POST', '/api/platform/v1/containers', {
|
||||
channelCode: first.channelCode, warehouseId: first.warehouseId, containerType: '40HC',
|
||||
capacityCbm: 76, maxWeightKg: 28000,
|
||||
@@ -847,19 +916,45 @@ export async function run(context) {
|
||||
&& 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 splitTreeBeforeReject = treeSignature(normalTree)
|
||||
const splitTreeBeforeGroup = treeSignature(normalTree)
|
||||
const splitGroupResponse = await context.request('POST', '/api/platform/v1/first-leg/coload-groups',
|
||||
{ orderIds: [seventh.orderId, id(remainder.orderId)] }, undefined,
|
||||
{ 'Idempotency-Key': `${context.marker}-coload-split-reject`.slice(0, 128) })
|
||||
conflict(context, '已有父子拆分订单创建同柜组被拒绝', splitGroupResponse,
|
||||
`rootOrderId=${seventh.orderId}, childOrderId=${id(remainder.orderId)}`)
|
||||
{ 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 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 splitTreeAfterResponse = await context.request('GET',
|
||||
`/api/platform/v1/orders/${encodeURIComponent(seventh.orderId)}/fulfillment-tree?tenantId=${encodeURIComponent(seventh.tenantId)}`)
|
||||
const splitTreeAfter = context.apiSuccess(MODULE, '父子订单建组拒绝后回读履约树', splitTreeAfterResponse,
|
||||
const splitTreeAfter = context.apiSuccess(MODULE, '拆分余量建组后回读根履约树', splitTreeAfterResponse,
|
||||
evidence(splitTreeAfterResponse), OPTIONS)
|
||||
context.assert(MODULE, '父子订单建组拒绝后原普通拆分事实零漂移', splitTreeAfterResponse,
|
||||
splitTreeBeforeReject === treeSignature(splitTreeAfter),
|
||||
`treeStable=${splitTreeBeforeReject === treeSignature(splitTreeAfter)}`, 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)
|
||||
}
|
||||
|
||||
context.createdResources.coloadGroupAcceptance = {
|
||||
marker: context.marker, groupId, groupNo: group?.groupNo || null, planId, bookingId,
|
||||
@@ -867,6 +962,7 @@ export async function run(context) {
|
||||
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),
|
||||
publicBypassResult: 'individual assignment/adjust/delete and short-pick completion rejected; full group consumed',
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user