2026-09-08 14:07:31 +08:00
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
2026-09-20 19:57:47 +08:00
import { verifyReceivedOrderHold } from './phase4-tail-order-controls.mjs'
2026-09-08 14:07:31 +08:00
const source = await readFile ( fileURLToPath ( new URL ( './phase4-tail-order-controls.mjs' , import . meta . url )), 'utf8' )
2026-09-20 19:57:47 +08:00
test ( 'hold positive lifecycle stays after receiving; pre-receipt verifies refusal without mutation' , async () => {
assert . match ( source , /入库前暂扣必须拒绝/ )
assert . match ( source , /拒绝暂扣后订单事实零漂移/ )
assert . match ( source , /representative\?\.currentStatus !== 'INBOUND_COMPLETED'/ )
const operations = await readFile ( new URL ( './phase4-overseas-pda-operations.mjs' , import . meta . url ), 'utf8' )
assert . ok ( operations . indexOf ( 'await verifyReceivedOrderHold(context)' ) > operations . indexOf ( 'handoff.completedFixtures' ))
assert . ok ( operations . indexOf ( 'await verifyReceivedOrderHold(context)' ) < operations . indexOf ( 'await verifyStocktake(context' ))
await assert . rejects ( verifyReceivedOrderHold ({ marker : 'run' , createdResources : {} }), /缺少本轮/ )
})
2026-09-08 14:07:31 +08:00
test ( 'tail order controls execute after current producer and before overseas PDA' , () => {
const modules = normalizeModuleOrder ([ 'phase4-overseas-pda' ])
const controls = modules . indexOf ( 'phase4-tail-order-controls' )
assert . ok ( controls > modules . indexOf ( 'phase4-carrier-pickup-ready' ))
assert . ok ( controls < modules . indexOf ( 'phase4-overseas-pda' ))
})
test ( 'module covers the public tail-order write surface before PDA receiving' , () => {
2026-10-08 17:07:57 +08:00
for ( const path of [ '/receiving-warehouse' , '/destination' , '/pallet-merge-required' , '/fees' , 'LAST_MILE_ORDER_LIST' ,
2026-09-08 14:07:31 +08:00
'/hold' , '/manual-nodes' , '/api/platform/v1/last-leg/inbound-plans' , '/manual' ]) {
assert . ok ( source . includes ( path ), `missing ${ path } ` )
}
for ( const evidence of [
'接柜仓变更同键同载荷重放' , '接柜仓变更同键异载荷返回A0409' , '接柜仓变更旧版本返回A0409' ,
'整柜恢复原公共接柜仓' , '单票变更FBA目的仓' , '批量变更FBA目的仓' ,
'单票开启PDA合托提示' , '批量开启PDA合托分组' , '批量取消PDA合托分组' ,
2026-10-08 17:07:57 +08:00
'尾程费用详情可追溯' , 'LAST_MILE_ORDER_LIST' ,
2026-09-08 14:07:31 +08:00
'暂扣命令同键同载荷重放' , '暂扣同键异载荷返回A0409' , '暂扣尾程订单不能生成人工计划' ,
'解除本轮单票尾程暂扣' , '保存尾程八个人工节点快照' , '人工节点同键同载荷重放' ,
'人工节点同键异载荷返回A0409' , '人工节点读取固定为MANUAL且不回退柜级节点' ,
2026-09-09 02:54:09 +08:00
'预览整柜待生成入仓计划清单' , '按柜确认并自动生成入仓计划与实体托盘' ,
'按柜确认同键同快照幂等重放' , '确认后预览待生成票数和托数归零' ,
2026-09-08 14:07:31 +08:00
]) assert . ok ( source . includes ( evidence ), `missing ${ evidence } ` )
assert . match ( source , /nodes\.data\?\.nodes\?\.length === 8/ )
2026-09-09 02:54:09 +08:00
assert . match ( source , /creationMode === 'AUTO'/ )
2026-10-08 17:07:57 +08:00
assert . match ( source , /verifyDownloadCenterExport\(context/ )
2026-09-08 14:07:31 +08:00
assert . match ( source , /palletMergeBatchId/ )
2026-09-09 19:11:37 +08:00
assert . match ( source , /feeItemCode/ )
assert . match ( source , /feeItemName/ )
assert . match ( source , /direction: 'RECEIVABLE'/ )
2026-09-09 21:13:10 +08:00
assert . match ( source , /allIds\.length === 1 \|\| nonBlank\(groupRead\.data\?\.order\?\.palletMergeBatchId\)/ )
2026-09-09 19:11:37 +08:00
assert . match ( source , /businessDate:/ )
2026-09-09 19:25:13 +08:00
assert . match ( source , /originalCurrency === 'USD'/ )
assert . match ( source , /currency === 'CNY'/ )
assert . match ( source , /reviewStatus === 'PENDING'/ )
assert . match ( source , /'Idempotency-Key': `\$\{context\.marker\}-tail-fee`/ )
2026-09-09 19:41:29 +08:00
assert . match ( source , /ensureCurrentUsdRate/ )
assert . match ( source , /monthFrom=\$\{month\}&monthTo=\$\{month\}/ )
assert . match ( source , /existing\?\.status === 'ACTIVE'/ )
assert . match ( source , /businessDate: `\$\{feeMonth\}-01`/ )
2026-09-09 19:11:37 +08:00
assert . doesNotMatch ( source , /supplierDescription/ )
2026-09-08 14:07:31 +08:00
})
test ( 'receiving warehouse verification reuses public master data and restores the original assignment' , () => {
assert . match ( source , /tenantId=0&page=1&size=200&capabilityCode=RECEIVING&warehouseType=OVERSEAS&status=ACTIVE/ )
assert . match ( source , /id\(row\.id\) !== originalWarehouseId/ )
assert . match ( source , /overseasWarehouseId: originalWarehouseId, expectedVersion: Number\(changed\.data\.assignmentVersion\)/ )
assert . doesNotMatch ( source , /request\('POST', '\/api\/platform\/v1\/warehouse-admin'/ )
})
test ( 'module is marker-owned and never borrows historical tail orders' , () => {
assert . match ( source , /matrix\?\.marker !== context\.marker/ )
assert . match ( source , /containerNo: producer\.containerNo/ )
assert . match ( source , /customerId\) === id\(producer\.tenantId\)/ )
assert . match ( source , /禁止借历史尾程订单/ )
assert . doesNotMatch ( source , /records\([^)]*\)\[0\]/ )
})
2026-09-09 02:54:09 +08:00
test ( 'container conversion preserves long identifiers and generated pallets for PDA consumption' , () => {
2026-09-08 14:07:31 +08:00
assert . match ( source , /orderId: id\(order\.id\)/ )
assert . doesNotMatch ( source , /plannedWarehouse(?:Id|Code|Name):/ )
assert . match ( source , /!order\.overseasWarehouseId/ )
2026-09-09 02:54:09 +08:00
assert . match ( source , /container-preview/ )
assert . match ( source , /snapshotVersion/ )
assert . match ( source , /snapshotHash/ )
assert . match ( source , /convert-container/ )
assert . match ( source , /自动托盘进入托盘列表并保留FBA目的仓/ )
assert . match ( source , /planNos: generated\.map/ )
assert . match ( source , /generatedPlans: generated/ )
assert . match ( source , /AUTO_CONTAINER_PLANS_READY/ )
assert . match ( source , /remain as marker-owned prerequisites consumed by overseas PDA receiving/ )
2026-09-08 14:07:31 +08:00
})
2026-10-03 02:12:42 +08:00
function holdModel ( fault = '' ) {
const buckets = { availablePackages : '4' , holdPackages : '0' , allocatedPackages : '0' , pickedPackages : '0' , receivingPackages : '0' , inTransitPackages : '0' , procurementPackages : '0' }
const order = { id : '9007199254740993' , customerId : 'C1' , overseasWarehouseId : 'WH1' , inboundCode : 'IN-1' , currentStatus : 'INBOUND_COMPLETED' , detained : false , heldChargeable : false , version : 4 }
let positions = [ 1 , 3 ]. map (( n , i )=>({ id : String ( 21 + i ), balanceId : '20' , customerId : 'C1' , overseasWarehouseId : 'WH1' , inboundOrderId : order . id ,
palletId : String ( 31 + i ), palletNo : `PALLET- ${ i } ` , batchNo : 'PLAN' , totalPackages : String ( n ), availablePackages : String ( n ), holdPackages : '0' , allocatedPackages : '0' , pickedPackages : '0' , receivingPackages : '0' }))
const original = structuredClone ( positions )
const balance = { id : '20' , customerId : 'C1' , overseasWarehouseId : 'WH1' , inboundOrderId : order . id , inboundCode : 'IN-1' , totalPackages : '4' ,... buckets }
const receipt = positions . map ( row =>({ customerId : 'C1' , orderId : order . id , inboundCode : 'IN-1' , inventoryBalanceId : '20' , inventoryPositionId : row . id , receivedPackages : row . availablePackages }))
const commands = new Map (), calls = []; let tick = 0 , cycle = 0 , other = false , replayRead = false , failedRead = false
const context = { marker : 'RUN' , token : 'admin' , createdResources : {
phase4TailOrderControls : { marker : 'RUN' , representativeOrderId : order . id },
phase4InboundProducerFixtures : { marker : 'RUN' , containers : [{ fulfillmentType : 'ONE_CLICK_DROPSHIP' , tenantId : 'C1' , containerNo : 'BOX' }]},
phase4OverseasPda : { marker : 'RUN' , receivingCompletionEvidence : { booking : { status : 'VERIFIED' , proof : { passed : true }, scope : { customerId : 'C1' , warehouseId : 'WH1' , containerNo : 'BOX' },
stages : { AFTER_COMPLETE : { captured : true , evidence : { balances : [ structuredClone ( balance )], positions : original , stockReceipts : receipt }}}}}}},
addStep (){}, assert ( _m , name , _r , passed ){ assert . ok ( passed , name )}, apiSuccess ( _m , name , r ){ assert . equal ( r . body . code , '200' , name ); return r . body . data },
async request ( method , raw , body , _token , headers = {}) {
calls . push ({ method , path : raw , body : structuredClone ( body ), headers })
const path = new URL ( raw , 'https://offline.invalid' ). pathname
const result = data =>({ http : 200 , body : { code : '200' , data : structuredClone ( data )}})
const conflict = ()=>({ http : 200 , body : { code : 'A0409' }, requestId : 'req' , traceId : 'trace' })
const counts = ()=>{
for ( const field of [ 'availablePackages' , 'holdPackages' , 'allocatedPackages' , 'pickedPackages' , 'receivingPackages' ]) balance [ field ] = String ( positions . reduce (( sum , row )=> sum + Number ( row [ field ]), 0 ))
balance . updatedAt = String ( tick )
}
if ( method === 'GET' && path . endsWith ( '/positions' )) {
if ( replayRead && [ 'read failure' , 'foreign hold' ]. includes ( fault ) && ! failedRead ){ failedRead = true ; if ( fault === 'foreign hold' ){ order . version ++ ; order . holdReason = 'ANOTHER-OPERATION' } throw new Error ( 'read failed' )}
return result ( fault === 'missing position' ? positions . slice ( 1 ) : positions . slice (). reverse ())
}
if ( method === 'GET' && path . includes ( '/inventory/balances/' )) { counts (); return result ( balance )}
if ( method === 'GET' && path . includes ( '/orders/' )) return result ({ order })
if ( path . endsWith ( '/manual' )) { if ( fault === 'ambiguous plan' ) return { http : 0 }; return conflict ()}
if ( path . includes ( '/orders/' ) && method === 'PUT' ) {
const key = headers [ 'Idempotency-Key' ], prior = commands . get ( key )
if ( prior ) {
if ( JSON . stringify ( body ) !== JSON . stringify ( prior . body )) return conflict ()
replayRead = true
if ( fault === 'replay drift' ){ positions [ 0 ]. allocatedPackages = '1' ; positions [ 0 ]. availablePackages = '0' ; tick ++ }
return result ( prior . response )
}
if ( body . held ) {
cycle ++
if ( ! ( fault === 'second cycle no movement' && cycle === 2 )) for ( const row of positions ){ row . holdPackages = String ( Number ( row . holdPackages ) + Number ( row . availablePackages )); row . availablePackages = '0' }
} else {
for ( const row of positions ){ const retained = other && row . id === '22' && fault !== 'release steals independent' ? 1 : 0 ; row . availablePackages = String ( Number ( row . availablePackages ) + Number ( row . holdPackages ) - retained ); row . holdPackages = String ( retained )}
}
order . detained = body . held ; order . heldChargeable = body . chargeable ; order . holdReason = body . held ? body . reason : null ; order . version ++ ; tick ++
const response = { orderId : order . id , held : body . held , chargeable : body . chargeable , reason : order . holdReason , version : order . version }
commands . set ( key ,{ body : structuredClone ( body ), response })
if ( fault === 'ambiguous order hold' && body . held ) return { http : 0 }
return result ( response )
}
if ( method === 'POST' && path . includes ( '/inventory/' )) {
const row = positions . find ( p => p . id === body . positionId ), held = path . endsWith ( '/hold' )
row . availablePackages = String ( Number ( row . availablePackages ) + ( held ?- 1 : 1 )); row . holdPackages = String ( Number ( row . holdPackages ) + ( held ? 1 :- 1 )); other = held ; tick ++
if ( fault === 'ambiguous independent hold' && held ) return { http : 0 }
return result ({ replayed : false })
}
throw new Error ( `unexpected ${ method } ${ raw } ` )
}}
if ( fault === 'stale marker' ) context . createdResources . phase4OverseasPda . marker = 'OLD'
if ( fault === 'no evidence' ) delete context . createdResources . phase4OverseasPda . receivingCompletionEvidence
if ( fault === 'wrong immutable customer' ) receipt [ 0 ]. customerId = 'OTHER'
if ( fault === 'unsafe bucket' ) balance . procurementPackages = '9007199254740993'
if ( fault === 'moved source' ) positions [ 1 ]. palletNo = 'MOVED'
if ( fault === 'not enough stock' ){
positions [ 1 ]. availablePackages = '1' ; positions [ 1 ]. totalPackages = '1'
const e = context . createdResources . phase4OverseasPda . receivingCompletionEvidence . booking . stages . AFTER_COMPLETE . evidence
e . positions [ 1 ]. availablePackages = '1' ; e . positions [ 1 ]. totalPackages = '1' ; e . stockReceipts [ 1 ]. receivedPackages = '1'
e . balances [ 0 ]. availablePackages = '2' ; e . balances [ 0 ]. totalPackages = '2' ; balance . totalPackages = '2'
}
return { context , calls , order , positions }
}
test ( 'a failed repeated verification clears previous success evidence before any write' , async ()=>{
const { context , calls } = holdModel ( 'no evidence' )
context . createdResources . phase4TailOrderControls . holdVerifiedAfterReceipt = true
context . createdResources . phase4TailOrderControls . holdInventoryProof = { status : 'VERIFIED' }
await assert . rejects ( verifyReceivedOrderHold ( context ))
assert . equal ( context . createdResources . phase4TailOrderControls . holdVerifiedAfterReceipt , false )
assert . equal ( context . createdResources . phase4TailOrderControls . holdInventoryProof , undefined )
assert . ok ( calls . every ( call => call . method === 'GET' ))
})
test ( 'fresh immutable IDs drive two full cycles and preserve independent HOLD across every raw position' , async ()=>{
const { context , calls , order } = holdModel ()
await verifyReceivedOrderHold ( context )
const proof = context . createdResources . phase4TailOrderControls . holdInventoryProof
assert . equal ( proof . status , 'VERIFIED' ); assert . equal ( proof . restored , true ); assert . equal ( order . version , 8 )
2026-10-03 12:13:34 +08:00
assert . equal ( proof . concurrentCommands . length , 2 )
for ( const command of proof . concurrentCommands ) {
assert . equal ( command . copies , 2 )
assert . deepEqual ( command . replies [ 0 ], command . replies [ 1 ])
assert . equal ( command . replies [ 0 ]. version , command . expectedVersion + 1 )
}
2026-10-03 02:12:42 +08:00
assert . equal ( proof . snapshots . C2_RELEASED . positions . find ( row => row . id === '22' ). holdPackages , 1 )
assert . equal ( proof . snapshots . AFTER . balances [ 0 ]. availablePackages , 4 )
assert . equal ( calls . filter ( c => c . path . endsWith ( '/manual' )). length , 2 )
assert . ok ( ! calls . some ( c => / inventory \ / ( balances | summary ) \ ? /.test(c.path)))
assert . equal ( calls . filter ( c => c . method === 'POST' && c . path . endsWith ( '/inventory/release' )). length , 1 )
})
2026-10-03 12:13:34 +08:00
test ( 'hold duplicate requests are in flight together, with bounded concurrency of two' , async ()=>{
const { context } = holdModel ()
const originalRequest = context . request
let active = 0 , peak = 0
context . request = async ( method , path ,... args )=>{
if ( method !== 'PUT' ||! path . includes ( '/orders/' )) return originalRequest ( method , path ,... args )
active ++ ; peak = Math . max ( peak , active )
try { await new Promise ( resolve => setImmediate ( resolve )); return await originalRequest ( method , path ,... args ) }
finally { active -- }
}
await verifyReceivedOrderHold ( context )
assert . equal ( peak , 2 )
assert . equal ( active , 0 )
})
test ( 'ambiguous parallel hold waits for its sibling and never starts recovery writes' , async ()=>{
const { context , calls } = holdModel ()
const originalRequest = context . request
let holdRequests = 0 , siblingSettled = false
context . request = async ( method , path , body ,... args )=>{
if ( method === 'PUT' && body ? . held && ++ holdRequests <= 2 ) {
if ( holdRequests === 1 ) throw new Error ( 'lost response' )
await new Promise ( resolve => setImmediate ( resolve ))
const response = await originalRequest ( method , path , body ,... args )
siblingSettled = true
return response
}
return originalRequest ( method , path , body ,... args )
}
await assert . rejects ( verifyReceivedOrderHold ( context ), /lost response/ )
assert . equal ( siblingSettled , true )
assert . equal ( context . createdResources . phase4TailOrderControls . holdInventoryProof . uncertainWrite , true )
assert . ok ( ! calls . some ( call => call . path . endsWith ( '/inventory/release' ) || call . body ? . held === false ))
})
2026-10-03 02:12:42 +08:00
for ( const fault of [ 'stale marker' , 'no evidence' , 'wrong immutable customer' , 'missing position' , 'unsafe bucket' , 'not enough stock' , 'moved source' ]) {
test ( `missing or unsafe fresh hold prerequisite blocks all writes: ${ fault } ` , async ()=>{
const { context , calls } = holdModel ( fault )
await assert . rejects ( verifyReceivedOrderHold ( context ))
assert . ok ( calls . every ( c => c . method === 'GET' ))
})
}
for ( const fault of [ 'second cycle no movement' , 'release steals independent' , 'replay drift' ]) {
test ( `inventory regression is detected and never declared verified: ${ fault } ` , async ()=>{
const { context } = holdModel ( fault )
await assert . rejects ( verifyReceivedOrderHold ( context ))
assert . equal ( context . createdResources . phase4TailOrderControls . holdInventoryProof . status , 'FAILED' )
assert . notEqual ( context . createdResources . phase4TailOrderControls . holdVerifiedAfterReceipt , true )
})
}
for ( const fault of [ 'ambiguous independent hold' , 'ambiguous order hold' , 'ambiguous plan' ]) {
test ( `unknown mutation response never triggers a blind recovery release: ${ fault } ` , async ()=>{
const { context , calls } = holdModel ( fault )
await assert . rejects ( verifyReceivedOrderHold ( context ))
const proof = context . createdResources . phase4TailOrderControls . holdInventoryProof
assert . equal ( proof . uncertainWrite , true ); assert . equal ( proof . restored , false )
assert . match ( proof . recoveryError , /禁止自动释放/ )
assert . ok ( ! calls . some ( c => c . path . endsWith ( '/inventory/release' ) || c . body ? . held === false ))
})
}
test ( 'read failure after confirmed hold recovers only the exact known order cycle and independent one box' , async ()=>{
const { context , calls } = holdModel ( 'read failure' )
await assert . rejects ( verifyReceivedOrderHold ( context ), /read failed/ )
const proof = context . createdResources . phase4TailOrderControls . holdInventoryProof
assert . equal ( proof . status , 'FAILED' ); assert . equal ( proof . restored , true )
assert . equal ( calls . filter ( c => c . body ? . held === false ). length , 1 )
assert . equal ( proof . snapshots . RECOVERED . balances [ 0 ]. holdPackages , 0 )
})
test ( 'recovery refuses to release a concurrently changed order hold' , async ()=>{
const { context , calls } = holdModel ( 'foreign hold' )
await assert . rejects ( verifyReceivedOrderHold ( context ), /read failed/ )
const proof = context . createdResources . phase4TailOrderControls . holdInventoryProof
assert . equal ( proof . restored , false ); assert . match ( proof . recoveryError , /只恢复本轮/ )
assert . ok ( ! calls . some ( c => c . path . endsWith ( '/inventory/release' ) || c . body ? . held === false ))
})