test(acceptance): verify native outbound ownership and exact relabel carton quantities

This commit is contained in:
2026-10-23 05:21:30 +08:00
parent 18ef338036
commit 2b54e709f8
7 changed files with 148 additions and 20 deletions
@@ -907,8 +907,7 @@ export async function verifyRelabelDispatchOutbound(context, relabel, token, war
`${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundNo)}`)
require('排车出库快照精确保留全部新箱唛且无原箱唛或重复箱',
success(detail.response) && detail.data?.outbound?.outboundNo === outboundNo
&& detail.data?.outbound?.dispatchNo === relabel.dispatchNo
&& id(detail.data?.outbound?.customerId) === customerId && detail.data?.outbound?.status === 'OUTBOUNDED'
&& dispatchOutboundMatches(detail.data, transferred.outbounds[0], relabel.dispatchNo, transferred.deliveries)
&& relabelOutboundMarksMatch(detail.data, relabel))
Object.assign(relabel, { outboundNo, deliveryNos: transferred.deliveries.map(row => row.deliveryNo).sort() })
for (const [index, delivery] of transferred.deliveries.entries()) {
@@ -933,7 +932,7 @@ export async function verifyRelabelDispatchOutbound(context, relabel, token, war
}
// 主车结案后串行复用同仓PDA及车辆池,客户原申请与旧换标样本分别核验。
async function finishRelabelDeliveries(context, mainTrip, warehouseId) {
export async function finishRelabelDeliveries(context, mainTrip, warehouseId) {
if (mainTrip.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') throw Error('主车尚未释放,不能启动换标履约')
const access = await resolvePickerAccess(context)
const picker = access ? await createPicker(context, 95, warehouseId, access) : null
@@ -1017,7 +1016,27 @@ export async function pickAll(context, token, dispatchNo) {
detail = await pda(context, token, 'GET', '扫码前刷新公共版本及真实来源', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
let item = detail.data?.items?.find(row => id(row.dispatchItemId) === id(original.dispatchItemId))
if (!success(detail.response) || !item) return false
const sources = item.returnCartons?.length ? item.returnCartons.map(carton => ({ carton })) : (item.positions || []).map(position => ({ position }))
if (Number(item.scheduledPackages) === Number(item.pickedPackages)) continue
let sources = item.returnCartons?.length ? item.returnCartons.map(carton => ({ carton })) : (item.positions || []).map(position => ({ position }))
if (item.sourceBusinessType === 'RELABEL_ORDER') {
const originalRelabel = [context.createdResources?.phase4OverseasRelabel, context.createdResources?.phase4OverseasClientServices?.relabelFulfillment]
.find(row => row?.marker === context.marker && row.relabelNo === item.sourceBusinessNo && id(row.customerId) === id(item.customerId))
if (!originalRelabel) throw Error('换标拣货缺少本轮原申请身份,不能猜测SKU件数')
const relabel = await platform(context, 'GET', '扫码前读取原换标逐箱SKU与实际位置', `${LASTLEG_BASE}/relabel-applications/${encodeURIComponent(item.sourceBusinessNo)}`)
const value = relabel.data
if (!success(relabel.response) || value?.application?.relabelNo !== item.sourceBusinessNo
|| id(value.application.overseasWarehouseId) !== id(originalRelabel.warehouseId ?? originalRelabel.overseasWarehouseId)
|| !Array.isArray(value.items)) throw Error('换标逐箱来源或仓库身份不一致')
sources = value.items.filter(row => (item.positions || []).some(position => id(position.positionId) === id(row.sourceInventoryPositionId)
&& Number(position.availablePackages) > 0)).map(row => {
if (!row.cartonMark || row.notifiedPackageCount !== 1 || !['CARTON_AND_SKU', 'SKU_LABEL'].includes(row.operationType)
|| !row.skuItems?.length || row.skuItems.some(sku => !sku.skuCode || !sku.newSkuCode || !Number.isSafeInteger(Number(sku.quantity)) || Number(sku.quantity) <= 0))
throw Error('换标原箱缺少明确逐SKU核验件数,不按箱数推算')
return { position: item.positions.find(position => id(position.positionId) === id(row.sourceInventoryPositionId)),
carton: { cartonMark: row.cartonMark, productQuantity: row.skuItems.reduce((sum, sku) => sum + Number(sku.quantity), 0) } }
})
if (new Set(sources.map(row => row.carton.cartonMark)).size !== sources.length) throw Error('换标逐箱来源重复')
}
for (const [sourceIndex, source] of sources.entries()) {
const remaining = Number(item.scheduledPackages) - Number(item.pickedPackages)
if (remaining <= 0) break
@@ -1123,6 +1142,21 @@ export async function createDeliveryReturn(context, { customerId, warehouseId, d
return { returned, returnBody }
}
/** 出库详情的客户归属来自关联派送明细,OutboundRow本身不声明customerId。 */
export function dispatchOutboundMatches(detail, reference, dispatchNo, deliveries) {
const outbound = detail?.outbound
const expected = Array.isArray(deliveries) ? deliveries.filter(row => row.outboundNo === reference?.outboundNo) : []
const actual = detail?.deliveries
return Boolean(reference?.customerId && reference.outboundNo && dispatchNo && expected.length
&& outbound?.outboundNo === reference.outboundNo && outbound.dispatchNo === dispatchNo && outbound.status === 'OUTBOUNDED'
&& Number(outbound.packageCount) === Number(reference.packageCount)
&& Array.isArray(actual) && actual.length === expected.length && new Set(actual.map(row => row.deliveryNo)).size === actual.length
&& expected.every(row => id(row.customerId) === id(reference.customerId) && actual.some(value => value.deliveryNo === row.deliveryNo
&& value.outboundNo === reference.outboundNo && value.dispatchNo === dispatchNo && id(value.customerId) === id(reference.customerId)
&& Number.isSafeInteger(Number(value.packageCount)) && Number(value.packageCount) > 0 && Number(value.packageCount) === Number(row.packageCount)))
&& actual.reduce((sum, row) => sum + Number(row.packageCount), 0) === Number(outbound.packageCount))
}
export async function run(context) {
context.createdResources.phase5OverseasFulfillment = {
marker: context.marker, dispatchNo: null, outboundNo: null, deliveryNo: null, deliveryNos: [],
@@ -1231,6 +1265,18 @@ export async function run(context) {
try { await disablePicker(context, picker, 5) }
finally { if (picker) picker.token = null; await context.checkpoint() }
}
return continueDepartedFulfillment(context, { customerId, warehouseId, dispatchNo, tripProof, deliveries, outbounds, merchantToken })
}
/** 已发车原单的签收、回访及结案阶段;不重新准备订单、扫码或发车。 */
export async function continueDepartedFulfillment(context, { customerId, warehouseId, dispatchNo, tripProof, deliveries, outbounds, merchantToken }) {
const proof = context.createdResources?.phase5OverseasFulfillment
if (!merchantToken || proof?.marker !== context.marker || proof.dispatchNo !== dispatchNo || tripProof?.dispatchNo !== dispatchNo
|| id(tripProof.warehouseId) !== warehouseId || !Array.isArray(deliveries) || !deliveries.length || !Array.isArray(outbounds) || !outbounds.length
|| proof.finalStatus || proof.receiptFiles?.length || tripProof.closure) throw Error('签收阶段必须使用本轮尚未签收的原发车证明')
const departed = await readAutomaticDispatchDelivery(context, dispatchNo)
if (!departed || JSON.stringify(departed.deliveries) !== JSON.stringify(deliveries) || JSON.stringify(departed.outbounds) !== JSON.stringify(outbounds))
throw Error('原发车单据与当前客户来源或数量不一致,停止签收')
const primaryDelivery = deliveries.find(row => id(row.customerId) === customerId)
if (!primaryDelivery) { blocker(context, '客户签收资料前置', '本轮主客户不在实际派送结果中'); return }
const deliveryNo = primaryDelivery.deliveryNo, outboundNo = primaryDelivery.outboundNo
@@ -1238,8 +1284,7 @@ export async function run(context) {
Object.assign(context.createdResources.phase5OverseasFulfillment, { outboundNo, deliveryNo, deliveryNos, outbounds, deliveries })
for (const outboundRef of outbounds) {
const outbound = await platform(context, 'GET', '逐客户回查发车后真实出库状态', `${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundRef.outboundNo)}`)
const passed = outbound.data?.outbound?.outboundNo === outboundRef.outboundNo
&& id(outbound.data?.outbound?.customerId) === id(outboundRef.customerId) && outbound.data?.outbound?.status === 'OUTBOUNDED'
const passed = dispatchOutboundMatches(outbound.data, outboundRef, dispatchNo, deliveries)
context.assert(MODULE, '每客户出库单独立OUTBOUNDED且归属不漂移', outbound.response, passed, trace(outbound.response), OPTIONS)
if (!passed) return
}