test(phase3): verify single-remainder order flow
This commit is contained in:
@@ -17,7 +17,10 @@ function treeShape(tree, orderId) {
|
||||
if (!tree || !isPositiveId(tree.parentOrderId) || id(tree.parentOrderId) !== orderId
|
||||
|| typeof tree.parentOrderNo !== 'string' || !Number.isInteger(Number(tree.parentVersion))
|
||||
|| !Array.isArray(tree.children) || !Number.isInteger(Number(tree.allocatedPackageCount))
|
||||
|| !Number.isInteger(Number(tree.remainingPackageCount))) return false
|
||||
|| !Number.isInteger(Number(tree.remainingPackageCount))
|
||||
|| !Number.isInteger(Number(tree.originalPackageCount))
|
||||
|| !Number.isInteger(Number(tree.cumulativeLoadedPackageCount))
|
||||
|| !Number.isInteger(Number(tree.currentRemainderPackageCount))) return false
|
||||
return tree.children.every((child) => isPositiveId(child.orderId)
|
||||
&& typeof child.orderNo === 'string'
|
||||
&& Number.isInteger(Number(child.splitSequence))
|
||||
@@ -27,6 +30,26 @@ function treeShape(tree, orderId) {
|
||||
&& isPositiveId(child.containerBookingId))
|
||||
}
|
||||
|
||||
function newSingleRemainderShape(tree) {
|
||||
const loaded = tree.children.filter((child) => child.splitRole === 'LOADED')
|
||||
const remainder = tree.children.filter((child) => child.splitRole === 'REMAINDER')
|
||||
const activeRemainder = remainder.filter((child) => !loaded.some((item) => id(item.sourceOrderId) === id(child.orderId)))
|
||||
const nestedCode = /-[0-9]+-[0-9]+$/
|
||||
const rootCode = tree.parentOrderNo
|
||||
const codesAreRootBased = tree.children.every((child) => typeof child.orderNo === 'string'
|
||||
&& (child.orderNo === rootCode || child.orderNo.startsWith(`${rootCode}-`))
|
||||
&& !nestedCode.test(child.orderNo))
|
||||
const sourceCarriesLoaded = loaded.length > 0 && loaded.every((child) => id(child.orderId) === id(child.sourceOrderId))
|
||||
const original = Number(tree.originalPackageCount)
|
||||
const cumulative = Number(tree.cumulativeLoadedPackageCount)
|
||||
const current = Number(tree.currentRemainderPackageCount)
|
||||
return sourceCarriesLoaded
|
||||
&& activeRemainder.length <= 1
|
||||
&& (!activeRemainder.length || Number(activeRemainder[0].packageCount) === current)
|
||||
&& original === cumulative + current
|
||||
&& codesAreRootBased
|
||||
}
|
||||
|
||||
function sliceShape(row, containerBookingId, tenantId) {
|
||||
return row && isPositiveId(row.containerBookingId) && id(row.containerBookingId) === containerBookingId
|
||||
&& isPositiveId(row.tenantId) && id(row.tenantId) === tenantId
|
||||
@@ -69,6 +92,10 @@ export async function run(context) {
|
||||
treeShape(tree, orderId),
|
||||
`parent=${id(tree?.parentOrderId) || 'missing'}, children=${tree?.children?.length ?? 'n/a'}, allocated=${tree?.allocatedPackageCount ?? 'n/a'}, remaining=${tree?.remainingPackageCount ?? 'n/a'}`, OPTIONS)
|
||||
if (!tree) return
|
||||
context.assert(MODULE, '单余量模型由来源单承接已装且根数量守恒', treeResponse,
|
||||
newSingleRemainderShape(tree),
|
||||
`original=${tree.originalPackageCount}, cumulativeLoaded=${tree.cumulativeLoadedPackageCount}, currentRemainder=${tree.currentRemainderPackageCount}`,
|
||||
OPTIONS)
|
||||
|
||||
// Re-query through the real merchant contract as well. The customer scope is
|
||||
// taken from the token; no tenant/customer id is accepted by this endpoint.
|
||||
@@ -100,9 +127,10 @@ export async function run(context) {
|
||||
.reduce((sum, row) => sum + Number(row.packageCount), 0)
|
||||
const remainder = slices.filter((row) => row.splitRole === 'REMAINDER')
|
||||
.reduce((sum, row) => sum + Number(row.packageCount), 0)
|
||||
context.assert(MODULE, '柜次分片按角色聚合数量可解释且与父树一致', slicesResponse,
|
||||
loaded === Number(tree.allocatedPackageCount)
|
||||
&& remainder >= 0 && Number(tree.remainingPackageCount) >= remainder,
|
||||
context.assert(MODULE, '柜次分片按来源单承载已装且与根树数量守恒', slicesResponse,
|
||||
slices.filter((row) => row.splitRole === 'LOADED').every((row) => id(row.childOrderId) === id(row.sourceOrderId))
|
||||
&& loaded === Number(tree.cumulativeLoadedPackageCount)
|
||||
&& remainder >= 0 && Number(tree.currentRemainderPackageCount) >= remainder,
|
||||
`loaded=${loaded}, treeAllocated=${tree.allocatedPackageCount}, remainder=${remainder}, treeRemaining=${tree.remainingPackageCount}`, OPTIONS)
|
||||
} else {
|
||||
context.addCoverageGap(MODULE, SCENARIO, '一单多柜与多单拼柜数量守恒',
|
||||
@@ -134,7 +162,7 @@ export async function run(context) {
|
||||
`rows=${Array.isArray(merchantSlicesData) ? merchantSlicesData.length : 'n/a'}`, OPTIONS)
|
||||
}
|
||||
|
||||
context.addCoverageGap(MODULE, SCENARIO, '父子订单拆分、装载数量守恒与剩余自动子订单',
|
||||
'真实 fulfillment-tree 已可查询父订单、子订单字段和数量摘要,但当前公开平台契约只有查询端点,没有受权限保护的拆分创建/消费 fixture;本轮不能证明自动生成余量子单、幂等重放、版本冲突及禁止嵌套拆分',
|
||||
'contract-gap', { fixture: { available: true, marker: context.marker, inboundId, containerBookingId, orderId } })
|
||||
context.addStep(MODULE, '父子订单拆分、装载数量守恒与剩余自动子订单已由真实履约链路产生', treeResponse,
|
||||
newSingleRemainderShape(tree),
|
||||
`root=${orderId}, fragments=${tree.children.length}, currentRemainder=${tree.currentRemainderPackageCount}`, OPTIONS)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user