优化
This commit is contained in:
@@ -34,6 +34,116 @@ export function measurementReplayDigest(facts) {
|
||||
return createHash('sha256').update(JSON.stringify(facts)).digest('hex')
|
||||
}
|
||||
|
||||
// Compare both task projections to independently frozen source identities, not
|
||||
// to the first carton returned by a shared pallet.
|
||||
export function sharedPalletFreezeEvidence(tasks, sources) {
|
||||
const errors = []
|
||||
const expected = new Map()
|
||||
for (const source of sources) {
|
||||
for (const boxCode of source.boxCodes || []) {
|
||||
if (expected.has(boxCode)) errors.push(`duplicate source box ${boxCode}`)
|
||||
expected.set(boxCode, source)
|
||||
}
|
||||
}
|
||||
if (sources.length < 2 || new Set(sources.map(source => asId(source.inboundId))).size !== sources.length) {
|
||||
errors.push('requires distinct source inbounds')
|
||||
}
|
||||
const snapshots = tasks.map(task => {
|
||||
if (task?.taskStatus !== 'COMPLETED' || task.pallets?.length !== 1) errors.push('task not completed on one pallet')
|
||||
const pallet = task?.pallets?.[0]
|
||||
if (!pallet?.palletId || !pallet.palletNo || pallet.status !== 'SEALED'
|
||||
|| !pallet.sealedAt || !Number.isInteger(Number(pallet.version))) errors.push('missing sealed pallet identity')
|
||||
const seen = new Set()
|
||||
const cartons = (pallet?.cartons || []).map(carton => {
|
||||
const source = expected.get(carton.boxCode)
|
||||
if (seen.has(carton.boxCode)) errors.push(`duplicate carton ${carton.boxCode}`)
|
||||
seen.add(carton.boxCode)
|
||||
if (!source || asId(carton.inboundId) !== asId(source.inboundId)) errors.push(`wrong owner ${carton.boxCode}`)
|
||||
if (asId(carton.palletId) !== asId(pallet?.palletId) || carton.palletNo !== pallet?.palletNo) {
|
||||
errors.push(`wrong pallet ${carton.boxCode}`)
|
||||
}
|
||||
const skuLines = (carton.skuLines || []).map(line => ({
|
||||
inboundDetailId: asId(line.inboundDetailId), skuCode: line.skuCode,
|
||||
expectedQuantity: Number(line.expectedQuantity), actualQuantity: Number(line.actualQuantity),
|
||||
})).sort((left, right) => left.inboundDetailId.localeCompare(right.inboundDetailId))
|
||||
const expectedLines = (source?.skuLinesByBox?.[carton.boxCode] || []).map(line => ({
|
||||
inboundDetailId: asId(line.inboundDetailId), skuCode: line.skuCode,
|
||||
expectedQuantity: Number(line.quantity), actualQuantity: Number(line.quantity),
|
||||
})).sort((left, right) => left.inboundDetailId.localeCompare(right.inboundDetailId))
|
||||
if (!expectedLines.length || JSON.stringify(skuLines) !== JSON.stringify(expectedLines)) {
|
||||
errors.push(`sku source mismatch ${carton.boxCode}`)
|
||||
}
|
||||
return { boxCode: carton.boxCode, inboundId: asId(carton.inboundId), skuLines }
|
||||
}).sort((left, right) => left.boxCode.localeCompare(right.boxCode))
|
||||
if (seen.size !== expected.size || [...expected.keys()].some(box => !seen.has(box))) errors.push('carton set incomplete')
|
||||
return { palletId: asId(pallet?.palletId), palletNo: pallet?.palletNo, status: pallet?.status,
|
||||
sealedAt: pallet?.sealedAt, version: Number(pallet?.version), cartons }
|
||||
})
|
||||
if (tasks.length !== sources.length || snapshots.some(snapshot => JSON.stringify(snapshot) !== JSON.stringify(snapshots[0]))) {
|
||||
errors.push('shared task freeze projections differ')
|
||||
}
|
||||
return { passed: errors.length === 0, errors, snapshot: snapshots[0] || null,
|
||||
digest: measurementReplayDigest(snapshots[0] || null), sources }
|
||||
}
|
||||
|
||||
export async function verifySharedPalletFreeze(context, sources) {
|
||||
const tasks = []
|
||||
for (const source of sources) {
|
||||
const response = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/pda-dropship/tasks/${source.taskId}?warehouseId=${source.warehouseId}`)
|
||||
tasks.push(context.apiSuccess(MODULE, '共托封托后逐票读取完整箱和SKU快照', response,
|
||||
`inboundId=${source.inboundId}, taskId=${source.taskId}`, OPTIONS))
|
||||
}
|
||||
const evidence = sharedPalletFreezeEvidence(tasks, sources)
|
||||
context.assert(MODULE, '两票共托逐箱唯一归属及全部SKU冻结组成一致', null,
|
||||
evidence.passed, JSON.stringify({ digest: evidence.digest, errors: evidence.errors,
|
||||
palletId: evidence.snapshot?.palletId, cartonCount: evidence.snapshot?.cartons.length }), OPTIONS)
|
||||
context.createdResources.sharedPalletFreeze = { marker: context.marker, ...evidence }
|
||||
return evidence
|
||||
}
|
||||
|
||||
export async function prepareSharedPalletFreeze(context) {
|
||||
const { run: createOrder } = await import('./orders.mjs')
|
||||
const originalMarker = context.marker
|
||||
const originalContext = { ...context.context }
|
||||
const originalManualCount = context.options.prepareDropshipManualCount
|
||||
const sources = []
|
||||
try {
|
||||
for (const label of ['A', 'B']) {
|
||||
context.marker = `${originalMarker}-PALLET-${label}`
|
||||
for (const key of ['orders', 'inboundMeasurement', 'dropshipManualCount']) delete context.createdResources[key]
|
||||
for (const key of ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']) delete context.context[key]
|
||||
context.context.orderDestinationType = 'OVERSEAS'
|
||||
context.context.orderFulfillmentType = 'ONE_CLICK_DROPSHIP'
|
||||
context.context.orderProfile = { taxMode: 'BUY_DECLARATION', pickupMethod: 'SELF_EXPRESS', containerMode: 'LCL' }
|
||||
context.context.waitSplitVariant = { skuCount: 1, packageCountPerSku: 2, packageCounts: [2],
|
||||
totalPackageCount: 2, palletCount: 1, skuCodes: [`SKU-COMMON-${originalMarker}`.slice(0, 64)] }
|
||||
context.options.prepareDropshipManualCount = true
|
||||
const start = context.steps.length
|
||||
await createOrder(context)
|
||||
await run(context)
|
||||
const source = context.createdResources.dropshipManualCount
|
||||
if (!source?.taskId || context.steps.slice(start).some(step => !step.passed)) throw new Error('shared pallet preparation failed')
|
||||
const task = await completeDropshipPalletization(context, { ...source,
|
||||
measurement: { actualPackageCount: source.totalCartons, expectedPalletCount: 1 },
|
||||
...(label === 'A' ? { deferSeal: true } : { targetPalletNo: sources[0].palletNo }),
|
||||
})
|
||||
if (!task || context.steps.slice(start).some(step => !step.passed)) throw new Error('shared pallet count or seal failed')
|
||||
sources.push({ taskId: asId(source.taskId), inboundId: asId(source.inboundId),
|
||||
inboundCode: source.inboundCode, warehouseId: asId(source.warehouseId),
|
||||
palletNo: task.pallets?.[0]?.palletNo, boxCodes: task.sealedCartonMarks,
|
||||
skuLinesByBox: task.sealedSkuSources })
|
||||
context.createdResources.sharedPalletFreezePreparation = { marker: originalMarker, sources }
|
||||
}
|
||||
context.marker = originalMarker
|
||||
return await verifySharedPalletFreeze(context, sources)
|
||||
} finally {
|
||||
context.marker = originalMarker
|
||||
context.context = originalContext
|
||||
context.options.prepareDropshipManualCount = originalManualCount
|
||||
}
|
||||
}
|
||||
|
||||
async function readMeasurementReplayFacts(context, { inboundId, tenantId, measurementId }, name) {
|
||||
const response = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement?${query(tenantId)}`)
|
||||
@@ -236,6 +346,7 @@ export async function completeDropshipPalletization(context, { inboundId, inboun
|
||||
|
||||
let countedCartons = 0
|
||||
const sealedCartonMarks = []
|
||||
const sealedSkuSources = {}
|
||||
|
||||
for (let index = 0; index < totalCartons; index += 1) {
|
||||
const boxCode = `${inboundCode}#${totalCartons}-${index + 1}`
|
||||
@@ -244,6 +355,9 @@ export async function completeDropshipPalletization(context, { inboundId, inboun
|
||||
const resolved = context.apiSuccess(MODULE, `解析第${index + 1}箱的冻结箱唛`, resolveResponse,
|
||||
`boxCode=${boxCode}`, OPTIONS)
|
||||
const resolvedSkuLines = Array.isArray(resolved?.skuLines) ? resolved.skuLines : []
|
||||
sealedSkuSources[boxCode] = resolvedSkuLines.map(line => ({
|
||||
inboundDetailId: asId(line.inboundDetailId), skuCode: line.skuCode, quantity: Number(line.expectedQuantity),
|
||||
}))
|
||||
context.assert(MODULE, `第${index + 1}箱允许清点且每箱SKU明细完整`, resolveResponse,
|
||||
resolved?.canCount === true && resolvedSkuLines.length > 0
|
||||
&& resolvedSkuLines.every((line) => asId(line?.inboundDetailId) && line?.skuCode
|
||||
@@ -361,7 +475,7 @@ export async function completeDropshipPalletization(context, { inboundId, inboun
|
||||
if (deferSeal) {
|
||||
context.assert(MODULE, '多票合托首票归托后保持开放等待下一票', null,
|
||||
sealed?.taskStatus !== 'COMPLETED' && sealed?.pallets?.every((item) => item.status === 'OPEN'), '', OPTIONS)
|
||||
return { ...sealed, sealedCartonMarks, sealedSourcePalletNos: [] }
|
||||
return { ...sealed, sealedCartonMarks, sealedSkuSources, sealedSourcePalletNos: [] }
|
||||
}
|
||||
let lastSealResponse = countedTaskResponse
|
||||
for (let index = 0; index < (sealed?.pallets || []).length; index += 1) {
|
||||
@@ -383,7 +497,7 @@ export async function completeDropshipPalletization(context, { inboundId, inboun
|
||||
&& Number(sealed?.unboundCartons) === 0
|
||||
&& sealed?.pallets?.every((item) => item?.status === 'SEALED'),
|
||||
`taskStatus=${sealed?.taskStatus ?? 'missing'}, counted=${sealed?.countedCartons ?? 'missing'}, bound=${sealed?.boundCartons ?? 'missing'}`, OPTIONS)
|
||||
return { ...sealed, sealedCartonMarks,
|
||||
return { ...sealed, sealedCartonMarks, sealedSkuSources,
|
||||
sealedSourcePalletNos: (sealed?.pallets || []).map((item) => item.palletNo).filter(Boolean) }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user