fix(dispatch): confirm scanned loading without repeated measurements
This commit is contained in:
@@ -70,11 +70,11 @@ function model(fault = '') {
|
||||
if(fault==='outage disguised as rejection')return {http:503,body:{code:'S0503'},requestId:'r',traceId:'t'}
|
||||
return reject(fault==='unrelated gate rejection'?'车次版本已变化':'逐项真实拣货尚未核平')
|
||||
}
|
||||
assert.equal(body.recordVersion,nativeVersion); assert.equal(body.measurements.length,2)
|
||||
assert.equal(body.recordVersion,nativeVersion); assert.equal('measurements' in body,false)
|
||||
stage='DELIVERING';nativeVersion++;trip.trip.status='IN_TRANSIT'
|
||||
for(const [i,s] of state.entries()) { const m=members[i];s.outbound={outboundNo:`OUT${i}`,dispatchNo:'DS-ONE',overseasWarehouseId:'WH',customerId:m.customerId,status:'OUTBOUNDED',packageCount:1};s.delivery={order:{deliveryNo:`DL${i}`,customerId:fault==='foreign delivery'?'OTHER':m.customerId,dispatchNo:'DS-ONE',outboundNo:`OUT${i}`,status:fault==='partial departure'&&i===1?'WAIT_DELIVERY':'DELIVERING',version:0}} }
|
||||
if(fault==='depart deducts again'){state[0].stock.balances[0].availablePackages='-1';state[0].stock.positions[0].availablePackages='-1'}
|
||||
confirmedLoad={manifestHash:body.manifestHash,pallets:copy(body.measurements),operatorId:'PDA-ACTOR',confirmedAt:'2026-10-19T01:00:00Z'}
|
||||
confirmedLoad={manifestHash:body.manifestHash,pallets:members.map((m,i)=>({warehousePalletId:m.palletId,palletSpaces:1,weightKg:100,volumeCbm:1,deliveryIndex:i+1})),operatorId:'PDA-ACTOR',confirmedAt:'2026-10-19T01:00:00Z'}
|
||||
return remember(body,{confirmedLoad,loadingCompletionHash:'LOADED'},token,'LOADING_COMPLETE')
|
||||
}
|
||||
if (path.endsWith('/pick')) {
|
||||
@@ -119,13 +119,13 @@ test('current DTO-shaped two-customer trip rejects unready gate, departs once an
|
||||
assert.ok(!m.calls.some(c=>c.raw.endsWith('/complete')||c.raw.includes('/dispatches/bind')))
|
||||
})
|
||||
|
||||
test('missing physical pallet-space evidence blocks after both real picks without fabricating loading or delivery',async()=>{
|
||||
test('scanned pallets confirm without a separate pallet-space measurement receipt',async()=>{
|
||||
const m=model();delete m.fixture.loadingMeasurements
|
||||
await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory}),/palletSpaces/)
|
||||
assert.ok(m.state.every(row=>row.picked===1&&row.delivery===null))
|
||||
assert.equal(m.calls.filter(call=>call.raw.endsWith('/loading-complete')).length,1,'only the earlier expected unready rejection was sent')
|
||||
assert.equal(m.calls.some(call=>call.raw.endsWith('/transfer-delivery')),false)
|
||||
await verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory})
|
||||
assert.ok(m.state.every(row=>row.picked===1&&row.delivery!==null))
|
||||
assert.ok(m.calls.filter(call=>call.raw.endsWith('/loading-complete')).every(call=>!('measurements' in call.body)))
|
||||
})
|
||||
|
||||
test('explicit rollback verifier runs after both picks and before successful departure',async()=>{
|
||||
const m=model();let calls=0
|
||||
const proof=await verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory,
|
||||
|
||||
@@ -169,7 +169,7 @@ async function runCrossCustomerCore(context, fixture, { readInventory, verifyDep
|
||||
&& canonical(priorProof.members) === canonical(proof.members)
|
||||
&& priorProof.requests.every(row => row.method === 'GET' || row.status === 'RESPONDED')
|
||||
&& Number(prior?.dispatch?.counts?.pickedPackageCount) === 2 && id(command?.body?.recordVersion) === id(prior?.dispatch?.recordVersion)
|
||||
&& canonical(command?.body?.measurements) === canonical(command?.result?.result?.confirmedLoad?.pallets)
|
||||
&& canonical(fixture.tripProof.loadingPreview?.pallets?.map(p=>({warehousePalletId:id(p.warehousePalletId),palletSpaces:1,weightKg:p.weightKg??null,volumeCbm:p.volumeCbm??null,deliveryIndex:p.deliveryIndex}))) === canonical(command?.result?.result?.confirmedLoad?.pallets)
|
||||
&& prior?.dispatch?.dispatchNo === fixture.dispatchNo && prior.dispatch.stage === 'LOADING'
|
||||
&& command?.body?.idempotencyKey && command.result?.action === 'LOADING_COMPLETE' && command.result.stage === 'DELIVERING'
|
||||
&& command.result.dispatchNo === fixture.dispatchNo && command.result.idempotencyKey === command.body.idempotencyKey
|
||||
@@ -200,7 +200,7 @@ async function runCrossCustomerCore(context, fixture, { readInventory, verifyDep
|
||||
&& rejectedBefore.dispatch.stage === 'LOADING' && rejectedBefore.task.pickingReconciled === false)
|
||||
const preview = await request('GET', `${pdaPath}/loading`, undefined, 'Read incomplete native loading manifest', true)
|
||||
await request('POST', `${pdaPath}/loading-complete`, { recordVersion: rejectedBefore.task.recordVersion, idempotencyKey: key('UNREADY-LOAD'),
|
||||
manifestHash: preview.manifestHash, measurements: [], remark: context.marker }, 'Unready second customer blocks explicit loading', true,
|
||||
manifestHash: preview.manifestHash, remark: context.marker }, 'Unready second customer blocks explicit loading', true,
|
||||
message => /拣货.*核平|尚未核平/.test(message))
|
||||
equal(rejectedBefore, await snapshot('GATE_AFTER'), 'Rejected departure leaves both complete inventories/outbounds/deliveries/trip unchanged')
|
||||
proof.gateProof = 'PREREQUISITE_REJECTION_VERIFIED'
|
||||
|
||||
@@ -143,9 +143,9 @@ function server(fault = '') {
|
||||
}
|
||||
assert.equal(detail.stage, 'LOADING'); assert.equal(detail.counts.pickedPackageCount, 3)
|
||||
assert.equal(body.recordVersion, detail.recordVersion); assert.equal(body.manifestHash, 'REAL-MANIFEST')
|
||||
assert.deepEqual(body.measurements, [{ warehousePalletId: PALLET, palletSpaces: 2, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1 }])
|
||||
assert.equal('measurements' in body, false)
|
||||
loadingActors.set(body.idempotencyKey, token ? 'PDA' : 'PLATFORM')
|
||||
detail.stage = 'DELIVERING'; bump(); return remember('LOADING_COMPLETE', body, { confirmedLoad: { manifestHash: body.manifestHash, pallets: body.measurements } })
|
||||
detail.stage = 'DELIVERING'; bump(); return remember('LOADING_COMPLETE', body, { confirmedLoad: { manifestHash: body.manifestHash, pallets: [{ warehousePalletId: PALLET, palletSpaces: 1, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1 }] } })
|
||||
}
|
||||
if (method === 'GET' && path.startsWith(`${F}/resource-occupancy?`)) {
|
||||
const query = new URL(`https://unused.test${path}`).searchParams, kind = query.get('kind')
|
||||
@@ -313,7 +313,7 @@ test('PDA original-actor replay restores loading while another platform actor is
|
||||
assert.equal(proof.loadingCommand.platformPositiveLoading, 'PENDING_SEPARATE_SCENARIO')
|
||||
assert.deepEqual(proof.loadingCommand.beforeReplay, proof.loadingCommand.afterReplay)
|
||||
assert.equal(proof.loadingCommand.platformReplay, undefined)
|
||||
assert.equal(writes[0].body.measurements[0].weightKg, '187.25')
|
||||
assert.equal('measurements' in writes[0].body, false)
|
||||
assert.ok(s.calls.every(c => !/\/fleet\/trips\/.*\/(?:assignment|loading-review|loading)$/.test(c.path)))
|
||||
const count = s.calls.length
|
||||
await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /已开始/)
|
||||
@@ -347,10 +347,11 @@ for (const fault of ['edit-mutates-on-rejection', 'replay-new-version']) test(`l
|
||||
if (fault === 'edit-mutates-on-rejection') assert.equal(s.calls.some(c => c.path.endsWith('/loading-complete')), false)
|
||||
})
|
||||
|
||||
test('missing actual producer measurement blocks before any edit rejection or loading write', async () => {
|
||||
const s = server(), proof = await prepare(s); s.pick(); const count = s.calls.length
|
||||
await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA'), /palletSpaces/)
|
||||
assert.equal(s.calls.length, count + 1); assert.equal(s.calls.at(-1).method, 'GET')
|
||||
test('scanned loading no longer requires the operator to supply a measurement receipt', async () => {
|
||||
const s = server(), proof = await prepare(s); s.pick()
|
||||
await confirmFleetLoading(s.context, 'native', proof, 'PDA')
|
||||
assert.equal(proof.loadingCommand.result.stage, 'DELIVERING')
|
||||
assert.equal('measurements' in proof.loadingCommand.body, false)
|
||||
})
|
||||
|
||||
test('wrong-run producer receipt and unreconciled picking block loading without mutation', async () => {
|
||||
|
||||
@@ -228,11 +228,11 @@ export async function confirmFleetLoading(context, module, proof, pdaToken, actu
|
||||
|| preview.loadingComplete?.allowed !== true || !preview.manifestHash) throw new Error(`整车未就绪:${preview.loadingComplete?.blockReason || '真实拣货未核平或阶段不符'}`)
|
||||
if (actualMeasurements && (actualMeasurements.environment !== 'TEST' || actualMeasurements.marker !== context.marker
|
||||
|| actualMeasurements.dispatchNo !== proof.dispatchNo || !Array.isArray(actualMeasurements.pallets))) throw new Error('实测回执不属于本轮TEST排车')
|
||||
const measurements = controlledPalletMeasurements(preview.pallets, proof.stops, actualMeasurements?.pallets)
|
||||
const measurements = preview.pallets.map(p => ({ warehousePalletId: id(p.warehousePalletId), palletSpaces: 1, weightKg: p.weightKg ?? null, volumeCbm: p.volumeCbm ?? null, deliveryIndex: p.deliveryIndex }))
|
||||
await verifyPreDepartureReassignment(context, module, proof)
|
||||
proof.loadingPreview = preview; proof.loadingMeasurementReceipt = actualMeasurements
|
||||
proof.loadingCommand = { path: `${pdaToken ? pdaPath : path}/loading-complete`, body: { recordVersion: preview.recordVersion,
|
||||
idempotencyKey: key(proof, 'LOADING_COMPLETE'), manifestHash: preview.manifestHash, measurements, remark: 'TEST本轮实际读取测量证据,不使用计费重量或固定值' } }
|
||||
idempotencyKey: key(proof, 'LOADING_COMPLETE'), manifestHash: preview.manifestHash, remark: 'TEST本轮实际读取测量证据,不使用计费重量或固定值' } }
|
||||
if (beforeComplete) await beforeComplete(proof.loadingCommand)
|
||||
const confirmed = await command(context, module, 'POST', proof.loadingCommand.path, proof.loadingCommand.body, '原生显式装车完成', pdaToken)
|
||||
check(context, module, '装车完成即进入派送中', confirmed.dispatchNo === proof.dispatchNo && confirmed.status === 'ACTIVE' && confirmed.stage === 'DELIVERING'
|
||||
@@ -240,8 +240,8 @@ export async function confirmFleetLoading(context, module, proof, pdaToken, actu
|
||||
const returned = confirmed.result.confirmedLoad.pallets
|
||||
check(context, module, '实载回执逐托保留原测量且不增加货量', returned?.length === measurements.length
|
||||
&& measurements.every(m => returned.some(r => id(r.warehousePalletId) === m.warehousePalletId
|
||||
&& r.palletSpaces === m.palletSpaces && decimal(r.weightKg) === decimal(m.weightKg)
|
||||
&& decimal(r.volumeCbm) === decimal(m.volumeCbm) && r.deliveryIndex === m.deliveryIndex)))
|
||||
&& r.palletSpaces === m.palletSpaces && (r.weightKg == null ? null : decimal(r.weightKg)) === (m.weightKg == null ? null : decimal(m.weightKg))
|
||||
&& (r.volumeCbm == null ? null : decimal(r.volumeCbm)) === (m.volumeCbm == null ? null : decimal(m.volumeCbm)) && r.deliveryIndex === m.deliveryIndex)))
|
||||
proof.loadingCommand.result = confirmed; proof.confirmedLoad = confirmed.result.confirmedLoad
|
||||
const snapshot = async label => ({
|
||||
main: await command(context, module, 'GET', path, undefined, `${label}主单`),
|
||||
|
||||
Reference in New Issue
Block a user